54 Commits
Author SHA1 Message Date
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
25 changed files with 1878 additions and 548 deletions
+6 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "osui" name = "osui"
version = "0.1.1" 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"
@@ -23,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
@@ -42,3 +44,6 @@ overflow-checks = false
lto = true lto = true
panic = 'abort' panic = 'abort'
codegen-units = 1 codegen-units = 1
[workspace]
members = [".", "macros"]
+10 -8
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");
rsx! {
"Hello, World"
} }
.draw(&screen);
screen.run() #[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
"Hello World"
}
.view(&cx)
} }
``` ```
+227
View File
@@ -0,0 +1,227 @@
Iterx72,Nestingx72,Time µs
14,14,11964
0,1,1078
5,8,1818
10,13,2632
14,2,3392
0,9,520
9,3,2360
10,5,2551
10,11,2548
6,8,1738
12,12,2922
0,8,601
12,4,2919
0,11,521
6,4,1942
11,7,2741
9,4,2496
5,9,1764
13,9,3069
2,5,1053
5,11,1790
14,1,3581
5,0,962
0,3,845
1,3,868
7,8,2013
12,3,2987
9,8,2328
10,7,2533
11,1,2766
7,1,1938
0,5,687
6,9,1830
10,14,2628
6,12,1786
4,0,835
4,14,2464
10,1,2633
12,7,2948
2,9,1066
4,6,2646
7,11,1999
5,13,1748
13,4,3110
13,13,3094
3,5,1166
11,12,2745
1,2,814
9,5,2419
2,11,1117
1,13,835
11,11,2759
1,12,882
9,1,2457
0,6,680
5,12,1794
9,11,2326
10,9,2546
10,10,2604
13,2,3250
13,7,3113
7,10,1959
1,14,833
6,6,1886
14,7,3339
6,7,1824
11,6,2756
12,6,3007
14,6,3315
4,3,11464
2,14,924
7,12,1966
11,14,2817
9,10,2367
1,11,827
8,14,2184
14,0,643
11,3,2781
2,4,1123
12,8,2950
13,6,3160
3,13,1289
7,7,1989
3,9,1142
3,6,1168
4,8,2788
14,12,3346
1,1,805
5,10,1781
7,13,1988
5,5,2015
0,14,520
11,8,2890
3,10,1140
13,10,3235
3,14,1185
8,10,2183
12,10,2896
2,8,1072
9,6,2358
13,1,3066
3,8,1133
11,10,2758
4,13,2550
1,0,643
4,2,5482
3,7,1155
1,4,726
3,3,1192
4,11,2576
8,0,619
9,2,2444
10,3,2581
4,5,5400
5,3,2444
1,10,804
10,2,2563
0,7,592
0,2,802
1,5,741
2,7,984
10,0,755
13,14,3112
5,14,1811
8,12,2180
6,11,1723
4,7,2897
10,8,2536
1,7,813
6,0,699
8,2,2187
5,7,1811
14,8,3304
2,0,661
8,13,2185
1,9,786
3,0,623
12,5,3012
8,1,2203
8,11,2285
14,10,3413
4,10,2613
11,5,2729
6,1,2035
12,1,2891
5,4,2036
2,3,1083
14,3,3312
3,2,1278
5,2,2392
8,7,2151
5,6,2072
3,12,1230
2,10,954
2,13,932
13,8,3128
0,12,521
11,4,2769
7,2,1947
4,1,1802
8,8,2163
7,3,1966
13,3,3207
1,8,746
12,9,2955
10,12,2570
9,9,2388
4,12,2566
7,0,619
7,9,2004
13,0,607
14,9,3355
3,4,1154
11,2,2753
11,13,2739
6,13,1735
7,6,1983
8,3,2166
7,14,1958
6,2,2101
13,5,3221
8,4,2120
10,4,2716
14,5,3264
14,13,3299
11,9,2725
13,11,3132
1,6,809
2,2,1155
4,4,2242
8,6,2218
12,0,645
7,4,1925
2,6,1054
9,13,2340
13,12,3112
14,4,3334
3,11,1139
12,14,3004
4,9,2649
0,13,527
12,11,2976
0,0,1559
12,13,2952
6,10,1749
14,11,3365
11,0,647
2,12,1024
10,6,2562
8,5,2227
9,12,2363
2,1,1023
0,10,528
6,3,2005
8,9,2195
7,5,1966
6,14,1755
9,0,608
14,14,3389
9,7,2391
0,4,688
5,1,2574
9,14,2353
12,2,2975
3,1,1276
6,5,1804
1 Iterx72 Nestingx72 Time µs
2 14 14 11964
3 0 1 1078
4 5 8 1818
5 10 13 2632
6 14 2 3392
7 0 9 520
8 9 3 2360
9 10 5 2551
10 10 11 2548
11 6 8 1738
12 12 12 2922
13 0 8 601
14 12 4 2919
15 0 11 521
16 6 4 1942
17 11 7 2741
18 9 4 2496
19 5 9 1764
20 13 9 3069
21 2 5 1053
22 5 11 1790
23 14 1 3581
24 5 0 962
25 0 3 845
26 1 3 868
27 7 8 2013
28 12 3 2987
29 9 8 2328
30 10 7 2533
31 11 1 2766
32 7 1 1938
33 0 5 687
34 6 9 1830
35 10 14 2628
36 6 12 1786
37 4 0 835
38 4 14 2464
39 10 1 2633
40 12 7 2948
41 2 9 1066
42 4 6 2646
43 7 11 1999
44 5 13 1748
45 13 4 3110
46 13 13 3094
47 3 5 1166
48 11 12 2745
49 1 2 814
50 9 5 2419
51 2 11 1117
52 1 13 835
53 11 11 2759
54 1 12 882
55 9 1 2457
56 0 6 680
57 5 12 1794
58 9 11 2326
59 10 9 2546
60 10 10 2604
61 13 2 3250
62 13 7 3113
63 7 10 1959
64 1 14 833
65 6 6 1886
66 14 7 3339
67 6 7 1824
68 11 6 2756
69 12 6 3007
70 14 6 3315
71 4 3 11464
72 2 14 924
73 7 12 1966
74 11 14 2817
75 9 10 2367
76 1 11 827
77 8 14 2184
78 14 0 643
79 11 3 2781
80 2 4 1123
81 12 8 2950
82 13 6 3160
83 3 13 1289
84 7 7 1989
85 3 9 1142
86 3 6 1168
87 4 8 2788
88 14 12 3346
89 1 1 805
90 5 10 1781
91 7 13 1988
92 5 5 2015
93 0 14 520
94 11 8 2890
95 3 10 1140
96 13 10 3235
97 3 14 1185
98 8 10 2183
99 12 10 2896
100 2 8 1072
101 9 6 2358
102 13 1 3066
103 3 8 1133
104 11 10 2758
105 4 13 2550
106 1 0 643
107 4 2 5482
108 3 7 1155
109 1 4 726
110 3 3 1192
111 4 11 2576
112 8 0 619
113 9 2 2444
114 10 3 2581
115 4 5 5400
116 5 3 2444
117 1 10 804
118 10 2 2563
119 0 7 592
120 0 2 802
121 1 5 741
122 2 7 984
123 10 0 755
124 13 14 3112
125 5 14 1811
126 8 12 2180
127 6 11 1723
128 4 7 2897
129 10 8 2536
130 1 7 813
131 6 0 699
132 8 2 2187
133 5 7 1811
134 14 8 3304
135 2 0 661
136 8 13 2185
137 1 9 786
138 3 0 623
139 12 5 3012
140 8 1 2203
141 8 11 2285
142 14 10 3413
143 4 10 2613
144 11 5 2729
145 6 1 2035
146 12 1 2891
147 5 4 2036
148 2 3 1083
149 14 3 3312
150 3 2 1278
151 5 2 2392
152 8 7 2151
153 5 6 2072
154 3 12 1230
155 2 10 954
156 2 13 932
157 13 8 3128
158 0 12 521
159 11 4 2769
160 7 2 1947
161 4 1 1802
162 8 8 2163
163 7 3 1966
164 13 3 3207
165 1 8 746
166 12 9 2955
167 10 12 2570
168 9 9 2388
169 4 12 2566
170 7 0 619
171 7 9 2004
172 13 0 607
173 14 9 3355
174 3 4 1154
175 11 2 2753
176 11 13 2739
177 6 13 1735
178 7 6 1983
179 8 3 2166
180 7 14 1958
181 6 2 2101
182 13 5 3221
183 8 4 2120
184 10 4 2716
185 14 5 3264
186 14 13 3299
187 11 9 2725
188 13 11 3132
189 1 6 809
190 2 2 1155
191 4 4 2242
192 8 6 2218
193 12 0 645
194 7 4 1925
195 2 6 1054
196 9 13 2340
197 13 12 3112
198 14 4 3334
199 3 11 1139
200 12 14 3004
201 4 9 2649
202 0 13 527
203 12 11 2976
204 0 0 1559
205 12 13 2952
206 6 10 1749
207 14 11 3365
208 11 0 647
209 2 12 1024
210 10 6 2562
211 8 5 2227
212 9 12 2363
213 2 1 1023
214 0 10 528
215 6 3 2005
216 8 9 2195
217 7 5 1966
218 6 14 1755
219 9 0 608
220 14 14 3389
221 9 7 2391
222 0 4 688
223 5 1 2574
224 9 14 2353
225 12 2 2975
226 3 1 1276
227 6 5 1804
+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)
}
}
+8 -62
View File
@@ -1,68 +1,14 @@
use std::sync::Arc; use osui::prelude::*;
use osui::{prelude::*, rsx}; pub fn main() {
let engine = Console::new();
pub struct Count(pub State<u16>); engine.run(App {}).expect("Failed to run engine");
fn main() {
let console = Console::new();
console.run(app);
} }
fn app(cx: &Arc<Context>) -> View { #[component]
let count = use_state(0); fn App(cx: &Arc<Context>) -> View {
let mount = use_mount_manual();
// Sync state with children
use_sync_effect(cx, &count, |v| Count(v.clone()), &[&mount, &count]);
use_effect(
{
let count = count.clone();
move || loop {
std::thread::sleep(std::time::Duration::from_millis(500));
let mut count = count.get();
if *count > 5 {
*count = 0;
continue;
}
*count += 1;
}
},
&[&mount],
);
rsx! { rsx! {
// Static scope "Hello World"
my_component (ctx, view) {
let area = ctx.allocate(5, 0, 10, 10);
ctx.draw_view(area, view);
} }
.view(&cx)
// Dynamic scope
if %count (*count.get() > 5) {
my_component (ctx, view) {
let area = ctx.allocate(18, 0, 10, 10);
ctx.draw_view(area, view);
}
}
// Dynamic scope
for %count (i in 0..count.get_dl()) {
"{i}" @Point { x: 0, y: i };
}
!mount mount
}
.view(cx.clone())
}
fn my_component(cx: &Arc<Context>) -> View {
// Sync state with parent
let count = use_sync_state(cx, 0, |Count(v)| v.get_dl());
rsx! {
"Count: {count}"
}
.view(cx.clone())
} }
+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"
+183
View File
@@ -0,0 +1,183 @@
//! # 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().map(|d| {
let ident = &d.ident;
quote! {
std::sync::Arc::new(#ident) as std::sync::Arc<dyn HookDependency>
}
});
quote! {
vec![ #(#deps),* ]
}
}
/// Emits code for a single node within a scope
fn emit_node_scope(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Text(_) => {
let emit = emit_node(node);
quote! {
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) => quote! {
scope.view(Arc::new(move |ctx| {
ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text))
}));
},
RsxNode::Component {
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,)*
}
}
};
quote! {
scope.child(#component_expr, None);
}
}
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()
}
+241
View File
@@ -0,0 +1,241 @@
//! # RSX Parser
//!
//! Parses RSX syntax into an AST that can be emitted as Rust code.
use syn::braced;
use syn::parse::discouraged::Speculative;
use syn::{
Expr, Ident, LitStr, Pat, Path, Result, Token,
parse::{Parse, ParseStream},
token::Brace,
};
/// 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(LitStr),
/// Expression node: `{expr}`
Expr(Expr),
/// Component instantiation: `Component { prop: value, ... }`
Component {
/// 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>,
}
fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
let mut deps = Vec::new();
if input.peek(Token![%]) {
input.parse::<Token![%]>()?;
loop {
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 });
if !input.peek(Token![,]) {
break;
}
input.parse::<Token![,]>()?;
}
}
Ok(deps)
}
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,
});
}
// "%$dep for $pat in $expr { $rsx }"
if input.peek(LitStr) {
return Ok(RsxNode::Text(input.parse()?));
}
parse_component_invocation(input)
}
}
/// Parses Path or Path { ... } into RsxNode::Component.
fn parse_component_invocation(input: ParseStream) -> Result<RsxNode> {
let path: Path = input.parse()?;
if !input.peek(Brace) {
return Ok(RsxNode::Component {
path,
props: Vec::new(),
children: Vec::new(),
});
}
let content;
braced!(content in input);
let (props, children) = parse_props_and_children(&content)?;
Ok(RsxNode::Component {
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)
}
-199
View File
@@ -1,199 +0,0 @@
use std::{
any::{Any, TypeId},
collections::HashMap,
sync::{Arc, Mutex},
};
use crate::{
render::DrawContext,
state::{use_effect, HookDependency},
View, ViewWrapper,
};
pub type Component = Arc<dyn Fn(&Arc<Context>) -> View + Send + Sync>;
pub type EventHandler = Arc<Mutex<dyn FnMut(&Arc<Context>, &dyn Any) + Send + Sync>>;
pub struct Scope {
pub children: Mutex<Vec<(Arc<Context>, Option<ViewWrapper>)>>,
}
pub struct Context {
component: Mutex<Component>,
event_handlers: Mutex<HashMap<TypeId, Vec<EventHandler>>>,
view: Mutex<View>,
pub(crate) scopes: Mutex<Vec<Arc<Scope>>>,
}
impl Context {
pub fn new<F: Fn(&Arc<Self>) -> View + Send + Sync + 'static>(component: F) -> Arc<Self> {
Arc::new(Self {
component: Mutex::new(Arc::new(component)),
event_handlers: Mutex::new(HashMap::new()),
view: Mutex::new(Arc::new(|_| {})),
scopes: Mutex::new(Vec::new()),
})
}
pub fn refresh(self: &Arc<Self>) {
let s = self.clone();
std::thread::spawn({
move || {
s.event_handlers.lock().unwrap().clear();
let c = s.component.lock().unwrap().clone();
*s.view.lock().unwrap() = (c)(&s);
}
});
}
pub fn refresh_atomic(self: &Arc<Self>) -> Arc<Mutex<bool>> {
let s = self.clone();
let done = Arc::new(Mutex::new(false));
std::thread::spawn({
let done = done.clone();
move || {
s.event_handlers.lock().unwrap().clear();
let c = s.component.lock().unwrap().clone();
*s.view.lock().unwrap() = (c)(&s);
*done.lock().unwrap() = true;
}
});
done
}
pub fn get_view(self: &Arc<Self>) -> View {
self.view.lock().unwrap().clone()
}
pub fn on_event<T: Any + 'static, F: Fn(&Arc<Self>, &T) + Send + Sync + 'static>(
self: &Arc<Self>,
handler: F,
) {
self.event_handlers
.lock()
.unwrap()
.entry(TypeId::of::<T>())
.or_insert_with(|| Vec::new())
.push(Arc::new(Mutex::new(
move |ctx: &Arc<Self>, event: &dyn Any| {
if let Some(e) = event.downcast_ref::<T>() {
(handler)(ctx, e);
}
},
)));
}
pub fn emit_event<E: Any + 'static>(self: &Arc<Self>, event: &E) {
if let Some(v) = self.event_handlers.lock().unwrap().get(&TypeId::of::<E>()) {
for i in v {
(i.lock().unwrap())(self, event);
}
}
for scope in self.scopes.lock().unwrap().iter() {
for (child, _) in scope.children.lock().unwrap().iter() {
child.emit_event(event);
}
}
}
pub fn emit_event_threaded<E: Any + Send + Sync + Clone + 'static>(
self: &Arc<Self>,
event: &E,
) {
if let Some(v) = self.event_handlers.lock().unwrap().get(&TypeId::of::<E>()) {
for i in v {
let i = i.clone();
let event = event.clone();
let s = self.clone();
std::thread::spawn(move || {
(i.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);
}
}
}
pub fn scope(self: &Arc<Self>) -> Arc<Scope> {
let scope = Arc::new(Scope {
children: Mutex::new(Vec::new()),
});
self.scopes.lock().unwrap().push(scope.clone());
scope
}
pub fn dyn_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(
self: &Arc<Self>,
drawer: F,
dependencies: &[&dyn HookDependency],
) -> Arc<Scope> {
let scope = Arc::new(Scope {
children: Mutex::new(Vec::new()),
});
self.scopes.lock().unwrap().push(scope.clone());
drawer(&scope);
use_effect(
{
let scope = scope.clone();
move || {
drawer(&scope);
}
},
dependencies,
);
scope
}
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);
}
}
}
}
}
impl Scope {
pub fn new() -> Arc<Self> {
Arc::new(Self {
children: Mutex::new(Vec::new()),
})
}
pub fn child<F: Fn(&Arc<Context>) -> View + Send + Sync + 'static>(
self: &Arc<Self>,
child: F,
view_wrapper: Option<ViewWrapper>,
) {
let ctx = Context::new(child);
ctx.refresh();
self.children.lock().unwrap().push((ctx, view_wrapper));
}
pub fn view(self: &Arc<Self>, view: View) {
let ctx = Context::new(move |_| view.clone());
ctx.refresh();
self.children.lock().unwrap().push((ctx, None));
}
}
+249
View File
@@ -0,0 +1,249 @@
//! # 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},
render::DrawContext,
state::{use_effect, HookDependency},
View,
};
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);
}
/// 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)
}
}
+44
View File
@@ -0,0 +1,44 @@
//! # 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 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)
}
}
+56
View File
@@ -0,0 +1,56 @@
//! # 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));
}
}
-87
View File
@@ -1,87 +0,0 @@
use std::{
io::{stdout, Write},
sync::{Arc, Mutex},
};
use crossterm::{cursor::MoveTo, execute, terminal::Clear};
use crate::{prelude::Context, render::Area, DrawContext, View};
pub trait Engine {
fn render_view(&self, area: &Area, view: &View) -> DrawContext;
fn draw_context(&self, ctx: &DrawContext);
}
pub struct Console {
threads: Mutex<Vec<Arc<dyn Fn(Arc<Context>) + Send + Sync>>>,
}
impl Console {
pub fn new() -> Self {
Self {
threads: Mutex::new(Vec::new()),
}
}
pub fn thread<F: Fn(Arc<Context>) + Send + Sync + 'static>(&self, run: F) {
self.threads.lock().unwrap().push(Arc::new(run));
}
pub fn run<F: Fn(&Arc<Context>) -> View + Send + Sync + 'static>(&self, component: F) {
let cx = Context::new(component);
cx.refresh();
for thread in self.threads.lock().unwrap().iter() {
let thread = thread.clone();
std::thread::spawn({
let cx = cx.clone();
move || thread(cx)
});
}
loop {
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(),
));
std::thread::sleep(std::time::Duration::from_millis(16));
}
}
}
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);
execute!(stdout(), MoveTo(x, y),).unwrap();
print!("{text}");
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))
}
}
}
}
}
+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
}
}
+153
View File
@@ -0,0 +1,153 @@
//! # 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);
execute!(stdout(), MoveTo(x, y),).unwrap();
print!("{text}");
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<()>;
}
+80 -16
View File
@@ -1,43 +1,84 @@
//! # Frontend Module
//!
//! Provides the RSX (React-like Syntax) system for composing components.
//! This module defines the structure for building component hierarchies
//! with static and dynamic scopes, similar to React's JSX.
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::component::{context::Context, scope::Scope};
component::{Context, Scope}, use crate::{render::Point, state::HookDependency, View};
state::HookDependency,
View,
};
pub enum RsxScope { /// Trait for converting values to RSX
Static(Arc<Scope>), pub trait ToRsx {
Dynamic( /// Convert to RSX representation
Arc<dyn Fn(&Arc<Scope>) + Send + Sync>, fn to_rsx(&self) -> Rsx;
Vec<Box<dyn HookDependency>>,
),
} }
/// Scope types for RSX components
#[derive(Clone)]
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>); pub struct Rsx(Vec<RsxScope>);
impl Rsx { impl Rsx {
/// Creates a new empty RSX
pub fn new() -> Self { pub fn new() -> Self {
Self(Vec::new()) Self(Vec::new())
} }
pub fn static_scope(&mut self, scope: Arc<Scope>) { /// Adds a static scope to this RSX
self.0.push(RsxScope::Static(scope)); ///
/// The provided function is executed once during rendering
/// and will not be re-executed on dependency changes.
pub fn static_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(&mut self, scope: F) {
self.0.push(RsxScope::Static(Arc::new(scope)));
} }
/// Adds a dynamic scope to this RSX
///
/// The provided function is executed when dependencies change,
/// allowing for reactive updates similar to React hooks.
pub fn dynamic_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>( pub fn dynamic_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(
&mut self, &mut self,
drawer: F, drawer: F,
dependencies: Vec<Box<dyn HookDependency>>, dependencies: Vec<Arc<dyn HookDependency>>,
) { ) {
self.0 self.0
.push(RsxScope::Dynamic(Arc::new(drawer), dependencies)); .push(RsxScope::Dynamic(Arc::new(drawer), dependencies));
} }
pub fn view(&self, context: Arc<Context>) -> View { /// Adds a child RSX
pub fn child<R: ToRsx>(&mut self, child: R) {
self.0.push(RsxScope::Child(child.to_rsx()));
}
/// Generates child components within the given context
pub fn generate_children(&self, context: &Arc<Context>) {
let executor = context.get_executor();
for scope in &self.0 { for scope in &self.0 {
match scope { match scope {
RsxScope::Static(scope) => context.scopes.lock().unwrap().push(scope.clone()), RsxScope::Static(scope_fn) => {
let scope = Scope::new(executor.clone());
(scope_fn)(&scope);
context.add_scope(scope)
}
RsxScope::Dynamic(drawer, dependencies) => { RsxScope::Dynamic(drawer, dependencies) => {
let drawer = drawer.clone(); let drawer = drawer.clone();
context.dyn_scope( context.dyn_scope(
@@ -45,10 +86,17 @@ impl Rsx {
&dependencies.iter().map(|d| d.as_ref()).collect::<Vec<_>>(), &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(); let context = context.clone();
self.generate_children(&context);
Arc::new({ Arc::new({
move |ctx| { move |ctx| {
context.draw_children(ctx); context.draw_children(ctx);
@@ -56,3 +104,19 @@ impl Rsx {
}) })
} }
} }
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)))
}))])
}
}
+72 -3
View File
@@ -1,3 +1,41 @@
//! # OSUI - A TUI Library for Advanced UIs
//!
//! OSUI is a Rust library for building sophisticated Terminal User Interfaces (TUIs).
//! It provides a component-based architecture with state management, event handling,
//! and rendering capabilities for creating interactive console applications.
//!
//! ## Key Features
//!
//! - **Component System**: Build UIs using composable components
//! - **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 std::sync::Arc;
//!
//! #[component]
//! pub fn Counter(cx: &Arc<Context>) -> View {
//! let count = use_state(0);
//!
//! Arc::new(move |ctx| {
//! ctx.draw_text(Point { x: 0, y: 0 }, &format!("Count: {}", count.get_dl()));
//! })
//! }
//! ```
use std::sync::Arc; use std::sync::Arc;
use crate::render::DrawContext; use crate::render::DrawContext;
@@ -5,17 +43,48 @@ use crate::render::DrawContext;
pub mod component; pub mod component;
pub mod engine; pub mod engine;
pub mod frontend; pub mod frontend;
pub mod macros;
pub mod render; pub mod render;
pub mod state; pub mod state;
pub mod prelude { pub mod prelude {
pub use crate::component::*; //! Prelude module - Re-exports commonly used items for convenience
pub use crate::component::{context::*, scope::*, *};
pub use crate::engine::*; pub use crate::engine::*;
pub use crate::frontend::*;
pub use crate::render::*; pub use crate::render::*;
pub use crate::state::*; pub use crate::state::*;
pub use crate::View; pub use crate::{sleep, Error, Result, View, ViewWrapper};
pub use crossterm;
pub use osui_macros::{component, rsx};
pub use std::sync::{Arc, Mutex};
} }
/// A View is an async function that renders content to a DrawContext.
/// It takes a mutable DrawContext and produces drawing instructions.
pub type View = Arc<dyn Fn(&mut DrawContext) + Send + Sync>; pub type View = Arc<dyn Fn(&mut DrawContext) + Send + Sync>;
/// A ViewWrapper is a higher-order function that wraps views.
/// It can modify or enhance how a view is rendered.
pub type ViewWrapper = Arc<dyn Fn(&mut DrawContext, View) + Send + Sync>; pub type ViewWrapper = Arc<dyn Fn(&mut DrawContext, View) + Send + Sync>;
/// Result type for OSUI operations
pub type Result<T> = std::result::Result<T, Error>;
/// 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
}
}
/// Sleep for the specified duration in milliseconds.
/// Useful for controlling render frame rate or delays.
pub fn sleep(delay_ms: u64) {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
-165
View File
@@ -1,165 +0,0 @@
#[macro_export]
macro_rules! rsx {
() => {
$crate::frontend::Rsx::new()
};
($($rsx:tt)+) => {{
let mut r = $crate::frontend::Rsx::new();
{
$crate::rsx_scope!(r, $($rsx)+);
}
r
}};
}
#[macro_export]
macro_rules! rsx_scope {
($rsx:expr, for $(%$($dep:ident $(as $dp:pat,)?),+)? ($p:pat in $v:expr) {$($inner:tt)*} $($rest:tt)*) => {
{
$($($crate::rsx_dep!($dep);)+)?
$rsx.dynamic_scope({
$($($crate::rsx_dep!($dep $(as $dp)?);)+)?
move |scope| {
scope.children.lock().unwrap().clear();
for $p in $v {
$crate::rsx_child!(scope, $($inner)*);
}
}
}, vec![$($(Box::new($dep)),+)?]);
}
$crate::rsx_scope!($rsx, $($rest)*);
};
($rsx:expr, if $(%$($dep:ident $(as $dp:pat,)?),+)? ($st:expr) {$($inner:tt)*} $($rest:tt)*) => {
{
$($($crate::rsx_dep!($dep);)+)?
$rsx.dynamic_scope({
$($($crate::rsx_dep!($dep $(as $dp)?);)+)?
move |scope| {
if $st {
if scope.children.lock().unwrap().len() == 0 {
$crate::rsx_child!(scope, $($inner)*);
}
} else {
scope.children.lock().unwrap().clear();
}
}
}, vec![$($(Box::new($dep)),+)?]);
}
$crate::rsx_scope!($rsx, $($rest)*);
};
($rsx:expr, $(%$($dep:ident $(as $dp:pat,)?),+)? $text:literal @$d:expr; $($rest:tt)*) => {
{
let scope = $crate::component::Scope::new();
$($($crate::rsx_dep!($dep $(as $dp)?);)+)?
$crate::rsx_child!(scope, $text @$d;);
$rsx.static_scope(scope);
}
$crate::rsx_scope!($rsx, $($rest)*);
};
($rsx:expr, $(%$($dep:ident $(as $dp:pat,)?),+)? $text:literal $($rest:tt)*) => {
{
let scope = $crate::component::Scope::new();
$($($crate::rsx_dep!($dep $(as $dp)?);)+)?
$crate::rsx_child!(scope, $text);
$rsx.static_scope(scope);
}
$crate::rsx_scope!($rsx, $($rest)*);
};
($rsx:expr, $component:ident ($ctx:ident, $view:ident) $body:block $($rest:tt)*) => {
{
let scope = $crate::component::Scope::new();
$crate::rsx_child!(scope, $component ($ctx, $view) $body);
$rsx.static_scope(scope);
}
$crate::rsx_scope!($rsx, $($rest)*);
};
($rsx:expr, $component:ident $($rest:tt)*) => {
{
let scope = $crate::component::Scope::new();
$crate::rsx_child!(scope, $component);
$rsx.static_scope(scope);
}
$crate::rsx_scope!($rsx, $($rest)*);
};
($rsx:expr, !mount $mount:ident $($rest:tt)*) => {
{
$mount.mount();
}
$crate::rsx_scope!($rsx, $($rest)*);
};
($rsx:expr,) => {};
}
#[macro_export]
macro_rules! rsx_child {
($scope:expr, $text:literal @$d:expr; $($rest:tt)*) => {
{
$scope.view(Arc::new(move |ctx| ctx.draw_text($d, &format!($text))));
}
$crate::rsx_child!($scope, $($rest)*);
};
($scope:expr, $text:literal $($rest:tt)*) => {
{
$scope.view(Arc::new(move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!($text))));
}
$crate::rsx_child!($scope, $($rest)*);
};
($scope:expr, $component:ident ($ctx:ident, $view:ident) $body:block $($rest:tt)*) => {
{
$scope.child($component, Some(Arc::new(|$ctx, $view| $body)));
}
$crate::rsx_child!($scope, $($rest)*);
};
($scope:expr, $component:ident $($rest:tt)*) => {
{
$scope.child($component, None);
}
$crate::rsx_child!($scope, $($rest)*);
};
($rsx:expr,) => {};
}
#[macro_export]
macro_rules! rsx_dep {
($dep:ident as $dp:pat) => {
let $dp = $dep.clone();
};
($dep:ident) => {
let $dep = $dep.clone();
};
}
+34
View File
@@ -1,40 +1,69 @@
//! # 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; use crate::View;
/// Represents a drawing instruction that can be executed by the rendering engine
#[derive(Clone)] #[derive(Clone)]
pub enum DrawInstruction { pub enum DrawInstruction {
/// Draw text at a specific point
Text(Point, String), Text(Point, String),
/// Render a view within a specified area
View(Area, View), View(Area, View),
/// Render a child drawing context at an offset
Child(Point, DrawContext), Child(Point, DrawContext),
} }
/// Represents the dimensions of a drawable area
#[derive(Clone)] #[derive(Clone)]
pub struct Size { pub struct Size {
/// Width in terminal columns
pub width: u16, pub width: u16,
/// Height in terminal rows
pub height: u16, pub height: u16,
} }
/// Represents a position in 2D space
#[derive(Clone)] #[derive(Clone)]
pub struct Point { pub struct Point {
/// X coordinate (column)
pub x: u16, pub x: u16,
/// Y coordinate (row)
pub y: u16, pub y: u16,
} }
/// Represents a rectangular area with position and dimensions
#[derive(Clone)] #[derive(Clone)]
pub struct Area { pub struct Area {
/// X coordinate (column) of the top-left corner
pub x: u16, pub x: u16,
/// Y coordinate (row) of the top-left corner
pub y: u16, pub y: u16,
/// Width in terminal columns
pub width: u16, pub width: u16,
/// Height in terminal rows
pub height: u16, 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)] #[derive(Clone)]
pub struct DrawContext { pub struct DrawContext {
/// The total area available for drawing
pub area: Area, pub area: Area,
/// The area that has been allocated for drawing (union of all allocations)
pub allocated: Area, pub allocated: Area,
/// List of drawing instructions to execute
pub drawing: Vec<DrawInstruction>, pub drawing: Vec<DrawInstruction>,
} }
impl DrawContext { impl DrawContext {
/// Creates a new DrawContext with the specified area
pub fn new(area: Area) -> Self { pub fn new(area: Area) -> Self {
Self { Self {
area, area,
@@ -48,6 +77,8 @@ impl DrawContext {
} }
} }
/// 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 { pub fn allocate(&mut self, x: u16, y: u16, width: u16, height: u16) -> Area {
self.allocated.x = self.allocated.x.min(x); self.allocated.x = self.allocated.x.min(x);
self.allocated.y = self.allocated.y.min(y); self.allocated.y = self.allocated.y.min(y);
@@ -62,15 +93,18 @@ impl DrawContext {
} }
} }
/// Adds a drawing instruction to be executed
pub fn draw(&mut self, inst: DrawInstruction) { pub fn draw(&mut self, inst: DrawInstruction) {
self.drawing.push(inst); self.drawing.push(inst);
} }
/// Draws text at the specified point
pub fn draw_text(&mut self, point: Point, text: &str) { pub fn draw_text(&mut self, point: Point, text: &str) {
self.drawing self.drawing
.push(DrawInstruction::Text(point, text.to_string())); .push(DrawInstruction::Text(point, text.to_string()));
} }
/// Draws a view within the specified area
pub fn draw_view(&mut self, area: Area, view: View) { pub fn draw_view(&mut self, area: Area, view: View) {
self.drawing.push(DrawInstruction::View(area, view)); self.drawing.push(DrawInstruction::View(area, view));
} }
+67 -8
View File
@@ -1,3 +1,8 @@
//! # State Management Module
//!
//! Provides React-like hooks for managing component state and side effects.
//! This module includes useState, useEffect, useMount, and state synchronization hooks.
use std::{ use std::{
any::Any, any::Any,
fmt::{Debug, Display, Formatter, Result as FmtResult}, fmt::{Debug, Display, Formatter, Result as FmtResult},
@@ -5,26 +10,44 @@ use std::{
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
}; };
use crate::prelude::Context; use crate::component::context::Context;
/// Effect callback that can be triggered by state changes
#[derive(Clone)] #[derive(Clone)]
pub struct HookEffect(Arc<Mutex<dyn FnMut() + Send + Sync>>); pub struct HookEffect(Arc<Mutex<dyn FnMut() + Send + Sync>>);
/// 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)] #[derive(Debug)]
pub struct State<T> { pub struct State<T> {
/// The actual state value
value: Arc<Mutex<T>>, value: Arc<Mutex<T>>,
/// Functions to call when state is updated
dependents: Arc<Mutex<Vec<HookEffect>>>, 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> { pub struct Inner<'a, T> {
value: MutexGuard<'a, T>, value: MutexGuard<'a, T>,
dependents: Arc<Mutex<Vec<HookEffect>>>, dependents: Arc<Mutex<Vec<HookEffect>>>,
updated: bool, updated: bool,
} }
/// Mount lifecycle hook
///
/// Tracks whether a component has been mounted and executes
/// any pending mount effects.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Mount(Arc<Mutex<bool>>, Arc<Mutex<Vec<HookEffect>>>); pub struct Mount(Arc<Mutex<bool>>, Arc<Mutex<Vec<HookEffect>>>);
/// 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> { pub fn use_state<T>(v: T) -> State<T> {
State { State {
value: Arc::new(Mutex::new(v)), value: Arc::new(Mutex::new(v)),
@@ -33,14 +56,20 @@ pub fn use_state<T>(v: T) -> State<T> {
} }
impl<T: Clone> State<T> { impl<T: Clone> State<T> {
/// Gets the cloned value, recommended for preventing deadlocks /// 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 { pub fn get_dl(&self) -> T {
self.value.lock().unwrap().clone() self.value.lock().unwrap().clone()
} }
} }
impl<T> State<T> { impl<T> State<T> {
/// Gets a lock on the state for read/write access. /// 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> { pub fn get(&self) -> Inner<'_, T> {
Inner { Inner {
value: self.value.lock().unwrap(), value: self.value.lock().unwrap(),
@@ -49,18 +78,20 @@ impl<T> State<T> {
} }
} }
/// Sets the value and marks it as changed. /// Sets the state value and triggers dependents
pub fn set(&self, v: T) { pub fn set(&self, v: T) {
*self.value.lock().unwrap() = v; *self.value.lock().unwrap() = v;
self.update(); self.update();
} }
/// Notifies all dependents of an update
pub fn update(&self) { pub fn update(&self) {
for d in self.dependents.lock().unwrap().iter() { for d in self.dependents.lock().unwrap().iter() {
d.call(); d.call();
} }
} }
/// Clones the State handle (not the value)
pub fn clone(&self) -> Self { pub fn clone(&self) -> Self {
Self { Self {
dependents: self.dependents.clone(), dependents: self.dependents.clone(),
@@ -82,15 +113,19 @@ impl Debug for HookEffect {
} }
impl HookEffect { impl HookEffect {
/// Creates a new effect from a function
pub fn new<F: Fn() + Send + Sync + 'static>(f: F) -> Self { pub fn new<F: Fn() + Send + Sync + 'static>(f: F) -> Self {
Self(Arc::new(Mutex::new(f))) Self(Arc::new(Mutex::new(f)))
} }
/// Executes the effect function
pub fn call(&self) { pub fn call(&self) {
(self.0.lock().unwrap())() (self.0.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> { impl<T> Drop for Inner<'_, T> {
fn drop(&mut self) { fn drop(&mut self) {
if self.updated { if self.updated {
@@ -115,11 +150,13 @@ impl<T> DerefMut for Inner<'_, T> {
} }
} }
pub trait HookDependency { /// 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); fn on_update(&self, hook: HookEffect);
} }
impl<T> HookDependency for State<T> { impl<T: Send + Sync> HookDependency for State<T> {
fn on_update(&self, hook: HookEffect) { fn on_update(&self, hook: HookEffect) {
self.dependents.lock().unwrap().push(hook); self.dependents.lock().unwrap().push(hook);
} }
@@ -136,6 +173,7 @@ impl HookDependency for Mount {
} }
impl Mount { impl Mount {
/// Mark the component as mounted and execute pending effects
pub fn mount(&self) { pub fn mount(&self) {
*self.0.lock().unwrap() = true; *self.0.lock().unwrap() = true;
for hook_effect in self.1.lock().unwrap().iter() { for hook_effect in self.1.lock().unwrap().iter() {
@@ -145,6 +183,10 @@ impl Mount {
} }
} }
/// 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]) { pub fn use_effect<F: FnMut() + Send + Sync + 'static>(f: F, dependencies: &[&dyn HookDependency]) {
let f = Arc::new(Mutex::new(f)); let f = Arc::new(Mutex::new(f));
let hook = HookEffect(Arc::new(Mutex::new({ let hook = HookEffect(Arc::new(Mutex::new({
@@ -160,10 +202,18 @@ pub fn use_effect<F: FnMut() + Send + Sync + 'static>(f: F, dependencies: &[&dyn
} }
} }
/// Creates a mount lifecycle hook
///
/// Returns a Mount that tracks component lifecycle and executes
/// effects after mounting.
pub fn use_mount() -> Mount { pub fn use_mount() -> Mount {
Mount(Arc::new(Mutex::new(true)), Arc::new(Mutex::new(Vec::new()))) 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 { pub fn use_mount_manual() -> Mount {
Mount( Mount(
Arc::new(Mutex::new(false)), Arc::new(Mutex::new(false)),
@@ -171,6 +221,10 @@ pub fn use_mount_manual() -> Mount {
) )
} }
/// 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< pub fn use_sync_state<
T: Send + Sync + 'static, T: Send + Sync + 'static,
E: Any + 'static, E: Any + 'static,
@@ -190,9 +244,13 @@ pub fn use_sync_state<
state state
} }
/// 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< pub fn use_sync_effect<
T: Send + Sync + 'static, T: Send + Sync + 'static,
Ev: 'static, Ev: Send + Sync + 'static,
E: Fn(&State<T>) -> Ev + Send + Sync + 'static, E: Fn(&State<T>) -> Ev + Send + Sync + 'static,
>( >(
cx: &Arc<Context>, cx: &Arc<Context>,
@@ -205,7 +263,8 @@ pub fn use_sync_effect<
let state = state.clone(); let state = state.clone();
let cx = cx.clone(); let cx = cx.clone();
move || { move || {
cx.emit_event(&encoder(&state)); let ev = encoder(&state);
cx.emit_event(ev);
} }
}, },
deps, deps,