71 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
32 changed files with 2608 additions and 764 deletions
+6 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "osui"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
license = "Apache-2.0"
description = "A TUI library for advanced uis"
@@ -23,8 +23,10 @@ path = "src/main.rs"
cc = "1.0"
[dependencies]
access-cell = "0.1.3"
crossterm = "0.28.1"
figlet-rs = "0.1.5"
osui-macros = { version = "0.1.0", path = "macros" }
[profile.dev]
opt-level = 1
@@ -42,3 +44,6 @@ overflow-checks = false
lto = true
panic = 'abort'
codegen-units = 1
[workspace]
members = [".", "macros"]
+9 -7
View File
@@ -17,21 +17,23 @@
</p>
<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>
```rust
use osui::prelude::*;
fn main() -> std::io::Result<()> {
let screen = Screen::new();
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run engine");
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
"Hello, World"
"Hello World"
}
.draw(&screen);
screen.run()
.view(&cx)
}
```
+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)
}
}
+23 -60
View File
@@ -1,68 +1,31 @@
use std::sync::Arc;
use osui::prelude::*;
use osui::{prelude::*, rsx};
pub struct Count(pub State<u16>);
fn main() {
let console = Console::new();
console.run(app);
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run engine");
}
fn app(cx: &Arc<Context>) -> View {
let count = use_state(0);
let mount = use_mount_manual();
#[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)
}
// 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],
);
#[component]
fn Card(cx: &Arc<Context>, content: String) -> View {
let bar = "-".repeat(content.len());
rsx! {
// Static scope
my_component (ctx, view) {
let area = ctx.allocate(5, 0, 10, 10);
ctx.draw_view(area, view);
}
// 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
"{bar}\n{content}\n{bar}"
}
.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())
.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
}
-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));
}
}
+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)));
}
}
-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
}
}
+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<()>;
}
+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 crate::{
component::{Context, Scope},
state::HookDependency,
View,
};
use crate::component::{context::Context, scope::Scope};
use crate::{render::Point, hooks::HookDependency, View};
pub enum RsxScope {
Static(Arc<Scope>),
Dynamic(
Arc<dyn Fn(&Arc<Scope>) + Send + Sync>,
Vec<Box<dyn HookDependency>>,
),
/// Trait for converting values to RSX
pub trait ToRsx {
/// Convert to RSX representation
fn to_rsx(&self) -> Rsx;
}
/// 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>);
impl Rsx {
/// Creates a new empty RSX
pub fn new() -> Self {
Self(Vec::new())
}
pub fn static_scope(&mut self, scope: Arc<Scope>) {
self.0.push(RsxScope::Static(scope));
/// Adds a static scope to this RSX
///
/// 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>(
&mut self,
drawer: F,
dependencies: Vec<Box<dyn HookDependency>>,
dependencies: Vec<Arc<dyn HookDependency>>,
) {
self.0
.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 {
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) => {
let drawer = drawer.clone();
context.dyn_scope(
@@ -45,10 +86,17 @@ impl Rsx {
&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);
@@ -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)))
}))])
}
}
+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())),
}
}
+84 -7
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 crate::render::DrawContext;
@@ -5,17 +43,56 @@ use crate::render::DrawContext;
pub mod component;
pub mod engine;
pub mod frontend;
pub mod macros;
pub mod hooks;
pub mod render;
pub mod state;
pub mod view_plugins;
pub mod prelude {
pub use crate::component::*;
pub use crate::engine::*;
pub use crate::render::*;
pub use crate::state::*;
pub use crate::View;
//! Prelude module - Re-exports commonly used items for convenience
pub use crate::component::components::*;
pub use crate::component_prelude::*;
}
pub mod component_prelude {
//! Prelude module - Re-exports commonly used items for convenience
pub use crate::component::{context::*, scope::*, *};
pub use crate::engine::*;
pub use crate::frontend::*;
pub use crate::hooks::*;
pub use crate::render::*;
pub use crate::view_plugins::*;
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>;
/// 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>;
/// 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();
};
}
+66 -9
View File
@@ -1,46 +1,75 @@
//! # 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),
}
#[derive(Clone)]
/// 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,
}
#[derive(Clone)]
/// Represents a position in 2D space
#[derive(Debug, Clone)]
pub struct Point {
/// X coordinate (column)
pub x: u16,
/// Y coordinate (row)
pub y: u16,
}
#[derive(Clone)]
/// 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: u16::MAX,
y: u16::MAX,
x: 0,
y: 0,
width: 0,
height: 0,
},
@@ -48,11 +77,13 @@ 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 {
self.allocated.x = self.allocated.x.min(x);
self.allocated.y = self.allocated.y.min(y);
self.allocated.width = self.allocated.width.max(width);
self.allocated.height = self.allocated.height.max(height);
self.allocated.x = x;
self.allocated.y = y;
self.allocated.width = width;
self.allocated.height = height;
Area {
x,
@@ -62,16 +93,42 @@ impl DrawContext {
}
}
/// 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;
}
}
-213
View File
@@ -1,213 +0,0 @@
use std::{
any::Any,
fmt::{Debug, Display, Formatter, Result as FmtResult},
ops::{Deref, DerefMut},
sync::{Arc, Mutex, MutexGuard},
};
use crate::prelude::Context;
#[derive(Clone)]
pub struct HookEffect(Arc<Mutex<dyn FnMut() + Send + Sync>>);
#[derive(Debug)]
pub struct State<T> {
value: Arc<Mutex<T>>,
dependents: Arc<Mutex<Vec<HookEffect>>>,
}
pub struct Inner<'a, T> {
value: MutexGuard<'a, T>,
dependents: Arc<Mutex<Vec<HookEffect>>>,
updated: bool,
}
#[derive(Debug, Clone)]
pub struct Mount(Arc<Mutex<bool>>, Arc<Mutex<Vec<HookEffect>>>);
pub fn use_state<T>(v: T) -> State<T> {
State {
value: Arc::new(Mutex::new(v)),
dependents: Arc::new(Mutex::new(Vec::new())),
}
}
impl<T: Clone> State<T> {
/// Gets the cloned value, recommended for preventing deadlocks
pub fn get_dl(&self) -> T {
self.value.lock().unwrap().clone()
}
}
impl<T> State<T> {
/// Gets a lock on the state for read/write access.
pub fn get(&self) -> Inner<'_, T> {
Inner {
value: self.value.lock().unwrap(),
dependents: self.dependents.clone(),
updated: false,
}
}
/// Sets the value and marks it as changed.
pub fn set(&self, v: T) {
*self.value.lock().unwrap() = v;
self.update();
}
pub fn update(&self) {
for d in self.dependents.lock().unwrap().iter() {
d.call();
}
}
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())
}
}
impl Debug for HookEffect {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "HookEffect")
}
}
impl HookEffect {
pub fn new<F: Fn() + Send + Sync + 'static>(f: F) -> Self {
Self(Arc::new(Mutex::new(f)))
}
pub fn call(&self) {
(self.0.lock().unwrap())()
}
}
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
}
}
pub trait HookDependency {
fn on_update(&self, hook: HookEffect);
}
impl<T> HookDependency for State<T> {
fn on_update(&self, hook: HookEffect) {
self.dependents.lock().unwrap().push(hook);
}
}
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 {
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();
}
}
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());
}
}
pub fn use_mount() -> Mount {
Mount(Arc::new(Mutex::new(true)), Arc::new(Mutex::new(Vec::new())))
}
pub fn use_mount_manual() -> Mount {
Mount(
Arc::new(Mutex::new(false)),
Arc::new(Mutex::new(Vec::new())),
)
}
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
}
pub fn use_sync_effect<
T: Send + Sync + 'static,
Ev: '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 || {
cx.emit_event(&encoder(&state));
}
},
deps,
);
}
+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());
}