Skip to main content

teksilo_core/
widget_builder_branching.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Branching widget types and child-insertion trait for the `teksu!` DSL.
5//!
6//! `TeksiBranch{,3,4}` are two-, three-, and four-way sum types over Widget
7//! implementations. They exist so `if`/`else` and small `match` arms in
8//! `teksu!` can yield heterogeneous widget types from the same position
9//! without boxing. Each variant implements Widget by delegating every
10//! method to the active arm.
11//!
12//! `IntoTeksiChild` is the dispatch trait the macro uses when it cannot
13//! decide at expansion time whether a child expression is a widget value
14//! or a pre-registered `WidgetId` (the `#{ expr }` escape case). It
15//! produces a `PendingChild`, which Category A containers already know
16//! how to route through their `child()` / `add_child()` path.
17
18use teksilo_canvas::{Canvas, Rect, SizeProposal};
19
20use crate::accessibility::AccessNodeBuilder;
21use crate::build_context::BuildContext;
22use crate::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
23use crate::widget_builder::HandlerSet;
24use crate::widget_id::WidgetId;
25
26// ---------------------------------------------------------------------------
27// TeksiBranch — two-way sum type
28// ---------------------------------------------------------------------------
29
30#[derive(Debug)]
31pub enum TeksiBranch<L: Widget, R: Widget> {
32    L(L),
33    R(R),
34}
35
36impl<L: Widget, R: Widget> Widget for TeksiBranch<L, R> {
37    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
38        match self {
39            TeksiBranch::L(w) => w.build(ctx),
40            TeksiBranch::R(w) => w.build(ctx),
41        }
42    }
43
44    fn layout_response(
45        &self,
46        proposal: SizeProposal,
47        ctx: &LayoutContext,
48    ) -> crate::widget::LayoutResponse {
49        match self {
50            TeksiBranch::L(w) => w.layout_response(proposal, ctx),
51            TeksiBranch::R(w) => w.layout_response(proposal, ctx),
52        }
53    }
54
55    fn place_children(
56        &self,
57        bounds: Rect,
58        proposal: SizeProposal,
59        children: &mut [WidgetPlacement],
60        ctx: &LayoutContext,
61    ) {
62        match self {
63            TeksiBranch::L(w) => w.place_children(bounds, proposal, children, ctx),
64            TeksiBranch::R(w) => w.place_children(bounds, proposal, children, ctx),
65        }
66    }
67
68    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
69        match self {
70            TeksiBranch::L(w) => w.paint(bounds, canvas, ctx),
71            TeksiBranch::R(w) => w.paint(bounds, canvas, ctx),
72        }
73    }
74
75    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
76        match self {
77            TeksiBranch::L(w) => w.accessibility(builder),
78            TeksiBranch::R(w) => w.accessibility(builder),
79        }
80    }
81
82    fn accessible_title_hint(&self) -> Option<String> {
83        match self {
84            TeksiBranch::L(w) => w.accessible_title_hint(),
85            TeksiBranch::R(w) => w.accessible_title_hint(),
86        }
87    }
88
89    fn initial_focus_hint(&self) -> Option<WidgetId> {
90        match self {
91            TeksiBranch::L(w) => w.initial_focus_hint(),
92            TeksiBranch::R(w) => w.initial_focus_hint(),
93        }
94    }
95
96    fn children(&self) -> Vec<WidgetId> {
97        match self {
98            TeksiBranch::L(w) => w.children(),
99            TeksiBranch::R(w) => w.children(),
100        }
101    }
102
103    fn clips_children(&self) -> bool {
104        match self {
105            TeksiBranch::L(w) => w.clips_children(),
106            TeksiBranch::R(w) => w.clips_children(),
107        }
108    }
109
110    fn take_handler_set(&mut self) -> Option<HandlerSet> {
111        match self {
112            TeksiBranch::L(w) => w.take_handler_set(),
113            TeksiBranch::R(w) => w.take_handler_set(),
114        }
115    }
116}
117
118// ---------------------------------------------------------------------------
119// TeksiBranch3 — three-way sum type
120// ---------------------------------------------------------------------------
121
122#[derive(Debug)]
123pub enum TeksiBranch3<A: Widget, B: Widget, C: Widget> {
124    A(A),
125    B(B),
126    C(C),
127}
128
129impl<A: Widget, B: Widget, C: Widget> Widget for TeksiBranch3<A, B, C> {
130    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
131        match self {
132            TeksiBranch3::A(w) => w.build(ctx),
133            TeksiBranch3::B(w) => w.build(ctx),
134            TeksiBranch3::C(w) => w.build(ctx),
135        }
136    }
137
138    fn layout_response(
139        &self,
140        proposal: SizeProposal,
141        ctx: &LayoutContext,
142    ) -> crate::widget::LayoutResponse {
143        match self {
144            TeksiBranch3::A(w) => w.layout_response(proposal, ctx),
145            TeksiBranch3::B(w) => w.layout_response(proposal, ctx),
146            TeksiBranch3::C(w) => w.layout_response(proposal, ctx),
147        }
148    }
149
150    fn place_children(
151        &self,
152        bounds: Rect,
153        proposal: SizeProposal,
154        children: &mut [WidgetPlacement],
155        ctx: &LayoutContext,
156    ) {
157        match self {
158            TeksiBranch3::A(w) => w.place_children(bounds, proposal, children, ctx),
159            TeksiBranch3::B(w) => w.place_children(bounds, proposal, children, ctx),
160            TeksiBranch3::C(w) => w.place_children(bounds, proposal, children, ctx),
161        }
162    }
163
164    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
165        match self {
166            TeksiBranch3::A(w) => w.paint(bounds, canvas, ctx),
167            TeksiBranch3::B(w) => w.paint(bounds, canvas, ctx),
168            TeksiBranch3::C(w) => w.paint(bounds, canvas, ctx),
169        }
170    }
171
172    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
173        match self {
174            TeksiBranch3::A(w) => w.accessibility(builder),
175            TeksiBranch3::B(w) => w.accessibility(builder),
176            TeksiBranch3::C(w) => w.accessibility(builder),
177        }
178    }
179
180    fn accessible_title_hint(&self) -> Option<String> {
181        match self {
182            TeksiBranch3::A(w) => w.accessible_title_hint(),
183            TeksiBranch3::B(w) => w.accessible_title_hint(),
184            TeksiBranch3::C(w) => w.accessible_title_hint(),
185        }
186    }
187
188    fn initial_focus_hint(&self) -> Option<WidgetId> {
189        match self {
190            TeksiBranch3::A(w) => w.initial_focus_hint(),
191            TeksiBranch3::B(w) => w.initial_focus_hint(),
192            TeksiBranch3::C(w) => w.initial_focus_hint(),
193        }
194    }
195
196    fn children(&self) -> Vec<WidgetId> {
197        match self {
198            TeksiBranch3::A(w) => w.children(),
199            TeksiBranch3::B(w) => w.children(),
200            TeksiBranch3::C(w) => w.children(),
201        }
202    }
203
204    fn clips_children(&self) -> bool {
205        match self {
206            TeksiBranch3::A(w) => w.clips_children(),
207            TeksiBranch3::B(w) => w.clips_children(),
208            TeksiBranch3::C(w) => w.clips_children(),
209        }
210    }
211
212    fn take_handler_set(&mut self) -> Option<HandlerSet> {
213        match self {
214            TeksiBranch3::A(w) => w.take_handler_set(),
215            TeksiBranch3::B(w) => w.take_handler_set(),
216            TeksiBranch3::C(w) => w.take_handler_set(),
217        }
218    }
219}
220
221// ---------------------------------------------------------------------------
222// TeksiBranch4 — four-way sum type
223// ---------------------------------------------------------------------------
224
225#[derive(Debug)]
226pub enum TeksiBranch4<A: Widget, B: Widget, C: Widget, D: Widget> {
227    A(A),
228    B(B),
229    C(C),
230    D(D),
231}
232
233impl<A: Widget, B: Widget, C: Widget, D: Widget> Widget for TeksiBranch4<A, B, C, D> {
234    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
235        match self {
236            TeksiBranch4::A(w) => w.build(ctx),
237            TeksiBranch4::B(w) => w.build(ctx),
238            TeksiBranch4::C(w) => w.build(ctx),
239            TeksiBranch4::D(w) => w.build(ctx),
240        }
241    }
242
243    fn layout_response(
244        &self,
245        proposal: SizeProposal,
246        ctx: &LayoutContext,
247    ) -> crate::widget::LayoutResponse {
248        match self {
249            TeksiBranch4::A(w) => w.layout_response(proposal, ctx),
250            TeksiBranch4::B(w) => w.layout_response(proposal, ctx),
251            TeksiBranch4::C(w) => w.layout_response(proposal, ctx),
252            TeksiBranch4::D(w) => w.layout_response(proposal, ctx),
253        }
254    }
255
256    fn place_children(
257        &self,
258        bounds: Rect,
259        proposal: SizeProposal,
260        children: &mut [WidgetPlacement],
261        ctx: &LayoutContext,
262    ) {
263        match self {
264            TeksiBranch4::A(w) => w.place_children(bounds, proposal, children, ctx),
265            TeksiBranch4::B(w) => w.place_children(bounds, proposal, children, ctx),
266            TeksiBranch4::C(w) => w.place_children(bounds, proposal, children, ctx),
267            TeksiBranch4::D(w) => w.place_children(bounds, proposal, children, ctx),
268        }
269    }
270
271    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
272        match self {
273            TeksiBranch4::A(w) => w.paint(bounds, canvas, ctx),
274            TeksiBranch4::B(w) => w.paint(bounds, canvas, ctx),
275            TeksiBranch4::C(w) => w.paint(bounds, canvas, ctx),
276            TeksiBranch4::D(w) => w.paint(bounds, canvas, ctx),
277        }
278    }
279
280    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
281        match self {
282            TeksiBranch4::A(w) => w.accessibility(builder),
283            TeksiBranch4::B(w) => w.accessibility(builder),
284            TeksiBranch4::C(w) => w.accessibility(builder),
285            TeksiBranch4::D(w) => w.accessibility(builder),
286        }
287    }
288
289    fn accessible_title_hint(&self) -> Option<String> {
290        match self {
291            TeksiBranch4::A(w) => w.accessible_title_hint(),
292            TeksiBranch4::B(w) => w.accessible_title_hint(),
293            TeksiBranch4::C(w) => w.accessible_title_hint(),
294            TeksiBranch4::D(w) => w.accessible_title_hint(),
295        }
296    }
297
298    fn initial_focus_hint(&self) -> Option<WidgetId> {
299        match self {
300            TeksiBranch4::A(w) => w.initial_focus_hint(),
301            TeksiBranch4::B(w) => w.initial_focus_hint(),
302            TeksiBranch4::C(w) => w.initial_focus_hint(),
303            TeksiBranch4::D(w) => w.initial_focus_hint(),
304        }
305    }
306
307    fn children(&self) -> Vec<WidgetId> {
308        match self {
309            TeksiBranch4::A(w) => w.children(),
310            TeksiBranch4::B(w) => w.children(),
311            TeksiBranch4::C(w) => w.children(),
312            TeksiBranch4::D(w) => w.children(),
313        }
314    }
315
316    fn clips_children(&self) -> bool {
317        match self {
318            TeksiBranch4::A(w) => w.clips_children(),
319            TeksiBranch4::B(w) => w.clips_children(),
320            TeksiBranch4::C(w) => w.clips_children(),
321            TeksiBranch4::D(w) => w.clips_children(),
322        }
323    }
324
325    fn take_handler_set(&mut self) -> Option<HandlerSet> {
326        match self {
327            TeksiBranch4::A(w) => w.take_handler_set(),
328            TeksiBranch4::B(w) => w.take_handler_set(),
329            TeksiBranch4::C(w) => w.take_handler_set(),
330            TeksiBranch4::D(w) => w.take_handler_set(),
331        }
332    }
333}
334
335// ---------------------------------------------------------------------------
336// IntoTeksiChild — widget-or-id dispatch for #{ expr } child positions
337// ---------------------------------------------------------------------------
338
339/// Dispatch trait the `teksu!` macro uses to route child expressions whose
340/// static type isn't known at expansion time (the `#{ expr }` escape).
341/// `impl Widget + 'static` values lower to `PendingChild::Deferred`;
342/// pre-registered `WidgetId` values lower to `PendingChild::Id`.
343pub trait IntoTeksiChild {
344    fn into_pending(self) -> PendingChild;
345}
346
347impl<W: Widget + 'static> IntoTeksiChild for W {
348    fn into_pending(self) -> PendingChild {
349        PendingChild::Deferred(Box::new(self))
350    }
351}
352
353impl IntoTeksiChild for WidgetId {
354    fn into_pending(self) -> PendingChild {
355        PendingChild::Id(self)
356    }
357}
358
359// ---------------------------------------------------------------------------
360// IntoTeksiCondition — reactive/static dispatch for `if bare_ident { ... }`
361// ---------------------------------------------------------------------------
362
363/// Dispatch trait the `teksu!` macro uses for `if bare_ident { Element }`
364/// — the `teksu!` "reactive conditionals" pattern. The bare-identifier form
365/// lowers to a call on this trait; which impl fires (and thus whether
366/// the element is conditionally built or always built with bound
367/// visibility) is decided at monomorphization.
368///
369/// - `bool`: static — the element is built only when the flag is true.
370///   Returns `Some(id)` if built, `None` if skipped.
371/// - `Signal<bool>` / `Prop<bool>`: reactive — the element is always
372///   built, and its visibility is bound to the signal via
373///   `BuildContext::visible_when`. Returns `Some(id)` unconditionally.
374///
375/// The return type is `Option<WidgetId>` so the macro can use a single
376/// lowering shape (`if let Some(id) = ... { parent.add_child(id) }`)
377/// that works for both cases.
378pub trait IntoTeksiCondition {
379    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
380        self,
381        child: W,
382        ctx: &mut crate::build_context::BuildContext,
383    ) -> Option<WidgetId>;
384}
385
386impl IntoTeksiCondition for bool {
387    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
388        self,
389        child: W,
390        ctx: &mut crate::build_context::BuildContext,
391    ) -> Option<WidgetId> {
392        if self { Some(ctx.add(child)) } else { None }
393    }
394}
395
396impl IntoTeksiCondition for crate::signal::Signal<bool> {
397    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
398        self,
399        child: W,
400        ctx: &mut crate::build_context::BuildContext,
401    ) -> Option<WidgetId> {
402        let id = ctx.add(child);
403        ctx.visible_when(id, self);
404        Some(id)
405    }
406}
407
408impl IntoTeksiCondition for crate::signal::Prop<bool> {
409    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
410        self,
411        child: W,
412        ctx: &mut crate::build_context::BuildContext,
413    ) -> Option<WidgetId> {
414        let id = ctx.add(child);
415        ctx.visible_when(id, self);
416        Some(id)
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::test_widgets::FillWidget;
424    use crate::widget_tree::WidgetTree;
425    use teksilo_canvas::SizeProposal;
426    use teksilo_tokens::Color;
427
428    #[test]
429    fn teksilo_branch_dispatches_to_active_variant() {
430        // Build two trees, one with each variant, confirm each variant's
431        // widget actually runs its own build/size/paint path.
432        let mut tree_l = WidgetTree::new();
433        let id_l = tree_l.add(TeksiBranch::<FillWidget, FillWidget>::L(
434            FillWidget::new().background(Color::RED),
435        ));
436        tree_l.layout(SizeProposal::exact(100.0, 50.0));
437        assert!((tree_l.bounds(id_l).width - 100.0).abs() < 0.01);
438
439        let mut tree_r = WidgetTree::new();
440        let id_r = tree_r.add(TeksiBranch::<FillWidget, FillWidget>::R(
441            FillWidget::new().background(Color::BLUE),
442        ));
443        tree_r.layout(SizeProposal::exact(80.0, 40.0));
444        assert!((tree_r.bounds(id_r).width - 80.0).abs() < 0.01);
445    }
446
447    #[test]
448    fn teksilo_branch3_dispatches_to_active_variant() {
449        let mut tree = WidgetTree::new();
450        let id = tree.add(TeksiBranch3::<FillWidget, FillWidget, FillWidget>::B(
451            FillWidget::new(),
452        ));
453        tree.layout(SizeProposal::exact(120.0, 60.0));
454        assert!((tree.bounds(id).width - 120.0).abs() < 0.01);
455    }
456
457    #[test]
458    fn into_teksilo_child_routes_widget_to_deferred() {
459        let pending = FillWidget::new().into_pending();
460        assert!(matches!(pending, PendingChild::Deferred(_)));
461    }
462
463    #[test]
464    fn into_teksilo_child_routes_widget_id_to_id() {
465        let mut tree = WidgetTree::new();
466        let leaf = tree.add(FillWidget::new());
467        let pending = leaf.into_pending();
468        match pending {
469            PendingChild::Id(id) => assert_eq!(id, leaf),
470            _ => panic!("expected PendingChild::Id"),
471        }
472    }
473}