17 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
18 changed files with 910 additions and 396 deletions
+224 -224
View File
@@ -1,227 +1,227 @@
Iterx72,Nestingx72,Time µs Iterx72,Nestingx72,Time µs
14,14,11964 14,14,7668
0,1,1078 0,4,714
5,8,1818 5,1,1550
10,13,2632 3,0,617
14,2,3392 9,6,2399
0,9,520 0,3,882
9,3,2360 11,10,2691
10,5,2551 4,10,1343
10,11,2548 5,5,1562
6,8,1738 6,6,1749
12,12,2922 11,3,2719
0,8,601 9,14,2315
12,4,2919 1,13,770
0,11,521 14,8,3290
6,4,1942 6,14,1767
11,7,2741 6,11,1857
9,4,2496 10,11,2530
5,9,1764 7,11,1936
13,9,3069 13,5,3130
2,5,1053 3,13,1161
5,11,1790 0,2,928
14,1,3581 6,3,1751
5,0,962 11,11,2727
0,3,845 11,6,2860
1,3,868 3,12,1170
7,8,2013 13,14,3090
12,3,2987 3,2,1140
9,8,2328 12,4,2952
10,7,2533 9,11,4884
11,1,2766 10,2,2726
7,1,1938 1,3,756
0,5,687 3,6,1151
6,9,1830 6,5,1731
10,14,2628 9,1,2348
6,12,1786 11,4,2688
4,0,835 2,13,944
4,14,2464 13,3,3125
10,1,2633 12,0,643
12,7,2948 1,4,721
2,9,1066 6,1,1757
4,6,2646 1,14,730
7,11,1999 10,7,2505
5,13,1748 7,9,2002
13,4,3110 13,6,3132
13,13,3094 4,6,1354
3,5,1166 4,7,1311
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 0,14,520
11,8,2890 4,8,1372
3,10,1140 14,7,3304
13,10,3235 7,0,620
3,14,1185 0,0,605
8,10,2183 11,9,2748
12,10,2896 4,13,1389
2,8,1072 12,10,2989
9,6,2358 8,1,2211
13,1,3066 9,9,2323
3,8,1133 4,11,1404
11,10,2758 5,9,1569
4,13,2550 3,11,1129
1,0,643 5,7,1567
4,2,5482 10,14,2543
3,7,1155 5,0,612
1,4,726 10,0,636
3,3,1192 6,4,1745
4,11,2576 5,4,1606
8,0,619 1,2,707
9,2,2444 11,12,2727
10,3,2581 13,0,605
4,5,5400 8,2,2127
5,3,2444 10,4,2512
1,10,804 4,4,1340
10,2,2563 6,8,1973
0,7,592 7,8,2185
0,2,802 3,10,1175
1,5,741 9,2,2300
2,7,984 1,6,743
10,0,755 10,1,2547
13,14,3112 1,5,715
5,14,1811 3,1,1209
8,12,2180 7,4,2217
6,11,1723 4,5,1346
4,7,2897 11,1,2719
10,8,2536 7,10,1972
1,7,813 9,3,2351
6,0,699 14,13,3301
8,2,2187 14,3,3315
5,7,1811 2,12,939
14,8,3304 13,8,3098
2,0,661 9,13,2339
8,13,2185 11,7,2749
1,9,786 6,7,1742
3,0,623 9,8,2348
12,5,3012 8,11,2171
8,1,2203 7,6,2370
8,11,2285 12,8,2988
14,10,3413 4,2,1349
4,10,2613 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 11,5,2729
6,1,2035 4,1,1364
12,1,2891 14,10,3324
5,4,2036 12,5,2925
2,3,1083 8,5,2121
14,3,3312 10,3,2519
3,2,1278 1,7,698
5,2,2392 2,4,959
8,7,2151 6,9,1973
5,6,2072 7,5,2240
3,12,1230 2,7,936
2,10,954 0,6,753
2,13,932 5,10,1557
13,8,3128 2,3,948
0,12,521 2,14,931
11,4,2769 8,6,2132
7,2,1947 2,10,936
4,1,1802 4,0,618
8,8,2163 13,4,3136
7,3,1966 5,6,1530
13,3,3207 5,8,1546
1,8,746 7,14,1920
12,9,2955 8,10,2110
10,12,2570 12,9,3021
9,9,2388 1,11,738
4,12,2566 13,11,3121
7,0,619 1,1,730
7,9,2004 0,1,981
13,0,607 3,8,1271
14,9,3355 10,8,2554
3,4,1154 14,14,3267
11,2,2753 0,9,600
11,13,2739 4,9,1328
6,13,1735 9,12,3707
7,6,1983 6,12,1800
8,3,2166 0,5,684
7,14,1958 7,2,2180
6,2,2101 9,10,7168
13,5,3221 8,14,2163
8,4,2120 7,7,3732
10,4,2716 8,0,633
14,5,3264 5,14,1645
14,13,3299 8,8,2129
11,9,2725 12,7,2898
13,11,3132 0,10,573
1,6,809 7,3,2255
2,2,1155 14,9,3272
4,4,2242 2,9,924
8,6,2218 9,4,2331
12,0,645 10,5,2513
7,4,1925 10,13,2514
2,6,1054 11,8,2733
9,13,2340 14,12,3326
13,12,3112 0,8,618
14,4,3334 6,0,609
3,11,1139 8,9,2157
12,14,3004 12,1,3013
4,9,2649 12,11,2916
0,13,527 12,3,2959
12,11,2976 9,0,623
0,0,1559 12,13,3064
12,13,2952 0,13,536
6,10,1749 5,13,1558
14,11,3365 6,10,1835
11,0,647 11,13,2784
2,12,1024 13,9,3071
10,6,2562 0,7,599
8,5,2227 2,0,617
9,12,2363 9,5,2303
2,1,1023 8,3,2152
0,10,528 12,2,2994
6,3,2005 0,12,542
8,9,2195 7,13,1923
7,5,1966 4,3,1350
6,14,1755 13,7,3177
9,0,608 10,10,2533
14,14,3389 6,2,1748
9,7,2391 3,7,1209
0,4,688 13,2,3097
5,1,2574 11,2,2731
9,14,2353 5,2,1562
12,2,2975 14,4,3273
3,1,1276 7,1,2088
6,5,1804 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 11964 7668
3 0 1 4 1078 714
4 5 8 1 1818 1550
5 10 3 13 0 2632 617
6 14 9 2 6 3392 2399
7 0 9 3 520 882
8 9 11 3 10 2360 2691
9 10 4 5 10 2551 1343
10 10 5 11 5 2548 1562
11 6 8 6 1738 1749
12 12 11 12 3 2922 2719
13 0 9 8 14 601 2315
14 12 1 4 13 2919 770
15 0 14 11 8 521 3290
16 6 4 14 1942 1767
17 11 6 7 11 2741 1857
18 9 10 4 11 2496 2530
19 5 7 9 11 1764 1936
20 13 9 5 3069 3130
21 2 3 5 13 1053 1161
22 5 0 11 2 1790 928
23 14 6 1 3 3581 1751
24 5 11 0 11 962 2727
25 0 11 3 6 845 2860
26 1 3 3 12 868 1170
27 7 13 8 14 2013 3090
28 12 3 3 2 2987 1140
29 9 12 8 4 2328 2952
30 10 9 7 11 2533 4884
31 11 10 1 2 2766 2726
32 7 1 1 3 1938 756
33 0 3 5 6 687 1151
34 6 9 5 1830 1731
35 10 9 14 1 2628 2348
36 6 11 12 4 1786 2688
37 4 2 0 13 835 944
38 4 13 14 3 2464 3125
39 10 12 1 0 2633 643
40 12 1 7 4 2948 721
41 2 6 9 1 1066 1757
42 4 1 6 14 2646 730
43 7 10 11 7 1999 2505
44 5 7 13 9 1748 2002
45 13 4 6 3110 3132
46 13 4 13 6 3094 1354
47 3 4 5 7 1166 1311
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
48 0 14 520
49 11 4 8 2890 1372
50 3 14 10 7 1140 3304
51 13 7 10 0 3235 620
52 3 0 14 0 1185 605
53 8 11 10 9 2183 2748
54 12 4 10 13 2896 1389
55 2 12 8 10 1072 2989
56 9 8 6 1 2358 2211
57 13 9 1 9 3066 2323
58 3 4 8 11 1133 1404
59 11 5 10 9 2758 1569
60 4 3 13 11 2550 1129
61 1 5 0 7 643 1567
62 4 10 2 14 5482 2543
63 3 5 7 0 1155 612
64 1 10 4 0 726 636
65 3 6 3 4 1192 1745
66 4 5 11 4 2576 1606
67 8 1 0 2 619 707
68 9 11 2 12 2444 2727
69 10 13 3 0 2581 605
70 4 8 5 2 5400 2127
71 5 10 3 4 2444 2512
72 1 4 10 4 804 1340
73 10 6 2 8 2563 1973
74 0 7 7 8 592 2185
75 0 3 2 10 802 1175
76 1 9 5 2 741 2300
77 2 1 7 6 984 743
78 10 0 1 755 2547
79 13 1 14 5 3112 715
80 5 3 14 1 1811 1209
81 8 7 12 4 2180 2217
82 6 4 11 5 1723 1346
83 4 11 7 1 2897 2719
84 10 7 8 10 2536 1972
85 1 9 7 3 813 2351
86 6 14 0 13 699 3301
87 8 14 2 3 2187 3315
88 5 2 7 12 1811 939
89 14 13 8 3304 3098
90 2 9 0 13 661 2339
91 8 11 13 7 2185 2749
92 1 6 9 7 786 1742
93 3 9 0 8 623 2348
94 12 8 5 11 3012 2171
95 8 7 1 6 2203 2370
96 8 12 11 8 2285 2988
97 14 4 10 2 3413 1349
98 4 2 10 8 2613 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 6 4 1 2035 1364
115 12 14 1 10 2891 3324
116 5 12 4 5 2036 2925
117 2 8 3 5 1083 2121
118 14 10 3 3312 2519
119 3 1 2 7 1278 698
120 5 2 2 4 2392 959
121 8 6 7 9 2151 1973
122 5 7 6 5 2072 2240
123 3 2 12 7 1230 936
124 2 0 10 6 954 753
125 2 5 13 10 932 1557
126 13 2 8 3 3128 948
127 0 2 12 14 521 931
128 11 8 4 6 2769 2132
129 7 2 2 10 1947 936
130 4 1 0 1802 618
131 8 13 8 4 2163 3136
132 7 5 3 6 1966 1530
133 13 5 3 8 3207 1546
134 1 7 8 14 746 1920
135 12 8 9 10 2955 2110
136 10 12 12 9 2570 3021
137 9 1 9 11 2388 738
138 4 13 12 11 2566 3121
139 7 1 0 1 619 730
140 7 0 9 1 2004 981
141 13 3 0 8 607 1271
142 14 10 9 8 3355 2554
143 3 14 4 14 1154 3267
144 11 0 2 9 2753 600
145 11 4 13 9 2739 1328
146 6 9 13 12 1735 3707
147 7 6 6 12 1983 1800
148 8 0 3 5 2166 684
149 7 14 2 1958 2180
150 6 9 2 10 2101 7168
151 13 8 5 14 3221 2163
152 8 7 4 7 2120 3732
153 10 8 4 0 2716 633
154 14 5 5 14 3264 1645
155 14 8 13 8 3299 2129
156 11 12 9 7 2725 2898
157 13 0 11 10 3132 573
158 1 7 6 3 809 2255
159 2 14 2 9 1155 3272
160 4 2 4 9 2242 924
161 8 9 6 4 2218 2331
162 12 10 0 5 645 2513
163 7 10 4 13 1925 2514
164 2 11 6 8 1054 2733
165 9 14 13 12 2340 3326
166 13 0 12 8 3112 618
167 14 6 4 0 3334 609
168 3 8 11 9 1139 2157
169 12 14 1 3004 3013
170 4 12 9 11 2649 2916
171 0 12 13 3 527 2959
172 12 9 11 0 2976 623
173 0 12 0 13 1559 3064
174 12 0 13 2952 536
175 6 5 10 13 1749 1558
176 14 6 11 10 3365 1835
177 11 0 13 647 2784
178 2 13 12 9 1024 3071
179 10 0 6 7 2562 599
180 8 2 5 0 2227 617
181 9 12 5 2363 2303
182 2 8 1 3 1023 2152
183 0 12 10 2 528 2994
184 6 0 3 12 2005 542
185 8 7 9 13 2195 1923
186 7 4 5 3 1966 1350
187 6 13 14 7 1755 3177
188 9 10 0 10 608 2533
189 14 6 14 2 3389 1748
190 9 3 7 2391 1209
191 0 13 4 2 688 3097
192 5 11 1 2 2574 2731
193 9 5 14 2 2353 1562
194 12 14 2 4 2975 3273
195 3 7 1 1276 2088
196 6 9 5 7 1804 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
+18 -1
View File
@@ -8,7 +8,24 @@ pub fn main() {
#[component] #[component]
fn App(cx: &Arc<Context>) -> View { fn App(cx: &Arc<Context>) -> View {
rsx! { rsx! {
"Hello World" // redraw is important because the effects won't be applied
impl size_auto, center, redraw
FlexRow {
impl size_auto, redraw
Card { content: "One".to_string() }
impl size_auto, redraw
Card { content: "Two".to_string() }
}
}
.view(&cx)
}
#[component]
fn Card(cx: &Arc<Context>, content: String) -> View {
let bar = "-".repeat(content.len());
rsx! {
"{bar}\n{content}\n{bar}"
} }
.view(&cx) .view(&cx)
} }
+63 -9
View File
@@ -38,7 +38,7 @@ fn emit_deps(deps: &[Dep]) -> TokenStream {
/// Emits a Vec of dependencies as HookDependency trait objects /// Emits a Vec of dependencies as HookDependency trait objects
fn emit_deps_vec(deps: &[Dep]) -> TokenStream { fn emit_deps_vec(deps: &[Dep]) -> TokenStream {
let deps = deps.iter().map(|d| { let deps = deps.iter().filter(|d| d.is_dep).map(|d| {
let ident = &d.ident; let ident = &d.ident;
quote! { quote! {
@@ -51,12 +51,29 @@ fn emit_deps_vec(deps: &[Dep]) -> TokenStream {
} }
} }
/// 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 /// Emits code for a single node within a scope
fn emit_node_scope(node: &RsxNode) -> TokenStream { fn emit_node_scope(node: &RsxNode) -> TokenStream {
match node { match node {
RsxNode::Text(_) => { RsxNode::Text { deps, .. } => {
let emit = emit_node(node); let emit = emit_node(node);
let deps_emit = emit_deps(deps);
quote! { quote! {
#deps_emit
r.static_scope(move |scope| {#emit}); r.static_scope(move |scope| {#emit});
} }
} }
@@ -129,13 +146,38 @@ fn emit_node_scope(node: &RsxNode) -> TokenStream {
fn emit_node(node: &RsxNode) -> TokenStream { fn emit_node(node: &RsxNode) -> TokenStream {
match node { match node {
RsxNode::Text(text) => quote! { RsxNode::Text {
scope.view(Arc::new(move |ctx| { text,
ctx.draw_text(Point { x: 0, y: 0 }, &format!(#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 { RsxNode::Component {
plugins,
path, path,
props, props,
children, children,
@@ -163,8 +205,20 @@ fn emit_node(node: &RsxNode) -> TokenStream {
} }
}; };
quote! { if plugins.len() == 0 {
scope.child(#component_expr, None); 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
})));
}
} }
} }
+120 -6
View File
@@ -2,13 +2,17 @@
//! //!
//! Parses RSX syntax into an AST that can be emitted as Rust code. //! Parses RSX syntax into an AST that can be emitted as Rust code.
use syn::braced; use std::collections::HashSet;
use proc_macro2::Span;
use syn::parse::discouraged::Speculative; use syn::parse::discouraged::Speculative;
use syn::punctuated::Punctuated;
use syn::{ use syn::{
Expr, Ident, LitStr, Pat, Path, Result, Token, Expr, Ident, LitStr, Pat, Path, Result, Token,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
token::Brace, token::Brace,
}; };
use syn::{braced, parenthesized};
/// Root of an RSX expression /// Root of an RSX expression
pub struct RsxRoot { pub struct RsxRoot {
@@ -37,11 +41,17 @@ pub struct RsxProp {
/// AST node representing different RSX constructs /// AST node representing different RSX constructs
pub enum RsxNode { pub enum RsxNode {
/// String literal: `"text"` /// String literal: `"text"`
Text(LitStr), Text {
plugins: Vec<ViewPlugin>,
text: LitStr,
deps: Vec<Dep>,
},
/// Expression node: `{expr}` /// Expression node: `{expr}`
Expr(Expr), Expr(Expr),
/// Component instantiation: `Component { prop: value, ... }` /// Component instantiation: `Component { prop: value, ... }`
Component { Component {
/// View plugins
plugins: Vec<ViewPlugin>,
/// Component path (e.g., `my_module::MyComponent`) /// Component path (e.g., `my_module::MyComponent`)
path: Path, path: Path,
/// Component properties /// Component properties
@@ -76,6 +86,12 @@ pub enum RsxNode {
pub struct Dep { pub struct Dep {
pub ident: Ident, pub ident: Ident,
pub pat: Option<Pat>, 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>> { fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
@@ -84,7 +100,15 @@ fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
if input.peek(Token![%]) { if input.peek(Token![%]) {
input.parse::<Token![%]>()?; input.parse::<Token![%]>()?;
loop { loop {
let is_ref = if input.peek(Token![ref]) {
input.parse::<Token![ref]>()?;
true
} else {
false
};
let ident: Ident = input.parse()?; let ident: Ident = input.parse()?;
let pat = if input.peek(Token![as]) { let pat = if input.peek(Token![as]) {
input.parse::<Token![as]>()?; input.parse::<Token![as]>()?;
// Patterns in syn 2.0 are parsed via `Pat::parse_multi` // Patterns in syn 2.0 are parsed via `Pat::parse_multi`
@@ -94,7 +118,11 @@ fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
None None
}; };
deps.push(Dep { ident, pat }); deps.push(Dep {
ident,
pat,
is_dep: !is_ref,
});
if !input.peek(Token![,]) { if !input.peek(Token![,]) {
break; break;
@@ -106,6 +134,35 @@ fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
Ok(deps) 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 { impl Parse for RsxNode {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
// @{ $expr } // @{ $expr }
@@ -159,21 +216,43 @@ impl Parse for RsxNode {
}); });
} }
let plugins = parse_plugins(input)?;
// "%$dep for $pat in $expr { $rsx }" // "%$dep for $pat in $expr { $rsx }"
if input.peek(LitStr) { if input.peek(LitStr) {
return Ok(RsxNode::Text(input.parse()?)); 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) parse_component_invocation(input, plugins)
} }
} }
/// Parses Path or Path { ... } into RsxNode::Component. /// Parses Path or Path { ... } into RsxNode::Component.
fn parse_component_invocation(input: ParseStream) -> Result<RsxNode> { fn parse_component_invocation(input: ParseStream, plugins: Vec<ViewPlugin>) -> Result<RsxNode> {
let path: Path = input.parse()?; let path: Path = input.parse()?;
if !input.peek(Brace) { if !input.peek(Brace) {
return Ok(RsxNode::Component { return Ok(RsxNode::Component {
plugins,
path, path,
props: Vec::new(), props: Vec::new(),
children: Vec::new(), children: Vec::new(),
@@ -186,6 +265,7 @@ fn parse_component_invocation(input: ParseStream) -> Result<RsxNode> {
let (props, children) = parse_props_and_children(&content)?; let (props, children) = parse_props_and_children(&content)?;
Ok(RsxNode::Component { Ok(RsxNode::Component {
plugins,
path, path,
props, props,
children, children,
@@ -239,3 +319,37 @@ fn parse_children(input: ParseStream) -> Result<Vec<RsxNode>> {
} }
Ok(nodes) Ok(nodes)
} }
fn extract_vars_from_lit(lit: &LitStr) -> HashSet<Ident> {
let s = lit.value();
let mut vars = HashSet::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' {
// skip escaped {{
if chars.peek() == Some(&'{') {
chars.next();
continue;
}
let mut name = String::new();
while let Some(&ch) = chars.peek() {
chars.next();
if ch == '}' {
break;
}
name.push(ch);
}
if !name.is_empty() {
vars.insert(Ident::new(
&name.split_once(':').unwrap_or((&name, &name)).0,
Span::call_site(),
));
}
}
}
vars
}
+51
View File
@@ -0,0 +1,51 @@
use crate::component_prelude::*;
#[component]
pub fn FlexRow(cx: &Arc<Context>, children: Rsx) -> View {
children.generate_children(&cx);
Arc::new({
let cx = cx.clone();
move |ctx| {
for (child, view_wrapper) in cx.get_children() {
let view = child.get_view();
if let Some(view_wrapper) = view_wrapper {
let mut ctx2 = ctx.clone();
view_wrapper(&mut ctx2, view);
ctx.drawing.append(&mut ctx2.drawing);
ctx.area.y += ctx2.allocated.height;
} else {
ctx.draw_view(ctx.area.clone(), view);
}
}
}
})
}
#[component]
pub fn FlexColumn(cx: &Arc<Context>, children: Rsx) -> View {
children.generate_children(&cx);
Arc::new({
let cx = cx.clone();
move |ctx| {
for (child, view_wrapper) in cx.get_children() {
let view = child.get_view();
if let Some(view_wrapper) = view_wrapper {
let mut ctx2 = ctx.clone();
view_wrapper(&mut ctx2, view);
ctx.drawing.append(&mut ctx2.drawing);
ctx.area.x += ctx2.allocated.width;
} else {
ctx.draw_view(ctx.area.clone(), view);
}
}
}
})
}
+3
View File
@@ -0,0 +1,3 @@
mod flex;
pub use flex::*;
+12 -2
View File
@@ -15,9 +15,9 @@ use access_cell::AccessCell;
use crate::{ use crate::{
component::EventHandler, component::EventHandler,
engine::{Command, CommandExecutor}, engine::{Command, CommandExecutor},
hooks::{use_effect, HookDependency},
render::DrawContext, render::DrawContext,
state::{use_effect, HookDependency}, View, ViewWrapper,
View,
}; };
use super::{scope::Scope, Component, ComponentImpl}; use super::{scope::Scope, Component, ComponentImpl};
@@ -216,6 +216,16 @@ impl Context {
self.scopes.lock().unwrap().push(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 /// Renders all child components to the draw context
pub fn draw_children(self: &Arc<Self>, ctx: &mut DrawContext) { pub fn draw_children(self: &Arc<Self>, ctx: &mut DrawContext) {
for scope in self.scopes.lock().unwrap().iter() { for scope in self.scopes.lock().unwrap().iter() {
+1
View File
@@ -4,6 +4,7 @@
//! Components are reusable units of UI that can manage their own state //! Components are reusable units of UI that can manage their own state
//! and respond to events. //! and respond to events.
pub mod components;
pub mod context; pub mod context;
pub mod scope; pub mod scope;
+9
View File
@@ -53,4 +53,13 @@ impl Scope {
self.children.lock().unwrap().push((ctx, None)); 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)));
}
} }
+4 -2
View File
@@ -64,8 +64,10 @@ impl Engine for Console {
match inst { match inst {
crate::render::DrawInstruction::Text(point, text) => { crate::render::DrawInstruction::Text(point, text) => {
let (x, y) = (ctx.area.x + point.x, ctx.area.y + point.y); let (x, y) = (ctx.area.x + point.x, ctx.area.y + point.y);
execute!(stdout(), MoveTo(x, y),).unwrap(); for (i, line) in text.lines().enumerate() {
print!("{text}"); execute!(stdout(), MoveTo(x, y + (i as u16))).unwrap();
print!("{line}");
}
stdout().flush().unwrap(); stdout().flush().unwrap();
} }
crate::render::DrawInstruction::Child(_point, child) => self.draw_context(child), crate::render::DrawInstruction::Child(_point, child) => self.draw_context(child),
+1 -1
View File
@@ -7,7 +7,7 @@
use std::sync::Arc; use std::sync::Arc;
use crate::component::{context::Context, scope::Scope}; use crate::component::{context::Context, scope::Scope};
use crate::{render::Point, state::HookDependency, View}; use crate::{render::Point, hooks::HookDependency, View};
/// Trait for converting values to RSX /// Trait for converting values to RSX
pub trait ToRsx { pub trait ToRsx {
+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();
}
}
+11 -131
View File
@@ -1,7 +1,7 @@
//! # State Management Module //! # State hooks module
//! //!
//! Provides React-like hooks for managing component state and side effects. //! Provides React-like use_state hooks for managing states.
//! This module includes useState, useEffect, useMount, and state synchronization hooks. //! This module includes use_state and use_sync_state.
use std::{ use std::{
any::Any, any::Any,
@@ -10,12 +10,9 @@ use std::{
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
}; };
use super::{HookDependency, HookEffect};
use crate::component::context::Context; use crate::component::context::Context;
/// Effect callback that can be triggered by state changes
#[derive(Clone)]
pub struct HookEffect(Arc<Mutex<dyn FnMut() + Send + Sync>>);
/// State holder for reactive values /// State holder for reactive values
/// ///
/// Similar to React's useState hook. Holds a value and tracks dependents /// Similar to React's useState hook. Holds a value and tracks dependents
@@ -38,23 +35,6 @@ pub struct Inner<'a, T> {
updated: bool, updated: bool,
} }
/// 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 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())),
}
}
impl<T: Clone> State<T> { impl<T: Clone> State<T> {
/// Gets a cloned copy of the state value /// Gets a cloned copy of the state value
/// ///
@@ -106,24 +86,6 @@ impl<T: Display> Display for State<T> {
} }
} }
impl Debug for HookEffect {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "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())()
}
}
/// Inner implements Deref for read access and DerefMut for write access /// Inner implements Deref for read access and DerefMut for write access
/// On drop after mutation, automatically triggers dependent effects /// On drop after mutation, automatically triggers dependent effects
impl<T> Drop for Inner<'_, T> { impl<T> Drop for Inner<'_, T> {
@@ -150,77 +112,12 @@ impl<T> DerefMut for Inner<'_, T> {
} }
} }
/// 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<T: Send + Sync> 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);
} }
} }
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();
}
}
/// 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());
}
}
/// 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())),
)
}
/// Synchronizes state with events from the context /// Synchronizes state with events from the context
/// ///
/// Creates state that is automatically updated when events are emitted /// Creates state that is automatically updated when events are emitted
@@ -244,29 +141,12 @@ pub fn use_sync_state<
state state
} }
/// Synchronizes state changes back to the context as events /// Creates a new state value
/// ///
/// Creates an effect that emits an event whenever the state changes. /// Returns a State that can be read and written from multiple threads.
/// The encoder function converts state values to events. pub fn use_state<T>(v: T) -> State<T> {
pub fn use_sync_effect< State {
T: Send + Sync + 'static, value: Arc::new(Mutex::new(v)),
Ev: Send + Sync + 'static, dependents: Arc::new(Mutex::new(Vec::new())),
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,
);
} }
+10 -2
View File
@@ -43,16 +43,24 @@ use crate::render::DrawContext;
pub mod component; pub mod component;
pub mod engine; pub mod engine;
pub mod frontend; pub mod frontend;
pub mod hooks;
pub mod render; pub mod render;
pub mod state; pub mod view_plugins;
pub mod prelude { pub mod prelude {
//! 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 //! Prelude module - Re-exports commonly used items for convenience
pub use crate::component::{context::*, scope::*, *}; pub use crate::component::{context::*, scope::*, *};
pub use crate::engine::*; pub use crate::engine::*;
pub use crate::frontend::*; pub use crate::frontend::*;
pub use crate::hooks::*;
pub use crate::render::*; pub use crate::render::*;
pub use crate::state::*; pub use crate::view_plugins::*;
pub use crate::{sleep, Error, Result, View, ViewWrapper}; pub use crate::{sleep, Error, Result, View, ViewWrapper};
pub use crossterm; pub use crossterm;
pub use osui_macros::{component, rsx}; pub use osui_macros::{component, rsx};
+32 -9
View File
@@ -18,7 +18,7 @@ pub enum DrawInstruction {
} }
/// Represents the dimensions of a drawable area /// Represents the dimensions of a drawable area
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct Size { pub struct Size {
/// Width in terminal columns /// Width in terminal columns
pub width: u16, pub width: u16,
@@ -27,7 +27,7 @@ pub struct Size {
} }
/// Represents a position in 2D space /// Represents a position in 2D space
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct Point { pub struct Point {
/// X coordinate (column) /// X coordinate (column)
pub x: u16, pub x: u16,
@@ -36,7 +36,7 @@ pub struct Point {
} }
/// Represents a rectangular area with position and dimensions /// Represents a rectangular area with position and dimensions
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct Area { pub struct Area {
/// X coordinate (column) of the top-left corner /// X coordinate (column) of the top-left corner
pub x: u16, pub x: u16,
@@ -68,8 +68,8 @@ impl DrawContext {
Self { Self {
area, area,
allocated: Area { allocated: Area {
x: u16::MAX, x: 0,
y: u16::MAX, y: 0,
width: 0, width: 0,
height: 0, height: 0,
}, },
@@ -80,10 +80,10 @@ impl DrawContext {
/// Allocates space within the drawable area and returns the allocated area /// Allocates space within the drawable area and returns the allocated area
/// Updates the allocated bounds to include this allocation /// 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 = x;
self.allocated.y = self.allocated.y.min(y); self.allocated.y = y;
self.allocated.width = self.allocated.width.max(width); self.allocated.width = width;
self.allocated.height = self.allocated.height.max(height); self.allocated.height = height;
Area { Area {
x, x,
@@ -108,4 +108,27 @@ impl DrawContext {
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));
} }
/// 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;
}
} }
+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());
}