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 accessible_title_node(&self) -> Option<crate::widget_id::WidgetId> {
90        match self {
91            TeksiBranch::L(w) => w.accessible_title_node(),
92            TeksiBranch::R(w) => w.accessible_title_node(),
93        }
94    }
95
96    fn initial_focus_hint(&self) -> Option<WidgetId> {
97        match self {
98            TeksiBranch::L(w) => w.initial_focus_hint(),
99            TeksiBranch::R(w) => w.initial_focus_hint(),
100        }
101    }
102
103    fn children(&self) -> Vec<WidgetId> {
104        match self {
105            TeksiBranch::L(w) => w.children(),
106            TeksiBranch::R(w) => w.children(),
107        }
108    }
109
110    fn clips_children(&self) -> bool {
111        match self {
112            TeksiBranch::L(w) => w.clips_children(),
113            TeksiBranch::R(w) => w.clips_children(),
114        }
115    }
116
117    fn take_handler_set(&mut self) -> Option<HandlerSet> {
118        match self {
119            TeksiBranch::L(w) => w.take_handler_set(),
120            TeksiBranch::R(w) => w.take_handler_set(),
121        }
122    }
123}
124
125// ---------------------------------------------------------------------------
126// TeksiBranch3 — three-way sum type
127// ---------------------------------------------------------------------------
128
129#[derive(Debug)]
130pub enum TeksiBranch3<A: Widget, B: Widget, C: Widget> {
131    A(A),
132    B(B),
133    C(C),
134}
135
136impl<A: Widget, B: Widget, C: Widget> Widget for TeksiBranch3<A, B, C> {
137    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
138        match self {
139            TeksiBranch3::A(w) => w.build(ctx),
140            TeksiBranch3::B(w) => w.build(ctx),
141            TeksiBranch3::C(w) => w.build(ctx),
142        }
143    }
144
145    fn layout_response(
146        &self,
147        proposal: SizeProposal,
148        ctx: &LayoutContext,
149    ) -> crate::widget::LayoutResponse {
150        match self {
151            TeksiBranch3::A(w) => w.layout_response(proposal, ctx),
152            TeksiBranch3::B(w) => w.layout_response(proposal, ctx),
153            TeksiBranch3::C(w) => w.layout_response(proposal, ctx),
154        }
155    }
156
157    fn place_children(
158        &self,
159        bounds: Rect,
160        proposal: SizeProposal,
161        children: &mut [WidgetPlacement],
162        ctx: &LayoutContext,
163    ) {
164        match self {
165            TeksiBranch3::A(w) => w.place_children(bounds, proposal, children, ctx),
166            TeksiBranch3::B(w) => w.place_children(bounds, proposal, children, ctx),
167            TeksiBranch3::C(w) => w.place_children(bounds, proposal, children, ctx),
168        }
169    }
170
171    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
172        match self {
173            TeksiBranch3::A(w) => w.paint(bounds, canvas, ctx),
174            TeksiBranch3::B(w) => w.paint(bounds, canvas, ctx),
175            TeksiBranch3::C(w) => w.paint(bounds, canvas, ctx),
176        }
177    }
178
179    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
180        match self {
181            TeksiBranch3::A(w) => w.accessibility(builder),
182            TeksiBranch3::B(w) => w.accessibility(builder),
183            TeksiBranch3::C(w) => w.accessibility(builder),
184        }
185    }
186
187    fn accessible_title_hint(&self) -> Option<String> {
188        match self {
189            TeksiBranch3::A(w) => w.accessible_title_hint(),
190            TeksiBranch3::B(w) => w.accessible_title_hint(),
191            TeksiBranch3::C(w) => w.accessible_title_hint(),
192        }
193    }
194
195    fn accessible_title_node(&self) -> Option<crate::widget_id::WidgetId> {
196        match self {
197            TeksiBranch3::A(w) => w.accessible_title_node(),
198            TeksiBranch3::B(w) => w.accessible_title_node(),
199            TeksiBranch3::C(w) => w.accessible_title_node(),
200        }
201    }
202
203    fn initial_focus_hint(&self) -> Option<WidgetId> {
204        match self {
205            TeksiBranch3::A(w) => w.initial_focus_hint(),
206            TeksiBranch3::B(w) => w.initial_focus_hint(),
207            TeksiBranch3::C(w) => w.initial_focus_hint(),
208        }
209    }
210
211    fn children(&self) -> Vec<WidgetId> {
212        match self {
213            TeksiBranch3::A(w) => w.children(),
214            TeksiBranch3::B(w) => w.children(),
215            TeksiBranch3::C(w) => w.children(),
216        }
217    }
218
219    fn clips_children(&self) -> bool {
220        match self {
221            TeksiBranch3::A(w) => w.clips_children(),
222            TeksiBranch3::B(w) => w.clips_children(),
223            TeksiBranch3::C(w) => w.clips_children(),
224        }
225    }
226
227    fn take_handler_set(&mut self) -> Option<HandlerSet> {
228        match self {
229            TeksiBranch3::A(w) => w.take_handler_set(),
230            TeksiBranch3::B(w) => w.take_handler_set(),
231            TeksiBranch3::C(w) => w.take_handler_set(),
232        }
233    }
234}
235
236// ---------------------------------------------------------------------------
237// TeksiBranch4 — four-way sum type
238// ---------------------------------------------------------------------------
239
240#[derive(Debug)]
241pub enum TeksiBranch4<A: Widget, B: Widget, C: Widget, D: Widget> {
242    A(A),
243    B(B),
244    C(C),
245    D(D),
246}
247
248impl<A: Widget, B: Widget, C: Widget, D: Widget> Widget for TeksiBranch4<A, B, C, D> {
249    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
250        match self {
251            TeksiBranch4::A(w) => w.build(ctx),
252            TeksiBranch4::B(w) => w.build(ctx),
253            TeksiBranch4::C(w) => w.build(ctx),
254            TeksiBranch4::D(w) => w.build(ctx),
255        }
256    }
257
258    fn layout_response(
259        &self,
260        proposal: SizeProposal,
261        ctx: &LayoutContext,
262    ) -> crate::widget::LayoutResponse {
263        match self {
264            TeksiBranch4::A(w) => w.layout_response(proposal, ctx),
265            TeksiBranch4::B(w) => w.layout_response(proposal, ctx),
266            TeksiBranch4::C(w) => w.layout_response(proposal, ctx),
267            TeksiBranch4::D(w) => w.layout_response(proposal, ctx),
268        }
269    }
270
271    fn place_children(
272        &self,
273        bounds: Rect,
274        proposal: SizeProposal,
275        children: &mut [WidgetPlacement],
276        ctx: &LayoutContext,
277    ) {
278        match self {
279            TeksiBranch4::A(w) => w.place_children(bounds, proposal, children, ctx),
280            TeksiBranch4::B(w) => w.place_children(bounds, proposal, children, ctx),
281            TeksiBranch4::C(w) => w.place_children(bounds, proposal, children, ctx),
282            TeksiBranch4::D(w) => w.place_children(bounds, proposal, children, ctx),
283        }
284    }
285
286    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
287        match self {
288            TeksiBranch4::A(w) => w.paint(bounds, canvas, ctx),
289            TeksiBranch4::B(w) => w.paint(bounds, canvas, ctx),
290            TeksiBranch4::C(w) => w.paint(bounds, canvas, ctx),
291            TeksiBranch4::D(w) => w.paint(bounds, canvas, ctx),
292        }
293    }
294
295    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
296        match self {
297            TeksiBranch4::A(w) => w.accessibility(builder),
298            TeksiBranch4::B(w) => w.accessibility(builder),
299            TeksiBranch4::C(w) => w.accessibility(builder),
300            TeksiBranch4::D(w) => w.accessibility(builder),
301        }
302    }
303
304    fn accessible_title_hint(&self) -> Option<String> {
305        match self {
306            TeksiBranch4::A(w) => w.accessible_title_hint(),
307            TeksiBranch4::B(w) => w.accessible_title_hint(),
308            TeksiBranch4::C(w) => w.accessible_title_hint(),
309            TeksiBranch4::D(w) => w.accessible_title_hint(),
310        }
311    }
312
313    fn accessible_title_node(&self) -> Option<crate::widget_id::WidgetId> {
314        match self {
315            TeksiBranch4::A(w) => w.accessible_title_node(),
316            TeksiBranch4::B(w) => w.accessible_title_node(),
317            TeksiBranch4::C(w) => w.accessible_title_node(),
318            TeksiBranch4::D(w) => w.accessible_title_node(),
319        }
320    }
321
322    fn initial_focus_hint(&self) -> Option<WidgetId> {
323        match self {
324            TeksiBranch4::A(w) => w.initial_focus_hint(),
325            TeksiBranch4::B(w) => w.initial_focus_hint(),
326            TeksiBranch4::C(w) => w.initial_focus_hint(),
327            TeksiBranch4::D(w) => w.initial_focus_hint(),
328        }
329    }
330
331    fn children(&self) -> Vec<WidgetId> {
332        match self {
333            TeksiBranch4::A(w) => w.children(),
334            TeksiBranch4::B(w) => w.children(),
335            TeksiBranch4::C(w) => w.children(),
336            TeksiBranch4::D(w) => w.children(),
337        }
338    }
339
340    fn clips_children(&self) -> bool {
341        match self {
342            TeksiBranch4::A(w) => w.clips_children(),
343            TeksiBranch4::B(w) => w.clips_children(),
344            TeksiBranch4::C(w) => w.clips_children(),
345            TeksiBranch4::D(w) => w.clips_children(),
346        }
347    }
348
349    fn take_handler_set(&mut self) -> Option<HandlerSet> {
350        match self {
351            TeksiBranch4::A(w) => w.take_handler_set(),
352            TeksiBranch4::B(w) => w.take_handler_set(),
353            TeksiBranch4::C(w) => w.take_handler_set(),
354            TeksiBranch4::D(w) => w.take_handler_set(),
355        }
356    }
357}
358
359// ---------------------------------------------------------------------------
360// IntoTeksiChild — widget-or-id dispatch for #{ expr } child positions
361// ---------------------------------------------------------------------------
362
363/// Dispatch trait the `teksu!` macro uses to route child expressions whose
364/// static type isn't known at expansion time (the `#{ expr }` escape).
365/// `impl Widget + 'static` values lower to `PendingChild::Deferred`;
366/// pre-registered `WidgetId` values lower to `PendingChild::Id`.
367pub trait IntoTeksiChild {
368    fn into_pending(self) -> PendingChild;
369}
370
371impl<W: Widget + 'static> IntoTeksiChild for W {
372    fn into_pending(self) -> PendingChild {
373        PendingChild::Deferred(Box::new(self))
374    }
375}
376
377impl IntoTeksiChild for WidgetId {
378    fn into_pending(self) -> PendingChild {
379        PendingChild::Id(self)
380    }
381}
382
383// ---------------------------------------------------------------------------
384// IntoTeksiCondition — reactive/static dispatch for `if bare_ident { ... }`
385// ---------------------------------------------------------------------------
386
387/// Dispatch trait the `teksu!` macro uses for `if bare_ident { Element }`
388/// — the `teksu!` "reactive conditionals" pattern. The bare-identifier form
389/// lowers to a call on this trait; which impl fires (and thus whether
390/// the element is conditionally built or always built with bound
391/// visibility) is decided at monomorphization.
392///
393/// - `bool`: static — the element is built only when the flag is true.
394///   Returns `Some(id)` if built, `None` if skipped.
395/// - `Signal<bool>` / `Prop<bool>`: reactive — the element is always
396///   built, and its visibility is bound to the signal via
397///   `BuildContext::visible_when`. Returns `Some(id)` unconditionally.
398///
399/// The return type is `Option<WidgetId>` so the macro can use a single
400/// lowering shape (`if let Some(id) = ... { parent.add_child(id) }`)
401/// that works for both cases.
402pub trait IntoTeksiCondition {
403    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
404        self,
405        child: W,
406        ctx: &mut crate::build_context::BuildContext,
407    ) -> Option<WidgetId>;
408}
409
410impl IntoTeksiCondition for bool {
411    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
412        self,
413        child: W,
414        ctx: &mut crate::build_context::BuildContext,
415    ) -> Option<WidgetId> {
416        if self { Some(ctx.add(child)) } else { None }
417    }
418}
419
420impl IntoTeksiCondition for crate::signal::Signal<bool> {
421    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
422        self,
423        child: W,
424        ctx: &mut crate::build_context::BuildContext,
425    ) -> Option<WidgetId> {
426        let id = ctx.add(child);
427        ctx.visible_when(id, self);
428        Some(id)
429    }
430}
431
432impl IntoTeksiCondition for crate::signal::Prop<bool> {
433    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
434        self,
435        child: W,
436        ctx: &mut crate::build_context::BuildContext,
437    ) -> Option<WidgetId> {
438        let id = ctx.add(child);
439        ctx.visible_when(id, self);
440        Some(id)
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use crate::test_widgets::FillWidget;
448    use crate::widget_tree::WidgetTree;
449    use teksilo_canvas::SizeProposal;
450    use teksilo_tokens::Color;
451
452    #[test]
453    fn teksilo_branch_dispatches_to_active_variant() {
454        // Build two trees, one with each variant, confirm each variant's
455        // widget actually runs its own build/size/paint path.
456        let mut tree_l = WidgetTree::new();
457        let id_l = tree_l.add(TeksiBranch::<FillWidget, FillWidget>::L(
458            FillWidget::new().background(Color::RED),
459        ));
460        tree_l.layout(SizeProposal::exact(100.0, 50.0));
461        assert!((tree_l.bounds(id_l).width - 100.0).abs() < 0.01);
462
463        let mut tree_r = WidgetTree::new();
464        let id_r = tree_r.add(TeksiBranch::<FillWidget, FillWidget>::R(
465            FillWidget::new().background(Color::BLUE),
466        ));
467        tree_r.layout(SizeProposal::exact(80.0, 40.0));
468        assert!((tree_r.bounds(id_r).width - 80.0).abs() < 0.01);
469    }
470
471    #[test]
472    fn teksilo_branch3_dispatches_to_active_variant() {
473        let mut tree = WidgetTree::new();
474        let id = tree.add(TeksiBranch3::<FillWidget, FillWidget, FillWidget>::B(
475            FillWidget::new(),
476        ));
477        tree.layout(SizeProposal::exact(120.0, 60.0));
478        assert!((tree.bounds(id).width - 120.0).abs() < 0.01);
479    }
480
481    #[test]
482    fn into_teksilo_child_routes_widget_to_deferred() {
483        let pending = FillWidget::new().into_pending();
484        assert!(matches!(pending, PendingChild::Deferred(_)));
485    }
486
487    #[test]
488    fn into_teksilo_child_routes_widget_id_to_id() {
489        let mut tree = WidgetTree::new();
490        let leaf = tree.add(FillWidget::new());
491        let pending = leaf.into_pending();
492        match pending {
493            PendingChild::Id(id) => assert_eq!(id, leaf),
494            _ => panic!("expected PendingChild::Id"),
495        }
496    }
497}