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 `Component::build`
4//! closure, a pure function of application state (`fn build(&mut State) -> impl View<State>`).
5//! They are *not* the retained tree — re-running the build closure on every state
6//! mutation must stay cheap by construction.
7//!
8//! The lifecycle mirrors `xilem_core`'s proven `View` design
9//! (`build`/`rebuild`/`teardown`/`message`) but owns its implementation: no
10//! xilem/masonry dependency. The `Action` generic is intentionally omitted for
11//! v0 — messages route directly against `State`.
12
13use std::any::Any;
14
15use crate::widget::Widget;
16
17/// Bitflags describing what work a [`View::rebuild`] pass invalidated.
18///
19/// Combine with `|`. `LAYOUT` implies a subsequent paint, but the flags are
20/// stored orthogonally so a caller can distinguish "geometry changed" from
21/// "only pixels changed"; use [`ChangeFlags::needs_paint`] for the common
22/// "does anything need repainting?" query.
23#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24pub struct ChangeFlags(u8);
25
26impl ChangeFlags {
27    /// Nothing changed; no downstream work required.
28    pub const NONE: Self = Self(0);
29    /// The widget must be re-painted.
30    pub const PAINT: Self = Self(0b0000_0001);
31    /// The widget must be re-laid-out (and therefore re-painted).
32    pub const LAYOUT: Self = Self(0b0000_0010);
33
34    /// Whether `self` contains every bit set in `other`.
35    pub const fn contains(self, other: Self) -> bool {
36        (self.0 & other.0) == other.0
37    }
38
39    /// The union of two flag sets.
40    pub const fn union(self, other: Self) -> Self {
41        Self(self.0 | other.0)
42    }
43
44    /// Whether no flags are set.
45    pub const fn is_empty(self) -> bool {
46        self.0 == 0
47    }
48
49    /// Whether this change requires a repaint (either `PAINT` or `LAYOUT`).
50    pub const fn needs_paint(self) -> bool {
51        !self.is_empty()
52    }
53
54    /// Whether this change requires a relayout.
55    pub const fn needs_layout(self) -> bool {
56        self.contains(Self::LAYOUT)
57    }
58}
59
60impl core::ops::BitOr for ChangeFlags {
61    type Output = Self;
62    fn bitor(self, rhs: Self) -> Self {
63        self.union(rhs)
64    }
65}
66
67impl core::ops::BitOrAssign for ChangeFlags {
68    fn bitor_assign(&mut self, rhs: Self) {
69        self.0 |= rhs.0;
70    }
71}
72
73impl Default for ChangeFlags {
74    fn default() -> Self {
75        Self::NONE
76    }
77}
78
79/// A stable identity for a node in the widget tree.
80///
81/// Wraps the `u64` node id used by `tree_arena`. Allocated by [`BuildCtx`].
82#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
83pub struct WidgetId(pub u64);
84
85impl From<WidgetId> for u64 {
86    fn from(id: WidgetId) -> Self {
87        id.0
88    }
89}
90
91/// Context threaded through [`View::build`] / [`View::rebuild`].
92///
93/// Two responsibilities: allocating unique [`WidgetId`]s (it borrows the id
94/// counter owned by the render root so ids stay monotonic across passes), and
95/// carrying the **effective focus chain** — whether every link from the root down
96/// to the node being built/rebuilt/torn down is focused (see
97/// [`BuildCtx::has_focus`]).
98pub struct BuildCtx<'a> {
99    next_id: &'a mut u64,
100    /// The effective focus chain: `true` only while every recorded focus link
101    /// from the root down to the current node is set. See
102    /// [`BuildCtx::has_focus`].
103    has_focus: bool,
104}
105
106impl<'a> BuildCtx<'a> {
107    /// Create a context borrowing the render root's id counter.
108    ///
109    /// The focus chain seeds **`true`** — "unknown, assume live". A caller that
110    /// does not thread the chain therefore keeps the pre-chain behavior (every
111    /// reconciler under it treats a `focused` pod as the live one) instead of
112    /// silently suppressing a release it owed: over-releasing costs the user one
113    /// tap, while under-releasing strands the shell's IME surface over a widget
114    /// that no longer exists and nothing on an idle screen ever corrects it (the
115    /// same default-to-must-run rule the frame gate follows). The seams that
116    /// *know* the real value — [`RenderRoot::rebuild`](crate::app::RenderRoot)'s
117    /// view diff and [`ComponentWidget`](crate::component::ComponentWidget)'s
118    /// inner context — set it explicitly with [`BuildCtx::set_has_focus`].
119    pub fn new(next_id: &'a mut u64) -> Self {
120        Self {
121            next_id,
122            has_focus: true,
123        }
124    }
125
126    /// Allocate a fresh, unique widget id.
127    pub fn alloc_id(&mut self) -> WidgetId {
128        *self.next_id += 1;
129        WidgetId(*self.next_id)
130    }
131
132    /// Whether the recorded focus path is live all the way from the root to the
133    /// node currently being built/rebuilt/torn down — the rebuild-pass mirror of
134    /// [`PaintCtx::has_focus`](crate::widget::PaintCtx::has_focus), composed the
135    /// same way (`self.focused && ctx.has_focus()`).
136    ///
137    /// This is what makes a `focused` [`ChildPod`](crate::widget::ChildPod) flag
138    /// *deep inside a blurred branch* harmless. A container-routed blur clears the
139    /// focus link at the nearest common ancestor only, so flags below it
140    /// legitimately go stale until focus next enters that subtree; a pod under a
141    /// cleared link sees `has_focus() == false` here, exactly as it sees `false`
142    /// in paint and exactly as focus-routed events never reach it. A reconciler
143    /// therefore raises [`mark_focus_orphaned`](crate::event::mark_focus_orphaned)
144    /// only when `ctx.has_focus() && pod.is_focused()` — only when the pod losing
145    /// its identity is the one whose session is actually live.
146    pub fn has_focus(&self) -> bool {
147        self.has_focus
148    }
149
150    /// Seed the effective focus chain (see [`BuildCtx::has_focus`]).
151    ///
152    /// For the two kinds of seam that *start* a chain rather than descend one:
153    /// [`RenderRoot`](crate::app::RenderRoot)'s view diff, which seeds it from the
154    /// root's own session mirror, and a component-style widget that builds an
155    /// inner `BuildCtx` over its own id counter
156    /// ([`ComponentWidget`](crate::component::ComponentWidget) is the in-crate
157    /// one) and must carry its outer chain across that boundary. A container
158    /// descending into a child pod uses [`BuildCtx::with_focus_link`] instead — it
159    /// can only narrow, which is what keeps this an AND-chain.
160    pub fn set_has_focus(&mut self, has_focus: bool) {
161        self.has_focus = has_focus;
162    }
163
164    /// Run `f` with the focus chain extended by one link, restoring the caller's
165    /// chain when it returns.
166    ///
167    /// `link_focused` is the descended-into pod's own
168    /// [`ChildPod::is_focused`](crate::widget::ChildPod::is_focused) flag, so the
169    /// closure sees `self.has_focus() && link_focused`: a cleared link anywhere
170    /// above forces `false` for the whole subtree below it, and no descent can
171    /// ever widen the chain. The rebuild-pass counterpart of
172    /// [`ChildPod::paint_child`](crate::widget::ChildPod::paint_child)'s
173    /// `set_has_focus(self.focused && ctx.has_focus())`, in the scoped-closure
174    /// shape [`SemanticsCtx::descend_into_pod`](crate::semantics::SemanticsCtx)
175    /// already uses for the semantics pass.
176    pub fn with_focus_link<R>(&mut self, link_focused: bool, f: impl FnOnce(&mut Self) -> R) -> R {
177        let outer = self.has_focus;
178        self.has_focus = outer && link_focused;
179        let result = f(self);
180        self.has_focus = outer;
181        result
182    }
183}
184
185/// A declarative description of a piece of UI.
186///
187/// Each `View` knows how to materialise itself into a retained [`Widget`]
188/// ([`View::build`]) and how to reconcile a previous version of itself against
189/// the live widget ([`View::rebuild`]). `State` is `'static` so views never
190/// capture borrowed data — they are values, re-created every frame.
191pub trait View<State: 'static>: 'static {
192    /// The retained widget this view produces.
193    type Element: Widget;
194
195    /// Materialise a fresh widget for this view.
196    fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element;
197
198    /// Reconcile `prev` (the previous view of the same type) against the live
199    /// `element`, mutating it in place and reporting what changed.
200    fn rebuild(
201        &self,
202        prev: &Self,
203        element: &mut Self::Element,
204        ctx: &mut BuildCtx<'_>,
205    ) -> ChangeFlags;
206
207    /// Tear down `element` when this view is being removed.
208    ///
209    /// A no-op for v0 leaf views; kept in the trait so the lifecycle is
210    /// complete and container/removal logic (later phases) has a hook.
211    fn teardown(&self, _element: &mut Self::Element, _ctx: &mut BuildCtx<'_>) {}
212
213    /// Deliver an event message to this view, mutating application state.
214    ///
215    /// Stubbed for v0 (no event routing yet); present so the trait shape is
216    /// stable as event routing is added later.
217    fn message(&self, _element: &mut Self::Element, _state: &mut State) {}
218}
219
220/// Object-safe mirror of [`View`], used only as the erased backing of
221/// [`AnyView`].
222///
223/// The methods mirror `build`/`rebuild`/`teardown` but drop the associated
224/// `Element` type in favour of a `Box<dyn Widget>`, and add [`ErasedView::as_any`]
225/// so a rebuild can downcast the *previous* erased view to detect a
226/// concrete-type change (the xilem `AnyView` trick).
227trait ErasedView<State: 'static>: 'static {
228    /// Materialise a fresh boxed widget for this view.
229    fn dyn_build(&self, ctx: &mut BuildCtx<'_>) -> Box<dyn Widget>;
230
231    /// Reconcile against `prev` (the previous erased view). If `prev` is the same
232    /// concrete type, do a typed in-place rebuild; otherwise tear the old widget
233    /// down and build a fresh one, replacing `element`.
234    fn dyn_rebuild(
235        &self,
236        prev: &dyn ErasedView<State>,
237        element: &mut Box<dyn Widget>,
238        ctx: &mut BuildCtx<'_>,
239    ) -> ChangeFlags;
240
241    /// Tear down `element` (dispatched to the concrete view's `teardown`).
242    fn dyn_teardown(&self, element: &mut Box<dyn Widget>, ctx: &mut BuildCtx<'_>);
243
244    /// Upcast to `&dyn Any` so a rebuild can downcast the previous view.
245    fn as_any(&self) -> &dyn Any;
246}
247
248impl<State: 'static, V: View<State>> ErasedView<State> for V {
249    fn dyn_build(&self, ctx: &mut BuildCtx<'_>) -> Box<dyn Widget> {
250        Box::new(self.build(ctx))
251    }
252
253    fn dyn_rebuild(
254        &self,
255        prev: &dyn ErasedView<State>,
256        element: &mut Box<dyn Widget>,
257        ctx: &mut BuildCtx<'_>,
258    ) -> ChangeFlags {
259        if let Some(prev) = prev.as_any().downcast_ref::<V>() {
260            // Same concrete view type: recover the typed element and rebuild in
261            // place. The element's erased type is `V::Element` because it was
262            // produced by this view's `build` (via `dyn_build`).
263            let element = (**element)
264                .downcast_mut::<V::Element>()
265                .expect("erased element type matches its originating view");
266            self.rebuild(prev, element, ctx)
267        } else {
268            // Concrete type changed: tear the old widget down through the *old*
269            // view, then build a fresh one and swap it in.
270            prev.dyn_teardown(element, ctx);
271            *element = self.dyn_build(ctx);
272            ChangeFlags::LAYOUT | ChangeFlags::PAINT
273        }
274    }
275
276    fn dyn_teardown(&self, element: &mut Box<dyn Widget>, ctx: &mut BuildCtx<'_>) {
277        if let Some(element) = (**element).downcast_mut::<V::Element>() {
278            self.teardown(element, ctx);
279        }
280    }
281
282    fn as_any(&self) -> &dyn Any {
283        self
284    }
285}
286
287/// A type-erased [`View`]: lets a piece of UI change its concrete view type
288/// between frames (e.g. a conditional `if cond { text(..) } else { button(..) }`)
289/// while still fitting the statically-typed rebuild machinery.
290///
291/// Rebuild follows the xilem `AnyView` pattern: the previous view is downcast to
292/// detect whether the concrete type is unchanged. Same type → a typed in-place
293/// rebuild; different type → the old widget is torn down and a fresh one built
294/// and swapped in (signalling `LAYOUT | PAINT`). Its `Element` is a
295/// `Box<dyn Widget>`, which implements [`Widget`] through the blanket impl so it
296/// satisfies `View::Element: Widget`.
297pub struct AnyView<State: 'static> {
298    inner: Box<dyn ErasedView<State>>,
299}
300
301impl<State: 'static> AnyView<State> {
302    /// Erase `view` into an `AnyView`.
303    pub fn new<V: View<State>>(view: V) -> Self {
304        Self {
305            inner: Box::new(view),
306        }
307    }
308}
309
310/// Erase `view` into an [`AnyView`] — the free-function spelling of
311/// [`AnyView::new`], mirroring the `text(..)`/`button(..)` view-fn vocabulary.
312pub fn any<State: 'static, V: View<State>>(view: V) -> AnyView<State> {
313    AnyView::new(view)
314}
315
316impl<State: 'static> View<State> for AnyView<State> {
317    type Element = Box<dyn Widget>;
318
319    fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
320        self.inner.dyn_build(ctx)
321    }
322
323    fn rebuild(
324        &self,
325        prev: &Self,
326        element: &mut Self::Element,
327        ctx: &mut BuildCtx<'_>,
328    ) -> ChangeFlags {
329        self.inner.dyn_rebuild(prev.inner.as_ref(), element, ctx)
330    }
331
332    fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
333        self.inner.dyn_teardown(element, ctx);
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::layout::BoxConstraints;
341    use crate::widget::{LayoutCtx, PaintCtx, PaintScene, Widget};
342    use kurbo::Size;
343    use std::cell::Cell;
344    use std::rc::Rc;
345
346    // --- AnyView fixtures: two distinct view/widget type pairs. ---
347
348    struct WidgetA {
349        n: u32,
350    }
351    impl Widget for WidgetA {
352        fn layout(&mut self, _ctx: &mut LayoutCtx, _bc: &BoxConstraints) -> Size {
353            Size::new(self.n as f64, 1.0)
354        }
355        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
356    }
357
358    struct WidgetB;
359    impl Widget for WidgetB {
360        fn layout(&mut self, _ctx: &mut LayoutCtx, _bc: &BoxConstraints) -> Size {
361            Size::new(99.0, 99.0)
362        }
363        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
364    }
365
366    struct ViewA {
367        n: u32,
368        torn: Rc<Cell<u32>>,
369    }
370    impl View<()> for ViewA {
371        type Element = WidgetA;
372        fn build(&self, _ctx: &mut BuildCtx<'_>) -> WidgetA {
373            WidgetA { n: self.n }
374        }
375        fn rebuild(
376            &self,
377            prev: &Self,
378            element: &mut WidgetA,
379            _ctx: &mut BuildCtx<'_>,
380        ) -> ChangeFlags {
381            if prev.n != self.n {
382                element.n = self.n;
383                ChangeFlags::PAINT
384            } else {
385                ChangeFlags::NONE
386            }
387        }
388        fn teardown(&self, _element: &mut WidgetA, _ctx: &mut BuildCtx<'_>) {
389            self.torn.set(self.torn.get() + 1);
390        }
391    }
392
393    struct ViewB;
394    impl View<()> for ViewB {
395        type Element = WidgetB;
396        fn build(&self, _ctx: &mut BuildCtx<'_>) -> WidgetB {
397            WidgetB
398        }
399        fn rebuild(
400            &self,
401            _prev: &Self,
402            _element: &mut WidgetB,
403            _ctx: &mut BuildCtx<'_>,
404        ) -> ChangeFlags {
405            ChangeFlags::NONE
406        }
407    }
408
409    #[test]
410    fn any_view_same_type_rebuilds_in_place() {
411        let torn = Rc::new(Cell::new(0));
412        let mut counter = 0u64;
413        let mut ctx = BuildCtx::new(&mut counter);
414
415        let prev = any(ViewA {
416            n: 1,
417            torn: torn.clone(),
418        });
419        let mut element = prev.build(&mut ctx);
420
421        let next = any(ViewA {
422            n: 2,
423            torn: torn.clone(),
424        });
425        let flags = next.rebuild(&prev, &mut element, &mut ctx);
426
427        // Same concrete type → typed in-place rebuild, no teardown.
428        assert_eq!(flags, ChangeFlags::PAINT);
429        assert_eq!(torn.get(), 0);
430        let a = (*element)
431            .downcast_mut::<WidgetA>()
432            .expect("still a WidgetA");
433        assert_eq!(a.n, 2);
434    }
435
436    #[test]
437    fn any_view_type_swap_tears_down_and_replaces() {
438        let torn = Rc::new(Cell::new(0));
439        let mut counter = 0u64;
440        let mut ctx = BuildCtx::new(&mut counter);
441
442        let prev = any(ViewA {
443            n: 7,
444            torn: torn.clone(),
445        });
446        let mut element = prev.build(&mut ctx);
447
448        let next = any(ViewB);
449        let flags = next.rebuild(&prev, &mut element, &mut ctx);
450
451        // Concrete type changed → the old view's teardown ran and the widget was
452        // replaced with the new type.
453        assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
454        assert_eq!(torn.get(), 1, "old view should be torn down exactly once");
455        assert!((*element).downcast_mut::<WidgetB>().is_some());
456        assert!((*element).downcast_mut::<WidgetA>().is_none());
457    }
458
459    #[test]
460    fn any_view_element_works_inside_a_child_pod() {
461        // Criterion 3: `Box<dyn Widget>` implements `Widget` — an AnyView's boxed
462        // element drives layout when nested in a container's ChildPod.
463        use crate::widget::ChildPod;
464
465        let torn = Rc::new(Cell::new(0));
466        let mut counter = 0u64;
467        let mut ctx = BuildCtx::new(&mut counter);
468
469        let view = any(ViewA { n: 5, torn });
470        let element: Box<dyn Widget> = view.build(&mut ctx); // Box<dyn Widget>
471
472        // Nest the boxed widget inside a ChildPod (double-boxed): the blanket
473        // `Widget for Box<dyn Widget>` impl forwards layout through both layers.
474        let mut pod = ChildPod::new(Box::new(element));
475        let mut lctx = LayoutCtx::new();
476        let size = pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
477        assert_eq!(size, Size::new(5.0, 1.0));
478    }
479
480    #[test]
481    fn change_flags_union_and_contains() {
482        let both = ChangeFlags::PAINT | ChangeFlags::LAYOUT;
483        assert!(both.contains(ChangeFlags::PAINT));
484        assert!(both.contains(ChangeFlags::LAYOUT));
485        assert!(both.needs_layout());
486        assert!(both.needs_paint());
487
488        assert!(ChangeFlags::NONE.is_empty());
489        assert!(!ChangeFlags::NONE.needs_paint());
490
491        let paint_only = ChangeFlags::PAINT;
492        assert!(paint_only.needs_paint());
493        assert!(!paint_only.needs_layout());
494    }
495
496    #[test]
497    fn change_flags_bitor_assign() {
498        let mut f = ChangeFlags::NONE;
499        f |= ChangeFlags::PAINT;
500        assert!(f.contains(ChangeFlags::PAINT));
501        assert!(!f.contains(ChangeFlags::LAYOUT));
502    }
503
504    #[test]
505    fn build_ctx_allocates_unique_ids() {
506        let mut counter = 0;
507        let mut ctx = BuildCtx::new(&mut counter);
508        let a = ctx.alloc_id();
509        let b = ctx.alloc_id();
510        assert_ne!(a, b);
511        assert_eq!(u64::from(a), 1);
512        assert_eq!(u64::from(b), 2);
513    }
514
515    #[test]
516    fn build_ctx_focus_chain_only_narrows_and_restores() {
517        let mut counter = 0;
518        let mut ctx = BuildCtx::new(&mut counter);
519        assert!(
520            ctx.has_focus(),
521            "an unseeded context assumes a live chain (see BuildCtx::new)"
522        );
523
524        // A focused link keeps a live chain live...
525        ctx.with_focus_link(true, |ctx| assert!(ctx.has_focus()));
526        // ...and the descent is scoped: the caller's chain comes back.
527        assert!(ctx.has_focus());
528
529        // A cleared link closes the chain for the whole subtree below it —
530        // including a *focused* link nested under the cleared one, which is the
531        // stale-flag-below-a-blurred-ancestor case in one line.
532        ctx.with_focus_link(false, |ctx| {
533            assert!(!ctx.has_focus());
534            ctx.with_focus_link(true, |ctx| {
535                assert!(
536                    !ctx.has_focus(),
537                    "no descent may widen the chain a cleared ancestor closed"
538                );
539            });
540            assert!(!ctx.has_focus());
541        });
542        assert!(
543            ctx.has_focus(),
544            "the outer chain is restored, not clobbered"
545        );
546
547        // Seeding is the one absolute write (the root / a component boundary).
548        ctx.set_has_focus(false);
549        assert!(!ctx.has_focus());
550        ctx.with_focus_link(true, |ctx| assert!(!ctx.has_focus()));
551    }
552}