168 Commits
Author SHA1 Message Date
Klesti Selimaj b8db28629a Merge pull request #47 from osui-rs/frontend-enhancements
Frontend enhancements
2026-02-01 19:25:49 +01:00
selimaj-dev 6ac9f33a62 Benchmark 2026-02-01 19:13:15 +01:00
selimaj-dev 2606e9b4e1 Area merging 2026-02-01 19:03:12 +01:00
selimaj-dev 25bebcbb81 Flex components 2026-02-01 18:20:20 +01:00
selimaj-dev 76984187a0 View plugins for literals 2026-02-01 17:46:40 +01:00
selimaj-dev 44a4837647 view plugins Documentation 2026-02-01 17:36:48 +01:00
selimaj-dev e9541c0d21 proper auto sizing 2026-02-01 17:25:49 +01:00
selimaj-dev c4addd7f01 View plugins 2026-02-01 17:18:59 +01:00
selimaj-dev 2ca419bfd4 Fixed newline drawing in engine 2026-02-01 16:37:07 +01:00
selimaj-dev b0eaa76398 View plugins 2026-02-01 16:34:27 +01:00
selimaj-dev d40a032bc1 Improved state structure 2026-02-01 13:40:22 +01:00
selimaj-dev 8c4576396b Improved state structure 2026-02-01 13:39:37 +01:00
selimaj-dev 039e751dd1 Removed dupes of deps in {} 2026-02-01 11:01:20 +01:00
selimaj-dev c3519fa6ed Improved Card by dynamicifying the bars 2026-02-01 10:31:37 +01:00
selimaj-dev 5e4b78f353 Removed excess clone in Card component 2026-02-01 10:23:34 +01:00
selimaj-dev b141e134d4 Direct text dependencies 2026-02-01 10:21:28 +01:00
selimaj-dev b23c8c68dc Text dependencies 2026-02-01 10:11:41 +01:00
selimaj-dev 0cead353ee macros fix 2026-01-31 21:15:20 +01:00
selimaj-dev f5cd3e7fbf macros fix 2026-01-31 21:14:26 +01:00
selimaj-dev e0171a9029 v0.2.0 2026-01-31 21:12:15 +01:00
selimaj-dev 73cfc36277 Documented 2026-01-31 18:21:46 +01:00
Klesti Selimaj 0f5422b66b Merge pull request #46 from osui-rs/scope-performance-patch
Scope performance patch
2026-01-31 17:55:21 +01:00
selimaj-dev dc2810b6d8 Readme update #40 2026-01-31 17:48:51 +01:00
selimaj-dev 8ebef06808 Benchmark 2026-01-31 17:47:08 +01:00
selimaj-dev 04f2abc47a Made scope back to Mutex due to performance 2026-01-31 17:45:14 +01:00
selimaj-dev 1f566e19e7 Improved draw_children performance 2026-01-31 16:16:08 +01:00
selimaj-dev 457b894864 Benchmark 2026-01-31 15:48:21 +01:00
selimaj-dev 89161ab39b Fixed draw_children 2026-01-31 15:46:59 +01:00
Klesti Selimaj 5017895a30 Merge pull request #45 from osui-rs/access-cell
Access cell
2026-01-31 15:04:56 +01:00
selimaj-dev dd7c53c9c1 Benchmarked 2026-01-31 15:00:16 +01:00
selimaj-dev 4e2636360f Fixed rsx emit with new AccessCell 2026-01-31 14:56:41 +01:00
selimaj-dev 2faac93944 Abstracted children in scope 2026-01-31 14:52:05 +01:00
selimaj-dev d8173b8ab3 Scope children with AccessCell 2026-01-31 14:44:28 +01:00
selimaj-dev 8685cdde2a Scopes with AccessCell 2026-01-31 14:32:49 +01:00
selimaj-dev ec320e47fd Access cell in event handlers 2026-01-31 14:19:37 +01:00
selimaj-dev 17f78b86f4 Implemented access cell for component and view 2026-01-31 13:02:22 +01:00
Klesti Selimaj d8f24d7eec Merge pull request #44 from osui-rs/benchmark
Benchmark
2026-01-31 01:48:00 +01:00
selimaj-dev d9e2006117 Benchmark 2026-01-31 01:44:50 +01:00
Klesti Selimaj 78c3e7f7b3 Merge pull request #43 from osui-rs/developed-structure
Developed structure
2026-01-31 00:30:43 +01:00
selimaj-dev e5812315c6 Hide cursor in engine and crossterm in prelude 2026-01-31 00:23:10 +01:00
selimaj-dev 85342d641f Improved prelude 2026-01-31 00:15:40 +01:00
selimaj-dev ab9548920c Improved rsx parsing 2026-01-31 00:09:14 +01:00
selimaj-dev eefa41318c Refactored component 2026-01-30 23:36:54 +01:00
selimaj-dev 4e2d4662d1 Display to rsx 2026-01-30 23:25:33 +01:00
selimaj-dev bf6fa42d96 Fixed simple example 2026-01-30 23:14:10 +01:00
selimaj-dev e31c17ba41 Rsx children scope placing 2026-01-30 23:13:05 +01:00
selimaj-dev 74e33fa4c2 Frontend generate children 2026-01-30 23:00:02 +01:00
selimaj-dev 03191b0a57 Rsx children 2026-01-30 22:54:56 +01:00
selimaj-dev 91f0878edc rsx params 2026-01-30 21:14:41 +01:00
selimaj-dev e85257a10e component generator 2026-01-30 19:29:52 +01:00
selimaj-dev 1f64b4f6e2 Component params 2026-01-30 19:02:26 +01:00
selimaj-dev 2799faaa73 Small stuff 2026-01-30 18:23:46 +01:00
selimaj-dev 1526daaba2 Chunked threads 2026-01-30 18:07:28 +01:00
selimaj-dev 5e2e9952d9 Benchmark result in microseconds 2026-01-30 17:39:15 +01:00
selimaj-dev 340b8c6f4d Multithreaded benchmark 2026-01-30 17:35:58 +01:00
selimaj-dev c9a7e3c835 Improved benchmark 2026-01-30 17:15:12 +01:00
selimaj-dev 41f4ad1022 Fixed rsx! for loops and if statements 2026-01-30 17:03:18 +01:00
selimaj-dev 41e7b2a10c Benchmarking 2026-01-30 15:08:01 +01:00
selimaj-dev 2d79b1dfbb Implemented engine return 2026-01-30 14:19:24 +01:00
selimaj-dev ccbbf29538 Separated simple benchmark 2026-01-29 10:57:17 +01:00
selimaj-dev 74c4a976dd Separated simple benchmark 2026-01-29 10:38:07 +01:00
selimaj-dev 059f8f07c1 Nanosecond benchmarking 2026-01-29 09:59:06 +01:00
Klesti Selimaj 75a7f20e4a Merge pull request #42 from osui-rs/engine-commands
Engine commands
2026-01-28 23:53:06 +01:00
selimaj-dev 0cf0eb42fc Context implementation 2026-01-28 23:50:08 +01:00
selimaj-dev 93ba2f4a78 Implemented exectution to Context 2026-01-28 23:41:54 +01:00
selimaj-dev 1df6cd4c5e Implemented commandExecutor logic 2026-01-28 23:33:22 +01:00
selimaj-dev 63abf41094 Implemented commands on scopes 2026-01-28 19:33:12 +01:00
selimaj-dev 715175c412 rsx proc macro 2026-01-28 15:27:38 +01:00
selimaj-dev 46587ff822 rsx proc macro 2026-01-28 15:21:45 +01:00
selimaj-dev 011b1ab963 Engine structure 2026-01-28 10:27:18 +01:00
Klesti Selimaj 665b502a09 Merge pull request #39 from osui-rs/frontend
Frontend
2026-01-26 17:54:34 +01:00
selimaj-dev 506ccae049 State syncing 2026-01-26 17:27:47 +01:00
selimaj-dev 86e3f4e684 Sync state (param passing) 2026-01-26 16:58:44 +01:00
selimaj-dev 89b460697f Any event 2026-01-26 09:38:36 +01:00
selimaj-dev bd53e6450c use effect instead of event 2026-01-26 09:34:23 +01:00
selimaj-dev 083812458b Children clear for loop 2026-01-25 21:12:34 +01:00
selimaj-dev 4364177538 Text @position 2026-01-25 21:11:42 +01:00
selimaj-dev 98598bec15 Initial dynamic render 2026-01-25 20:46:26 +01:00
selimaj-dev eb702dce45 RSX dependency 2026-01-25 20:40:24 +01:00
selimaj-dev 229353f5f0 RSX dependency 2026-01-25 20:39:17 +01:00
selimaj-dev 04a69b138a RSX For loop 2026-01-25 20:12:47 +01:00
selimaj-dev 993a790da7 If statement rsx 2026-01-25 20:00:21 +01:00
selimaj-dev ecc1172737 Rsx scope child 2026-01-25 19:22:36 +01:00
selimaj-dev a9ecc8da8b working Rsx 2026-01-25 18:58:24 +01:00
selimaj-dev f6a4bbd8ac Frontend structure 2026-01-25 18:38:07 +01:00
selimaj-dev 4afe1c24c2 Added use_mount 2026-01-25 17:22:40 +01:00
Klesti Selimaj b50ca3263b Merge pull request #38 from osui-rs/render-scoping
Render scoping
2026-01-25 17:15:04 +01:00
selimaj-dev 6b3e9239c9 Scope view 2026-01-25 02:27:36 +01:00
selimaj-dev 470512a542 Dynamic scoping with for loop 2026-01-25 02:22:16 +01:00
selimaj-dev 6e481862ec Dynamic scoping with if statement 2026-01-25 02:17:08 +01:00
selimaj-dev 05a390b10f Dynamic scoping 2026-01-25 00:30:53 +01:00
selimaj-dev 926405a863 Dynamic scoping 2026-01-24 21:38:45 +01:00
selimaj-dev 9953e4a0bc Render position 2026-01-24 17:46:33 +01:00
selimaj-dev 8f0712d4f8 Event passing 2026-01-24 17:31:58 +01:00
selimaj-dev 9d55dacde2 Context children 2026-01-24 17:22:59 +01:00
selimaj-dev abe37686d8 Refresh on children 2026-01-24 16:40:03 +01:00
selimaj-dev 02f3aea4e3 Fixed refresh 2026-01-24 16:01:22 +01:00
selimaj-dev 33370287b2 Renderer 2026-01-24 14:49:47 +01:00
selimaj-dev 0f12a371e5 Renderer 2026-01-24 14:47:53 +01:00
selimaj-dev 1f9bf48bb4 View render 2026-01-24 14:37:19 +01:00
selimaj-dev 49bb99fedb Simple test 2026-01-24 14:26:04 +01:00
selimaj-dev 979af999e4 Simple renderer 2026-01-24 14:17:05 +01:00
selimaj-dev 0515087f0d Renderer 2026-01-24 10:44:30 +01:00
selimaj-dev 2a62e53ed5 Engine 2026-01-24 01:00:21 +01:00
selimaj-dev c2b9bb3aa1 DrawContext size 2026-01-24 00:42:54 +01:00
selimaj-dev 0a026c4ee7 View 2026-01-24 00:28:43 +01:00
selimaj-dev 15ff801cb0 Emit event threaded 2025-11-23 18:01:21 +01:00
selimaj-dev 538cc8731f Improved state mangement 2025-11-23 11:23:28 +01:00
selimaj-dev b326fc48ed Improved state management 2025-11-23 11:10:07 +01:00
selimaj-dev 8740bbee89 Working event handling 2025-11-23 10:37:20 +01:00
selimaj-dev 22d02e7d1a Multithreaded 2025-11-23 09:58:01 +01:00
selimaj-dev 0cefbaa223 Testing 2025-11-20 23:23:01 +01:00
selimaj-dev b57fffe9b7 Testing 2025-11-20 23:15:31 +01:00
selimaj-dev 4d2f0d7b9d State management 2025-11-20 17:17:18 +01:00
selimaj-dev ff351caee0 Testing new structure 2025-11-20 17:10:12 +01:00
selimaj-dev e4110976cd Testing new structure 2025-11-20 16:57:14 +01:00
selimaj-dev 1fbff7a0bc Testing new structure 2025-11-19 09:14:03 +01:00
selimaj-dev 6e8ca5535e Testing new structure 2025-11-18 20:17:02 +01:00
selimaj-dev e031f9bcef Testing new structure 2025-11-18 20:04:14 +01:00
Klesti Selimaj c6d11315d6 Merge pull request #36 from osui-rs/refactor
Refactor
2025-11-02 12:31:59 +01:00
selimaj-dev 68d00be96c Simple velocity example 2025-11-02 10:49:25 +01:00
selimaj-dev f5d1b42db4 Velocity element 2025-11-02 10:26:15 +01:00
selimaj-dev 63ed7b8da4 Y offset fix on print 2025-11-02 09:16:03 +01:00
selimaj-dev 98b2a4e667 Outline position fix 2025-11-02 09:10:34 +01:00
selimaj-dev e4b905d085 Refactored render scope 2025-11-02 09:00:33 +01:00
Klesti Selimaj 1d6ce1506e Merge pull request #34 from osui-rs/scroll
Scroll
2025-11-01 23:34:04 +01:00
selimaj-dev 0e3c1ccafe Fixed bad scissor 2025-11-01 23:19:22 +01:00
selimaj-dev dbd16956dd Seperated row and column flex 2025-11-01 22:50:19 +01:00
selimaj-dev c91a473263 Added auth example 2025-11-01 22:47:27 +01:00
selimaj-dev 6424ad3390 Small change in iter 2025-11-01 16:58:10 +01:00
selimaj-dev 23edb6c917 Mouse input 2025-11-01 15:30:03 +01:00
selimaj-dev 72e8df007e Scroll 2025-11-01 15:14:54 +01:00
selimaj-dev a2350f10f5 Dynamic flex size 2025-11-01 13:30:41 +01:00
selimaj-dev 2d41f3b50b Fixed padding 2025-11-01 13:02:10 +01:00
selimaj-dev 5cb6d341af Even width and height comparisons 2025-11-01 13:01:44 +01:00
selimaj-dev f41a4936f9 Fixed paginator render 2025-11-01 12:45:46 +01:00
selimaj-dev dbb8a0e01a Fixed area rendering and merging issues 2025-11-01 12:40:30 +01:00
selimaj-dev fd8e466830 Parent transform 2025-11-01 11:56:55 +01:00
selimaj-dev 29432a939b Paginator 2025-11-01 09:48:04 +01:00
selimaj-dev 9d4b247b2a Working area stop 2025-11-01 09:25:05 +01:00
selimaj-dev 916818f915 1k commits 🥳 (Fixed delete key not working in input) 2025-09-05 20:26:37 +02:00
Leo Codes 5cf59f751d Merge pull request #32 from osui-rs/use-effect
Added a `run!` macro for useEffect behavior
2025-08-14 09:43:30 -04:00
selimaj-dev c460e073a7 Updated the run macro #31 #30 2025-08-14 15:42:21 +02:00
selimaj-dev f51182b2b1 Updated the run macro #30 #31 2025-08-14 15:40:39 +02:00
selimaj-dev 6100023a2a Added a run macro with useEffect like properties #30 2025-08-14 05:01:03 +02:00
Leo Codes 3d9aa35964 Merge pull request #29 from osui-rs/iteration-patch
Added iteration and improved examples
2025-08-12 23:43:39 -04:00
selimaj-dev 9a03f241d8 Added rsx iteration 2025-08-12 23:40:56 -04:00
selimaj-dev fde8ad17d6 added Iterator 2025-08-12 23:18:35 -04:00
selimaj-dev e0cf149790 Updated examples with a few small changes to resolve warnings 2025-08-12 21:12:54 -04:00
Leo Codes 1e590597f3 Merge pull request #28 from osui-rs/render-refactor
Render refactor
2025-08-10 14:46:47 -04:00
selimaj-dev 935f18178e changed demo exit key from q to escape 2025-08-10 13:45:45 -05:00
selimaj-dev 20a9d2b216 Improved ghost with paginator 2025-08-09 02:44:17 +02:00
selimaj-dev 3520369e35 Added ghost elements to avoid focusing on them 2025-08-09 02:27:16 +02:00
selimaj-dev c8ebe36d78 Added focus cursor in input 2025-08-08 23:51:15 +02:00
selimaj-dev d3861909e7 added RenderContext 2025-08-08 23:47:21 +02:00
selimaj-dev 1bdbc447ab Merge branch 'render-refactor' of github.com:osui-rs/osui into render-refactor 2025-08-08 23:38:21 +02:00
selimaj-dev e0059585ce Implemented the ElementRenderer for widgets 2025-08-04 12:35:56 -05:00
selimaj-dev b363c81a24 Added a ElementRenderer trait for element renderers 2025-08-04 12:16:23 -05:00
Leo Codes 2fae5c71f8 Merge pull request #27 from osui-rs/elem-nav
Elem nav
2025-08-04 08:25:54 -05:00
selimaj-dev 9868f02c33 Version update to 0.1.1 (for later) 2025-08-04 08:24:35 -05:00
selimaj-dev 896dfb0a36 Added a AlwaysFocused component #26 2025-08-04 08:23:51 -05:00
selimaj-dev 3a5286a54f Added focus #26 2025-08-04 08:16:34 -05:00
selimaj-dev ca4ee54e9d Added after render and render on extension with child handlers #26 2025-08-04 05:58:11 -05:00
selimaj-dev 62e0c1eac1 Added event on Extension #26 2025-08-04 04:50:52 -05:00
selimaj-dev 48397c06fa Added ExtensionContext #26 2025-08-04 04:45:55 -05:00
selimaj-dev 14602f95c8 First widget focus #26 2025-08-04 04:24:41 -05:00
selimaj-dev fbd84f98d3 Added focus on widget #26 2025-08-04 04:17:40 -05:00
selimaj-dev db1de03fbb Refactored widget #26 2025-08-04 04:05:38 -05:00
47 changed files with 2696 additions and 2517 deletions
+7 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "osui" name = "osui"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
license = "Apache-2.0" license = "Apache-2.0"
description = "A TUI library for advanced uis" description = "A TUI library for advanced uis"
@@ -9,8 +9,7 @@ homepage = "https://osui.netlify.app"
documentation = "https://osui.netlify.app/docs/" documentation = "https://osui.netlify.app/docs/"
[features] [features]
no_rsx = [] rsx = []
no_elem = []
[lib] [lib]
name = "osui" name = "osui"
@@ -24,8 +23,10 @@ path = "src/main.rs"
cc = "1.0" cc = "1.0"
[dependencies] [dependencies]
access-cell = "0.1.3"
crossterm = "0.28.1" crossterm = "0.28.1"
figlet-rs = "0.1.5" figlet-rs = "0.1.5"
osui-macros = { version = "0.1.0", path = "macros" }
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
@@ -43,3 +44,6 @@ overflow-checks = false
lto = true lto = true
panic = 'abort' panic = 'abort'
codegen-units = 1 codegen-units = 1
[workspace]
members = [".", "macros"]
+9 -7
View File
@@ -17,21 +17,23 @@
</p> </p>
<p align="center"> <p align="center">
<b>OSUI is a customizable terminal user interface (TUI) library written in Rust. It provides a set of components and rsx to build interactive command-line interfaces with ease.</b> <b>OSUI is a customizable terminal user interface (TUI) library written in Rust.</b>
</p> </p>
```rust ```rust
use osui::prelude::*; use osui::prelude::*;
fn main() -> std::io::Result<()> { pub fn main() {
let screen = Screen::new(); let engine = Console::new();
engine.run(App {}).expect("Failed to run engine");
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! { rsx! {
"Hello, World" "Hello World"
} }
.draw(&screen); .view(&cx)
screen.run()
} }
``` ```
+227
View File
@@ -0,0 +1,227 @@
Iterx72,Nestingx72,Time µs
14,14,7668
0,4,714
5,1,1550
3,0,617
9,6,2399
0,3,882
11,10,2691
4,10,1343
5,5,1562
6,6,1749
11,3,2719
9,14,2315
1,13,770
14,8,3290
6,14,1767
6,11,1857
10,11,2530
7,11,1936
13,5,3130
3,13,1161
0,2,928
6,3,1751
11,11,2727
11,6,2860
3,12,1170
13,14,3090
3,2,1140
12,4,2952
9,11,4884
10,2,2726
1,3,756
3,6,1151
6,5,1731
9,1,2348
11,4,2688
2,13,944
13,3,3125
12,0,643
1,4,721
6,1,1757
1,14,730
10,7,2505
7,9,2002
13,6,3132
4,6,1354
4,7,1311
0,14,520
4,8,1372
14,7,3304
7,0,620
0,0,605
11,9,2748
4,13,1389
12,10,2989
8,1,2211
9,9,2323
4,11,1404
5,9,1569
3,11,1129
5,7,1567
10,14,2543
5,0,612
10,0,636
6,4,1745
5,4,1606
1,2,707
11,12,2727
13,0,605
8,2,2127
10,4,2512
4,4,1340
6,8,1973
7,8,2185
3,10,1175
9,2,2300
1,6,743
10,1,2547
1,5,715
3,1,1209
7,4,2217
4,5,1346
11,1,2719
7,10,1972
9,3,2351
14,13,3301
14,3,3315
2,12,939
13,8,3098
9,13,2339
11,7,2749
6,7,1742
9,8,2348
8,11,2171
7,6,2370
12,8,2988
4,2,1349
2,8,955
8,4,2165
2,5,938
1,0,765
14,1,3275
14,11,3349
2,11,1024
14,2,3423
12,12,2844
8,12,2219
14,5,3308
3,14,1136
1,9,710
6,13,1831
8,13,2296
11,5,2729
4,1,1364
14,10,3324
12,5,2925
8,5,2121
10,3,2519
1,7,698
2,4,959
6,9,1973
7,5,2240
2,7,936
0,6,753
5,10,1557
2,3,948
2,14,931
8,6,2132
2,10,936
4,0,618
13,4,3136
5,6,1530
5,8,1546
7,14,1920
8,10,2110
12,9,3021
1,11,738
13,11,3121
1,1,730
0,1,981
3,8,1271
10,8,2554
14,14,3267
0,9,600
4,9,1328
9,12,3707
6,12,1800
0,5,684
7,2,2180
9,10,7168
8,14,2163
7,7,3732
8,0,633
5,14,1645
8,8,2129
12,7,2898
0,10,573
7,3,2255
14,9,3272
2,9,924
9,4,2331
10,5,2513
10,13,2514
11,8,2733
14,12,3326
0,8,618
6,0,609
8,9,2157
12,1,3013
12,11,2916
12,3,2959
9,0,623
12,13,3064
0,13,536
5,13,1558
6,10,1835
11,13,2784
13,9,3071
0,7,599
2,0,617
9,5,2303
8,3,2152
12,2,2994
0,12,542
7,13,1923
4,3,1350
13,7,3177
10,10,2533
6,2,1748
3,7,1209
13,2,3097
11,2,2731
5,2,1562
14,4,3273
7,1,2088
9,7,2333
5,12,1556
3,5,1177
12,14,2888
3,4,1153
1,10,710
11,0,614
10,12,2505
11,14,2827
1,8,710
14,6,3282
7,12,1938
2,2,943
10,9,2578
0,11,537
2,1,926
4,14,1367
14,0,614
1,12,716
2,6,935
13,10,3105
10,6,2524
4,12,1393
8,7,2156
12,6,2897
5,11,1528
13,13,3129
5,3,1536
3,3,1179
3,9,1173
13,12,3094
13,1,3097
1 Iterx72 Nestingx72 Time µs
2 14 14 7668
3 0 4 714
4 5 1 1550
5 3 0 617
6 9 6 2399
7 0 3 882
8 11 10 2691
9 4 10 1343
10 5 5 1562
11 6 6 1749
12 11 3 2719
13 9 14 2315
14 1 13 770
15 14 8 3290
16 6 14 1767
17 6 11 1857
18 10 11 2530
19 7 11 1936
20 13 5 3130
21 3 13 1161
22 0 2 928
23 6 3 1751
24 11 11 2727
25 11 6 2860
26 3 12 1170
27 13 14 3090
28 3 2 1140
29 12 4 2952
30 9 11 4884
31 10 2 2726
32 1 3 756
33 3 6 1151
34 6 5 1731
35 9 1 2348
36 11 4 2688
37 2 13 944
38 13 3 3125
39 12 0 643
40 1 4 721
41 6 1 1757
42 1 14 730
43 10 7 2505
44 7 9 2002
45 13 6 3132
46 4 6 1354
47 4 7 1311
48 0 14 520
49 4 8 1372
50 14 7 3304
51 7 0 620
52 0 0 605
53 11 9 2748
54 4 13 1389
55 12 10 2989
56 8 1 2211
57 9 9 2323
58 4 11 1404
59 5 9 1569
60 3 11 1129
61 5 7 1567
62 10 14 2543
63 5 0 612
64 10 0 636
65 6 4 1745
66 5 4 1606
67 1 2 707
68 11 12 2727
69 13 0 605
70 8 2 2127
71 10 4 2512
72 4 4 1340
73 6 8 1973
74 7 8 2185
75 3 10 1175
76 9 2 2300
77 1 6 743
78 10 1 2547
79 1 5 715
80 3 1 1209
81 7 4 2217
82 4 5 1346
83 11 1 2719
84 7 10 1972
85 9 3 2351
86 14 13 3301
87 14 3 3315
88 2 12 939
89 13 8 3098
90 9 13 2339
91 11 7 2749
92 6 7 1742
93 9 8 2348
94 8 11 2171
95 7 6 2370
96 12 8 2988
97 4 2 1349
98 2 8 955
99 8 4 2165
100 2 5 938
101 1 0 765
102 14 1 3275
103 14 11 3349
104 2 11 1024
105 14 2 3423
106 12 12 2844
107 8 12 2219
108 14 5 3308
109 3 14 1136
110 1 9 710
111 6 13 1831
112 8 13 2296
113 11 5 2729
114 4 1 1364
115 14 10 3324
116 12 5 2925
117 8 5 2121
118 10 3 2519
119 1 7 698
120 2 4 959
121 6 9 1973
122 7 5 2240
123 2 7 936
124 0 6 753
125 5 10 1557
126 2 3 948
127 2 14 931
128 8 6 2132
129 2 10 936
130 4 0 618
131 13 4 3136
132 5 6 1530
133 5 8 1546
134 7 14 1920
135 8 10 2110
136 12 9 3021
137 1 11 738
138 13 11 3121
139 1 1 730
140 0 1 981
141 3 8 1271
142 10 8 2554
143 14 14 3267
144 0 9 600
145 4 9 1328
146 9 12 3707
147 6 12 1800
148 0 5 684
149 7 2 2180
150 9 10 7168
151 8 14 2163
152 7 7 3732
153 8 0 633
154 5 14 1645
155 8 8 2129
156 12 7 2898
157 0 10 573
158 7 3 2255
159 14 9 3272
160 2 9 924
161 9 4 2331
162 10 5 2513
163 10 13 2514
164 11 8 2733
165 14 12 3326
166 0 8 618
167 6 0 609
168 8 9 2157
169 12 1 3013
170 12 11 2916
171 12 3 2959
172 9 0 623
173 12 13 3064
174 0 13 536
175 5 13 1558
176 6 10 1835
177 11 13 2784
178 13 9 3071
179 0 7 599
180 2 0 617
181 9 5 2303
182 8 3 2152
183 12 2 2994
184 0 12 542
185 7 13 1923
186 4 3 1350
187 13 7 3177
188 10 10 2533
189 6 2 1748
190 3 7 1209
191 13 2 3097
192 11 2 2731
193 5 2 1562
194 14 4 3273
195 7 1 2088
196 9 7 2333
197 5 12 1556
198 3 5 1177
199 12 14 2888
200 3 4 1153
201 1 10 710
202 11 0 614
203 10 12 2505
204 11 14 2827
205 1 8 710
206 14 6 3282
207 7 12 1938
208 2 2 943
209 10 9 2578
210 0 11 537
211 2 1 926
212 4 14 1367
213 14 0 614
214 1 12 716
215 2 6 935
216 13 10 3105
217 10 6 2524
218 4 12 1393
219 8 7 2156
220 12 6 2897
221 5 11 1528
222 13 13 3129
223 5 3 1536
224 3 3 1179
225 3 9 1173
226 13 12 3094
227 13 1 3097
+65
View File
@@ -0,0 +1,65 @@
use osui::prelude::*;
use std::collections::HashMap;
pub fn main() {
let engine = Arc::new(Benchmark::new(Console::new()));
let mut benchmark_result: HashMap<(usize, usize), BenchmarkResult> = HashMap::new();
for i in 0..15 {
for n in 0..15 {
let res = {
let mut results = Vec::with_capacity(6);
for _ in 0..6 {
results.push(
engine
.run(App {
n: n * 72,
i: i * 72,
})
.expect("Failed to run engine"),
);
}
results.sort_by_key(|r| r.total_render);
results[3].clone()
};
benchmark_result.insert((i, n as usize), res);
}
}
let max = benchmark_result
.values()
.map(|b| b.total_render)
.max()
.unwrap_or(0);
println!("Iterx72,Nestingx72,Time µs\n14,14,{}", { max + 500 });
for ((i, n), bench) in benchmark_result.iter() {
println!("{i},{n},{}", bench.total_render);
}
}
#[component]
fn App(cx: &Arc<Context>, n: usize, i: usize) -> View {
let n = n.clone();
let i = i.clone();
if n == 0 {
rsx! {
"Hello, world!"
}
.view(&cx)
} else {
rsx! {
for _ in (0..i) {
App { n: n - 1, i: 0 }
}
}
.view(&cx)
}
}
+31
View File
@@ -0,0 +1,31 @@
use osui::prelude::*;
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run engine");
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
// redraw is important because the effects won't be applied
impl size_auto, center, redraw
FlexRow {
impl size_auto, redraw
Card { content: "One".to_string() }
impl size_auto, redraw
Card { content: "Two".to_string() }
}
}
.view(&cx)
}
#[component]
fn Card(cx: &Arc<Context>, content: String) -> View {
let bar = "-".repeat(content.len());
rsx! {
"{bar}\n{content}\n{bar}"
}
.view(&cx)
}
+19
View File
@@ -0,0 +1,19 @@
use osui::prelude::*;
pub fn main() {
let engine = Benchmark::new(Console::new());
let benchmark_result = engine.run(App {}).expect("Failed to run engine");
println!("Avg: {} μs", benchmark_result.average);
println!("Min: {} μs", benchmark_result.min);
println!("Max: {} μs", benchmark_result.max);
println!("Tot: {} μs", benchmark_result.total);
println!("Tot Render: {} μs", benchmark_result.total_render);
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
"Hello, world!"
}
.view(&cx)
}
+23
View File
@@ -0,0 +1,23 @@
use osui::prelude::*;
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run engine");
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
MyComponent { "------ example" }
}
.view(&cx)
}
#[component]
fn MyComponent(cx: &Arc<Context>, children: &Rsx) -> View {
rsx! {
@{children}
"Simple"
}
.view(&cx)
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "osui-macros"
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
description = "Macros for osui-rs"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
+237
View File
@@ -0,0 +1,237 @@
//! # RSX Emission
//!
//! Converts parsed RSX AST into Rust code that constructs RSX objects.
use crate::parse::*;
use proc_macro2::TokenStream;
use quote::quote;
/// Emits code for the root RSX
pub fn emit_rsx(root: RsxRoot) -> TokenStream {
emit_rsx_vec(&root.nodes)
}
/// Emits code for a vector of RSX nodes
pub fn emit_rsx_vec(nodes: &Vec<RsxNode>) -> TokenStream {
let nodes = nodes.iter().map(emit_node_scope);
quote! {{
let mut r = osui::frontend::Rsx::new();
#(#nodes)*
r
}}
}
/// Emits variable bindings for dependencies
fn emit_deps(deps: &[Dep]) -> TokenStream {
deps.iter()
.map(|d| {
let ident = &d.ident;
if let Some(pat) = &d.pat {
quote!( let #pat = #ident.clone(); )
} else {
quote!( let #ident = #ident.clone(); )
}
})
.collect()
}
/// Emits a Vec of dependencies as HookDependency trait objects
fn emit_deps_vec(deps: &[Dep]) -> TokenStream {
let deps = deps.iter().filter(|d| d.is_dep).map(|d| {
let ident = &d.ident;
quote! {
std::sync::Arc::new(#ident) as std::sync::Arc<dyn HookDependency>
}
});
quote! {
vec![ #(#deps),* ]
}
}
/// Emits a Vec of plugins
fn emit_plugins(deps: &[ViewPlugin]) -> TokenStream {
deps.iter()
.map(|d| {
let path = &d.path;
if let Some(args) = &d.args {
quote!( #path(ctx, &view #(,#args)*); )
} else {
quote!( #path(ctx, &view); )
}
})
.collect()
}
/// Emits code for a single node within a scope
fn emit_node_scope(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Text { deps, .. } => {
let emit = emit_node(node);
let deps_emit = emit_deps(deps);
quote! {
#deps_emit
r.static_scope(move |scope| {#emit});
}
}
RsxNode::Component { .. } => {
let emit = emit_node(node);
quote! {
r.static_scope(move |scope| {#emit});
}
}
RsxNode::Mount(m) => quote! {
#m.mount();
},
RsxNode::If {
deps,
cond,
children,
} => {
let deps_emit = emit_deps(deps);
let deps_vec_emit = emit_deps_vec(deps);
let kids = children.iter().map(emit_node);
quote! {
{
#deps_emit
r.dynamic_scope(move |scope| {
if #cond {
if scope.children.lock().unwrap().is_empty() {
#(#kids)*
}
} else {
scope.children.lock().unwrap().clear();
}
}, #deps_vec_emit);
}
}
}
RsxNode::For {
deps,
pat,
expr,
children,
} => {
let deps_emit = emit_deps(deps);
let deps_vec_emit = emit_deps_vec(deps);
let kids = children.iter().map(emit_node);
quote! {
{
#deps_emit
#[allow(unused_parens)]
r.dynamic_scope(move |scope| {
scope.children.lock().unwrap().clear();
for #pat in #expr {
#(#kids)*
}
}, #deps_vec_emit);
}
}
}
RsxNode::Expr(expr) => quote! {
r.child(#expr);
},
}
}
fn emit_node(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Text {
text,
deps,
plugins,
} => {
let deps_emit = emit_deps(deps);
if plugins.len() == 0 {
quote! {
scope.view(Arc::new({
#deps_emit
move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text))
}));
}
} else {
let plugins_emit = emit_plugins(plugins);
quote! {
scope.view_wrapped(Arc::new({
#deps_emit
move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text))
}), std::sync::Arc::new(|ctx, view| {
let area = ctx.allocate(ctx.area.x, ctx.area.y, ctx.area.width, ctx.area.height);
ctx.draw_view(area, view.clone());
#plugins_emit
}));
}
}
}
RsxNode::Component {
plugins,
path,
props,
children,
} => {
let prop_inits = props.iter().map(|p| {
let name = &p.name;
let value = &p.value;
quote! { #name: #value }
});
let emit_children = emit_rsx_vec(children);
let component_expr = if children.len() > 0 {
quote! {
#path {
#(#prop_inits,)*
children: #emit_children
}
}
} else {
quote! {
#path {
#(#prop_inits,)*
}
}
};
if plugins.len() == 0 {
quote! {
scope.child(#component_expr, None);
}
} else {
let plugins_emit = emit_plugins(plugins);
quote! {
scope.child(#component_expr, Some(std::sync::Arc::new(|ctx, view| {
let area = ctx.allocate(ctx.area.x, ctx.area.y, ctx.area.width, ctx.area.height);
ctx.draw_view(area, view.clone());
#plugins_emit
})));
}
}
}
RsxNode::Mount(m) => quote! {
#m.mount();
},
RsxNode::If { .. } => panic!("Invalid if statement"),
RsxNode::For { .. } => panic!("Invalid for loop"),
RsxNode::Expr(expr) => quote! {
r.child(#expr);
},
}
}
+149
View File
@@ -0,0 +1,149 @@
//! # OSUI Macros
//!
//! Procedural macros for OSUI that provide ergonomic syntax for defining components.
//!
//! ## Features
//!
//! - `#[component]` - Transforms a function into a reusable component with props
//! - `rsx!` - Creates RSX (React-like Syntax) for component hierarchies
use proc_macro::TokenStream;
use quote::quote;
use syn::{FnArg, ItemFn, Pat, ReturnType, Type, parse_macro_input};
mod emit;
mod parse;
/// RSX (React-like Syntax) macro for building component hierarchies
///
/// # Example
///
/// ```rust,ignore
/// rsx! {
/// Component {
/// prop: value,
/// }
/// }
/// ```
#[proc_macro]
pub fn rsx(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as parse::RsxRoot);
emit::emit_rsx(ast).into()
}
/// Component attribute macro for defining reusable components
///
/// Transforms a function into a component with automatic prop handling.
/// The first parameter must be `cx: &Arc<Context>`.
/// Remaining parameters become component props.
///
/// # Example
///
/// ```rust,ignore
/// #[component]
/// pub fn Counter(cx: &Arc<Context>, initial: &i32) -> View {
/// let count = use_state(*initial);
///
/// Arc::new(move |ctx| {
/// ctx.draw_text(Point { x: 0, y: 0 }, &format!("Count: {}", count.get_dl()));
/// })
/// }
/// ```
#[proc_macro_attribute]
pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let name = &input.sig.ident;
let vis = &input.vis;
let body = &input.block;
let return_ty = match &input.sig.output {
ReturnType::Type(_, ty) => ty,
_ => {
return syn::Error::new_spanned(&input.sig, "component must return View")
.to_compile_error()
.into();
}
};
let mut inputs = input.sig.inputs.iter();
// ---- First param must be cx ----
let cx = match inputs.next() {
Some(FnArg::Typed(pat)) => pat,
_ => {
return syn::Error::new_spanned(&input.sig, "first argument must be cx: &Arc<Context>")
.to_compile_error()
.into();
}
};
let cx_ident = match &*cx.pat {
Pat::Ident(id) => &id.ident,
_ => unreachable!(),
};
let cx_ty = &cx.ty;
// ---- Remaining params are props ----
let mut struct_fields = Vec::new();
let mut render_params = Vec::new();
let mut call_args = Vec::new();
for arg in inputs {
let FnArg::Typed(pat) = arg else { continue };
let ident = match &*pat.pat {
Pat::Ident(id) => &id.ident,
_ => {
return syn::Error::new_spanned(pat, "unsupported prop pattern")
.to_compile_error()
.into();
}
};
// Strip leading &
let owned_ty = match &*pat.ty {
Type::Reference(r) => &r.elem,
ty => ty,
};
struct_fields.push(quote! {
pub #ident: #owned_ty
});
render_params.push(quote! {
#ident: &#owned_ty
});
call_args.push(quote! {
&self.#ident
});
}
let expanded = quote! {
#vis struct #name {
#(#struct_fields,)*
}
impl #name {
pub fn component(
#cx_ident: #cx_ty,
#(#render_params,)*
) -> #return_ty {
#body
}
}
impl ComponentImpl for #name {
fn call(&self, cx: &std::sync::Arc<Context>) -> #return_ty {
Self::component(
cx,
#(#call_args,)*
)
}
}
};
expanded.into()
}
+355
View File
@@ -0,0 +1,355 @@
//! # RSX Parser
//!
//! Parses RSX syntax into an AST that can be emitted as Rust code.
use std::collections::HashSet;
use proc_macro2::Span;
use syn::parse::discouraged::Speculative;
use syn::punctuated::Punctuated;
use syn::{
Expr, Ident, LitStr, Pat, Path, Result, Token,
parse::{Parse, ParseStream},
token::Brace,
};
use syn::{braced, parenthesized};
/// Root of an RSX expression
pub struct RsxRoot {
/// Top-level nodes in the RSX
pub nodes: Vec<RsxNode>,
}
impl Parse for RsxRoot {
fn parse(input: ParseStream) -> Result<Self> {
let mut nodes = Vec::new();
while !input.is_empty() {
nodes.push(input.parse()?);
}
Ok(Self { nodes })
}
}
/// A single component prop: `name: value`
pub struct RsxProp {
/// Property name
pub name: Ident,
/// Property value expression
pub value: Expr,
}
/// AST node representing different RSX constructs
pub enum RsxNode {
/// String literal: `"text"`
Text {
plugins: Vec<ViewPlugin>,
text: LitStr,
deps: Vec<Dep>,
},
/// Expression node: `{expr}`
Expr(Expr),
/// Component instantiation: `Component { prop: value, ... }`
Component {
/// View plugins
plugins: Vec<ViewPlugin>,
/// Component path (e.g., `my_module::MyComponent`)
path: Path,
/// Component properties
props: Vec<RsxProp>,
/// Child nodes
children: Vec<RsxNode>,
},
/// Mount lifecycle: `@mount`
Mount(Ident),
/// Conditional rendering: `@if condition { ... }`
If {
/// Dependencies to track for reactivity
deps: Vec<Dep>,
/// Condition expression
cond: Expr,
/// Child nodes to render if true
children: Vec<RsxNode>,
},
/// Loop rendering: `@for pattern in expr { ... }`
For {
/// Dependencies to track for reactivity
deps: Vec<Dep>,
/// Loop pattern (e.g., `(key, value)`)
pat: Pat,
/// Iterable expression
expr: Expr,
/// Child nodes to render for each iteration
children: Vec<RsxNode>,
},
}
pub struct Dep {
pub ident: Ident,
pub pat: Option<Pat>,
pub is_dep: bool,
}
pub struct ViewPlugin {
pub path: Path,
pub args: Option<Vec<Expr>>,
}
fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
let mut deps = Vec::new();
if input.peek(Token![%]) {
input.parse::<Token![%]>()?;
loop {
let is_ref = if input.peek(Token![ref]) {
input.parse::<Token![ref]>()?;
true
} else {
false
};
let ident: Ident = input.parse()?;
let pat = if input.peek(Token![as]) {
input.parse::<Token![as]>()?;
// Patterns in syn 2.0 are parsed via `Pat::parse_multi`
// instead of implementing the `Parse` trait directly.
Some(Pat::parse_multi(input)?)
} else {
None
};
deps.push(Dep {
ident,
pat,
is_dep: !is_ref,
});
if !input.peek(Token![,]) {
break;
}
input.parse::<Token![,]>()?;
}
}
Ok(deps)
}
fn parse_plugins(input: ParseStream) -> Result<Vec<ViewPlugin>> {
let mut plugins = Vec::new();
if input.peek(Token![impl]) {
input.parse::<Token![impl]>()?;
loop {
let path: Path = input.parse()?;
let args = if input.peek(syn::token::Paren) {
let content;
parenthesized!(content in input);
let exprs = Punctuated::<Expr, Token![,]>::parse_terminated(&content)?;
Some(exprs.into_iter().collect())
} else {
None
};
plugins.push(ViewPlugin { path, args });
if !input.peek(Token![,]) {
break;
}
input.parse::<Token![,]>()?;
}
}
Ok(plugins)
}
impl Parse for RsxNode {
fn parse(input: ParseStream) -> Result<Self> {
// @{ $expr }
if input.peek(Token![@]) {
input.parse::<Token![@]>()?;
let content;
braced!(content in input);
let expr: Expr = content.parse()?;
return Ok(RsxNode::Expr(expr));
}
// !$ident
if input.peek(Token![!]) {
input.parse::<Token![!]>()?;
let mount: Ident = input.parse()?;
return Ok(RsxNode::Mount(mount));
}
let deps = parse_deps(input)?;
// %$dep if $expr { $rsx }
if input.peek(Token![if]) {
input.parse::<Token![if]>()?;
let cond: Expr = input.parse()?;
let content;
braced!(content in input);
let children = parse_children(&content)?;
return Ok(RsxNode::If {
deps,
cond,
children,
});
}
// %$dep for $pat in $expr { $rsx }
if input.peek(Token![for]) {
input.parse::<Token![for]>()?;
let pat: Pat = Pat::parse_multi(input)?;
input.parse::<Token![in]>()?;
let expr: Expr = input.parse()?;
let content;
braced!(content in input);
let children = parse_children(&content)?;
return Ok(RsxNode::For {
deps,
pat,
expr,
children,
});
}
let plugins = parse_plugins(input)?;
// "%$dep for $pat in $expr { $rsx }"
if input.peek(LitStr) {
let text = input.parse()?;
let mut deps = deps;
deps.append(
&mut extract_vars_from_lit(&text)
.into_iter()
.map(|ident| Dep {
ident,
pat: None,
is_dep: false,
})
.collect(),
);
return Ok(RsxNode::Text {
text,
deps,
plugins,
});
}
parse_component_invocation(input, plugins)
}
}
/// Parses Path or Path { ... } into RsxNode::Component.
fn parse_component_invocation(input: ParseStream, plugins: Vec<ViewPlugin>) -> Result<RsxNode> {
let path: Path = input.parse()?;
if !input.peek(Brace) {
return Ok(RsxNode::Component {
plugins,
path,
props: Vec::new(),
children: Vec::new(),
});
}
let content;
braced!(content in input);
let (props, children) = parse_props_and_children(&content)?;
Ok(RsxNode::Component {
plugins,
path,
props,
children,
})
}
fn parse_props_and_children(input: ParseStream) -> Result<(Vec<RsxProp>, Vec<RsxNode>)> {
Ok((parse_props(input)?, RsxRoot::parse(input)?.nodes))
}
fn parse_props(input: ParseStream) -> Result<Vec<RsxProp>> {
let mut props = Vec::new();
while !input.is_empty() {
let fork = input.fork();
let path: Path = match fork.parse() {
Ok(p) => p,
Err(_) => break,
};
if fork.peek(Token![:]) {
let name = path
.get_ident()
.cloned()
.ok_or_else(|| fork.error("prop name must be a single identifier"))?;
fork.parse::<Token![:]>()?;
let value: Expr = fork.parse()?;
input.advance_to(&fork);
props.push(RsxProp { name, value });
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
} else {
// ❌ do not commit → nothing consumed
break;
}
}
Ok(props)
}
fn parse_children(input: ParseStream) -> Result<Vec<RsxNode>> {
let mut nodes = Vec::new();
while !input.is_empty() {
nodes.push(input.parse()?);
}
Ok(nodes)
}
fn extract_vars_from_lit(lit: &LitStr) -> HashSet<Ident> {
let s = lit.value();
let mut vars = HashSet::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' {
// skip escaped {{
if chars.peek() == Some(&'{') {
chars.next();
continue;
}
let mut name = String::new();
while let Some(&ch) = chars.peek() {
chars.next();
if ch == '}' {
break;
}
name.push(ch);
}
if !name.is_empty() {
vars.insert(Ident::new(
&name.split_once(':').unwrap_or((&name, &name)).0,
Span::call_site(),
));
}
}
}
vars
}
+51
View File
@@ -0,0 +1,51 @@
use crate::component_prelude::*;
#[component]
pub fn FlexRow(cx: &Arc<Context>, children: Rsx) -> View {
children.generate_children(&cx);
Arc::new({
let cx = cx.clone();
move |ctx| {
for (child, view_wrapper) in cx.get_children() {
let view = child.get_view();
if let Some(view_wrapper) = view_wrapper {
let mut ctx2 = ctx.clone();
view_wrapper(&mut ctx2, view);
ctx.drawing.append(&mut ctx2.drawing);
ctx.area.y += ctx2.allocated.height;
} else {
ctx.draw_view(ctx.area.clone(), view);
}
}
}
})
}
#[component]
pub fn FlexColumn(cx: &Arc<Context>, children: Rsx) -> View {
children.generate_children(&cx);
Arc::new({
let cx = cx.clone();
move |ctx| {
for (child, view_wrapper) in cx.get_children() {
let view = child.get_view();
if let Some(view_wrapper) = view_wrapper {
let mut ctx2 = ctx.clone();
view_wrapper(&mut ctx2, view);
ctx.drawing.append(&mut ctx2.drawing);
ctx.area.x += ctx2.allocated.width;
} else {
ctx.draw_view(ctx.area.clone(), view);
}
}
}
})
}
+3
View File
@@ -0,0 +1,3 @@
mod flex;
pub use flex::*;
+259
View File
@@ -0,0 +1,259 @@
//! # Context Module
//!
//! Provides the Context type which is central to component state management.
//! Context holds component state, manages event handlers, and coordinates
//! rendering and updates across the component tree.
use std::{
any::{Any, TypeId},
collections::HashMap,
sync::{Arc, Mutex},
};
use access_cell::AccessCell;
use crate::{
component::EventHandler,
engine::{Command, CommandExecutor},
hooks::{use_effect, HookDependency},
render::DrawContext,
View, ViewWrapper,
};
use super::{scope::Scope, Component, ComponentImpl};
/// Context represents the runtime state and behavior of a component
///
/// Each component instance has a Context that holds:
/// - The component implementation
/// - The current view (render result)
/// - Event handlers for responding to events
/// - Child scopes for managing child components
pub struct Context {
/// The component implementation
component: AccessCell<Component>,
/// The current rendered view
view: AccessCell<View>,
/// Event handlers grouped by event type
event_handlers: AccessCell<HashMap<TypeId, Vec<EventHandler>>>,
/// Child scopes (component hierarchies)
pub(crate) scopes: Mutex<Vec<Arc<Scope>>>,
/// Command executor for this context's command handling
executor: Arc<dyn CommandExecutor>,
}
impl Context {
/// Creates a new context for the given component
pub fn new<F: ComponentImpl + 'static>(
component: F,
executor: Arc<dyn CommandExecutor>,
) -> Arc<Self> {
Arc::new(Self {
component: AccessCell::new(Arc::new(component)),
view: AccessCell::new(Arc::new(|_| {})),
event_handlers: AccessCell::new(HashMap::new()),
scopes: Mutex::new(Vec::new()),
executor,
})
}
/// Refreshes the component by re-rendering it
///
/// Clears event handlers and calls the component to produce a new view.
pub fn refresh(self: &Arc<Self>) {
self.event_handlers
.access(|event_handlers| event_handlers.clear());
self.component.access({
let s = self.clone();
move |component| {
let component = component.clone();
s.view.access({
let s = s.clone();
move |view| *view = component.call(&s)
})
}
});
}
/// Synchronously refreshes the component
///
/// Blocks until the component has finished rendering.
pub fn refresh_sync(self: &Arc<Self>) {
let (tx, rx) = std::sync::mpsc::channel::<()>();
self.event_handlers
.access(|event_handlers| event_handlers.clear());
self.component.access({
let s = self.clone();
move |component| {
let component = component.clone();
s.view.access({
let s = s.clone();
let tx = tx.clone();
move |view| {
*view = component.call(&s);
let _ = tx.send(()); // signal completion
}
});
}
});
// BLOCK until view closure finishes
let _ = rx.recv();
}
/// Gets the current view
pub fn get_view(self: &Arc<Self>) -> View {
self.view.access_ref().clone()
}
/// Registers an event handler for events of type T
///
/// When an event of type T is emitted, the handler is called with
/// the context and a reference to the event.
pub fn on_event<T: Any + 'static, F: Fn(&Arc<Self>, &T) + Send + Sync + 'static>(
self: &Arc<Self>,
handler: F,
) {
let new_handler: EventHandler =
Arc::new(Mutex::new(move |ctx: &Arc<Context>, event: &dyn Any| {
if let Some(e) = event.downcast_ref::<T>() {
(handler)(ctx, e);
}
}));
self.event_handlers.access(|event_handlers| {
event_handlers
.entry(TypeId::of::<T>())
.or_insert_with(Vec::new)
.push(new_handler);
});
}
/// Emits an event to this component and all descendants
///
/// Calls all registered handlers for this event type,
/// then propagates the event to child components.
pub fn emit_event<E: Send + Sync + Any + 'static>(self: &Arc<Self>, event: E) {
let event = Arc::new(event);
let handlers_to_call: Vec<EventHandler> = {
let guard = self.event_handlers.access_ref();
guard.get(&TypeId::of::<E>()).cloned().unwrap_or_default()
};
for h in &handlers_to_call {
(h.lock().unwrap())(self, event.as_ref());
}
for scope in self.scopes.lock().unwrap().iter() {
for (child, _) in scope.children.lock().unwrap().iter() {
child.emit_event(event.clone());
}
}
}
/// Emits an event to this component in a spawned thread
///
/// Similar to emit_event but handlers are called in spawned threads
/// for concurrent execution.
pub fn emit_event_threaded<E: Any + Send + Sync + Clone + 'static>(
self: &Arc<Self>,
event: &E,
) {
let handlers_to_call: Vec<EventHandler> = {
let guard = self.event_handlers.access_ref();
guard.get(&TypeId::of::<E>()).cloned().unwrap_or_default()
};
for h in handlers_to_call {
let event = event.clone();
let s = self.clone();
std::thread::spawn(move || {
(h.lock().unwrap())(&s, &event);
});
}
for scope in self.scopes.lock().unwrap().iter() {
for (child, _) in scope.children.lock().unwrap().iter() {
child.emit_event_threaded(event);
}
}
}
/// Creates a new child scope
pub fn scope(self: &Arc<Self>) -> Arc<Scope> {
let scope = Scope::new(self.executor.clone());
self.scopes.lock().unwrap().push(scope.clone());
scope
}
/// Creates a dynamic child scope that re-renders when dependencies change
pub fn dyn_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(
self: &Arc<Self>,
drawer: F,
dependencies: &[&dyn HookDependency],
) -> Arc<Scope> {
let scope = Scope::new(self.executor.clone());
self.scopes.lock().unwrap().push(scope.clone());
drawer(&scope);
use_effect(
{
let scope = scope.clone();
move || {
drawer(&scope);
}
},
dependencies,
);
scope
}
/// Adds a pre-constructed scope as a child
pub fn add_scope(self: &Arc<Self>, scope: Arc<Scope>) {
self.scopes.lock().unwrap().push(scope);
}
/// Gets the children
pub fn get_children(self: &Arc<Self>) -> Vec<(Arc<Context>, Option<ViewWrapper>)> {
self.scopes
.lock()
.unwrap()
.iter()
.flat_map(|s| s.children.lock().unwrap().clone())
.collect::<Vec<_>>()
}
/// Renders all child components to the draw context
pub fn draw_children(self: &Arc<Self>, ctx: &mut DrawContext) {
for scope in self.scopes.lock().unwrap().iter() {
for (child, view_wrapper) in scope.children.lock().unwrap().iter() {
let view = child.get_view();
if let Some(view_wrapper) = view_wrapper {
view_wrapper(ctx, view)
} else {
ctx.draw_view(ctx.area.clone(), view);
}
}
}
}
/// Gets the command executor for this context
pub fn get_executor(self: &Arc<Self>) -> Arc<dyn CommandExecutor> {
self.executor.clone()
}
/// Executes a command
pub fn execute<T: Command + 'static>(self: &Arc<Self>, command: T) -> crate::Result<()> {
self.executor
.execute_command(&(Arc::new(command) as Arc<dyn Command>))
}
/// Stops the application
pub fn stop(self: &Arc<Self>) -> crate::Result<()> {
self.execute(crate::engine::commands::Stop)
}
}
+45
View File
@@ -0,0 +1,45 @@
//! # Component Module
//!
//! Provides the component system that forms the foundation of OSUI.
//! Components are reusable units of UI that can manage their own state
//! and respond to events.
pub mod components;
pub mod context;
pub mod scope;
use std::{
any::Any,
sync::{Arc, Mutex},
};
use crate::View;
use context::Context;
/// A Component is an implementor of the ComponentImpl trait, wrapped in Arc
pub type Component = Arc<dyn ComponentImpl>;
/// An event handler function stored in a mutex for thread-safe mutation
pub type EventHandler = Arc<Mutex<dyn FnMut(&Arc<Context>, &dyn Any) + Send + Sync>>;
/// Trait implemented by components to render themselves
pub trait ComponentImpl: Send + Sync {
/// Renders the component within the given context, returning a View
fn call(&self, cx: &Arc<Context>) -> View;
}
impl ComponentImpl for View {
fn call(&self, _: &Arc<Context>) -> View {
self.clone()
}
}
impl<F> ComponentImpl for F
where
F: Fn(&Arc<Context>) -> View + Send + Sync,
{
fn call(&self, ctx: &Arc<Context>) -> View {
self(ctx)
}
}
+65
View File
@@ -0,0 +1,65 @@
//! # Scope Module
//!
//! Provides the Scope type for managing component hierarchies.
//! Scopes group child components and manage their lifecycle.
use std::sync::{Arc, Mutex};
use crate::{engine::CommandExecutor, View, ViewWrapper};
use super::{context::Context, ComponentImpl};
/// A scope groups child components and manages their rendering
///
/// Scopes form the hierarchical structure of a component tree.
/// Each scope contains references to its child components and their
/// optional view wrappers (for layout/styling).
pub struct Scope {
/// Child components with optional view wrappers
pub children: Mutex<Vec<(Arc<Context>, Option<ViewWrapper>)>>,
/// Command executor for this scope's children
executor: Arc<dyn CommandExecutor>,
}
impl Scope {
/// Creates a new scope with the given command executor
pub fn new(executor: Arc<dyn CommandExecutor>) -> Arc<Self> {
Arc::new(Self {
children: Mutex::new(Vec::new()),
executor,
})
}
/// Adds a child component to this scope
///
/// The view_wrapper is optional and can be used for layout or styling.
pub fn child<F: ComponentImpl + 'static>(
self: &Arc<Self>,
child: F,
view_wrapper: Option<ViewWrapper>,
) {
let ctx = Context::new(child, self.executor.clone());
ctx.refresh();
self.children.lock().unwrap().push((ctx, view_wrapper));
}
/// Adds a view directly to this scope
pub fn view(self: &Arc<Self>, view: View) {
let ctx = Context::new(view, self.executor.clone());
ctx.refresh();
self.children.lock().unwrap().push((ctx, None));
}
/// Adds a view directly to this scope
pub fn view_wrapped(self: &Arc<Self>, view: View, view_wrapper: ViewWrapper) {
let ctx = Context::new(view, self.executor.clone());
ctx.refresh();
self.children.lock().unwrap().push((ctx, Some(view_wrapper)));
}
}
-62
View File
@@ -1,62 +0,0 @@
use std::sync::Arc;
use osui::prelude::*;
pub fn app(screen: Arc<Screen>) -> Rsx {
let count = use_state(0);
std::thread::spawn({
let count = count.clone();
move || loop {
**count.get() += 1;
std::thread::sleep(std::time::Duration::from_secs(1));
}
});
rsx! {
@Handler::new({
let screen = screen.clone();
move |_, e: &crossterm::event::Event| {
if let crossterm::event::Event::Key(crossterm::event::KeyEvent { code, .. }) = e {
if *code == crossterm::event::KeyCode::Char('q') {
screen.close();
}
}
}});
Paginator {
FlexRow {
Heading, smooth: false, { "OSUI" }
"Welcome to the OSUI demo!"
"Press tab to switch to the next page or shift+tab to the previous page"
}
FlexCol, gap: 3, {
@Transform::new().padding(2, 2);
@Style { foreground: None, background: Background::RoundedOutline(0x00ff00) };
Div {
"This is text inside a div"
}
@Transform::new().padding(2, 2);
@Style { foreground: None, background: Background::Outline(0x00ff00) };
Div {
"This is text inside a div with square outlines"
}
// TODO: div with width full
}
FlexCol, gap: 2, {
@transform!{ y: Center };
static Div { // static only affects the element, not children
%count
"This will increment every second: {count}"
}
@Transform::new().padding(1, 1).dimensions(40, 1);
@Style { foreground: Some(0xffffff), background: Background::RoundedOutline(0xff0000) };
Input { }
}
}
}
}
-75
View File
@@ -1,75 +0,0 @@
use std::sync::Arc;
use crate::{
widget::{Element, Widget},
NoRender, NoRenderRoot,
};
pub struct Div {
children: Vec<Arc<Widget>>,
size: (u16, u16),
}
impl Div {
pub fn new() -> Self {
Div {
children: Vec::new(),
size: (0, 0),
}
}
}
impl Element for Div {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let (width, height) = scope.get_size_or(self.size.0, self.size.1);
scope.use_area(width, height);
}
fn after_render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let mut transform = scope.get_transform().clone();
let (w, h) = scope.get_parent_size();
scope.set_parent_size(transform.width, transform.height);
for elem in &self.children {
if elem.get::<NoRender>().is_some() {
continue;
}
scope.clear();
if let Some(style) = elem.get() {
scope.set_style(style);
}
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
elem.get_elem().render(scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
let t = scope.get_transform_mut();
transform.width = transform.width.max(t.x + t.width + (t.px * 2));
transform.height = transform.height.max(t.y + t.height + (t.py * 2));
t.x += transform.x + transform.px;
t.y += transform.y + transform.py;
scope.draw();
elem.get_elem().after_render(scope);
}
scope.set_parent_size(w, h);
self.size = (transform.width, transform.height);
}
fn draw_child(&mut self, element: &Arc<Widget>) {
self.children.push(element.clone());
element.inject(|w| w.component(NoRenderRoot));
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
-158
View File
@@ -1,158 +0,0 @@
use std::sync::Arc;
use crate::{
widget::{Element, Widget},
NoRender, NoRenderRoot,
};
pub struct FlexRow {
pub gap: u16,
children: Vec<Arc<Widget>>,
size: (u16, u16),
}
pub struct FlexCol {
pub gap: u16,
children: Vec<Arc<Widget>>,
size: (u16, u16),
}
impl FlexRow {
pub fn new() -> Self {
Self {
children: Vec::new(),
size: (0, 0),
gap: 0,
}
}
}
impl FlexCol {
pub fn new() -> Self {
Self {
children: Vec::new(),
size: (0, 0),
gap: 0,
}
}
}
impl Element for FlexRow {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let (width, height) = scope.get_size_or(self.size.0, self.size.1);
scope.use_area(width, height);
}
fn after_render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let mut transform = scope.get_transform().clone();
let (w, h) = scope.get_parent_size();
scope.set_parent_size(transform.width, transform.height);
let mut v = 0;
for elem in &self.children {
if elem.get::<NoRender>().is_some() {
continue;
}
scope.clear();
if let Some(style) = elem.get() {
scope.set_style(style);
}
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
elem.get_elem().render(scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
let t = scope.get_transform_mut();
transform.width = transform.width.max(t.width);
transform.height = transform.height.max(v + t.height);
t.x += transform.x;
t.y += transform.y + v;
v += t.height + self.gap + (t.py * 2);
t.px += transform.px;
t.py += transform.py;
scope.draw();
elem.get_elem().after_render(scope);
}
scope.set_parent_size(w, h);
self.size = (transform.width, transform.height);
}
fn draw_child(&mut self, element: &Arc<Widget>) {
self.children.push(element.clone());
element.inject(|w| w.component(NoRenderRoot));
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl Element for FlexCol {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let (width, height) = scope.get_size_or(self.size.0, self.size.1);
scope.use_area(width, height);
}
fn after_render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let mut transform = scope.get_transform().clone();
let (w, h) = scope.get_parent_size();
scope.set_parent_size(transform.width, transform.height);
let mut v = 0;
for elem in &self.children {
if elem.get::<NoRender>().is_some() {
continue;
}
scope.clear();
if let Some(style) = elem.get() {
scope.set_style(style);
}
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
elem.get_elem().render(scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
let t = scope.get_transform_mut();
transform.width = transform.width.max(v + t.width + (t.px * 2));
transform.height = transform.height.max(t.height + (t.py * 2));
t.x += transform.x + v;
t.y += transform.y;
v += t.width + self.gap + (t.px * 2);
t.px += transform.px;
t.py += transform.py;
scope.draw();
elem.get_elem().after_render(scope);
}
scope.set_parent_size(w, h);
self.size = (transform.width, transform.height);
}
fn draw_child(&mut self, element: &Arc<Widget>) {
self.children.push(element.clone());
element.inject(|w| w.component(NoRenderRoot));
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
-63
View File
@@ -1,63 +0,0 @@
use std::sync::Arc;
use figlet_rs::FIGfont;
use crate::{
widget::{Element, Widget},
NoRender, NoRenderRoot,
};
pub struct Heading {
pub font: FIGfont,
pub smooth: bool,
children: Vec<Arc<Widget>>,
}
impl Heading {
pub fn new() -> Heading {
Heading {
font: FIGfont::standard().unwrap(),
smooth: false,
children: Vec::new(),
}
}
}
impl Element for Heading {
fn render(&mut self, scope: &mut crate::prelude::RenderScope) {
let mut s = String::new();
for element in &self.children {
if element.get::<NoRender>().is_some() {
continue;
}
if let Some(e) = element.get_elem().as_any().downcast_ref::<String>() {
s += e;
}
}
if let Some(t) = self.font.convert(&s) {
scope.draw_text(
0,
0,
&if self.smooth {
format!("{t}").replace("-", "─").replace("|", "│")
} else {
format!("{t}")
},
);
}
}
fn draw_child(&mut self, element: &Arc<Widget>) {
element.inject(|w| w.component(NoRenderRoot));
self.children.push(element.clone());
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
-80
View File
@@ -1,80 +0,0 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::{
state::{use_state, State},
widget::Element,
};
pub struct Input {
pub state: State<String>,
cursor: usize,
}
impl Element for Input {
fn render(&mut self, scope: &mut crate::prelude::RenderScope) {
let s = self.state.get();
scope.draw_text(0, 0, &s);
if let Some(c) = s.chars().nth(self.cursor) {
scope.draw_text_inverted(self.cursor as u16, 0, &c.to_string());
} else {
scope.draw_text_inverted(s.len() as u16, 0, " ");
}
}
fn event(&mut self, event: &dyn crate::prelude::Event) {
if let Some(crossterm::event::Event::Key(KeyEvent {
code, modifiers, ..
})) = event.get()
{
if !modifiers.is_empty() && !modifiers.contains(KeyModifiers::SHIFT) {
return;
}
match code {
KeyCode::Char(c) => {
self.state.get().insert(self.cursor, *c);
self.cursor += 1;
}
KeyCode::Backspace => {
if self.cursor > 0 {
self.state.get().remove(self.cursor - 1);
self.cursor -= 1;
}
}
KeyCode::Delete => {
if self.cursor > 0 && self.state.get().len() > self.cursor {
self.state.get().remove(self.cursor);
}
}
KeyCode::Left => {
if self.cursor > 0 {
self.cursor -= 1;
}
}
KeyCode::Right => {
if self.cursor < self.state.get().len() {
self.cursor += 1;
}
}
_ => {}
}
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl Input {
pub fn new() -> Self {
Self {
state: use_state(String::new()),
cursor: 0,
}
}
}
-41
View File
@@ -1,41 +0,0 @@
pub mod div;
pub mod flex;
pub mod heading;
pub mod input;
pub mod paginator;
pub use div::*;
pub use flex::*;
pub use heading::*;
pub use input::*;
pub use paginator::*;
use crate::Element;
impl Element for String {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
scope.draw_text(0, 0, self);
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl Element for (String, u32) {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
scope.draw_text_colored(0, 0, &self.0, self.1);
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
-102
View File
@@ -1,102 +0,0 @@
use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent};
use crate::{
widget::{Element, Widget},
NoRender, NoRenderRoot,
};
pub struct Paginator {
children: Vec<Arc<Widget>>,
size: (u16, u16),
index: usize,
}
impl Paginator {
pub fn new() -> Self {
Self {
children: Vec::new(),
size: (0, 0),
index: 0,
}
}
}
impl Element for Paginator {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let (width, height) = scope.get_size_or(self.size.0, self.size.1);
scope.use_area(width, height);
}
fn after_render(&mut self, scope: &mut crate::render_scope::RenderScope) {
if let Some(elem) = self.children.get(self.index) {
if elem.get::<NoRender>().is_some() {
return;
}
let mut transform = scope.get_transform().clone();
let (w, h) = scope.get_parent_size();
scope.set_parent_size(transform.width, transform.height);
scope.clear();
if let Some(style) = elem.get() {
scope.set_style(style);
}
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
elem.get_elem().render(scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
let t = scope.get_transform_mut();
transform.width = transform.width.max(t.x + t.width + (t.px * 2));
transform.height = transform.height.max(t.y + t.height + (t.py * 2));
t.x += transform.x + transform.px;
t.y += transform.y + transform.py;
scope.draw();
elem.get_elem().after_render(scope);
scope.set_parent_size(w, h);
self.size = (transform.width, transform.height);
}
}
fn event(&mut self, event: &dyn crate::prelude::Event) {
if let Some(crossterm::event::Event::Key(KeyEvent { code, .. })) = event.get() {
let cl = self.children.len();
match code {
KeyCode::Tab => {
if self.index + 1 < cl {
self.index += 1;
} else {
self.index = 0;
}
}
KeyCode::BackTab => {
if self.index > 0 {
self.index -= 1;
} else if cl > 0 {
self.index = cl - 1;
}
}
_ => {}
}
}
}
fn draw_child(&mut self, element: &Arc<Widget>) {
self.children.push(element.clone());
element.inject(|w| w.component(NoRenderRoot));
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
+110
View File
@@ -0,0 +1,110 @@
//! # Benchmark Module
//!
//! Provides performance benchmarking capabilities for rendering engines.
use std::{io::stdout, sync::Arc, time::Instant};
use crossterm::{cursor::MoveTo, execute, terminal::Clear};
use crate::component::{context::Context, ComponentImpl};
use crate::{render::Area, DrawContext, View};
use super::Engine;
/// Results of a benchmark run
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
/// Average render time in microseconds
pub average: u128,
/// Minimum render time in microseconds
pub min: u128,
/// Maximum render time in microseconds
pub max: u128,
/// Total time spent rendering in microseconds
pub total_render: u128,
/// Total benchmark time including setup in microseconds
pub total: u128,
}
/// Wraps an engine to benchmark its rendering performance
pub struct Benchmark<T: Engine>(T);
impl<T: Engine> Benchmark<T> {
/// Creates a new benchmark wrapper around the given engine
pub fn new(engine: T) -> Self {
Self(engine)
}
}
impl<T: Engine> Engine<BenchmarkResult> for Benchmark<T> {
fn run<F: ComponentImpl + 'static>(&self, component: F) -> crate::Result<BenchmarkResult> {
let mut times: Vec<u128> = Vec::new();
let cx = self.init(component);
let start = Instant::now();
// Run 40 render cycles and measure each
for _ in 0..40 {
let start = Instant::now();
self.render(&cx);
let end = Instant::now();
times.push(end.duration_since(start).as_micros());
}
let end = Instant::now();
execute!(stdout(), Clear(crossterm::terminal::ClearType::Purge)).unwrap();
execute!(stdout(), Clear(crossterm::terminal::ClearType::All)).unwrap();
execute!(stdout(), MoveTo(0, 0)).unwrap();
Ok(BenchmarkResult {
min: *times.iter().min().unwrap_or(&0),
max: *times.iter().max().unwrap_or(&0),
total: end.duration_since(start).as_micros(),
total_render: times.iter().sum::<u128>(),
average: if times.len() > 0 {
times.iter().sum::<u128>() / (times.len() as u128)
} else {
0
},
})
}
fn init<F: ComponentImpl + 'static>(&self, component: F) -> Arc<Context> {
self.0.init(component)
}
fn draw_context(&self, ctx: &DrawContext) {
self.0.draw_context(ctx)
}
fn render(&self, cx: &Arc<Context>) {
self.0.render(cx)
}
fn render_view(&self, area: &Area, view: &View) -> DrawContext {
self.0.render_view(area, view)
}
fn render_delay(&self) {
self.0.render_delay();
}
fn executor(&self) -> Arc<dyn super::CommandExecutor> {
self.0.executor()
}
}
impl std::fmt::Display for BenchmarkResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"- Average: {} µs\n\
- Min: {} µs\n\
- Max: {} µs\n\
- Total Render: {} µs\n\
- Total: {} µs",
self.average, self.min, self.max, self.total_render, self.total
)
}
}
+14
View File
@@ -0,0 +1,14 @@
//! # Commands Module
//!
//! Defines built-in commands for controlling the engine.
use crate::engine::Command;
/// Command to stop the engine and terminate the application
pub struct Stop;
impl Command for Stop {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
+155
View File
@@ -0,0 +1,155 @@
//! # Console Engine Implementation
//!
//! Provides a Console implementation of the Engine trait for rendering
//! to the terminal using crossterm.
use std::{
io::{stdout, Write},
sync::{Arc, Mutex},
};
use crossterm::{cursor::MoveTo, execute, terminal::Clear};
use crate::component::{context::Context, ComponentImpl};
use crate::{
engine::{commands, CommandExecutor},
render::Area,
DrawContext, View,
};
use super::Engine;
/// Executes commands for the console engine
pub struct ConsoleExecutor {
/// Flag indicating whether the application is running
running: Mutex<bool>,
}
/// Console-based rendering engine
///
/// Renders components to the terminal using crossterm for cross-platform support.
pub struct Console {
/// Thread functions to execute
threads: Mutex<Vec<Arc<dyn Fn(Arc<Context>) + Send + Sync>>>,
/// The executor for this console
executor: Arc<ConsoleExecutor>,
}
impl Console {
/// Creates a new console engine
pub fn new() -> Self {
Self {
threads: Mutex::new(Vec::new()),
executor: Arc::new(ConsoleExecutor {
running: Mutex::new(true),
}),
}
}
/// Registers a thread function to run alongside the engine
pub fn thread<F: Fn(Arc<Context>) + Send + Sync + 'static>(&self, run: F) {
self.threads.lock().unwrap().push(Arc::new(run));
}
}
impl Engine for Console {
fn render_view(&self, area: &Area, view: &View) -> DrawContext {
let mut context = DrawContext::new(area.clone());
view(&mut context);
context
}
fn draw_context(&self, ctx: &DrawContext) {
for inst in &ctx.drawing {
match inst {
crate::render::DrawInstruction::Text(point, text) => {
let (x, y) = (ctx.area.x + point.x, ctx.area.y + point.y);
for (i, line) in text.lines().enumerate() {
execute!(stdout(), MoveTo(x, y + (i as u16))).unwrap();
print!("{line}");
}
stdout().flush().unwrap();
}
crate::render::DrawInstruction::Child(_point, child) => self.draw_context(child),
crate::render::DrawInstruction::View(area, view) => {
self.draw_context(&self.render_view(area, view))
}
}
}
}
fn render(&self, cx: &Arc<Context>) {
let (width, height) = crossterm::terminal::size().unwrap();
execute!(stdout(), Clear(crossterm::terminal::ClearType::Purge)).unwrap();
execute!(stdout(), Clear(crossterm::terminal::ClearType::All)).unwrap();
self.draw_context(&self.render_view(
&Area {
x: 0,
y: 0,
width,
height,
},
&cx.get_view(),
));
}
fn init<C: ComponentImpl + 'static>(&self, component: C) -> Arc<Context> {
crossterm::execute!(std::io::stdout(), crossterm::cursor::Hide).unwrap();
let cx = Context::new(component, self.executor.clone());
cx.refresh();
for thread in self.threads.lock().unwrap().iter() {
let thread = thread.clone();
std::thread::spawn({
let cx = cx.clone();
move || thread(cx)
});
}
cx
}
fn executor(&self) -> Arc<dyn super::CommandExecutor> {
self.executor.clone()
}
fn run<F: ComponentImpl + 'static>(&self, component: F) -> crate::Result<()> {
let cx = self.init(component);
while self.executor.is_running() {
self.render(&cx);
self.render_delay();
}
Ok(())
}
}
impl ConsoleExecutor {
/// Checks if the engine is still running
pub fn is_running(self: &Arc<ConsoleExecutor>) -> bool {
*self.running.lock().unwrap()
}
/// Stops the engine
pub fn stop(&self) -> crate::Result<()> {
*self.running.lock()? = false;
Ok(())
}
}
impl CommandExecutor for ConsoleExecutor {
fn execute_command(&self, command: &Arc<dyn super::Command>) -> crate::Result<()> {
let command = command.as_any();
if let Some(commands::Stop) = command.downcast_ref() {
self.stop()?;
}
Ok(())
}
}
+55
View File
@@ -0,0 +1,55 @@
//! # Engine Module
//!
//! Provides the rendering engine and command execution system.
//! The engine is responsible for initializing components, rendering frames,
//! and handling user commands.
pub mod benchmark;
pub mod commands;
pub mod console;
pub use benchmark::*;
pub use console::*;
use std::{any::Any, sync::Arc};
use crate::component::{context::Context, ComponentImpl};
use crate::{render::Area, DrawContext, View};
/// Main engine trait for rendering and running components
pub trait Engine<Output = ()> {
/// Runs a component to completion
fn run<C: ComponentImpl + 'static>(&self, component: C) -> crate::Result<Output>;
/// Initializes a component and returns its context
fn init<C: ComponentImpl + 'static>(&self, component: C) -> Arc<Context>;
/// Renders the current state of a component
fn render(&self, cx: &Arc<Context>);
/// Sleeps between render frames (default 16ms for ~60fps)
fn render_delay(&self) {
crate::sleep(16);
}
/// Renders a view within an area and returns the draw context
fn render_view(&self, area: &Area, view: &View) -> DrawContext;
/// Executes the drawing instructions in a draw context
fn draw_context(&self, ctx: &DrawContext);
/// Returns the command executor for this engine
fn executor(&self) -> Arc<dyn CommandExecutor>;
}
/// Trait for commands that can be executed by the engine
pub trait Command {
/// Returns the command as Any for downcasting
fn as_any(&self) -> &dyn Any;
}
/// Executes commands during the application lifecycle
pub trait CommandExecutor: Send + Sync {
/// Executes the given command
fn execute_command(&self, command: &Arc<dyn Command>) -> crate::Result<()>;
}
-27
View File
@@ -1,27 +0,0 @@
use std::sync::Arc;
use crate::{component, extensions::Extension, widget::Widget, Screen};
pub struct IdExtension(pub Arc<Screen>);
component!(Id(pub usize));
impl Extension for Arc<IdExtension> {}
impl IdExtension {
pub fn new(screen: Arc<Screen>) -> Arc<Self> {
Arc::new(IdExtension(screen))
}
pub fn get_element(self: &Arc<IdExtension>, id: usize) -> Option<Arc<Widget>> {
for elem in self.0.widgets.lock().unwrap().iter() {
if let Some(current_id) = elem.get::<Id>() {
if current_id.0 == id {
return Some(elem.clone());
}
}
}
None
}
}
-26
View File
@@ -1,26 +0,0 @@
use crate::extensions::Extension;
pub struct InputExtension;
impl Extension for InputExtension {
fn init(&mut self, screen: std::sync::Arc<crate::Screen>) {
crossterm::terminal::enable_raw_mode().unwrap();
std::thread::spawn(move || loop {
if let Ok(e) = crossterm::event::read() {
for widget in screen.widgets.lock().unwrap().iter() {
widget.event(&e);
}
}
});
}
fn on_close(&mut self, _screen: std::sync::Arc<crate::Screen>) {
crossterm::terminal::disable_raw_mode().unwrap();
}
}
impl crate::extensions::Event for crossterm::event::Event {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
-66
View File
@@ -1,66 +0,0 @@
pub mod id;
pub mod input_handling;
pub mod tick;
pub mod velocity;
pub use id::*;
pub use input_handling::*;
pub use tick::*;
pub use velocity::*;
use std::{
any::{type_name, Any},
fmt::Debug,
sync::{Arc, Mutex},
};
use crate::{
render_scope::RenderScope,
widget::{Component, Widget},
Screen,
};
pub trait Extension {
fn init(&mut self, _screen: Arc<Screen>) {}
fn on_close(&mut self, _screen: Arc<Screen>) {}
fn render_widget(&mut self, _scope: &mut RenderScope, _widget: &Arc<Widget>) {}
}
pub trait Event: Send + Sync {
fn as_any(&self) -> &dyn Any;
}
#[derive(Clone)]
pub struct Handler<E: Event>(Arc<Mutex<dyn FnMut(&Arc<Widget>, &E) + Send + Sync>>);
impl<E: Event + 'static> Component for Handler<E> {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl<E: Event + 'static> Handler<E> {
pub fn new<F: FnMut(&Arc<Widget>, &E) + Send + Sync + 'static>(f: F) -> Handler<E> {
Handler(Arc::new(Mutex::new(f)))
}
pub fn call(&self, w: &Arc<Widget>, e: &E) {
(self.0.lock().unwrap())(w, e)
}
}
impl<E: Event> Debug for Handler<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Handler({})", type_name::<E>())
}
}
impl<'a> dyn Event + 'a {
pub fn get<T: Event + 'static>(&self) -> Option<&T> {
self.as_any().downcast_ref()
}
}
-26
View File
@@ -1,26 +0,0 @@
use std::{fmt::Debug, sync::Arc};
use crate::extensions::Extension;
use crate::{event, Screen};
pub struct TickExtension(pub u16);
event!(TickEvent(pub u32));
impl Extension for TickExtension {
fn init(&mut self, screen: Arc<Screen>) {
let rate_dur = 1000 / self.0 as u64;
std::thread::spawn({
move || {
let mut tick = 0;
loop {
for w in screen.widgets.lock().unwrap().iter() {
w.event(&TickEvent(tick));
}
tick += 1;
std::thread::sleep(std::time::Duration::from_millis(rate_dur));
}
}
});
}
}
-64
View File
@@ -1,64 +0,0 @@
use std::sync::Arc;
use crate::{
component,
extensions::Extension,
style::{Position, Transform},
widget::Widget,
Screen,
};
pub struct VelocityExtension;
component!(Velocity(pub i32, pub i32));
impl VelocityExtension {
fn apply_velocity(ticks: u16, velocity: i32, x: &mut u16) {
if velocity.abs() != 0 && ticks as i32 % (1000 / velocity.abs()) == 0 {
if velocity > 0 {
*x += 1;
} else if velocity < 0 {
*x -= 1;
}
}
}
fn apply_velocity_xy(ticks: u16, widget: &Arc<Widget>) {
if let Some(velocity) = widget.get::<Velocity>() {
if let Some(mut t) = widget.get::<Transform>() {
match &mut t.x {
Position::Const(x) => Self::apply_velocity(ticks, velocity.0, x),
_ => {}
}
match &mut t.y {
Position::Const(y) => Self::apply_velocity(ticks, velocity.1, y),
_ => {}
}
widget.set_component(t);
}
}
}
}
impl Extension for VelocityExtension {
fn init(&mut self, screen: Arc<Screen>) {
std::thread::spawn({
move || {
let mut tick = 0;
loop {
for widget in screen.widgets.lock().unwrap().iter() {
Self::apply_velocity_xy(tick, widget);
}
if tick > 1000 {
tick = 0;
} else {
tick += 1;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
});
}
}
+101 -82
View File
@@ -1,103 +1,122 @@
//! The frontend binder for OSUI. //! # Frontend Module
//! //!
//! This module defines the internal representation of RSX elements — the tree-like structure used //! Provides the RSX (React-like Syntax) system for composing components.
//! by the `rsx!` macro in OSUI to declaratively build terminal user interfaces. //! This module defines the structure for building component hierarchies
//! //! with static and dynamic scopes, similar to React's JSX.
//! RSX elements can be either static widgets or dynamically loaded ones with dependencies.
//! This structure enables recursive rendering, parent-child composition, and runtime dependency handling.
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::component::{context::Context, scope::Scope};
state::DependencyHandler, use crate::{render::Point, hooks::HookDependency, View};
widget::{StaticWidget, Widget, WidgetLoad},
Screen,
};
/// Represents a single element in the RSX tree. /// Trait for converting values to RSX
pub enum RsxElement { pub trait ToRsx {
/// A static widget with children. /// Convert to RSX representation
Element(StaticWidget, Rsx), fn to_rsx(&self) -> Rsx;
/// A dynamically generated widget (e.g., with state) with associated dependencies and children.
DynElement(
Box<dyn FnMut() -> WidgetLoad + Send + Sync>,
Vec<Box<dyn DependencyHandler>>,
Rsx,
),
} }
/// A container representing a group of RSX elements. /// Scope types for RSX components
/// This is typically created via the `rsx!` macro and rendered using a `Screen`. #[derive(Clone)]
pub struct Rsx(pub Vec<RsxElement>); pub enum RsxScope {
/// Static scope - executed once and never updated
Static(Arc<dyn Fn(&Arc<Scope>) + Send + Sync>),
/// Dynamic scope - re-executed when dependencies change
Dynamic(
Arc<dyn Fn(&Arc<Scope>) + Send + Sync>,
Vec<Arc<dyn HookDependency>>,
),
/// Child RSX scope for composition
Child(Rsx),
}
/// RSX (React-like Syntax) builder for component hierarchies
///
/// Represents a collection of scopes that define component structure.
/// Scopes can be static (execute once) or dynamic (reactive to changes).
#[derive(Clone)]
pub struct Rsx(Vec<RsxScope>);
impl Rsx { impl Rsx {
/// Draws the RSX tree onto the given screen without a parent widget. /// Creates a new empty RSX
pub fn new() -> Self {
Self(Vec::new())
}
/// Adds a static scope to this RSX
/// ///
/// This is the entry point for rendering the UI. /// The provided function is executed once during rendering
pub fn draw(self, screen: &Arc<Screen>) { /// and will not be re-executed on dependency changes.
self.draw_parent(screen, None); pub fn static_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(&mut self, scope: F) {
self.0.push(RsxScope::Static(Arc::new(scope)));
} }
/// Recursively draws the RSX tree with an optional parent widget. /// Adds a dynamic scope to this RSX
/// ///
/// Used internally to establish parent-child widget relationships. /// The provided function is executed when dependencies change,
pub fn draw_parent(self, screen: &Arc<Screen>, parent: Option<Arc<Widget>>) { /// allowing for reactive updates similar to React hooks.
for rsx_elem in self.0 { pub fn dynamic_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(
match rsx_elem {
RsxElement::DynElement(f, dep, child) => {
let w = screen.draw_box_dyn(f);
if let Some(parent) = &parent {
parent.get_elem().draw_child(&w);
}
for d in dep {
w.dependency_box(d);
}
child.draw_parent(screen, Some(w.clone()));
}
RsxElement::Element(ws, child) => {
let w = Arc::new(Widget::Static(ws));
screen.draw_widget(w.clone());
if let Some(parent) = &parent {
parent.get_elem().draw_child(&w);
}
child.draw_parent(screen, Some(w.clone()));
}
}
}
}
/// Adds a dynamically constructed element to the RSX tree.
///
/// - `load`: A closure returning a `WidgetLoad` used to generate the widget.
/// - `dependencies`: A list of dependency handlers for state or event updates.
/// - `children`: Child RSX elements.
pub fn create_element<F: FnMut() -> WidgetLoad + Send + Sync + 'static>(
&mut self, &mut self,
load: F, drawer: F,
dependencies: Vec<Box<dyn DependencyHandler>>, dependencies: Vec<Arc<dyn HookDependency>>,
children: Rsx,
) { ) {
self.0.push(RsxElement::DynElement( self.0
Box::new(load), .push(RsxScope::Dynamic(Arc::new(drawer), dependencies));
dependencies,
children,
));
} }
/// Adds a statically defined element to the RSX tree with its children. /// Adds a child RSX
pub fn create_element_static(&mut self, element: StaticWidget, children: Rsx) { pub fn child<R: ToRsx>(&mut self, child: R) {
self.0.push(RsxElement::Element(element, children)); self.0.push(RsxScope::Child(child.to_rsx()));
} }
/// Appends the elements from another `Rsx` tree into this one. /// Generates child components within the given context
pub fn expand(&mut self, other: &mut Rsx) { pub fn generate_children(&self, context: &Arc<Context>) {
self.0.append(&mut other.0); let executor = context.get_executor();
for scope in &self.0 {
match scope {
RsxScope::Static(scope_fn) => {
let scope = Scope::new(executor.clone());
(scope_fn)(&scope);
context.add_scope(scope)
}
RsxScope::Dynamic(drawer, dependencies) => {
let drawer = drawer.clone();
context.dyn_scope(
move |c| drawer(c),
&dependencies.iter().map(|d| d.as_ref()).collect::<Vec<_>>(),
);
}
RsxScope::Child(child) => child.generate_children(context),
}
}
}
/// Converts this RSX to a View
pub fn view(&self, context: &Arc<Context>) -> View {
let context = context.clone();
self.generate_children(&context);
Arc::new({
move |ctx| {
context.draw_children(ctx);
}
})
}
}
impl ToRsx for &Rsx {
fn to_rsx(&self) -> Rsx {
Rsx(self.0.clone())
}
}
impl<T: std::fmt::Display> ToRsx for T {
fn to_rsx(&self) -> Rsx {
let s = self.to_string();
Rsx(vec![RsxScope::Static(Arc::new(move |scope| {
let s = s.clone();
scope.view(Arc::new(move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &s)))
}))])
} }
} }
+55
View File
@@ -0,0 +1,55 @@
//! # Effect hooks module
//!
//! Provides React-like use_effect hooks for managing effects.
//! This module includes use_effect and use_sync_effect.
use std::sync::{Arc, Mutex};
use super::{state::State, HookDependency, HookEffect};
use crate::component::context::Context;
/// Executes a function when dependencies change
///
/// Similar to React's useEffect. The provided function is executed
/// when any of the dependencies change.
pub fn use_effect<F: FnMut() + Send + Sync + 'static>(f: F, dependencies: &[&dyn HookDependency]) {
let f = Arc::new(Mutex::new(f));
let hook = HookEffect(Arc::new(Mutex::new({
let f = f.clone();
move || {
let f = f.clone();
std::thread::spawn(move || (f.lock().unwrap())());
}
})));
for d in dependencies {
d.on_update(hook.clone());
}
}
/// Synchronizes state changes back to the context as events
///
/// Creates an effect that emits an event whenever the state changes.
/// The encoder function converts state values to events.
pub fn use_sync_effect<
T: Send + Sync + 'static,
Ev: Send + Sync + 'static,
E: Fn(&State<T>) -> Ev + Send + Sync + 'static,
>(
cx: &Arc<Context>,
state: &State<T>,
encoder: E,
deps: &[&dyn HookDependency],
) {
use_effect(
{
let state = state.clone();
let cx = cx.clone();
move || {
let ev = encoder(&state);
cx.emit_event(ev);
}
},
deps,
);
}
+44
View File
@@ -0,0 +1,44 @@
//! # State Management and Hooks Module
//!
//! Provides React-like hooks for managing component state and side effects.
//! This module includes useState, useEffect, useMount, and state synchronization hooks.
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
sync::{Arc, Mutex},
};
mod effect;
mod mount;
mod state;
pub use effect::*;
pub use mount::*;
pub use state::*;
/// Effect callback that can be triggered by state changes
#[derive(Clone)]
pub struct HookEffect(Arc<Mutex<dyn FnMut() + Send + Sync>>);
/// Trait for values that can be tracked as dependencies in hooks
pub trait HookDependency: Send + Sync {
/// Register an effect to be triggered on updates
fn on_update(&self, hook: HookEffect);
}
impl HookEffect {
/// Creates a new effect from a function
pub fn new<F: Fn() + Send + Sync + 'static>(f: F) -> Self {
Self(Arc::new(Mutex::new(f)))
}
/// Executes the effect function
pub fn call(&self) {
(self.0.lock().unwrap())()
}
}
impl Debug for HookEffect {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "HookEffect")
}
}
+58
View File
@@ -0,0 +1,58 @@
//! # Mount hooks module
//!
//! Provides React-like Mount hooks for managing effects.
//! This module includes Mount, use_mount and use_mount_manual.
use std::{
fmt::Debug,
sync::{Arc, Mutex},
};
use super::{HookDependency, HookEffect};
/// Mount lifecycle hook
///
/// Tracks whether a component has been mounted and executes
/// any pending mount effects.
#[derive(Debug, Clone)]
pub struct Mount(Arc<Mutex<bool>>, Arc<Mutex<Vec<HookEffect>>>);
/// Creates a mount lifecycle hook
///
/// Returns a Mount that tracks component lifecycle and executes
/// effects after mounting.
pub fn use_mount() -> Mount {
Mount(Arc::new(Mutex::new(true)), Arc::new(Mutex::new(Vec::new())))
}
/// Creates a manual mount lifecycle hook
///
/// Similar to use_mount but the component starts as unmounted.
/// Must call .mount() to trigger mounted effects.
pub fn use_mount_manual() -> Mount {
Mount(
Arc::new(Mutex::new(false)),
Arc::new(Mutex::new(Vec::new())),
)
}
impl HookDependency for Mount {
fn on_update(&self, hook: HookEffect) {
if *self.0.lock().unwrap() {
hook.call();
} else {
self.1.lock().unwrap().push(hook);
}
}
}
impl Mount {
/// Mark the component as mounted and execute pending effects
pub fn mount(&self) {
*self.0.lock().unwrap() = true;
for hook_effect in self.1.lock().unwrap().iter() {
hook_effect.call();
}
self.1.lock().unwrap().clear();
}
}
+152
View File
@@ -0,0 +1,152 @@
//! # State hooks module
//!
//! Provides React-like use_state hooks for managing states.
//! This module includes use_state and use_sync_state.
use std::{
any::Any,
fmt::{Debug, Display, Formatter, Result as FmtResult},
ops::{Deref, DerefMut},
sync::{Arc, Mutex, MutexGuard},
};
use super::{HookDependency, HookEffect};
use crate::component::context::Context;
/// State holder for reactive values
///
/// Similar to React's useState hook. Holds a value and tracks dependents
/// that need to be notified when the value changes.
#[derive(Debug)]
pub struct State<T> {
/// The actual state value
value: Arc<Mutex<T>>,
/// Functions to call when state is updated
dependents: Arc<Mutex<Vec<HookEffect>>>,
}
/// Guard for accessing and potentially modifying state
///
/// Dereferences to the state value. When dropped after modification,
/// automatically triggers all dependent effects.
pub struct Inner<'a, T> {
value: MutexGuard<'a, T>,
dependents: Arc<Mutex<Vec<HookEffect>>>,
updated: bool,
}
impl<T: Clone> State<T> {
/// Gets a cloned copy of the state value
///
/// Recommended over `get()` to prevent deadlocks when cloning is acceptable.
/// "dl" stands for "deadlock-less".
pub fn get_dl(&self) -> T {
self.value.lock().unwrap().clone()
}
}
impl<T> State<T> {
/// Acquires a lock on the state for read/write access
///
/// Returns an Inner guard that implements Deref and DerefMut.
/// When dropped after modification, triggers dependent effects.
pub fn get(&self) -> Inner<'_, T> {
Inner {
value: self.value.lock().unwrap(),
dependents: self.dependents.clone(),
updated: false,
}
}
/// Sets the state value and triggers dependents
pub fn set(&self, v: T) {
*self.value.lock().unwrap() = v;
self.update();
}
/// Notifies all dependents of an update
pub fn update(&self) {
for d in self.dependents.lock().unwrap().iter() {
d.call();
}
}
/// Clones the State handle (not the value)
pub fn clone(&self) -> Self {
Self {
dependents: self.dependents.clone(),
value: self.value.clone(),
}
}
}
impl<T: Display> Display for State<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.value.lock().unwrap())
}
}
/// Inner implements Deref for read access and DerefMut for write access
/// On drop after mutation, automatically triggers dependent effects
impl<T> Drop for Inner<'_, T> {
fn drop(&mut self) {
if self.updated {
for d in self.dependents.lock().unwrap().iter() {
d.call();
}
}
}
}
impl<T> Deref for Inner<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for Inner<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.updated = true;
&mut self.value
}
}
impl<T: Send + Sync> HookDependency for State<T> {
fn on_update(&self, hook: HookEffect) {
self.dependents.lock().unwrap().push(hook);
}
}
/// Synchronizes state with events from the context
///
/// Creates state that is automatically updated when events are emitted
/// to the context. The decoder function converts events to state values.
pub fn use_sync_state<
T: Send + Sync + 'static,
E: Any + 'static,
D: Fn(&E) -> T + Send + Sync + 'static,
>(
cx: &Arc<Context>,
v: T,
decoder: D,
) -> State<T> {
let state = use_state(v);
cx.on_event({
let state = state.clone();
move |_, v: &E| state.set(decoder(v))
});
state
}
/// Creates a new state value
///
/// Returns a State that can be read and written from multiple threads.
pub fn use_state<T>(v: T) -> State<T> {
State {
value: Arc::new(Mutex::new(v)),
dependents: Arc::new(Mutex::new(Vec::new())),
}
}
+76 -213
View File
@@ -1,235 +1,98 @@
//! **OSUI** – A Rust Terminal User Interface Library //! # OSUI - A TUI Library for Advanced UIs
//! //!
//! OSUI is a library for building interactive and customizable terminal user interfaces in Rust. //! OSUI is a Rust library for building sophisticated Terminal User Interfaces (TUIs).
//! It provides a component system, real-time keyboard input handling, and a `rsx!` macro //! It provides a component-based architecture with state management, event handling,
//! for defining UI elements in a declarative way, however that is optional. //! and rendering capabilities for creating interactive console applications.
//! //!
//! ✅ **Features** //! ## Key Features
//! - 🧱 RSX-like syntax with `rsx!` macro
//! - 🖥️ Virtual screen abstraction
//! - 🎹 Keyboard input handling
//! - 🎯 Component-based design
//! - ⚡ Real-time rendering
//! //!
//! 🚀 **Quick Example** //! - **Component System**: Build UIs using composable components
//! ```rust //! - **State Management**: React-like hooks for managing component state
//! - **Event Handling**: Type-safe event system with reactive updates
//! - **RSX Syntax**: Macro-based DSL for defining component hierarchies
//! - **Console Engine**: Terminal rendering with crossterm support
//!
//! ## Architecture
//!
//! - [`component`] - Component system and context management
//! - [`state`] - State management with hooks (useState, useEffect, etc.)
//! - [`engine`] - Rendering engine and command execution
//! - [`frontend`] - RSX (React-like Syntax) for component definitions
//! - [`render`] - Low-level rendering primitives
//!
//! ## Example
//!
//! ```rust,no_run
//! use osui::prelude::*; //! use osui::prelude::*;
//! use std::sync::Arc;
//! //!
//! fn main() -> std::io::Result<()> { //! #[component]
//! let screen = Screen::new(); //! pub fn Counter(cx: &Arc<Context>) -> View {
//! rsx! { //! let count = use_state(0);
//! "👋 Hello, World!" //!
//! }.draw(&screen); //! Arc::new(move |ctx| {
//! screen.run() //! ctx.draw_text(Point { x: 0, y: 0 }, &format!("Count: {}", count.get_dl()));
//! })
//! } //! }
//! ``` //! ```
//!
//! ---
//! 🧰 For full documentation, visit: [osui.netlify.app/docs](https://osui.netlify.app/docs)
//!
//! 🧪 Examples and demos: [github.com/osui-rs/osui/demos](https://github.com/osui-rs/osui/tree/master/src/demos)
use std::sync::{Arc, Mutex}; use std::sync::Arc;
use crate::{ use crate::render::DrawContext;
extensions::{Extension, Handler},
render_scope::RenderScope,
widget::{BoxedElement, DynWidget, Element, StaticWidget, Widget, WidgetLoad},
};
pub mod elements; pub mod component;
pub mod extensions; pub mod engine;
pub mod frontend; pub mod frontend;
pub mod macros; pub mod hooks;
pub mod render_scope; pub mod render;
pub mod state; pub mod view_plugins;
pub mod style;
pub mod utils;
pub mod widget;
pub mod prelude { pub mod prelude {
pub use crate::{ //! Prelude module - Re-exports commonly used items for convenience
elements::*, extensions::*, frontend::*, render_scope::*, state::*, style::*, utils::*, pub use crate::component::components::*;
widget::*, *, pub use crate::component_prelude::*;
};
pub use crate::style::{
Dimension::{Content, Full},
Position::{Center, End},
};
} }
/// The main screen abstraction for rendering and managing widgets and extensions. pub mod component_prelude {
/// //! Prelude module - Re-exports commonly used items for convenience
/// `Screen` holds the root widget list and registered extensions. It provides methods pub use crate::component::{context::*, scope::*, *};
/// for drawing elements, adding extensions, and running the main rendering loop. pub use crate::engine::*;
/// pub use crate::frontend::*;
/// # Examples pub use crate::hooks::*;
/// ```rust pub use crate::render::*;
/// let screen = Screen::new(); pub use crate::view_plugins::*;
/// rsx! { "Hello" }.draw(&screen); pub use crate::{sleep, Error, Result, View, ViewWrapper};
/// screen.run()?; pub use crossterm;
/// ``` pub use osui_macros::{component, rsx};
pub struct Screen { pub use std::sync::{Arc, Mutex};
/// The list of widgets currently managed by the screen.
pub widgets: Mutex<Vec<Arc<Widget>>>,
/// Registered extensions for the screen.
extensions: Mutex<Vec<Arc<Mutex<Box<dyn Extension + Send + Sync>>>>>,
/// If it's still running
running: Mutex<bool>,
} }
event!(RenderWrapperEvent(*mut RenderScope)); /// A View is an async function that renders content to a DrawContext.
component!(NoRender); /// It takes a mutable DrawContext and produces drawing instructions.
component!(NoRenderRoot); pub type View = Arc<dyn Fn(&mut DrawContext) + Send + Sync>;
impl RenderWrapperEvent { /// A ViewWrapper is a higher-order function that wraps views.
/// Returns a mutable reference to the underlying `RenderScope`. /// It can modify or enhance how a view is rendered.
/// pub type ViewWrapper = Arc<dyn Fn(&mut DrawContext, View) + Send + Sync>;
/// # Safety
/// The caller must ensure the pointer is valid for the lifetime of the event. /// Result type for OSUI operations
pub fn get_scope(&self) -> &mut RenderScope { pub type Result<T> = std::result::Result<T, Error>;
unsafe { &mut *self.0 }
/// Error type for OSUI operations
#[derive(Debug, Clone)]
pub enum Error {
/// Error that occurs when a mutex is poisoned
PoisonError,
}
impl From<std::sync::PoisonError<std::sync::MutexGuard<'_, bool>>> for Error {
fn from(_value: std::sync::PoisonError<std::sync::MutexGuard<'_, bool>>) -> Self {
Error::PoisonError
} }
} }
unsafe impl Send for RenderWrapperEvent {} /// Sleep for the specified duration in milliseconds.
unsafe impl Sync for RenderWrapperEvent {} /// Useful for controlling render frame rate or delays.
pub fn sleep(delay_ms: u64) {
impl Screen { std::thread::sleep(std::time::Duration::from_millis(delay_ms));
/// Creates a new screen instance wrapped in an `Arc`.
pub fn new() -> Arc<Self> {
Arc::new(Self {
widgets: Mutex::new(Vec::new()),
extensions: Mutex::new(Vec::new()),
running: Mutex::new(true),
})
}
/// Draws a static element and returns its widget handle.
pub fn draw<E: Element + 'static + Send + Sync>(self: &Arc<Self>, element: E) -> Arc<Widget> {
let w = Arc::new(Widget::Static(StaticWidget::new(Box::new(element))));
self.widgets.lock().unwrap().push(w.clone());
w
}
/// Draws a boxed element and returns its widget handle.
pub fn draw_box(self: &Arc<Self>, element: BoxedElement) -> Arc<Widget> {
let w = Arc::new(Widget::Static(StaticWidget::new(element)));
self.widgets.lock().unwrap().push(w.clone());
w
}
/// Adds an existing widget to the screen.
pub fn draw_widget(self: &Arc<Self>, widget: Arc<Widget>) {
self.widgets.lock().unwrap().push(widget);
}
/// Draws a dynamic element using a closure and returns its widget handle.
pub fn draw_dyn<F: FnMut() -> WidgetLoad + 'static + Send + Sync>(
self: &Arc<Self>,
element: F,
) -> Arc<Widget> {
let w = Arc::new(Widget::Dynamic(DynWidget::new(element)));
self.widgets.lock().unwrap().push(w.clone());
w
}
/// Draws a dynamic element from a boxed closure and returns its widget handle.
pub fn draw_box_dyn(
self: &Arc<Self>,
element: Box<dyn FnMut() -> WidgetLoad + Send + Sync>,
) -> Arc<Widget> {
let w = Arc::new(Widget::Dynamic(DynWidget::new(element)));
self.widgets.lock().unwrap().push(w.clone());
w
}
/// Registers an extension with the screen.
pub fn extension<E: Extension + Send + Sync + 'static>(self: &Arc<Self>, ext: E) {
self.extensions
.lock()
.unwrap()
.push(Arc::new(Mutex::new(Box::new(ext))));
}
/// Runs the main rendering loop, calling extensions and rendering widgets.
///
/// This method blocks and repeatedly renders the screen at a fixed interval.
pub fn run(self: &Arc<Self>) -> std::io::Result<()> {
for ext in self.extensions.lock().unwrap().iter() {
ext.lock().unwrap().init(self.clone());
}
utils::hide_cursor()?;
while *self.running.lock().unwrap() {
self.render()?;
std::thread::sleep(std::time::Duration::from_millis(28));
}
Ok(())
}
/// Renders all widgets and applies extensions.
///
/// This method is called internally by `run`.
pub fn render(self: &Arc<Self>) -> std::io::Result<()> {
let mut scope = RenderScope::new();
let (w, h) = crossterm::terminal::size().unwrap();
scope.set_parent_size(w, h);
utils::clear()?;
for elem in self.widgets.lock().unwrap().iter() {
if elem.get::<NoRender>().is_some() || elem.get::<NoRenderRoot>().is_some() {
for ext in self.extensions.lock().unwrap().iter() {
ext.lock().unwrap().render_widget(&mut scope, elem);
}
elem.auto_refresh();
continue;
}
if let Some(wrapper) = elem.get::<Handler<RenderWrapperEvent>>() {
wrapper.call(elem, &RenderWrapperEvent(&mut scope));
} else {
scope.clear();
if let Some(style) = elem.get() {
scope.set_style(style);
}
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
for ext in self.extensions.lock().unwrap().iter() {
ext.lock().unwrap().render_widget(&mut scope, elem);
}
elem.get_elem().render(&mut scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
scope.draw();
elem.get_elem().after_render(&mut scope);
}
elem.auto_refresh();
}
Ok(())
}
/// Closes the loop and calls `on_close` in the extensions
pub fn close(self: &Arc<Self>) {
*self.running.lock().unwrap() = false;
utils::show_cursor().unwrap();
utils::clear().unwrap();
for ext in self.extensions.lock().unwrap().iter() {
ext.lock().unwrap().on_close(self.clone());
}
}
} }
-301
View File
@@ -1,301 +0,0 @@
/// Declares a struct that implements the `Event` trait.
///
/// This macro simplifies the creation of event types used within OSUI's reactive system.
///
/// # Variants
///
/// - `event!(Name)`
/// Defines a unit struct named `Name`.
///
/// - `event!(Name { ... })`
/// Defines a named struct with fields.
///
/// - `event!(Name (...))`
/// Defines a tuple struct.
///
/// # Examples
/// ```rust
/// event!(Clicked);
/// event!(Resized { width: u32, height: u32 });
/// event!(Moved(u32, u32));
/// ```
#[macro_export]
macro_rules! event {
($name:ident) => {
#[derive(Debug, Clone)]
pub struct $name;
impl $crate::extensions::Event for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
};
($name:ident {$($inner:tt)*}) => {
#[derive(Debug, Clone)]
pub struct $name {
$($inner)*
}
impl $crate::extensions::Event for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
};
($name:ident ($($inner:tt)*)) => {
#[derive(Debug, Clone)]
pub struct $name ($($inner)*);
impl $crate::extensions::Event for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
};
}
/// Declares a struct that implements the `Component` trait.
///
/// Components allow widgets to extend their behavior or contain additional data.
/// This macro helps avoid boilerplate when defining new components.
///
/// # Variants
///
/// - `component!(Name)`
/// Defines a unit struct.
///
/// - `component!(Name { ... })`
/// Defines a named struct with fields.
///
/// - `component!(Name (...))`
/// Defines a tuple struct.
///
/// # Examples
/// ```rust
/// component!(Focusable);
/// component!(Tooltip { text: String });
/// component!(Size(u32, u32));
/// ```
#[macro_export]
macro_rules! component {
($name:ident) => {
#[derive(Debug, Clone)]
pub struct $name;
impl $crate::widget::Component for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
};
($name:ident {$($inner:tt)*}) => {
#[derive(Debug, Clone)]
pub struct $name {
$($inner)*
}
impl $crate::widget::Component for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
};
($name:ident ($($inner:tt)*)) => {
#[derive(Debug, Clone)]
pub struct $name ($($inner)*);
impl $crate::widget::Component for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
};
}
/// Creates an event handler closure that calls a method on `self`.
///
/// Useful when you need to register `'static` event handlers that interact with the current instance.
///
/// # Arguments
/// - `$self_ty`: The type of `self`.
/// - `$self`: The instance variable (usually `self`) being used.
/// - `$events`: The event source object with an `.on` method.
/// - `$method`: The method to call when an event is received.
///
/// # Example
/// ```rust
/// event_handler!(Self, self, events, on_event);
/// ```
/// Expands roughly to:
/// ```rust
/// let self_ref = self as *mut Self;
/// events.on(move |es, e| unsafe { (*self_ref).on_event(es, e) });
/// ```
///
/// # Safety
/// This macro uses `unsafe` code to cast `self` to a raw pointer and dereference it.
/// Make sure the reference is valid for the closure’s lifetime.
#[macro_export]
macro_rules! event_handler {
($self_ty:ty, $self:ident, $events:ident, $method:ident) => {{
let self_ref = $self as *mut $self_ty;
$events.on(move |es, e| unsafe { (*self_ref).$method(es, e) });
}};
}
/// Creates a `Transform` with property overrides.
///
/// Each key-value pair sets a field on a `Transform` struct.
///
/// # Example
/// ```rust
/// let t = transform!(
/// x: 10,
/// y: 20,
/// scale: 1.5,
/// );
/// ```
///
/// This expands to:
/// ```rust
/// let mut t = Transform::new();
/// t.x = 10.into();
/// t.y = 20.into();
/// t.scale = 1.5.into();
/// ```
#[macro_export]
macro_rules! transform {
($($f:ident: $v:expr),* $(,)?) => {{
let mut t = Transform::new();
$(t.$f = $v.into();)*
t
}};
}
/// Constructs an `Rsx` tree using declarative syntax.
///
/// This macro is similar to JSX in UI frameworks, allowing you to nest widgets and assign components or dependencies.
/// It expands into a tree of `RsxElement` objects.
///
/// Internally calls the recursive `rsx_inner!` macro.
///
/// # Example
/// ```rust
/// rsx! {
/// "Hello"
/// static Label { } ("Text")
/// %state Label { }
/// @Velocity(20, 0); Transform::new(); "Moving Text"
/// }
/// ```
#[macro_export]
macro_rules! rsx {
($($inner:tt)*) => {{
let mut r = $crate::frontend::Rsx(Vec::new());
$crate::rsx_inner! { r, $($inner)* };
r
}};
}
/// Internal macro used by `rsx!` to recursively build `Rsx` trees.
///
/// This macro handles various syntactic forms used in the declarative layout system:
///
/// - Static text or components
/// - Dynamic widgets with dependencies (`%dep`)
/// - Component annotations (`@comp`)
/// - Argument passing to constructors
/// - Nesting and expansion from other `Rsx` blocks
///
/// **This macro is not intended for direct use**; use `rsx!` instead.
///
/// # Example expansion
/// ```rust
/// rsx! {
/// static Label { } ("Title")
/// %state Label { inner }
/// "Text"
/// }
/// ```
///
/// Would produce a nested `Rsx` tree of static and dynamic widgets.
#[macro_export]
macro_rules! rsx_inner {
// static
($r:expr, $(@$comp:expr;)* static $s:literal $($rest:tt)*) => {
let w = $crate::widget::StaticWidget::new(Box::new(format!($s)));
$(w.component($comp);)*
$r.create_element_static(w, $crate::frontend::Rsx(Vec::new()));
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr, $(@$comp:expr;)* $(%$dep:ident)* $s:literal $($rest:tt)*) => {
$r.create_element({ $(let $dep = $dep.clone();)* move || {
$crate::widget::WidgetLoad::new(format!($s)) $(.component($comp))*
} }, vec![$(Box::new($dep.clone())),*], $crate::frontend::Rsx(Vec::new()));
$crate::rsx_inner! { $r, $($rest)* };
};
// static
($r:expr, $(@$comp:expr;)* static $name:path, $($f:ident $(: $v:expr)?),+, { $($inner:tt)* } $($rest:tt)*) => {
let w = $crate::widget::StaticWidget::new(Box::new({
let mut elem = <$name>::new();
$(elem.$f = $($v)?;)+
elem
}));
$(w.component($comp);)*
$r.create_element_static(w, $crate::rsx!{ $($inner)* });
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr, $(@$comp:expr;)* static $name:path { $($inner:tt)* } $($rest:tt)*) => {
let w = $crate::widget::StaticWidget::new(Box::new(<$name>::new()));
$(w.component($comp);)*
$r.create_element_static(w, $crate::rsx!{ $($inner)* });
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr, $(@$comp:expr;)* $(%$dep:ident)* $name:path, $($f:ident $(: $v:expr)?),+, { $($inner:tt)* } $($rest:tt)*) => {
$r.create_element({ $(let $dep = $dep.clone();)* move || {
$crate::widget::WidgetLoad::new({
let mut elem = <$name>::new();
$(elem.$f = $($v)?;)+
elem
}) $(.component($comp))*
} }, vec![$(Box::new($dep.clone())),*], $crate::rsx!{ $($inner)* });
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr, $(@$comp:expr;)* $(%$dep:ident)* $name:path { $($inner:tt)* } $($rest:tt)*) => {
$r.create_element({ $(let $dep = $dep.clone();)* move || {
$crate::widget::WidgetLoad::new(<$name>::new()) $(.component($comp))*
} }, vec![$(Box::new($dep.clone())),*], $crate::rsx!{ $($inner)* });
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr, $expand:ident => ($($inner:tt)*) $($rest:tt)*) => {
$r.expand(&mut $expand($($inner)*));
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr,) => {};
}
-11
View File
@@ -1,11 +0,0 @@
mod demos;
use osui::prelude::*;
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension);
demos::app(screen.clone()).draw(&screen);
screen.run()
}
+134
View File
@@ -0,0 +1,134 @@
//! # Rendering Module
//!
//! This module provides low-level rendering primitives and data structures
//! for drawing content to the terminal. It includes geometric primitives
//! (Point, Area, Size) and drawing instructions.
use crate::View;
/// Represents a drawing instruction that can be executed by the rendering engine
#[derive(Clone)]
pub enum DrawInstruction {
/// Draw text at a specific point
Text(Point, String),
/// Render a view within a specified area
View(Area, View),
/// Render a child drawing context at an offset
Child(Point, DrawContext),
}
/// Represents the dimensions of a drawable area
#[derive(Debug, Clone)]
pub struct Size {
/// Width in terminal columns
pub width: u16,
/// Height in terminal rows
pub height: u16,
}
/// Represents a position in 2D space
#[derive(Debug, Clone)]
pub struct Point {
/// X coordinate (column)
pub x: u16,
/// Y coordinate (row)
pub y: u16,
}
/// Represents a rectangular area with position and dimensions
#[derive(Debug, Clone)]
pub struct Area {
/// X coordinate (column) of the top-left corner
pub x: u16,
/// Y coordinate (row) of the top-left corner
pub y: u16,
/// Width in terminal columns
pub width: u16,
/// Height in terminal rows
pub height: u16,
}
/// Context for drawing operations
///
/// Accumulates drawing instructions that are executed by the rendering engine.
/// Tracks allocated space within the drawable area.
#[derive(Clone)]
pub struct DrawContext {
/// The total area available for drawing
pub area: Area,
/// The area that has been allocated for drawing (union of all allocations)
pub allocated: Area,
/// List of drawing instructions to execute
pub drawing: Vec<DrawInstruction>,
}
impl DrawContext {
/// Creates a new DrawContext with the specified area
pub fn new(area: Area) -> Self {
Self {
area,
allocated: Area {
x: 0,
y: 0,
width: 0,
height: 0,
},
drawing: Vec::new(),
}
}
/// Allocates space within the drawable area and returns the allocated area
/// Updates the allocated bounds to include this allocation
pub fn allocate(&mut self, x: u16, y: u16, width: u16, height: u16) -> Area {
self.allocated.x = x;
self.allocated.y = y;
self.allocated.width = width;
self.allocated.height = height;
Area {
x,
y,
width,
height,
}
}
/// Adds a drawing instruction to be executed
pub fn draw(&mut self, inst: DrawInstruction) {
self.drawing.push(inst);
}
/// Draws text at the specified point
pub fn draw_text(&mut self, point: Point, text: &str) {
self.drawing
.push(DrawInstruction::Text(point, text.to_string()));
}
/// Draws a view within the specified area
pub fn draw_view(&mut self, area: Area, view: View) {
self.drawing.push(DrawInstruction::View(area, view));
}
/// Clears drawing
pub fn clear(&mut self) {
self.drawing.clear();
}
}
impl Area {
fn right(&self) -> u16 {
self.x + self.width
}
fn bottom(&self) -> u16 {
self.y + self.height
}
pub fn merge(&mut self, other: &Self) {
self.x = self.x.min(other.x);
self.y = self.y.min(other.y);
self.width = self.right().max(other.right()) - self.x;
self.height = self.bottom().max(other.bottom()) - self.y;
}
}
-300
View File
@@ -1,300 +0,0 @@
//! Rendering logic and layout scope management for OSUI.
//!
//! `RenderScope` is responsible for handling transformations, parent-child dimensions,
//! stacking draw operations, and rendering styled output to the terminal.
//!
//! This module allows widgets to accumulate rendering commands (text, shapes, colors),
//! and then flush them to the screen with correct styling and positioning.
//!
//! Used internally by OSUI's layout and rendering system.
use std::fmt::Debug;
use crate::{
style::{RawTransform, Style, Transform},
utils::{self, hex_ansi_bg},
};
/// Represents a single render instruction.
#[derive(Clone)]
enum RenderMethod {
/// Plain text rendering at current transform.
Text(u16, u16, String),
/// Plain text rendering at current transform with the bg and fg swapped.
TextInverted(u16, u16, String),
/// Text rendered with a specific 24-bit color.
TextColored(u16, u16, String, u32),
/// A filled rectangle of a given size and background color.
Rectangle(u16, u16, u16, u16, u32),
}
/// Stores renderable state and transformation data for a single UI widget.
///
/// `RenderScope` tracks dimensions, styles, and draw commands such as text or
/// background rectangles. It accumulates instructions that are later executed in the `draw` method.
#[derive(Clone)]
pub struct RenderScope {
transform: RawTransform,
render_stack: Vec<RenderMethod>,
parent_width: u16,
parent_height: u16,
style: Style,
}
impl RenderScope {
/// Creates a new, empty `RenderScope`.
pub fn new() -> RenderScope {
RenderScope {
transform: RawTransform::new(),
render_stack: Vec::new(),
parent_width: 0,
parent_height: 0,
style: Style::new(),
}
}
/// Directly sets the raw transform (position and size) for this scope.
pub fn set_transform_raw(&mut self, transform: RawTransform) {
self.transform = transform;
}
/// Applies a `Transform` to this scope, factoring in parent dimensions.
pub fn set_transform(&mut self, transform: &Transform) {
transform.use_dimensions(self.parent_width, self.parent_height, &mut self.transform);
transform.use_position(self.parent_width, self.parent_height, &mut self.transform);
self.transform.px = transform.px;
self.transform.py = transform.py;
}
/// Adds a text draw instruction.
pub fn draw_text(&mut self, x: u16, y: u16, text: &str) {
self.render_stack
.push(RenderMethod::Text(x, y, text.to_string()));
let (w, h) = utils::str_size(text);
self.transform.width = self.transform.width.max(w);
self.transform.height = self.transform.height.max(h);
}
/// Adds a text draw instruction.
pub fn draw_text_inverted(&mut self, x: u16, y: u16, text: &str) {
self.render_stack
.push(RenderMethod::TextInverted(x, y, text.to_string()));
let (w, h) = utils::str_size(text);
self.transform.width = self.transform.width.max(w);
self.transform.height = self.transform.height.max(h);
}
/// Adds a colored text draw instruction.
pub fn draw_text_colored(&mut self, x: u16, y: u16, text: &str, color: u32) {
self.render_stack
.push(RenderMethod::TextColored(x, y, text.to_string(), color));
let (w, h) = utils::str_size(text);
self.transform.width = self.transform.width.max(w);
self.transform.height = self.transform.height.max(h);
}
/// Adds a background rectangle draw instruction.
pub fn draw_rect(&mut self, x: u16, y: u16, width: u16, height: u16, color: u32) {
self.render_stack
.push(RenderMethod::Rectangle(x, y, width, height, color));
self.transform.width = self.transform.width.max(width);
self.transform.height = self.transform.height.max(height);
}
/// Manually ensures a minimum area is allocated.
pub fn use_area(&mut self, width: u16, height: u16) {
self.transform.width = self.transform.width.max(width);
self.transform.height = self.transform.height.max(height);
}
/// Renders the current stack to the terminal.
///
/// This will draw background styles (e.g. solid fill or outline) first,
/// followed by each draw instruction in the stack.
pub fn draw(&self) {
let width = self.transform.width;
let height = self.transform.height;
match self.style.background {
crate::style::Background::NoBackground => {}
crate::style::Background::Outline(c) => {
let width = width + self.transform.px * 2;
let height = height + self.transform.py * 2;
if width > 1 && height > 1 {
let d = "─".repeat(width as usize - 2);
utils::print_liner(
self.transform.x,
self.transform.y,
&utils::hex_ansi(c),
&format!(
"┌{d}┐{}\n└{d}┘",
format!("\n│{}│", " ".repeat(width as usize - 2))
.repeat(height as usize - 2),
),
);
}
}
crate::style::Background::RoundedOutline(c) => {
let width = width + self.transform.px * 2;
let height = height + self.transform.py * 2;
if width > 1 && height > 1 {
let d = "─".repeat(width as usize - 2);
utils::print_liner(
self.transform.x,
self.transform.y,
&utils::hex_ansi(c),
&format!(
"╭{d}╮{}\n╰{d}╯",
format!("\n│{}│", " ".repeat(width as usize - 2))
.repeat(height as usize - 2),
),
);
}
}
crate::style::Background::Solid(c) => utils::print_liner(
self.transform.x,
self.transform.y,
&hex_ansi_bg(c),
&std::iter::repeat(" ".repeat(width as usize))
.take(height as usize)
.collect::<Vec<_>>()
.join("\n"),
),
}
for m in &self.render_stack {
match m {
RenderMethod::Text(x, y, t) => {
if let Some(c) = self.style.foreground {
utils::print_liner(
self.transform.x + self.transform.px + x,
self.transform.y + self.transform.py + y,
&utils::hex_ansi(c),
t,
);
} else {
utils::print(
self.transform.x + self.transform.px,
self.transform.y + self.transform.py,
t,
);
}
}
RenderMethod::TextInverted(x, y, t) => {
if let Some(c) = self.style.foreground {
utils::print_liner(
self.transform.x + self.transform.px + x,
self.transform.y + self.transform.py + y,
&utils::hex_ansi_bg(c),
t,
);
} else {
utils::print(
self.transform.x + self.transform.px,
self.transform.y + self.transform.py,
t,
);
}
}
RenderMethod::TextColored(x, y, t, c) => utils::print_liner(
self.transform.x + self.transform.px + x,
self.transform.y + self.transform.py + y,
&utils::hex_ansi(*c),
t,
),
RenderMethod::Rectangle(x, y, width, height, color) => {
utils::print_liner(
self.transform.x + self.transform.px + x,
self.transform.y + self.transform.py + y,
&hex_ansi_bg(*color),
&std::iter::repeat(" ".repeat(*width as usize))
.take(*height as usize)
.collect::<Vec<_>>()
.join("\n"),
);
}
}
}
}
/// Clears all render instructions and resets the internal state.
pub fn clear(&mut self) {
self.render_stack.clear();
self.transform = RawTransform::new();
self.style = Style::new();
}
/// Gets the currently used width and height.
pub fn get_size(&self) -> (u16, u16) {
(self.transform.width, self.transform.height)
}
/// Returns current size, or defaults if size is zero.
pub fn get_size_or(&self, width: u16, height: u16) -> (u16, u16) {
(
if self.transform.width == 0 {
width
} else {
self.transform.width
},
if self.transform.height == 0 {
height
} else {
self.transform.height
},
)
}
/// Returns size, falling back to parent size if unset.
pub fn get_size_or_parent(&self) -> (u16, u16) {
(
if self.transform.width == 0 {
self.parent_width
} else {
self.transform.width
},
if self.transform.height == 0 {
self.parent_height
} else {
self.transform.height
},
)
}
/// Gets the dimensions of the parent container.
pub fn get_parent_size(&self) -> (u16, u16) {
(self.parent_width, self.parent_height)
}
/// Sets the dimensions of the parent container.
pub fn set_parent_size(&mut self, width: u16, height: u16) {
self.parent_width = width;
self.parent_height = height;
}
/// Returns a mutable reference to the internal raw transform.
pub fn get_transform_mut(&mut self) -> &mut RawTransform {
&mut self.transform
}
/// Returns a reference to the internal raw transform.
pub fn get_transform(&self) -> &RawTransform {
&self.transform
}
/// Sets the style for the current render scope.
pub fn set_style(&mut self, style: Style) {
self.style = style;
}
/// Gets a mutable reference to the style.
pub fn get_style(&mut self) -> &mut Style {
&mut self.style
}
}
impl Debug for RenderScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RenderScope")
}
}
-100
View File
@@ -1,100 +0,0 @@
use std::{
fmt::{Debug, Display, Formatter, Result as FmtResult},
ops::{Deref, DerefMut},
sync::{Arc, Mutex, MutexGuard},
};
/// Trait for tracking dependencies and reactivity.
pub trait DependencyHandler: std::fmt::Debug + Send + Sync {
/// Called when a dependent is registered.
fn add(&self);
/// Returns `true` if the state has changed since the last check.
fn check(&self) -> bool;
}
#[derive(Debug, Clone)]
pub struct State<T> {
inner: Arc<Mutex<Inner<T>>>,
}
#[derive(Debug)]
pub struct Inner<T> {
value: T,
dependencies: usize,
changed: usize,
}
pub fn use_state<T>(v: T) -> State<T> {
State {
inner: Arc::new(Mutex::new(Inner {
value: v,
dependencies: 0,
changed: 0,
})),
}
}
impl<T: Clone> State<T> {
/// Gets the cloned value, recommended for preventing deadlocks
pub fn get_dl(&self) -> T {
self.inner.lock().unwrap().value.clone()
}
}
impl<T> State<T> {
/// Gets a lock on the state for read/write access.
pub fn get(&self) -> MutexGuard<'_, Inner<T>> {
self.inner.lock().unwrap()
}
/// Sets the value and marks it as changed.
pub fn set(&self, v: T) {
let mut inner = self.inner.lock().unwrap();
inner.value = v;
inner.changed = inner.dependencies;
}
/// Marks the state as updated.
pub fn update(&self) {
let mut inner = self.inner.lock().unwrap();
inner.changed = inner.dependencies;
}
}
impl<T: Debug + Send + Sync> DependencyHandler for State<T> {
fn check(&self) -> bool {
let mut inner = self.inner.lock().unwrap();
let i = inner.changed > 0;
if i {
inner.changed -= 1;
}
i
}
fn add(&self) {
let mut inner = self.inner.lock().unwrap();
inner.dependencies += 1;
}
}
impl<T: Display> Display for State<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let inner = self.inner.lock().unwrap();
write!(f, "{}", inner.value)
}
}
impl<T> Deref for Inner<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for Inner<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.changed = self.dependencies;
&mut self.value
}
}
-228
View File
@@ -1,228 +0,0 @@
//! Layout and style definitions for OSUI widgets.
//!
//! This module defines the structures used to manage rendering geometry (`Transform`, `RawTransform`),
//! position and size enums (`Position`, `Dimension`), and styling (`Style`, `Background`, etc.).
//!
//! These types are used internally by components to calculate layout and control their appearance.
use crate::component;
/// Resolved layout information for a widget after layout calculations.
///
/// This struct holds concrete values for position (`x`, `y`), dimensions
/// (`width`, `height`), and padding (`px`, `py`).
#[derive(Debug, Clone)]
pub struct RawTransform {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
pub px: u16,
pub py: u16,
}
/// Horizontal or vertical position relative to a parent container.
#[derive(Debug, Clone)]
pub enum Position {
/// Fixed position in cells from the origin.
Const(u16),
/// Centered in the parent.
Center,
/// Aligned to the end (right or bottom) of the parent.
End,
}
/// Sizing rule for width or height.
#[derive(Debug, Clone)]
pub enum Dimension {
/// Fills the available space from the parent.
Full,
/// Automatically sized to fit content.
Content,
/// Fixed size in cells.
Const(u16),
}
/// Background appearance for a widget.
#[derive(Debug, Clone)]
pub enum Background {
/// Transparent / no background.
NoBackground,
/// Draws a basic outline using the given color.
Outline(u32),
/// Draws a rounded outline using the given color.
RoundedOutline(u32),
/// Fills the background with the specified color.
Solid(u32),
}
component!(Transform {
pub x: Position,
pub y: Position,
pub mx: i32,
pub my: i32,
pub px: u16,
pub py: u16,
pub width: Dimension,
pub height: Dimension,
});
component!(Style {
pub background: Background,
pub foreground: Option<u32>,
});
impl Style {
/// Creates a style with no background or foreground.
pub fn new() -> Self {
Self {
background: Background::NoBackground,
foreground: None,
}
}
}
impl Transform {
/// Creates a default transform with top-left alignment and content sizing.
pub fn new() -> Transform {
Transform {
x: Position::Const(0),
y: Position::Const(0),
mx: 0,
my: 0,
px: 0,
py: 0,
width: Dimension::Content,
height: Dimension::Content,
}
}
/// Shortcut for centering both horizontally and vertically.
pub fn center() -> Transform {
Transform {
x: Position::Center,
y: Position::Center,
mx: 0,
my: 0,
px: 0,
py: 0,
width: Dimension::Content,
height: Dimension::Content,
}
}
/// Aligns the widget to the bottom of its parent.
pub fn bottom(mut self) -> Self {
self.y = Position::End;
self
}
/// Aligns the widget to the right of its parent.
pub fn right(mut self) -> Self {
self.x = Position::End;
self
}
/// Adds margin (offset) from parent edge.
pub fn margin(mut self, x: i32, y: i32) -> Self {
self.mx = x;
self.my = y;
self
}
/// Adds internal spacing (padding) around content.
pub fn padding(mut self, x: u16, y: u16) -> Self {
self.px = x;
self.py = y;
self
}
/// Sets constant dimensions.
pub fn dimensions(mut self, width: u16, height: u16) -> Self {
self.width = Dimension::Const(width);
self.height = Dimension::Const(height);
self
}
/// Resolves `Dimension` rules into absolute values for the given parent size.
pub fn use_dimensions(&self, parent_width: u16, parent_height: u16, raw: &mut RawTransform) {
self.width.use_dimension(parent_width, &mut raw.width);
self.height.use_dimension(parent_height, &mut raw.height);
}
/// Resolves `Position` rules into absolute positions for the given parent size.
pub fn use_position(&self, parent_width: u16, parent_height: u16, raw: &mut RawTransform) {
self.x
.use_position(raw.width, parent_width, self.mx, &mut raw.x);
self.y
.use_position(raw.height, parent_height, self.my, &mut raw.y);
}
}
impl RawTransform {
/// Creates a new transform with all fields set to 0.
pub fn new() -> RawTransform {
RawTransform {
x: 0,
y: 0,
width: 0,
height: 0,
px: 0,
py: 0,
}
}
}
impl Dimension {
/// Applies a dimension rule to determine a final size.
pub fn use_dimension(&self, parent: u16, r: &mut u16) {
match self {
Self::Full => *r = parent,
Self::Content => {}
Self::Const(n) => *r = *n,
}
}
}
impl Position {
/// Applies a position rule to determine a final coordinate, based on size and parent.
pub fn use_position(&self, size: u16, parent: u16, m: i32, r: &mut u16) {
let base = match self {
Self::Center => {
if parent < size {
return;
}
(parent - size) / 2
}
Self::Const(n) => *n,
Self::End => {
if parent < size {
return;
}
parent - size
}
};
let adjusted = if m >= 0 {
base.checked_add(m as u16)
} else {
base.checked_sub((-m) as u16)
};
if let Some(val) = adjusted {
*r = val;
}
}
}
impl From<u16> for Position {
fn from(value: u16) -> Self {
Self::Const(value)
}
}
impl From<u16> for Dimension {
fn from(value: u16) -> Self {
Self::Const(value)
}
}
-122
View File
@@ -1,122 +0,0 @@
//! The `utils` module provides utility functions for terminal manipulation
//! and string operations, designed to enhance terminal-based applications.
use std::io::{self, Write};
/// Clears the terminal screen and moves the cursor to the top-left corner.
///
/// # Returns
/// A `io::Result<()>` indicating whether the operation succeeded.
///
/// # Example
/// ```
/// utils::clear().unwrap();
/// ```
pub fn clear() -> io::Result<()> {
print!("\x1B[2J\x1B[H");
std::io::stdout().flush()
}
/// Hides the terminal cursor.
///
/// # Returns
/// A `io::Result<()>` indicating whether the operation succeeded.
///
/// # Example
/// ```
/// utils::hide_cursor().unwrap();
/// ```
pub fn hide_cursor() -> io::Result<()> {
print!("\x1b[?25l");
std::io::stdout().flush()
}
/// Shows the terminal cursor.
///
/// # Returns
/// A `io::Result<()>` indicating whether the operation succeeded.
///
/// # Example
/// ```
/// utils::show_cursor().unwrap();
/// ```
pub fn show_cursor() -> io::Result<()> {
print!("\x1B[?25h");
std::io::stdout().flush()
}
/// Flushes the terminal's stdout buffer.
///
/// # Returns
/// A `io::Result<()>` indicating whether the operation succeeded.
///
/// # Example
/// ```
/// utils::flush().unwrap();
/// ```
pub fn flush() -> io::Result<()> {
std::io::stdout().flush()
}
/// Calculates the width and height of a string when rendered in a terminal.
///
/// # Arguments
/// - `s`: The input string.
///
/// # Returns
/// A tuple `(u16, u16)` where:
/// - The first value is the maximum width of the string in characters.
/// - The second value is the height of the string in lines.
///
/// # Example
/// ```
/// let (width, height) = utils::str_size("Hello\nWorld!");
/// assert_eq!((5, 2), (width, height));
/// ```
pub fn str_size(s: &str) -> (u16, u16) {
let mut height = 1;
let mut max_width = 0;
let mut current_width = 0;
for b in s.bytes() {
if b == b'\n' {
height += 1;
max_width = max_width.max(current_width);
current_width = 0;
} else {
current_width += 1;
}
}
max_width = max_width.max(current_width);
(max_width, height)
}
pub fn hex_ansi(hex: u32) -> String {
let r = ((hex >> 16) & 0xFF) as u8;
let g = ((hex >> 8) & 0xFF) as u8;
let b = (hex & 0xFF) as u8;
format!("\x1b[38;2;{r};{g};{b}m")
}
pub fn hex_ansi_bg(hex: u32) -> String {
let r = ((hex >> 16) & 0xFF) as u8;
let g = ((hex >> 8) & 0xFF) as u8;
let b = (hex & 0xFF) as u8;
format!("\x1b[48;2;{r};{g};{b}m")
}
pub(crate) fn print(x: u16, y: u16, text: &str) {
for (i, line) in text.lines().enumerate() {
print!("\x1b[{};{}H{line}\x1b[0m", y + i as u16 + 1, x + 1);
flush().unwrap();
}
}
pub(crate) fn print_liner(x: u16, y: u16, liner: &str, text: &str) {
for (i, line) in text.lines().enumerate() {
print!("\x1b[{};{}H{liner}{line}\x1b[0m", y + i as u16 + 1, x + 1);
flush().unwrap();
}
}
+185
View File
@@ -0,0 +1,185 @@
//! # `View` Plugins Module
//!
//! Provides the essential plugins for modifying a `View`
use crate::{
render::{DrawContext, DrawInstruction},
View,
};
/// Horizontally centers the allocated area within the available draw area.
///
/// # Behavior
/// - Modifies `ctx.allocated.x`
/// - Does **not** modify height or width
/// - Assumes `ctx.allocated.width <= ctx.area.width`
///
/// # Notes
/// This function only affects positioning, not sizing.
pub fn x_center(ctx: &mut DrawContext, _view: &View) {
ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2;
}
/// Vertically centers the allocated area within the available draw area.
///
/// # Behavior
/// - Modifies `ctx.allocated.y`
/// - Does **not** modify width or height
/// - Assumes `ctx.allocated.height <= ctx.area.height`
///
/// # Typical usage
/// Called after size has been resolved (e.g. after `size_auto`,
/// `height_auto`, or a fixed height has been set).
pub fn y_center(ctx: &mut DrawContext, _view: &View) {
ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2;
}
/// Centers the allocated area both horizontally and vertically.
///
/// # Behavior
/// - Modifies `ctx.allocated.x` and `ctx.allocated.y`
/// - Does **not** modify width or height
///
/// # Order
/// Should generally be called **after** size resolution
/// (e.g. `size_auto`, `width_auto`, `height_auto`).
pub fn center(ctx: &mut DrawContext, _view: &View) {
ctx.allocated.x = (ctx.area.width - ctx.allocated.width) / 2;
ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2;
}
/// Automatically computes both width and height based on drawn content.
///
/// # Sizing rules
/// - `Text`:
/// - Width = longest line length
/// - Height = number of lines
/// - `View`:
/// - Recursively executes the view in a fresh `DrawContext`
/// - Uses the child view's allocated size
/// - `Child`: ignored
///
/// # Notes
/// - This function performs a layout pass only.
/// - It does not draw anything.
/// - Later instructions may overwrite the computed size.
pub fn size_auto(ctx: &mut DrawContext, _view: &View) {
for i in &ctx.drawing {
match i {
DrawInstruction::Text(_, text) => {
let mut height = 0;
for line in text.lines() {
ctx.allocated.width = ctx.allocated.width.max(line.len() as u16);
height += 1;
}
ctx.allocated.height = height;
}
DrawInstruction::View(area, view) => {
let mut c = DrawContext::new(area.clone());
view(&mut c);
size_auto(&mut c, view);
ctx.allocated.width = c.allocated.width;
ctx.allocated.height = c.allocated.height;
}
DrawInstruction::Child(_, _) => {}
}
}
}
/// Automatically computes both width and height based on drawn content.
/// Merges the allocation for multiple drawings.
pub fn size_auto_merge(ctx: &mut DrawContext, _view: &View) {
ctx.allocated.x = u16::MAX;
ctx.allocated.y = u16::MAX;
ctx.allocated.width = 0;
ctx.allocated.height = 0;
for i in &ctx.drawing {
match i {
DrawInstruction::Text(_, text) => {
let mut height = 0;
for line in text.lines() {
ctx.allocated.width = ctx.allocated.width.max(line.len() as u16);
height += 1;
}
ctx.allocated.height = height;
}
DrawInstruction::View(area, view) => {
let mut c = DrawContext::new(area.clone());
view(&mut c);
size_auto(&mut c, view);
ctx.allocated.merge(&c.allocated);
}
DrawInstruction::Child(_, _) => {}
}
}
}
/// Automatically computes width based on drawn content.
///
/// # Sizing rules
/// - `Text`: width is the longest line
/// - `View`: width is taken from the child view's auto-sized result
/// - Height is left unchanged
///
/// # When to use
/// Useful when height is fixed or externally constrained.
pub fn width_auto(ctx: &mut DrawContext, _view: &View) {
for i in &ctx.drawing {
match i {
DrawInstruction::Text(_, text) => {
for line in text.lines() {
ctx.allocated.width = ctx.allocated.width.max(line.len() as u16);
}
}
DrawInstruction::View(area, view) => {
let mut c = DrawContext::new(area.clone());
view(&mut c);
size_auto(&mut c, view);
ctx.allocated.width = c.allocated.width;
}
DrawInstruction::Child(_, _) => {}
}
}
}
/// Automatically computes height based on drawn content.
///
/// # Sizing rules
/// - `Text`: height is the number of lines
/// - `View`: height is taken from the child view's auto-sized result
/// - Width is left unchanged
///
/// # Notes
/// This pass ignores line width and wrapping concerns.
pub fn height_auto(ctx: &mut DrawContext, _view: &View) {
for i in &ctx.drawing {
match i {
DrawInstruction::Text(_, text) => {
let mut height = 0;
for _ in text.lines() {
height += 1;
}
ctx.allocated.height = height;
}
DrawInstruction::View(area, view) => {
let mut c = DrawContext::new(area.clone());
view(&mut c);
size_auto(&mut c, view);
ctx.allocated.height = c.allocated.height;
}
DrawInstruction::Child(_, _) => {}
}
}
}
pub fn redraw(ctx: &mut DrawContext, view: &View) {
ctx.clear();
ctx.draw_view(ctx.allocated.clone(), view.clone());
}
-362
View File
@@ -1,362 +0,0 @@
//! Core widget infrastructure for OSUI.
//!
//! This module defines the traits and types that power the widget system, including:
//! - `Element` and `Component`: building blocks for renderable and state-carrying objects
//! - `Widget`: container type for wrapping elements and components
//! - `StaticWidget` and `DynWidget`: concrete widget implementations
//! - Dependency tracking and reactive updates for dynamic widgets
use std::{
any::{Any, TypeId},
collections::HashMap,
sync::{Arc, Mutex, MutexGuard},
};
use crate::{
prelude::{Event, Handler},
render_scope::RenderScope,
state::DependencyHandler,
};
/// A trait object for any renderable UI element.
pub type BoxedElement = Box<dyn Element + Send + Sync>;
/// A trait object for any component attached to a widget.
pub type BoxedComponent = Box<dyn Component + Send + Sync>;
/// Core trait for anything that can be rendered in the UI.
///
/// Elements are responsible for their own rendering logic and can define hooks
/// for lifecycle events and child rendering.
pub trait Element: Send + Sync {
/// Called to perform rendering for the element.
#[allow(unused)]
fn render(&mut self, scope: &mut RenderScope) {}
/// Called after rendering, for follow-up logic or cleanup.
#[allow(unused)]
fn after_render(&mut self, scope: &mut RenderScope) {}
/// Called to draw child widgets, if any.
#[allow(unused)]
fn draw_child(&mut self, element: &Arc<Widget>) {}
#[allow(unused)]
fn event(&mut self, event: &dyn Event) {}
/// Returns a type-erased reference to this object.
fn as_any(&self) -> &dyn Any;
/// Returns a mutable type-erased reference to this object.
fn as_any_mut(&mut self) -> &mut dyn Any;
}
/// Optional trait for state or metadata attached to widgets.
///
/// Components can be used to store data such as layout style,
/// animation state, bindings, or other logic.
pub trait Component: Send + Sync {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
/// Container for a widget during initial construction.
///
/// Holds the root element and any associated components.
pub struct WidgetLoad(BoxedElement, HashMap<TypeId, BoxedComponent>);
/// A widget with fixed content and no dynamic behavior.
pub struct StaticWidget(Mutex<BoxedElement>, Mutex<HashMap<TypeId, BoxedComponent>>);
/// A widget with dynamic content and dependency tracking.
///
/// This widget supports reactive updates and can be rebuilt using
/// a provided `FnMut()` function when dependencies change.
pub struct DynWidget(
Mutex<BoxedElement>,
Mutex<HashMap<TypeId, BoxedComponent>>,
Mutex<Box<dyn FnMut() -> WidgetLoad + Send + Sync>>,
Mutex<Vec<Box<dyn DependencyHandler>>>,
Mutex<Option<Box<dyn FnMut(WidgetLoad) -> WidgetLoad + Send + Sync>>>,
);
/// A reference-counted wrapper around either a static or dynamic widget.
///
/// Use `Arc<Widget>` as the standard way to store and pass around widgets in the UI tree.
pub enum Widget {
Static(StaticWidget),
Dynamic(DynWidget),
}
impl WidgetLoad {
/// Creates a new `WidgetLoad` with a given root element.
pub fn new<E: Element + 'static>(e: E) -> Self {
Self(Box::new(e), HashMap::new())
}
/// Attaches a component if one of its type doesn't already exist.
pub fn component<C: Component + 'static>(mut self, c: C) -> Self {
self.1.entry(c.type_id()).or_insert_with(|| Box::new(c));
self
}
/// Replaces any existing component of the same type.
pub fn set_component<C: Component + 'static>(mut self, c: C) -> Self {
self.1.insert(c.type_id(), Box::new(c));
self
}
/// Attempts to retrieve a component of the given type.
pub fn get<C: Component + 'static + Clone>(&self) -> Option<C> {
self.1
.get(&TypeId::of::<C>())
.and_then(|c| c.as_any().downcast_ref::<C>())
.map(|c| c.clone())
}
}
impl Widget {
pub fn new_static(e: BoxedElement) -> Self {
Self::Static(StaticWidget(Mutex::new(e), Mutex::new(HashMap::new())))
}
pub fn new_dyn<F: FnMut() -> WidgetLoad + 'static + Send + Sync>(mut e: F) -> Self {
let wl = e();
Self::Dynamic(DynWidget(
Mutex::new(wl.0),
Mutex::new(wl.1),
Mutex::new(Box::new(e)),
Mutex::new(Vec::new()),
Mutex::new(None),
))
}
}
impl Widget {
pub fn get_elem(&self) -> MutexGuard<BoxedElement> {
match self {
Widget::Static(w) => w.get_elem(),
Widget::Dynamic(w) => w.get_elem(),
}
}
pub fn after_render(&self) {
match self {
Widget::Static(w) => w.after_render(),
Widget::Dynamic(w) => w.after_render(),
}
}
pub fn component<C: Component + 'static>(self: &Arc<Self>, c: C) -> &Arc<Self> {
match &**self {
Widget::Static(w) => {
w.component(c);
}
Widget::Dynamic(w) => {
w.component(c);
}
}
self
}
pub fn set_component<C: Component + 'static>(self: &Arc<Self>, c: C) -> &Arc<Self> {
match &**self {
Widget::Static(w) => {
w.set_component(c);
}
Widget::Dynamic(w) => {
w.set_component(c);
}
}
self
}
pub fn get<C: Component + 'static + Clone>(&self) -> Option<C> {
match self {
Widget::Static(w) => w.get(),
Widget::Dynamic(w) => w.get(),
}
}
pub fn inject<F: FnMut(WidgetLoad) -> WidgetLoad + 'static + Send + Sync>(
self: &Arc<Self>,
mut f: F,
) {
match &**self {
Widget::Dynamic(w) => {
w.inject(f);
}
Widget::Static(w) => {
let wl = WidgetLoad::new(String::new());
w.1.lock().unwrap().extend(f(wl).1);
}
}
}
pub fn refresh(self: &Arc<Self>) {
match &**self {
Widget::Dynamic(w) => {
w.refresh();
}
Widget::Static(_) => {}
}
}
pub fn auto_refresh(self: &Arc<Self>) {
match &**self {
Widget::Dynamic(w) => {
w.auto_refresh();
}
Widget::Static(_) => {}
}
}
pub fn dependency<D: DependencyHandler + 'static>(self: &Arc<Self>, d: D) -> &Arc<Self> {
match &**self {
Widget::Dynamic(w) => {
w.dependency(d);
}
Widget::Static(_) => {}
}
self
}
pub fn dependency_box(self: &Arc<Self>, d: Box<dyn DependencyHandler>) -> &Arc<Self> {
match &**self {
Widget::Dynamic(w) => {
w.dependency_box(d);
}
Widget::Static(_) => {}
}
self
}
pub fn event<E: Event + Clone + 'static>(self: &Arc<Self>, e: &E) {
if let Some(wrapper) = self.get::<Handler<E>>() {
wrapper.call(self, e);
}
match &**self {
Widget::Dynamic(w) => {
w.get_elem().event(e);
}
Widget::Static(w) => {
w.get_elem().event(e);
}
}
}
}
impl StaticWidget {
fn after_render(&self) {}
fn get_elem(&self) -> MutexGuard<BoxedElement> {
self.0.lock().unwrap()
}
}
impl StaticWidget {
pub fn new(e: BoxedElement) -> Self {
Self(Mutex::new(e), Mutex::new(HashMap::new()))
}
pub fn component<C: Component + 'static>(&self, c: C) {
self.1
.lock()
.unwrap()
.entry(c.type_id())
.or_insert_with(|| Box::new(c));
}
pub fn set_component<C: Component + 'static>(&self, c: C) {
self.1.lock().unwrap().insert(c.type_id(), Box::new(c));
}
pub fn get<C: Component + 'static + Clone>(&self) -> Option<C> {
self.1
.lock()
.unwrap()
.get(&TypeId::of::<C>())
.and_then(|c| c.as_any().downcast_ref::<C>())
.map(Clone::clone)
}
}
impl DynWidget {
fn after_render(&self) {}
fn get_elem(&self) -> MutexGuard<BoxedElement> {
self.0.lock().unwrap()
}
}
impl DynWidget {
pub fn new<F: FnMut() -> WidgetLoad + 'static + Send + Sync>(mut e: F) -> Self {
let wl = e();
Self(
Mutex::new(wl.0),
Mutex::new(wl.1),
Mutex::new(Box::new(e)),
Mutex::new(Vec::new()),
Mutex::new(None),
)
}
/// Replace or modify the widget's structure on reload and init.
pub fn inject<F: FnMut(WidgetLoad) -> WidgetLoad + 'static + Send + Sync>(&self, f: F) {
*self.4.lock().unwrap() = Some(Box::new(f));
self.refresh();
}
/// Rebuild the widget's content by re-evaluating the original function.
pub fn refresh(&self) {
let mut w = (self.2.lock().unwrap())();
if let Some(we) = &mut *self.4.lock().unwrap() {
w = (we)(w);
}
*self.0.lock().unwrap() = w.0;
*self.1.lock().unwrap() = w.1;
}
/// Re-evaluates the widget if any dependency has changed.
pub fn auto_refresh(&self) {
for d in self.3.lock().unwrap().iter() {
if d.check() {
self.refresh();
}
}
}
/// Adds a dependency to this widget.
pub fn dependency<D: DependencyHandler + 'static>(&self, d: D) {
d.add();
self.3.lock().unwrap().push(Box::new(d));
}
/// Adds a boxed dependency.
pub fn dependency_box(&self, d: Box<dyn DependencyHandler>) {
d.add();
self.3.lock().unwrap().push(d);
}
pub fn component<C: Component + 'static>(&self, c: C) {
self.1
.lock()
.unwrap()
.entry(c.type_id())
.or_insert_with(|| Box::new(c));
}
pub fn set_component<C: Component + 'static>(&self, c: C) {
self.1.lock().unwrap().insert(c.type_id(), Box::new(c));
}
pub fn get<C: Component + 'static + Clone>(&self) -> Option<C> {
self.1
.lock()
.unwrap()
.get(&TypeId::of::<C>())
.and_then(|c| c.as_any().downcast_ref::<C>())
.map(Clone::clone)
}
}