Skip to main content

frust_core/
view.rs

1//! Layer 1: the declarative [`View`] trait.
2//!
3//! Views are cheap, short-lived descriptors produced by a root component's
4//! `Component::build`, a pure function of application state. They are not the
5//! retained tree: re-running the build on every state mutation must stay cheap.
6//!
7//! The lifecycle mirrors `xilem_core`'s `View` (`build`/`rebuild`/`teardown`/
8//! `message`) but owns its implementation. There is no `Action` generic:
9//! messages route directly against `State`.
10
11use std::any::Any;
12
13use crate::widget::Widget;
14
15/// Bitflags describing what work a [`View::rebuild`] pass invalidated; combine with `|`.
16///
17/// `LAYOUT` implies a paint but the flags are stored orthogonally;
18/// [`ChangeFlags::needs_paint`] answers "does anything need repainting?".
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20pub struct ChangeFlags(u8);
21
22impl ChangeFlags {
23    /// Nothing changed.
24    pub const NONE: Self = Self(0);
25    /// The widget must be re-painted.
26    pub const PAINT: Self = Self(0b0000_0001);
27    /// The widget must be re-laid-out (and therefore re-painted).
28    pub const LAYOUT: Self = Self(0b0000_0010);
29
30    /// Whether `self` contains every bit set in `other`.
31    pub const fn contains(self, other: Self) -> bool {
32        (self.0 & other.0) == other.0
33    }
34
35    /// The union of two flag sets.
36    pub const fn union(self, other: Self) -> Self {
37        Self(self.0 | other.0)
38    }
39
40    /// Whether no flags are set.
41    pub const fn is_empty(self) -> bool {
42        self.0 == 0
43    }
44
45    /// Whether this change requires a repaint (either `PAINT` or `LAYOUT`).
46    pub const fn needs_paint(self) -> bool {
47        !self.is_empty()
48    }
49
50    /// Whether this change requires a relayout.
51    pub const fn needs_layout(self) -> bool {
52        self.contains(Self::LAYOUT)
53    }
54}
55
56impl core::ops::BitOr for ChangeFlags {
57    type Output = Self;
58    fn bitor(self, rhs: Self) -> Self {
59        self.union(rhs)
60    }
61}
62
63impl core::ops::BitOrAssign for ChangeFlags {
64    fn bitor_assign(&mut self, rhs: Self) {
65        self.0 |= rhs.0;
66    }
67}
68
69impl Default for ChangeFlags {
70    fn default() -> Self {
71        Self::NONE
72    }
73}
74
75/// A stable identity for a node in the widget tree: the `tree_arena` node id, allocated by [`BuildCtx`].
76#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
77pub struct WidgetId(pub u64);
78
79impl From<WidgetId> for u64 {
80    fn from(id: WidgetId) -> Self {
81        id.0
82    }
83}
84
85/// Context threaded through [`View::build`] / [`View::rebuild`]: allocates unique
86/// [`WidgetId`]s from the render root's counter and carries the effective focus
87/// chain ([`BuildCtx::has_focus`]) and element-replaced signal
88/// ([`BuildCtx::mark_element_replaced`]).
89pub struct BuildCtx<'a> {
90    next_id: &'a mut u64,
91    has_focus: bool,
92    element_replaced: bool,
93    /// The owning root's last paint snapshot; `None` outside a root.
94    hints: Option<std::rc::Rc<[crate::shortcuts::ShortcutHint]>>,
95}
96
97impl<'a> BuildCtx<'a> {
98    /// Create a context borrowing the render root's id counter.
99    ///
100    /// The focus chain seeds `true` ("unknown, assume live"): over-releasing costs
101    /// one tap, while under-releasing strands the shell's IME surface over a dead
102    /// widget. Seams that know the real value set it with [`BuildCtx::set_has_focus`].
103    pub fn new(next_id: &'a mut u64) -> Self {
104        Self {
105            next_id,
106            has_focus: true,
107            element_replaced: false,
108            hints: None,
109        }
110    }
111
112    /// Hand the context its root's last paint snapshot ([`BuildCtx::shortcut_hints`]).
113    pub fn with_shortcut_hints(
114        mut self,
115        hints: std::rc::Rc<[crate::shortcuts::ShortcutHint]>,
116    ) -> Self {
117        self.hints = Some(hints);
118        self
119    }
120
121    /// Allocate a fresh, unique widget id.
122    pub fn alloc_id(&mut self) -> WidgetId {
123        *self.next_id += 1;
124        WidgetId(*self.next_id)
125    }
126
127    /// Whether the recorded focus path is live from the root to the node being
128    /// built/rebuilt/torn down: the rebuild-pass mirror of
129    /// [`PaintCtx::has_focus`](crate::widget::PaintCtx::has_focus).
130    ///
131    /// A blur clears the link at the nearest common ancestor only, so a `focused`
132    /// [`ChildPod`](crate::widget::ChildPod) flag below it goes stale harmlessly. A
133    /// reconciler raises [`mark_focus_orphaned`](crate::event::mark_focus_orphaned)
134    /// only when `ctx.has_focus() && pod.is_focused()`: the session is actually live.
135    pub fn has_focus(&self) -> bool {
136        self.has_focus
137    }
138
139    /// The previous paint's shortcut declarations in paint order, unreachable ones
140    /// included; label each chord with `Chord::display`. They lag this build by a frame, and are
141    /// empty on a context its root did not seed.
142    pub fn shortcut_hints(&self) -> std::rc::Rc<[crate::shortcuts::ShortcutHint]> {
143        self.hints
144            .clone()
145            .unwrap_or_else(|| std::rc::Rc::from(Vec::new()))
146    }
147
148    /// Seed the focus chain for a seam that starts one (`RenderRoot`'s view diff, or a
149    /// component carrying its outer chain across an inner `BuildCtx`). A container
150    /// descending into a pod uses [`BuildCtx::with_focus_link`], which can only narrow.
151    pub fn set_has_focus(&mut self, has_focus: bool) {
152        self.has_focus = has_focus;
153    }
154
155    /// Run `f` with the focus chain extended by one link (`link_focused` is the pod's
156    /// `is_focused` flag), restoring it afterwards: the closure sees
157    /// `self.has_focus() && link_focused`, so no descent can widen the chain.
158    pub fn with_focus_link<R>(&mut self, link_focused: bool, f: impl FnOnce(&mut Self) -> R) -> R {
159        let outer = self.has_focus;
160        self.has_focus = outer && link_focused;
161        let result = f(self);
162        self.has_focus = outer;
163        result
164    }
165
166    /// Report that this view's `rebuild` replaced its element's widget while keeping
167    /// the element type (an enum view switching arms, e.g. `Either`).
168    ///
169    /// A reconciler detects an [`AnyView`] type swap by type id and drops the child
170    /// pod's capture and focus links; an enum keeps one static type, so it raises this
171    /// signal instead. The nearest enclosing
172    /// [`track_element_replaced`](BuildCtx::track_element_replaced) scope consumes it
173    /// (a pod inside the swapped subtree consumes its own); with no scope it is inert
174    /// or reaches the next one up: an over-release, never an under-release.
175    pub fn mark_element_replaced(&mut self) {
176        self.element_replaced = true;
177    }
178
179    /// Run `f` as a tracking scope for [`mark_element_replaced`](BuildCtx::mark_element_replaced),
180    /// returning `f`'s result and whether anything inside raised the signal. The
181    /// caller's pending value is saved and restored.
182    pub fn track_element_replaced<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> (R, bool) {
183        let outer = std::mem::replace(&mut self.element_replaced, false);
184        let result = f(self);
185        let replaced = std::mem::replace(&mut self.element_replaced, outer);
186        (result, replaced)
187    }
188}
189
190/// A declarative description of a piece of UI.
191///
192/// A `View` builds itself into a retained [`Widget`] ([`View::build`]) and
193/// reconciles a previous version against the live widget ([`View::rebuild`]).
194/// `State` is `'static`, so views are values re-created every frame.
195pub trait View<State: 'static>: 'static {
196    /// The retained widget this view produces.
197    type Element: Widget;
198
199    /// Materialise a fresh widget for this view.
200    fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element;
201
202    /// Reconcile `prev` (the previous view of the same type) against the live `element`, in place.
203    fn rebuild(
204        &self,
205        prev: &Self,
206        element: &mut Self::Element,
207        ctx: &mut BuildCtx<'_>,
208    ) -> ChangeFlags;
209
210    /// Tear down `element` when this view is being removed; a no-op for leaf views.
211    fn teardown(&self, _element: &mut Self::Element, _ctx: &mut BuildCtx<'_>) {}
212
213    /// Deliver an event message to this view, mutating application state (stubbed:
214    /// no event routing goes through it yet).
215    fn message(&self, _element: &mut Self::Element, _state: &mut State) {}
216}
217
218/// Object-safe mirror of [`View`], the erased backing of [`AnyView`]; `as_any` lets a rebuild downcast the previous view.
219trait ErasedView<State: 'static>: 'static {
220    fn dyn_build(&self, ctx: &mut BuildCtx<'_>) -> Box<dyn Widget>;
221
222    /// Reconcile against `prev`: a typed in-place rebuild if it is the same concrete
223    /// type, otherwise tear the old widget down and build a fresh one.
224    fn dyn_rebuild(
225        &self,
226        prev: &dyn ErasedView<State>,
227        element: &mut Box<dyn Widget>,
228        ctx: &mut BuildCtx<'_>,
229    ) -> ChangeFlags;
230
231    fn dyn_teardown(&self, element: &mut Box<dyn Widget>, ctx: &mut BuildCtx<'_>);
232
233    fn as_any(&self) -> &dyn Any;
234}
235
236impl<State: 'static, V: View<State>> ErasedView<State> for V {
237    fn dyn_build(&self, ctx: &mut BuildCtx<'_>) -> Box<dyn Widget> {
238        Box::new(self.build(ctx))
239    }
240
241    fn dyn_rebuild(
242        &self,
243        prev: &dyn ErasedView<State>,
244        element: &mut Box<dyn Widget>,
245        ctx: &mut BuildCtx<'_>,
246    ) -> ChangeFlags {
247        if let Some(prev) = prev.as_any().downcast_ref::<V>() {
248            let element = (**element)
249                .downcast_mut::<V::Element>()
250                .expect("erased element type matches its originating view");
251            self.rebuild(prev, element, ctx)
252        } else {
253            prev.dyn_teardown(element, ctx);
254            *element = self.dyn_build(ctx);
255            ChangeFlags::LAYOUT | ChangeFlags::PAINT
256        }
257    }
258
259    fn dyn_teardown(&self, element: &mut Box<dyn Widget>, ctx: &mut BuildCtx<'_>) {
260        if let Some(element) = (**element).downcast_mut::<V::Element>() {
261            self.teardown(element, ctx);
262        }
263    }
264
265    fn as_any(&self) -> &dyn Any {
266        self
267    }
268}
269
270/// A type-erased [`View`]: lets a piece of UI change its concrete view type
271/// between frames while still fitting the statically typed rebuild machinery.
272///
273/// The previous view is downcast to detect a type change: same type is a typed
274/// in-place rebuild; otherwise the old widget is torn down and a fresh one swapped
275/// in (`LAYOUT | PAINT`). Its `Element` is a `Box<dyn Widget>`.
276///
277/// Generic builders erase internally, so app code needs `AnyView` only for stored
278/// views, branches of more than eight arms, and heterogeneous dynamic lists (see
279/// [`any`]); `either(..)` and `EitherOf3`..`EitherOf8` stay statically typed.
280pub struct AnyView<State: 'static> {
281    inner: Box<dyn ErasedView<State>>,
282}
283
284impl<State: 'static> AnyView<State> {
285    /// Erase `view` into an `AnyView`; idempotent, so `any(any(v))` and
286    /// `AnyView::new(any_view)` do not box twice.
287    pub fn new<V: View<State>>(view: V) -> Self {
288        let mut slot = Some(view);
289        if let Some(erased) =
290            (&mut slot as &mut dyn core::any::Any).downcast_mut::<Option<AnyView<State>>>()
291        {
292            return erased.take().expect("slot is Some");
293        }
294        Self {
295            inner: Box::new(slot.expect("slot is Some")),
296        }
297    }
298}
299
300/// Erase `view` into an [`AnyView`], the free-function spelling of [`AnyView::new`].
301///
302/// Idempotent, so double erasure is safe for swap detection in containers like
303/// `inflexible(any(x))`. Wrapping an argument in `any(..)` is redundant since the
304/// generic builders erase; reach for it to *store* a view, for a branch of more
305/// than eight arms, or for a heterogeneous dynamic list.
306pub fn any<State: 'static, V: View<State>>(view: V) -> AnyView<State> {
307    AnyView::new(view)
308}
309
310impl<State: 'static> View<State> for AnyView<State> {
311    type Element = Box<dyn Widget>;
312
313    fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
314        self.inner.dyn_build(ctx)
315    }
316
317    fn rebuild(
318        &self,
319        prev: &Self,
320        element: &mut Self::Element,
321        ctx: &mut BuildCtx<'_>,
322    ) -> ChangeFlags {
323        self.inner.dyn_rebuild(prev.inner.as_ref(), element, ctx)
324    }
325
326    fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
327        self.inner.dyn_teardown(element, ctx);
328    }
329}
330
331/// A child sequence: what a container's child-list parameter accepts, so app code
332/// passes heterogeneous children without writing [`any`].
333///
334/// A container takes `impl ViewSeq<State, M>` and calls [`ViewSeq::extend_views`]
335/// once, the one place erasure happens ([`AnyView::new`], idempotent). Implemented
336/// for any single [`View`], tuples of 1 to 12 sequences (nest beyond that; nested
337/// tuples flatten in order), `Vec<S>`, `[S; N]`, `Option<S>` and [`views(iter)`](views).
338///
339/// Identity is positional: a `None` contributes nothing, so later siblings move up
340/// one slot when it flips. `M` lets the single-view impl coexist with the container
341/// impls (else a downstream `View for Vec` could overlap); inference resolves it.
342#[diagnostic::on_unimplemented(
343    message = "this is not a child sequence",
344    note = "a child list is a View, a tuple of up to 12 views (nest tuples beyond 12), an array, a Vec, an Option, or views(iter) for an iterator"
345)]
346pub trait ViewSeq<State: 'static, M> {
347    /// Push every element of this sequence, in order, onto `out`, erasing each with [`AnyView::new`].
348    fn extend_views(self, out: &mut Vec<AnyView<State>>);
349}
350
351/// Inference markers for [`ViewSeq`]'s second parameter: one per impl, so the
352/// single-[`View`] impl does not overlap the container impls. Never named by callers.
353pub mod seq_marker {
354    use core::marker::PhantomData;
355
356    pub enum One {}
357
358    pub struct VecOf<M>(PhantomData<fn() -> M>);
359
360    pub struct ArrayOf<M>(PhantomData<fn() -> M>);
361
362    pub struct OptionOf<M>(PhantomData<fn() -> M>);
363
364    pub enum Iter {}
365}
366
367impl<State: 'static, V: View<State>> ViewSeq<State, seq_marker::One> for V {
368    fn extend_views(self, out: &mut Vec<AnyView<State>>) {
369        out.push(AnyView::new(self));
370    }
371}
372
373impl<State: 'static, M, S: ViewSeq<State, M>> ViewSeq<State, seq_marker::VecOf<M>> for Vec<S> {
374    fn extend_views(self, out: &mut Vec<AnyView<State>>) {
375        out.reserve(self.len());
376        for seq in self {
377            seq.extend_views(out);
378        }
379    }
380}
381
382impl<State: 'static, M, S: ViewSeq<State, M>, const N: usize> ViewSeq<State, seq_marker::ArrayOf<M>>
383    for [S; N]
384{
385    fn extend_views(self, out: &mut Vec<AnyView<State>>) {
386        out.reserve(N);
387        for seq in self {
388            seq.extend_views(out);
389        }
390    }
391}
392
393impl<State: 'static, M, S: ViewSeq<State, M>> ViewSeq<State, seq_marker::OptionOf<M>>
394    for Option<S>
395{
396    fn extend_views(self, out: &mut Vec<AnyView<State>>) {
397        if let Some(seq) = self {
398            seq.extend_views(out);
399        }
400    }
401}
402
403/// Implements [`ViewSeq`] per tuple arity; the tuple's marker is the tuple of its elements' markers.
404macro_rules! impl_view_seq_for_tuples {
405    ($( ($($S:ident $M:ident $idx:tt),+) )+) => {$(
406        impl<State: 'static, $($M, $S: ViewSeq<State, $M>),+> ViewSeq<State, ($($M,)+)>
407            for ($($S,)+)
408        {
409            fn extend_views(self, out: &mut Vec<AnyView<State>>) {
410                $(self.$idx.extend_views(out);)+
411            }
412        }
413    )+};
414}
415
416impl_view_seq_for_tuples! {
417    (S0 M0 0)
418    (S0 M0 0, S1 M1 1)
419    (S0 M0 0, S1 M1 1, S2 M2 2)
420    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3)
421    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3, S4 M4 4)
422    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3, S4 M4 4, S5 M5 5)
423    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3, S4 M4 4, S5 M5 5, S6 M6 6)
424    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3, S4 M4 4, S5 M5 5, S6 M6 6, S7 M7 7)
425    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3, S4 M4 4, S5 M5 5, S6 M6 6, S7 M7 7, S8 M8 8)
426    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3, S4 M4 4, S5 M5 5, S6 M6 6, S7 M7 7, S8 M8 8,
427        S9 M9 9)
428    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3, S4 M4 4, S5 M5 5, S6 M6 6, S7 M7 7, S8 M8 8,
429        S9 M9 9, S10 M10 10)
430    (S0 M0 0, S1 M1 1, S2 M2 2, S3 M3 3, S4 M4 4, S5 M5 5, S6 M6 6, S7 M7 7, S8 M8 8,
431        S9 M9 9, S10 M10 10, S11 M11 11)
432}
433
434/// The [`ViewSeq`] adapter for an iterator of views, built by [`views`].
435pub struct Views<I>(I);
436
437impl<State: 'static, I> ViewSeq<State, seq_marker::Iter> for Views<I>
438where
439    I: IntoIterator,
440    I::Item: View<State>,
441{
442    fn extend_views(self, out: &mut Vec<AnyView<State>>) {
443        out.extend(self.0.into_iter().map(AnyView::new));
444    }
445}
446
447/// Wrap an iterator of views as a [`ViewSeq`] (one child per item), e.g.
448/// `views(items.iter().map(|i| text(&i.name)))`. A `Vec` or array needs no wrapping.
449pub fn views<State: 'static, I>(iter: I) -> Views<I>
450where
451    I: IntoIterator,
452    I::Item: View<State>,
453{
454    Views(iter)
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use crate::layout::BoxConstraints;
461    use crate::widget::{LayoutCtx, PaintCtx, PaintScene, Widget};
462    use kurbo::Size;
463    use std::cell::Cell;
464    use std::rc::Rc;
465
466    // --- AnyView fixtures: two distinct view/widget type pairs. ---
467
468    struct WidgetA {
469        n: u32,
470    }
471    impl Widget for WidgetA {
472        fn layout(&mut self, _ctx: &mut LayoutCtx, _bc: &BoxConstraints) -> Size {
473            Size::new(self.n as f64, 1.0)
474        }
475        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
476    }
477
478    struct WidgetB;
479    impl Widget for WidgetB {
480        fn layout(&mut self, _ctx: &mut LayoutCtx, _bc: &BoxConstraints) -> Size {
481            Size::new(99.0, 99.0)
482        }
483        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
484    }
485
486    struct ViewA {
487        n: u32,
488        torn: Rc<Cell<u32>>,
489    }
490    impl View<()> for ViewA {
491        type Element = WidgetA;
492        fn build(&self, _ctx: &mut BuildCtx<'_>) -> WidgetA {
493            WidgetA { n: self.n }
494        }
495        fn rebuild(
496            &self,
497            prev: &Self,
498            element: &mut WidgetA,
499            _ctx: &mut BuildCtx<'_>,
500        ) -> ChangeFlags {
501            if prev.n != self.n {
502                element.n = self.n;
503                ChangeFlags::PAINT
504            } else {
505                ChangeFlags::NONE
506            }
507        }
508        fn teardown(&self, _element: &mut WidgetA, _ctx: &mut BuildCtx<'_>) {
509            self.torn.set(self.torn.get() + 1);
510        }
511    }
512
513    struct ViewB;
514    impl View<()> for ViewB {
515        type Element = WidgetB;
516        fn build(&self, _ctx: &mut BuildCtx<'_>) -> WidgetB {
517            WidgetB
518        }
519        fn rebuild(
520            &self,
521            _prev: &Self,
522            _element: &mut WidgetB,
523            _ctx: &mut BuildCtx<'_>,
524        ) -> ChangeFlags {
525            ChangeFlags::NONE
526        }
527    }
528
529    #[test]
530    fn any_view_same_type_rebuilds_in_place() {
531        let torn = Rc::new(Cell::new(0));
532        let mut counter = 0u64;
533        let mut ctx = BuildCtx::new(&mut counter);
534
535        let prev = any(ViewA {
536            n: 1,
537            torn: torn.clone(),
538        });
539        let mut element = prev.build(&mut ctx);
540
541        let next = any(ViewA {
542            n: 2,
543            torn: torn.clone(),
544        });
545        let flags = next.rebuild(&prev, &mut element, &mut ctx);
546
547        // Same concrete type → typed in-place rebuild, no teardown.
548        assert_eq!(flags, ChangeFlags::PAINT);
549        assert_eq!(torn.get(), 0);
550        let a = (*element)
551            .downcast_mut::<WidgetA>()
552            .expect("still a WidgetA");
553        assert_eq!(a.n, 2);
554    }
555
556    #[test]
557    fn any_view_type_swap_tears_down_and_replaces() {
558        let torn = Rc::new(Cell::new(0));
559        let mut counter = 0u64;
560        let mut ctx = BuildCtx::new(&mut counter);
561
562        let prev = any(ViewA {
563            n: 7,
564            torn: torn.clone(),
565        });
566        let mut element = prev.build(&mut ctx);
567
568        let next = any(ViewB);
569        let flags = next.rebuild(&prev, &mut element, &mut ctx);
570
571        // Concrete type changed → the old view's teardown ran and the widget was
572        // replaced with the new type.
573        assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
574        assert_eq!(torn.get(), 1, "old view should be torn down exactly once");
575        assert!((*element).downcast_mut::<WidgetB>().is_some());
576        assert!((*element).downcast_mut::<WidgetA>().is_none());
577    }
578
579    #[test]
580    fn any_view_element_works_inside_a_child_pod() {
581        // Criterion 3: `Box<dyn Widget>` implements `Widget` — an AnyView's boxed
582        // element drives layout when nested in a container's ChildPod.
583        use crate::widget::ChildPod;
584
585        let torn = Rc::new(Cell::new(0));
586        let mut counter = 0u64;
587        let mut ctx = BuildCtx::new(&mut counter);
588
589        let view = any(ViewA { n: 5, torn });
590        let element: Box<dyn Widget> = view.build(&mut ctx); // Box<dyn Widget>
591
592        // Nest the boxed widget inside a ChildPod (double-boxed): the blanket
593        // `Widget for Box<dyn Widget>` impl forwards layout through both layers.
594        let mut pod = ChildPod::new(Box::new(element));
595        let mut lctx = LayoutCtx::new();
596        let size = pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
597        assert_eq!(size, Size::new(5.0, 1.0));
598    }
599
600    #[test]
601    fn change_flags_union_and_contains() {
602        let both = ChangeFlags::PAINT | ChangeFlags::LAYOUT;
603        assert!(both.contains(ChangeFlags::PAINT));
604        assert!(both.contains(ChangeFlags::LAYOUT));
605        assert!(both.needs_layout());
606        assert!(both.needs_paint());
607
608        assert!(ChangeFlags::NONE.is_empty());
609        assert!(!ChangeFlags::NONE.needs_paint());
610
611        let paint_only = ChangeFlags::PAINT;
612        assert!(paint_only.needs_paint());
613        assert!(!paint_only.needs_layout());
614    }
615
616    #[test]
617    fn change_flags_bitor_assign() {
618        let mut f = ChangeFlags::NONE;
619        f |= ChangeFlags::PAINT;
620        assert!(f.contains(ChangeFlags::PAINT));
621        assert!(!f.contains(ChangeFlags::LAYOUT));
622    }
623
624    #[test]
625    fn double_erasure_is_idempotent() {
626        // Criterion: `any(any(view))` returns the inner `AnyView` unchanged,
627        // not a doubly-boxed wrapper. The element's TypeId matches the inner
628        // view's concrete type, not `Box<dyn Widget>`.
629        let torn = Rc::new(Cell::new(0));
630        let mut counter = 0u64;
631        let mut ctx = BuildCtx::new(&mut counter);
632
633        let double_erased = any(any(ViewA {
634            n: 5,
635            torn: torn.clone(),
636        }));
637        let mut element = double_erased.build(&mut ctx);
638
639        // The element should downcast directly to WidgetA, not to Box<dyn Widget>
640        // as it would if double-erased.
641        let a = (*element)
642            .downcast_mut::<WidgetA>()
643            .expect("double-erased any(any(ViewA)) should have WidgetA element directly");
644        assert_eq!(a.n, 5);
645
646        // Single-erasure behavior unchanged: rebuild with same type reports no swap
647        let double_erased_2 = any(any(ViewA {
648            n: 10,
649            torn: torn.clone(),
650        }));
651        let flags = double_erased_2.rebuild(&double_erased, &mut element, &mut ctx);
652
653        // Same concrete type → typed in-place rebuild, no teardown.
654        assert_eq!(flags, ChangeFlags::PAINT);
655        assert_eq!(torn.get(), 0, "no teardown on same-type rebuild");
656        let a = (*element)
657            .downcast_mut::<WidgetA>()
658            .expect("still a WidgetA after rebuild");
659        assert_eq!(a.n, 10);
660    }
661
662    #[test]
663    fn build_ctx_allocates_unique_ids() {
664        let mut counter = 0;
665        let mut ctx = BuildCtx::new(&mut counter);
666        let a = ctx.alloc_id();
667        let b = ctx.alloc_id();
668        assert_ne!(a, b);
669        assert_eq!(u64::from(a), 1);
670        assert_eq!(u64::from(b), 2);
671    }
672
673    #[test]
674    fn build_ctx_focus_chain_only_narrows_and_restores() {
675        let mut counter = 0;
676        let mut ctx = BuildCtx::new(&mut counter);
677        assert!(
678            ctx.has_focus(),
679            "an unseeded context assumes a live chain (see BuildCtx::new)"
680        );
681
682        // A focused link keeps a live chain live...
683        ctx.with_focus_link(true, |ctx| assert!(ctx.has_focus()));
684        // ...and the descent is scoped: the caller's chain comes back.
685        assert!(ctx.has_focus());
686
687        // A cleared link closes the chain for the whole subtree below it —
688        // including a *focused* link nested under the cleared one, which is the
689        // stale-flag-below-a-blurred-ancestor case in one line.
690        ctx.with_focus_link(false, |ctx| {
691            assert!(!ctx.has_focus());
692            ctx.with_focus_link(true, |ctx| {
693                assert!(
694                    !ctx.has_focus(),
695                    "no descent may widen the chain a cleared ancestor closed"
696                );
697            });
698            assert!(!ctx.has_focus());
699        });
700        assert!(
701            ctx.has_focus(),
702            "the outer chain is restored, not clobbered"
703        );
704
705        // Seeding is the one absolute write (the root / a component boundary).
706        ctx.set_has_focus(false);
707        assert!(!ctx.has_focus());
708        ctx.with_focus_link(true, |ctx| assert!(!ctx.has_focus()));
709    }
710
711    #[test]
712    fn build_ctx_element_replaced_is_consumed_by_the_nearest_scope() {
713        let mut counter = 0;
714        let mut ctx = BuildCtx::new(&mut counter);
715
716        // A scope with nothing raised inside reports nothing.
717        let ((), replaced) = ctx.track_element_replaced(|_| {});
718        assert!(!replaced);
719
720        // A signal raised inside is reported, and the result passes through.
721        let (n, replaced) = ctx.track_element_replaced(|ctx| {
722            ctx.mark_element_replaced();
723            7
724        });
725        assert_eq!(n, 7);
726        assert!(replaced);
727
728        // The nearest scope consumes it: an inner scope (a pod inside the
729        // subtree) takes its own signal, and the outer scope sees none.
730        let (inner, outer) = ctx.track_element_replaced(|ctx| {
731            let ((), inner) = ctx.track_element_replaced(|ctx| ctx.mark_element_replaced());
732            inner
733        });
734        assert!(inner, "the inner scope consumes its own signal");
735        assert!(!outer, "a consumed signal does not reach the outer scope");
736
737        // A pending signal raised outside any scope is neither consumed nor lost
738        // by a sibling's scope: it is restored when that scope returns.
739        ctx.mark_element_replaced();
740        let ((), sibling) = ctx.track_element_replaced(|_| {});
741        assert!(!sibling, "a scope sees only signals raised inside it");
742        let ((), outer) = ctx.track_element_replaced(|ctx| {
743            ctx.mark_element_replaced();
744        });
745        assert!(outer);
746        // The originally pending value came back after both scopes.
747        let ((), _) = ctx.track_element_replaced(|_| {});
748        assert!(
749            std::mem::take(&mut ctx.element_replaced),
750            "the caller's pending signal is restored, not clobbered"
751        );
752    }
753
754    // --- ViewSeq: child sequences erase once, in order. ---
755
756    fn seq<M>(children: impl ViewSeq<(), M>) -> Vec<AnyView<()>> {
757        let mut out = Vec::new();
758        children.extend_views(&mut out);
759        out
760    }
761
762    fn a(n: u32) -> ViewA {
763        ViewA {
764            n,
765            torn: Rc::new(Cell::new(0)),
766        }
767    }
768
769    /// Build each erased child and report it: `WidgetA`'s `n`, or 99 for a
770    /// `WidgetB`. The element is downcast *directly*, so a doubly-boxed child
771    /// (whose element would be a `Box<dyn Widget>`) fails the expect.
772    fn tags(children: &[AnyView<()>]) -> Vec<u32> {
773        let mut counter = 0u64;
774        let mut ctx = BuildCtx::new(&mut counter);
775        children
776            .iter()
777            .map(|child| {
778                let mut element = child.build(&mut ctx);
779                if let Some(a) = (*element).downcast_mut::<WidgetA>() {
780                    a.n
781                } else {
782                    (*element)
783                        .downcast_mut::<WidgetB>()
784                        .expect("element is the concrete widget, not a re-boxed one");
785                    99
786                }
787            })
788            .collect()
789    }
790
791    #[test]
792    fn view_seq_single_view_is_one_element() {
793        assert_eq!(tags(&seq(a(4))), vec![4]);
794    }
795
796    #[test]
797    fn view_seq_tuple_of_mixed_views_keeps_order() {
798        let children = seq((a(1), ViewB, a(3)));
799        assert_eq!(children.len(), 3);
800        assert_eq!(tags(&children), vec![1, 99, 3]);
801    }
802
803    #[test]
804    fn view_seq_nested_tuples_flatten_in_order() {
805        let children = seq(((a(1), (ViewB, a(2))), a(3), (a(4),)));
806        assert_eq!(tags(&children), vec![1, 99, 2, 3, 4]);
807    }
808
809    #[test]
810    fn view_seq_vec_array_and_option_counts() {
811        assert_eq!(tags(&seq(vec![a(1), a(2)])), vec![1, 2]);
812        assert!(seq(Vec::<ViewB>::new()).is_empty());
813        assert_eq!(tags(&seq([a(5), a(6), a(7)])), vec![5, 6, 7]);
814        assert_eq!(tags(&seq(Some(a(8)))), vec![8]);
815        assert!(seq(None::<ViewA>).is_empty());
816        // `None` contributes nothing, so a later sibling moves up a slot.
817        assert_eq!(tags(&seq((None::<ViewA>, ViewB))), vec![99]);
818        // Container elements may themselves be sequences.
819        assert_eq!(
820            tags(&seq(vec![(a(1), ViewB), (a(2), ViewB)])),
821            vec![1, 99, 2, 99]
822        );
823    }
824
825    #[test]
826    fn view_seq_does_not_rebox_an_any_view_element() {
827        // Erasure is idempotent: an element already an `AnyView` is moved into
828        // the child vector as-is, so its element is still the concrete widget.
829        let children = seq((any(a(1)), any(any(ViewB)), vec![any(a(2))]));
830        assert_eq!(tags(&children), vec![1, 99, 2]);
831    }
832
833    #[test]
834    fn view_seq_views_adapter_maps_an_iterator() {
835        let children = seq(views((1..=3).map(a)));
836        assert_eq!(tags(&children), vec![1, 2, 3]);
837        // The adapter is itself a sequence element.
838        let children = seq((ViewB, views([4, 5].into_iter().map(a))));
839        assert_eq!(tags(&children), vec![99, 4, 5]);
840    }
841
842    #[test]
843    fn view_seq_twelve_tuple_compiles_and_nesting_goes_beyond() {
844        let twelve = (
845            a(1),
846            a(2),
847            a(3),
848            a(4),
849            a(5),
850            a(6),
851            a(7),
852            a(8),
853            a(9),
854            a(10),
855            a(11),
856            a(12),
857        );
858        assert_eq!(tags(&seq(twelve)), (1..=12).collect::<Vec<_>>());
859        // Past 12, nest: the order is the flattened order.
860        let thirteen = (
861            (
862                a(1),
863                a(2),
864                a(3),
865                a(4),
866                a(5),
867                a(6),
868                a(7),
869                a(8),
870                a(9),
871                a(10),
872                a(11),
873                a(12),
874            ),
875            a(13),
876        );
877        assert_eq!(tags(&seq(thirteen)), (1..=13).collect::<Vec<_>>());
878    }
879}