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, Point, Rect, SizeProposal};
19
20use crate::accessibility::AccessNodeBuilder;
21use crate::build_context::BuildContext;
22use crate::widget::{
23    LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement, WidgetTreeView,
24};
25use crate::widget_builder::HandlerSet;
26use crate::widget_id::WidgetId;
27
28// ---------------------------------------------------------------------------
29// The delegation, written once
30// ---------------------------------------------------------------------------
31
32/// Emit `impl Widget` for one branch enum, forwarding every method to the
33/// active arm.
34///
35/// Each branch type occupies its arm's own arena node — the tree never sees the
36/// arm again — so a method absent from this list is not overridden, it is gone:
37/// the trait's default answers in its place and the arm silently loses the
38/// behaviour behind it. The three branch widths would be three copies of that
39/// hazard, so they share one list here, and each generated impl denies
40/// `missing_trait_methods` so a method added to `Widget` fails the lint rather
41/// than the app.
42///
43/// The variant names and the type-parameter names are the same identifiers by
44/// construction (`L`/`R`, `A`/`B`/`C`, …), which is what lets one repetition
45/// serve as both.
46macro_rules! impl_widget_for_branch {
47    ($branch:ident, $($arm:ident),+) => {
48        #[deny(clippy::missing_trait_methods)]
49        impl<$($arm: Widget),+> Widget for $branch<$($arm),+> {
50            fn type_name(&self) -> &'static str {
51                match self { $($branch::$arm(w) => w.type_name()),+ }
52            }
53
54            fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
55                match self { $($branch::$arm(w) => w.build(ctx)),+ }
56            }
57
58            fn layout_response(
59                &self,
60                proposal: SizeProposal,
61                ctx: &LayoutContext,
62            ) -> crate::widget::LayoutResponse {
63                match self { $($branch::$arm(w) => w.layout_response(proposal, ctx)),+ }
64            }
65
66            fn cacheable_layout(&self) -> bool {
67                match self { $($branch::$arm(w) => w.cacheable_layout()),+ }
68            }
69
70            fn place_children(
71                &self,
72                bounds: Rect,
73                proposal: SizeProposal,
74                children: &mut [WidgetPlacement],
75                ctx: &LayoutContext,
76            ) {
77                match self {
78                    $($branch::$arm(w) => w.place_children(bounds, proposal, children, ctx)),+
79                }
80            }
81
82            fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
83                match self { $($branch::$arm(w) => w.paint(bounds, canvas, ctx)),+ }
84            }
85
86            fn wants_after_paint(&self) -> bool {
87                match self { $($branch::$arm(w) => w.wants_after_paint()),+ }
88            }
89
90            fn after_paint(&self, view: &WidgetTreeView<'_>, ctx: &PaintContext) {
91                match self { $($branch::$arm(w) => w.after_paint(view, ctx)),+ }
92            }
93
94            fn wants_post_paint(&self) -> bool {
95                match self { $($branch::$arm(w) => w.wants_post_paint()),+ }
96            }
97
98            fn post_paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
99                match self { $($branch::$arm(w) => w.post_paint(bounds, canvas, ctx)),+ }
100            }
101
102            fn accessibility(&self, builder: &mut AccessNodeBuilder) {
103                match self { $($branch::$arm(w) => w.accessibility(builder)),+ }
104            }
105
106            fn culls_children(&self) -> bool {
107                match self { $($branch::$arm(w) => w.culls_children()),+ }
108            }
109
110            fn wants_descendant_redirects(&self) -> bool {
111                match self { $($branch::$arm(w) => w.wants_descendant_redirects()),+ }
112            }
113
114            fn a11y_redirect_descendant(
115                &self,
116                self_id: WidgetId,
117                descendant: WidgetId,
118            ) -> Option<accesskit::NodeId> {
119                match self {
120                    $($branch::$arm(w) => w.a11y_redirect_descendant(self_id, descendant)),+
121                }
122            }
123
124            fn accessible_title_hint(&self) -> Option<String> {
125                match self { $($branch::$arm(w) => w.accessible_title_hint()),+ }
126            }
127
128            fn accessible_title_node(&self) -> Option<crate::widget_id::WidgetId> {
129                match self { $($branch::$arm(w) => w.accessible_title_node()),+ }
130            }
131
132            fn initial_focus_hint(&self) -> Option<WidgetId> {
133                match self { $($branch::$arm(w) => w.initial_focus_hint()),+ }
134            }
135
136            fn context_menu_key_target(&self) -> Option<WidgetId> {
137                match self { $($branch::$arm(w) => w.context_menu_key_target()),+ }
138            }
139
140            fn children(&self) -> Vec<WidgetId> {
141                match self { $($branch::$arm(w) => w.children()),+ }
142            }
143
144            fn accessibility_children(&self) -> Option<Vec<WidgetId>> {
145                match self { $($branch::$arm(w) => w.accessibility_children()),+ }
146            }
147
148            fn as_any(&self) -> Option<&dyn std::any::Any> {
149                match self { $($branch::$arm(w) => w.as_any()),+ }
150            }
151
152            fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
153                match self { $($branch::$arm(w) => w.as_any_mut()),+ }
154            }
155
156            fn clips_children(&self) -> bool {
157                match self { $($branch::$arm(w) => w.clips_children()),+ }
158            }
159
160            fn focus_reveal_rect(&self, bounds: Rect) -> Option<Rect> {
161                match self { $($branch::$arm(w) => w.focus_reveal_rect(bounds)),+ }
162            }
163
164            fn hit_shape(&self, local_point: Point, bounds: Rect) -> bool {
165                match self { $($branch::$arm(w) => w.hit_shape(local_point, bounds)),+ }
166            }
167
168            fn accepts_child_hit(&self, child: WidgetId, point: Point) -> bool {
169                match self { $($branch::$arm(w) => w.accepts_child_hit(child, point)),+ }
170            }
171
172            fn hit_outset(
173                &self,
174                kind: teksilo_tokens::PointerKind,
175                tokens: &teksilo_tokens::InputTokens,
176            ) -> teksilo_canvas::EdgeInsets {
177                match self { $($branch::$arm(w) => w.hit_outset(kind, tokens)),+ }
178            }
179
180            fn hit_slop(
181                &self,
182                kind: teksilo_tokens::PointerKind,
183                tokens: &teksilo_tokens::InputTokens,
184            ) -> Option<crate::pointer::hit_slop::HitSlop> {
185                // Named on `WidgetBuilder` too, where it is a consuming builder
186                // method — spelled out so the arm's `Widget` impl is the one
187                // called no matter what is in scope at the expansion site.
188                match self { $($branch::$arm(w) => Widget::hit_slop(w, kind, tokens)),+ }
189            }
190
191            fn hit_distance(&self, local_point: Point, bounds: Rect) -> Option<f32> {
192                match self { $($branch::$arm(w) => w.hit_distance(local_point, bounds)),+ }
193            }
194
195            fn target_regions(&self, bounds: Rect) -> Vec<crate::partition::TargetRegion> {
196                match self { $($branch::$arm(w) => w.target_regions(bounds)),+ }
197            }
198
199            fn preserves_children_on_rebuild(&self) -> bool {
200                match self { $($branch::$arm(w) => w.preserves_children_on_rebuild()),+ }
201            }
202
203            fn tooltip_has_content(&self) -> bool {
204                match self { $($branch::$arm(w) => w.tooltip_has_content()),+ }
205            }
206
207            fn declare_shortcuts(&self) -> Vec<crate::shortcut::Shortcut> {
208                match self { $($branch::$arm(w) => w.declare_shortcuts()),+ }
209            }
210
211            fn take_handler_set(&mut self) -> Option<HandlerSet> {
212                match self { $($branch::$arm(w) => w.take_handler_set()),+ }
213            }
214        }
215    };
216}
217
218// ---------------------------------------------------------------------------
219// TeksiBranch — two-way sum type
220// ---------------------------------------------------------------------------
221
222#[derive(Debug)]
223pub enum TeksiBranch<L: Widget, R: Widget> {
224    L(L),
225    R(R),
226}
227
228impl_widget_for_branch!(TeksiBranch, L, R);
229
230// ---------------------------------------------------------------------------
231// TeksiBranch3 — three-way sum type
232// ---------------------------------------------------------------------------
233
234#[derive(Debug)]
235pub enum TeksiBranch3<A: Widget, B: Widget, C: Widget> {
236    A(A),
237    B(B),
238    C(C),
239}
240
241impl_widget_for_branch!(TeksiBranch3, A, B, C);
242
243// ---------------------------------------------------------------------------
244// TeksiBranch4 — four-way sum type
245// ---------------------------------------------------------------------------
246
247#[derive(Debug)]
248pub enum TeksiBranch4<A: Widget, B: Widget, C: Widget, D: Widget> {
249    A(A),
250    B(B),
251    C(C),
252    D(D),
253}
254
255impl_widget_for_branch!(TeksiBranch4, A, B, C, D);
256
257// ---------------------------------------------------------------------------
258// IntoTeksiChild — widget-or-id dispatch for #{ expr } child positions
259// ---------------------------------------------------------------------------
260
261/// Dispatch trait the `teksu!` macro uses to route child expressions whose
262/// static type isn't known at expansion time (the `#{ expr }` escape).
263/// `impl Widget + 'static` values lower to `PendingChild::Deferred`;
264/// pre-registered `WidgetId` values lower to `PendingChild::Id`.
265pub trait IntoTeksiChild {
266    fn into_pending(self) -> PendingChild;
267}
268
269impl<W: Widget + 'static> IntoTeksiChild for W {
270    fn into_pending(self) -> PendingChild {
271        PendingChild::Deferred(Box::new(self))
272    }
273}
274
275impl IntoTeksiChild for WidgetId {
276    fn into_pending(self) -> PendingChild {
277        PendingChild::Id(self)
278    }
279}
280
281// ---------------------------------------------------------------------------
282// IntoTeksiCondition — reactive/static dispatch for `if bare_ident { ... }`
283// ---------------------------------------------------------------------------
284
285/// Dispatch trait the `teksu!` macro uses for `if bare_ident { Element }`
286/// — the `teksu!` "reactive conditionals" pattern. The bare-identifier form
287/// lowers to a call on this trait; which impl fires (and thus whether
288/// the element is conditionally built or always built with bound
289/// visibility) is decided at monomorphization.
290///
291/// - `bool`: static — the element is built only when the flag is true.
292///   Returns `Some(id)` if built, `None` if skipped.
293/// - `Signal<bool>` / `Prop<bool>`: reactive — the element is always
294///   built, and its visibility is bound to the signal via
295///   `BuildContext::visible_when`. Returns `Some(id)` unconditionally.
296///
297/// The return type is `Option<WidgetId>` so the macro can use a single
298/// lowering shape (`if let Some(id) = ... { parent.child(id) }`)
299/// that works for both cases.
300pub trait IntoTeksiCondition {
301    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
302        self,
303        child: W,
304        ctx: &mut crate::build_context::BuildContext,
305    ) -> Option<WidgetId>;
306}
307
308impl IntoTeksiCondition for bool {
309    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
310        self,
311        child: W,
312        ctx: &mut crate::build_context::BuildContext,
313    ) -> Option<WidgetId> {
314        if self { Some(ctx.add(child)) } else { None }
315    }
316}
317
318impl IntoTeksiCondition for crate::signal::Signal<bool> {
319    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
320        self,
321        child: W,
322        ctx: &mut crate::build_context::BuildContext,
323    ) -> Option<WidgetId> {
324        let id = ctx.add(child);
325        ctx.visible_when(id, self);
326        Some(id)
327    }
328}
329
330impl IntoTeksiCondition for crate::signal::Prop<bool> {
331    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
332        self,
333        child: W,
334        ctx: &mut crate::build_context::BuildContext,
335    ) -> Option<WidgetId> {
336        let id = ctx.add(child);
337        ctx.visible_when(id, self);
338        Some(id)
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::test_widgets::FillWidget;
346    use crate::widget_tree::WidgetTree;
347    use teksilo_canvas::SizeProposal;
348    use teksilo_tokens::Color;
349
350    #[test]
351    fn teksilo_branch_dispatches_to_active_variant() {
352        // Build two trees, one with each variant, confirm each variant's
353        // widget actually runs its own build/size/paint path.
354        let mut tree_l = WidgetTree::new();
355        let id_l = tree_l.add(TeksiBranch::<FillWidget, FillWidget>::L(
356            FillWidget::new().background(Color::RED),
357        ));
358        tree_l.layout(SizeProposal::exact(100.0, 50.0));
359        assert!((tree_l.bounds(id_l).width - 100.0).abs() < 0.01);
360
361        let mut tree_r = WidgetTree::new();
362        let id_r = tree_r.add(TeksiBranch::<FillWidget, FillWidget>::R(
363            FillWidget::new().background(Color::BLUE),
364        ));
365        tree_r.layout(SizeProposal::exact(80.0, 40.0));
366        assert!((tree_r.bounds(id_r).width - 80.0).abs() < 0.01);
367    }
368
369    #[test]
370    fn teksilo_branch3_dispatches_to_active_variant() {
371        let mut tree = WidgetTree::new();
372        let id = tree.add(TeksiBranch3::<FillWidget, FillWidget, FillWidget>::B(
373            FillWidget::new(),
374        ));
375        tree.layout(SizeProposal::exact(120.0, 60.0));
376        assert!((tree.bounds(id).width - 120.0).abs() < 0.01);
377    }
378
379    #[test]
380    fn into_teksilo_child_routes_widget_to_deferred() {
381        let pending = FillWidget::new().into_pending();
382        assert!(matches!(pending, PendingChild::Deferred(_)));
383    }
384
385    #[test]
386    fn into_teksilo_child_routes_widget_id_to_id() {
387        let mut tree = WidgetTree::new();
388        let leaf = tree.add(FillWidget::new());
389        let pending = leaf.into_pending();
390        match pending {
391            PendingChild::Id(id) => assert_eq!(id, leaf),
392            _ => panic!("expected PendingChild::Id"),
393        }
394    }
395}