Skip to main content

frust_widgets/
either.rs

1//! A two-arm view: [`Either`] and the lazy [`either`] helper.
2//!
3//! A helper whose body is an `if`/`else` (or a two-arm `match`) over two
4//! different view types can return `impl View<State>` through [`either`]
5//! instead of erasing both arms into an [`AnyView`](frust_core::AnyView). Both
6//! arms stay statically typed; only the arm that is live is built.
7//!
8//! # When to use it
9//!
10//! * **Two arms** of different concrete types: [`either`] / [`Either`].
11//! * **Three or more arms**: stay with [`any`](frust_core::any) /
12//!   [`AnyView`](frust_core::AnyView) (nest `Either`s only if the nesting reads
13//!   better than the erasure).
14//! * **An optional child** inside a container: use the container's `.when(..)` /
15//!   `.when_some(..)` sugar rather than an `Either` with an empty arm.
16//!
17//! # Swap semantics
18//!
19//! Switching arms tears the old arm's element down through the old arm's view,
20//! builds the new arm's element from scratch and reports
21//! `LAYOUT | PAINT`: the previous arm's retained state is dropped, exactly as
22//! an `AnyView` concrete-type swap does. Rebuilding the *same* arm reconciles in
23//! place and preserves its state.
24//!
25//! # Focus and capture bookkeeping
26//!
27//! [`EitherWidget`] is an enum widget delegating every pass to the live arm; it
28//! holds no `AnyView` or `Box<dyn Widget>` field, so it is not the swap-blind
29//! wrapper shape (`focus-wrapper-erasure-swap-blind`). One gap remains: both
30//! arms share the single element type `EitherWidget<L, R>`, so a parent
31//! reconciler comparing element type ids across the rebuild cannot see an arm
32//! swap. `Either` therefore does the focus half of that bookkeeping itself: when
33//! an arm swap happens while the focus chain runs through this view
34//! ([`BuildCtx::has_focus`]), the focused widget just stopped existing and the
35//! root is told to release the focus/IME session
36//! ([`mark_focus_orphaned`](frust_core::mark_focus_orphaned)).
37
38use frust_core::{
39    BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent, LayoutCtx,
40    PaintCtx, PaintScene, SemanticsCtx, View, Widget, mark_focus_orphaned,
41};
42use kurbo::Size;
43
44/// A view that is one of two statically-typed arms. See the [module docs](self).
45pub enum Either<L, R> {
46    /// The left arm.
47    Left(L),
48    /// The right arm.
49    Right(R),
50}
51
52/// Pick an arm by `cond`: `l` when true, `r` when false. Only the chosen
53/// closure runs, so the unused arm is never constructed.
54///
55/// Use it for exactly two arms; for three or more, erase with
56/// [`any`](frust_core::any). For an optional child use `.when(..)` /
57/// `.when_some(..)` on the container.
58pub fn either<L, R>(cond: bool, l: impl FnOnce() -> L, r: impl FnOnce() -> R) -> Either<L, R> {
59    if cond {
60        Either::Left(l())
61    } else {
62        Either::Right(r())
63    }
64}
65
66/// Retained widget for [`Either`]: the live arm's widget, delegated to for every
67/// pass.
68pub enum EitherWidget<L, R> {
69    /// The left arm's widget.
70    Left(L),
71    /// The right arm's widget.
72    Right(R),
73}
74
75impl<L: Widget, R: Widget> Widget for EitherWidget<L, R> {
76    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
77        match self {
78            Self::Left(w) => w.layout(ctx, bc),
79            Self::Right(w) => w.layout(ctx, bc),
80        }
81    }
82
83    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
84        match self {
85            Self::Left(w) => w.paint(ctx, scene),
86            Self::Right(w) => w.paint(ctx, scene),
87        }
88    }
89
90    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
91        match self {
92            Self::Left(w) => w.event(ctx, event),
93            Self::Right(w) => w.event(ctx, event),
94        }
95    }
96
97    fn semantics(&self, ctx: &mut SemanticsCtx) {
98        match self {
99            Self::Left(w) => w.semantics(ctx),
100            Self::Right(w) => w.semantics(ctx),
101        }
102    }
103
104    fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
105        match self {
106            Self::Left(w) => w.visit_children(visitor),
107            Self::Right(w) => w.visit_children(visitor),
108        }
109    }
110
111    /// Report the live arm's name: the enum is a storage detail, never a tree
112    /// element in its own right.
113    fn type_name(&self) -> &'static str {
114        match self {
115            Self::Left(w) => w.type_name(),
116            Self::Right(w) => w.type_name(),
117        }
118    }
119}
120
121impl<State: 'static, L: View<State>, R: View<State>> View<State> for Either<L, R> {
122    type Element = EitherWidget<L::Element, R::Element>;
123
124    fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
125        match self {
126            Self::Left(v) => EitherWidget::Left(v.build(ctx)),
127            Self::Right(v) => EitherWidget::Right(v.build(ctx)),
128        }
129    }
130
131    fn rebuild(
132        &self,
133        prev: &Self,
134        element: &mut Self::Element,
135        ctx: &mut BuildCtx<'_>,
136    ) -> ChangeFlags {
137        match (self, prev, &mut *element) {
138            (Self::Left(v), Self::Left(p), EitherWidget::Left(w)) => v.rebuild(p, w, ctx),
139            (Self::Right(v), Self::Right(p), EitherWidget::Right(w)) => v.rebuild(p, w, ctx),
140            _ => {
141                // Arm swap: tear the old arm down through its own view, then
142                // build the new arm fresh.
143                prev.teardown(element, ctx);
144                // Both arms share one element type, so the parent's type-id
145                // comparison cannot see this swap; raise the focus-orphan
146                // signal here when the live chain ran through this view.
147                if ctx.has_focus() {
148                    mark_focus_orphaned();
149                }
150                *element = self.build(ctx);
151                ChangeFlags::LAYOUT | ChangeFlags::PAINT
152            }
153        }
154    }
155
156    fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
157        match (self, element) {
158            (Self::Left(v), EitherWidget::Left(w)) => v.teardown(w, ctx),
159            (Self::Right(v), EitherWidget::Right(w)) => v.teardown(w, ctx),
160            _ => {}
161        }
162    }
163
164    fn message(&self, element: &mut Self::Element, state: &mut State) {
165        match (self, element) {
166            (Self::Left(v), EitherWidget::Left(w)) => v.message(w, state),
167            (Self::Right(v), EitherWidget::Right(w)) => v.message(w, state),
168            _ => {}
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use std::cell::RefCell;
176    use std::rc::Rc;
177
178    use frust_core::accesskit::Role;
179    use frust_core::{DiscardScene, FrameTime, RenderRoot, SemanticsUpdate, take_focus_orphaned};
180    use frust_text::TextContext;
181
182    use super::*;
183    use crate::{ButtonView, CheckboxView, button, checkbox};
184
185    type Log = Rc<RefCell<Vec<String>>>;
186
187    /// A fixed-size arm that logs build / teardown / drop under its tag.
188    struct Arm {
189        tag: &'static str,
190        size: Size,
191        log: Log,
192    }
193
194    struct ArmWidget {
195        tag: &'static str,
196        size: Size,
197        log: Log,
198        painted: u32,
199    }
200
201    impl Drop for ArmWidget {
202        fn drop(&mut self) {
203            self.log.borrow_mut().push(format!("drop {}", self.tag));
204        }
205    }
206
207    impl View<()> for Arm {
208        type Element = ArmWidget;
209        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ArmWidget {
210            self.log.borrow_mut().push(format!("build {}", self.tag));
211            ArmWidget {
212                tag: self.tag,
213                size: self.size,
214                log: Rc::clone(&self.log),
215                painted: 0,
216            }
217        }
218        fn rebuild(
219            &self,
220            _prev: &Self,
221            el: &mut ArmWidget,
222            _ctx: &mut BuildCtx<'_>,
223        ) -> ChangeFlags {
224            el.size = self.size;
225            ChangeFlags::NONE
226        }
227        fn teardown(&self, _el: &mut ArmWidget, _ctx: &mut BuildCtx<'_>) {
228            self.log.borrow_mut().push(format!("teardown {}", self.tag));
229        }
230    }
231
232    impl Widget for ArmWidget {
233        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
234            bc.constrain(self.size)
235        }
236        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
237            self.painted += 1;
238            self.log
239                .borrow_mut()
240                .push(format!("paint {} #{}", self.tag, self.painted));
241        }
242    }
243
244    fn arm(tag: &'static str, w: f64, h: f64, log: &Log) -> Arm {
245        Arm {
246            tag,
247            size: Size::new(w, h),
248            log: Rc::clone(log),
249        }
250    }
251
252    fn new_log() -> Log {
253        Rc::new(RefCell::new(Vec::new()))
254    }
255
256    fn drain(log: &Log) -> Vec<String> {
257        std::mem::take(&mut *log.borrow_mut())
258    }
259
260    type TwoArms = Either<Arm, Arm>;
261
262    #[test]
263    fn helper_is_lazy_and_picks_the_arm() {
264        let built = Rc::new(RefCell::new(Vec::new()));
265        let (b1, b2) = (Rc::clone(&built), Rc::clone(&built));
266        let v = either(
267            true,
268            move || {
269                b1.borrow_mut().push("l");
270                1u8
271            },
272            move || {
273                b2.borrow_mut().push("r");
274                2u8
275            },
276        );
277        assert!(matches!(v, Either::Left(1)));
278        assert_eq!(*built.borrow(), vec!["l"], "only the chosen arm runs");
279        assert!(matches!(either(false, || 1u8, || 2u8), Either::Right(2)));
280    }
281
282    #[test]
283    fn layout_and_paint_delegate_to_the_active_arm() {
284        let log = new_log();
285        let mut root: RenderRoot<(), TwoArms> = RenderRoot::new();
286        let mut state = ();
287        let mut logic = |_: &mut ()| {
288            either(
289                false,
290                || arm("a", 10.0, 10.0, &log),
291                || arm("b", 30.0, 20.0, &log),
292            )
293        };
294        root.rebuild(&mut logic, &mut state);
295        root.layout(Size::new(200.0, 200.0));
296        root.paint(&mut DiscardScene, FrameTime::from_nanos(0));
297        let nodes = root.inspect();
298        assert!(
299            nodes[0].type_name.contains("EitherWidget"),
300            "root is the either widget, got {}",
301            nodes[0].type_name
302        );
303        assert_eq!(nodes[0].bounds.size(), Size::new(30.0, 20.0));
304        let seen = drain(&log);
305        assert!(seen.contains(&"build b".to_string()));
306        assert!(seen.contains(&"paint b #1".to_string()));
307        assert!(
308            !seen.iter().any(|e| e.contains(" a")),
309            "inactive arm untouched: {seen:?}"
310        );
311    }
312
313    #[test]
314    fn same_arm_rebuild_keeps_state_swap_resets_it() {
315        let log = new_log();
316        let mut root: RenderRoot<(), TwoArms> = RenderRoot::new();
317        let mut state = ();
318        let left = std::cell::Cell::new(true);
319        let mut logic = |_: &mut ()| {
320            either(
321                left.get(),
322                || arm("a", 10.0, 10.0, &log),
323                || arm("b", 10.0, 10.0, &log),
324            )
325        };
326        root.rebuild(&mut logic, &mut state);
327        root.layout(Size::new(100.0, 100.0));
328        root.paint(&mut DiscardScene, FrameTime::from_nanos(0));
329        root.paint(&mut DiscardScene, FrameTime::from_nanos(1));
330        assert_eq!(drain(&log), ["build a", "paint a #1", "paint a #2"]);
331
332        // Same arm: reconciled in place, the paint counter survives.
333        root.rebuild(&mut logic, &mut state);
334        root.layout(Size::new(100.0, 100.0));
335        root.paint(&mut DiscardScene, FrameTime::from_nanos(2));
336        assert_eq!(drain(&log), ["paint a #3"]);
337
338        // Swap: old arm torn down and dropped, new arm built fresh.
339        left.set(false);
340        root.rebuild(&mut logic, &mut state);
341        root.layout(Size::new(100.0, 100.0));
342        root.paint(&mut DiscardScene, FrameTime::from_nanos(3));
343        assert_eq!(
344            drain(&log),
345            ["teardown a", "build b", "drop a", "paint b #1"]
346        );
347
348        // Swap back: b's state is gone, a starts at #1 again.
349        left.set(true);
350        root.rebuild(&mut logic, &mut state);
351        root.paint(&mut DiscardScene, FrameTime::from_nanos(4));
352        assert_eq!(
353            drain(&log),
354            ["teardown b", "build a", "drop b", "paint a #1"]
355        );
356    }
357
358    #[test]
359    fn swap_reports_full_rebuild_flags_and_same_arm_does_not() {
360        let log = new_log();
361        let mut counter = 0u64;
362        let mut ctx = BuildCtx::new(&mut counter);
363        let prev: TwoArms = Either::Left(arm("a", 1.0, 1.0, &log));
364        let mut el = prev.build(&mut ctx);
365        let same: TwoArms = Either::Left(arm("a", 1.0, 1.0, &log));
366        assert_eq!(same.rebuild(&prev, &mut el, &mut ctx), ChangeFlags::NONE);
367        let next: TwoArms = Either::Right(arm("b", 1.0, 1.0, &log));
368        let flags = next.rebuild(&same, &mut el, &mut ctx);
369        assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
370        assert!(matches!(el, EitherWidget::Right(_)));
371    }
372
373    #[test]
374    fn swap_under_a_live_focus_chain_orphans_focus_only_then() {
375        let log = new_log();
376        let mut counter = 0u64;
377        let mut ctx = BuildCtx::new(&mut counter);
378        let a: TwoArms = Either::Left(arm("a", 1.0, 1.0, &log));
379        let b: TwoArms = Either::Right(arm("b", 1.0, 1.0, &log));
380        let _ = take_focus_orphaned();
381
382        // No live chain: a swap raises nothing.
383        ctx.set_has_focus(false);
384        let mut el = a.build(&mut ctx);
385        b.rebuild(&a, &mut el, &mut ctx);
386        assert!(!take_focus_orphaned(), "unfocused swap marks nothing");
387
388        // Live chain, same arm: nothing.
389        ctx.set_has_focus(true);
390        let b2: TwoArms = Either::Right(arm("b", 1.0, 1.0, &log));
391        b2.rebuild(&b, &mut el, &mut ctx);
392        assert!(!take_focus_orphaned(), "same-arm rebuild marks nothing");
393
394        // Live chain, swap: the session is released.
395        a.rebuild(&b2, &mut el, &mut ctx);
396        assert!(take_focus_orphaned(), "focused swap orphans focus");
397    }
398
399    type Ui = Either<ButtonView<()>, CheckboxView<()>>;
400
401    fn semantics_for(show_button: bool) -> SemanticsUpdate {
402        let mut root: RenderRoot<(), Ui> = RenderRoot::new();
403        let mut state = ();
404        let mut logic = |_: &mut ()| -> Ui {
405            either(
406                show_button,
407                || button::<(), _>("Go", |_| {}),
408                || checkbox::<(), _>(true, "Agree", |_, _| {}),
409            )
410        };
411        root.rebuild(&mut logic, &mut state);
412        let mut tcx = TextContext::new();
413        root.layout_with_text(Size::new(300.0, 300.0), &mut tcx as &mut dyn std::any::Any);
414        root.semantics()
415    }
416
417    #[test]
418    fn semantics_tree_reflects_the_active_arm() {
419        let role_of_child = |u: &SemanticsUpdate| {
420            let root = &u.nodes.iter().find(|(id, _)| *id == u.root).unwrap().1;
421            let kids = root.children();
422            assert_eq!(kids.len(), 1, "exactly the live arm contributes");
423            u.nodes
424                .iter()
425                .find(|(id, _)| *id == kids[0])
426                .unwrap()
427                .1
428                .role()
429        };
430        assert_eq!(role_of_child(&semantics_for(true)), Role::Button);
431        assert_eq!(role_of_child(&semantics_for(false)), Role::CheckBox);
432    }
433
434    #[test]
435    fn semantics_follow_a_live_swap() {
436        let mut root: RenderRoot<(), Ui> = RenderRoot::new();
437        let mut state = ();
438        let show_button = std::cell::Cell::new(true);
439        let mut logic = |_: &mut ()| -> Ui {
440            either(
441                show_button.get(),
442                || button::<(), _>("Go", |_| {}),
443                || checkbox::<(), _>(true, "Agree", |_, _| {}),
444            )
445        };
446        let mut tcx = TextContext::new();
447        root.rebuild(&mut logic, &mut state);
448        root.layout_with_text(Size::new(300.0, 300.0), &mut tcx as &mut dyn std::any::Any);
449        let before = root.semantics();
450        show_button.set(false);
451        root.rebuild(&mut logic, &mut state);
452        root.layout_with_text(Size::new(300.0, 300.0), &mut tcx as &mut dyn std::any::Any);
453        let after = root.semantics();
454        let roles = |u: &SemanticsUpdate| u.nodes.iter().map(|(_, n)| n.role()).collect::<Vec<_>>();
455        assert!(roles(&before).contains(&Role::Button));
456        assert!(!roles(&before).contains(&Role::CheckBox));
457        assert!(roles(&after).contains(&Role::CheckBox));
458        assert!(!roles(&after).contains(&Role::Button));
459    }
460}