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}