frust_widgets/authoring.rs
1//! The widget-authoring toolkit: the shared child plumbing, event routing and
2//! callback erasure every container/interactive widget is assembled from — the
3//! surface for authoring widgets, and whole design systems, **outside this
4//! crate**.
5//!
6//! A container (a `View`/`Widget` pair owning children) or an interactive leaf
7//! (one reporting a press back into application state) needs three things
8//! `frust-core` deliberately does not provide:
9//!
10//! - **Child plumbing**: [`build_child`]/[`rebuild_child`]/[`teardown_child`] for
11//! a single child — stored as its concrete view type, or as an [`AnyView`]
12//! when its type varies at runtime — and [`rebuild_children`] for a `Vec` of
13//! [`AnyView`]s (positional or [`ChildKey`]-keyed reconciliation, with the
14//! focus/capture retention rules a hand-rolled diff gets wrong).
15//! - **Event routing**: [`route_event`] (multi-child containers, reverse paint
16//! order + capture/focus fast paths) and [`route_event_single`] (one-child
17//! wrappers).
18//! - **Callback erasure**: [`erase_callback`]/[`erase_callback_arg`], turning a
19//! view-held `Rc<dyn Fn(&mut State)>` into the [`ErasedCallback`]/
20//! [`ErasedArgCallback`] adapter a non-generic widget holds, so the retained
21//! widget never becomes generic over the application-state type.
22//! - **Child visitation**: [`visit_children!`] over [`VisitPods`], writing a
23//! container's [`Widget::visit_children`] body from its child fields — the
24//! read-only seam that lets tooling walk into a container's retained subtree
25//! (`frust-core`'s `WidgetTree::inspect`). One line per container; a leaf
26//! needs nothing.
27//! - **Themed text roles**: [`ThemeTextColor`] (re-exported here) and
28//! [`TextView::themed_role`](crate::TextView::themed_role) — how a widget labels
29//! a child [`text`](crate::text) run with the themed color role it should
30//! default to, instead of hardcoding a color; and [`ThemeTextType`]
31//! (re-exported here) with
32//! [`TextView::themed_family`](crate::TextView::themed_family) — how it opts
33//! the run's font family into the active theme's type-scale role, resolved
34//! at layout so a live theme swap re-shapes it, instead of shaping in
35//! `TextStyle::default()`'s system UI font.
36//!
37//! # Hover: claim it, latch it, then correct it at paint time
38//!
39//! There is no hover phase and no Enter/Leave event. A widget that wants hover
40//! chrome opts in with three things, each covering a case the others cannot:
41//!
42//! - In its **uncaptured** [`PointerPhase::Move`] arm, hit-test the event position
43//! against its own bounds and call
44//! [`EventCtx::claim_hover`](frust_core::EventCtx::claim_hover) when it is
45//! inside — on *every* such move, not just on entry (the claim is per-pass, not
46//! sticky).
47//! - Latch that same hit test into an internal hover flag, and gate
48//! `request_redraw` on the flag actually *changing*. This is the frame source
49//! for hover gain (and for the link moving from a sibling onto this widget):
50//! `claim_hover` asks for no redraw, and the pipeline manufactures one only when
51//! a hover ends with nothing taking it. Gating on the change is also what keeps
52//! a pointer wandering inside one widget from repainting per event.
53//! - In `paint`, read
54//! [`PaintCtx::is_hovered`](frust_core::PaintCtx::is_hovered) as the
55//! authoritative value and self-correct the flag from it. This is not optional
56//! belt-and-braces: a pointer leaving the widget routes its next move onto
57//! whatever it moved *onto*, so the widget it left never gets an event saying so.
58//!
59//! Everything else is the pipeline's job. `frust-core` stamps the claim down the
60//! pod chain and strands the previous claimant by advancing a hover epoch, so a
61//! container needs no hover bookkeeping at all — [`route_event`] and
62//! [`route_event_single`] carry it for free, and a hand-rolled router that
63//! forwards through [`ChildPod::event_child`] does too. Because the claim is
64//! recorded as a *path*, an enclosing container reads hovered while the pointer is
65//! over a claiming child (CSS `:hover` semantics), and every off-path widget reads
66//! `false`.
67//!
68//! A container that wants hover chrome of its **own** has one extra rule: it
69//! claims *after* routing the move to its children, never before. Only one claim
70//! per pass is recorded and the first one recorded wins, so an ancestor that claims
71//! before it forwards makes every descendant ineligible for the pass — the child
72//! under the pointer never reads hovered, while its latched flag keeps flipping and
73//! asking for a frame on every move. Claiming after routing makes the container's
74//! claim a fallback: a child's claim wins and the container still reads hovered
75//! through the path, and when no child claims the container's own claim is what
76//! records.
77//!
78//! A **captured** pointer can never create hover, so a drag never paints hover
79//! under the finger and a widget that captures its own gesture is hover-free for
80//! the length of it. Nothing distinguishes a touch contact from a mouse, though: a
81//! touch drag that captured nothing is an ordinary hover pass, so a non-capturing
82//! consumer can tint transiently under a finger — the `Up` at lift ends the link
83//! (see `docs/LIMITATIONS.md`'s `hover-window-leave-standing`).
84//!
85//! **Application code should prefer `frust::authoring`**, which re-exports
86//! everything below plus the `frust-core` trait vocabulary and `kurbo`/`peniko`
87//! geometry — so an app depends on `frust` alone. The example spells its imports
88//! the long way only because this crate cannot name `frust` without a
89//! dev-dependency cycle; `frust::authoring`'s own module docs carry the
90//! facade-spelled version.
91//!
92//! **Stability: pre-1.0, and a real supported public API** — not `#[doc(hidden)]`
93//! plumbing, so a change to any signature here is a **breaking change**, released
94//! as such. The three catalogs shipping with this crate (`material`, `cupertino`,
95//! `glyph`) consume exactly this surface and nothing more, so a third-party design
96//! system authored against it is at parity with the built-ins by construction.
97//!
98//! # Example: a one-child container widget
99//!
100//! A container that offsets its single child, wired through the full lifecycle —
101//! build, rebuild, teardown, layout, paint, event routing, semantics forwarding:
102//!
103//! ```
104//! use frust_core::{
105//! AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult,
106//! InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
107//! };
108//! use frust_widgets::authoring::{build_child, rebuild_child, route_event_single, teardown_child};
109//! use kurbo::{Point, Size};
110//!
111//! /// The declarative half: a child plus the offset to apply to it.
112//! struct OffsetView<State: 'static> {
113//! offset: Point,
114//! child: AnyView<State>,
115//! }
116//!
117//! /// The view-fn app code calls; `any` erases the child so `OffsetView` is one
118//! /// type whatever the child is. A view generic over its child
119//! /// (`OffsetView<V>` holding `child: V`) stores it concrete instead — the
120//! /// same three helpers take either (see `build_child`).
121//! fn offset<State: 'static, V: View<State>>(offset: Point, child: V) -> OffsetView<State> {
122//! OffsetView { offset, child: any(child) }
123//! }
124//!
125//! /// The retained half: the live child pod plus the applied offset.
126//! struct OffsetWidget {
127//! offset: Point,
128//! child: ChildPod,
129//! }
130//!
131//! impl<State: 'static> View<State> for OffsetView<State> {
132//! type Element = OffsetWidget;
133//!
134//! fn build(&self, ctx: &mut BuildCtx<'_>) -> OffsetWidget {
135//! OffsetWidget { offset: self.offset, child: build_child(&self.child, ctx) }
136//! }
137//!
138//! fn rebuild(
139//! &self,
140//! prev: &Self,
141//! element: &mut OffsetWidget,
142//! ctx: &mut BuildCtx<'_>,
143//! ) -> ChangeFlags {
144//! let mut flags = ChangeFlags::NONE;
145//! if prev.offset != self.offset {
146//! element.offset = self.offset;
147//! flags |= ChangeFlags::LAYOUT;
148//! }
149//! flags | rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
150//! }
151//!
152//! fn teardown(&self, element: &mut OffsetWidget, ctx: &mut BuildCtx<'_>) {
153//! teardown_child(&self.child, &mut element.child, ctx);
154//! }
155//! }
156//!
157//! impl Widget for OffsetWidget {
158//! fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
159//! let size = self.child.layout_child(ctx, bc);
160//! self.child.set_origin(self.offset);
161//! bc.constrain(size)
162//! }
163//!
164//! fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
165//! self.child.paint_child(ctx, scene);
166//! }
167//!
168//! fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
169//! // Never re-hit-test a captured child by hand — this helper owns the
170//! // capture/focus fast paths and the blur-on-outside-tap rule.
171//! route_event_single(&mut self.child, ctx, event)
172//! }
173//!
174//! fn semantics(&self, ctx: &mut SemanticsCtx) {
175//! // A transparent wrapper still MUST forward, or the child's whole
176//! // subtree drops out of the accessibility tree.
177//! self.child.semantics_child(ctx);
178//! }
179//!
180//! // The read-only tooling seam: publish every pod this widget owns.
181//! frust_widgets::authoring::visit_children!(child);
182//! }
183//! # fn main() {}
184//! ```
185
186use std::any::Any;
187use std::collections::{HashMap, HashSet};
188use std::rc::Rc;
189
190use frust_core::{
191 AnyView, BuildCtx, ChangeFlags, EventCtx, EventResult, InputEvent, PointerButton, PointerEvent,
192 PointerPhase, View, Widget,
193};
194use kurbo::Point;
195
196use crate::ChildKey;
197
198pub use crate::image::ImageSource;
199pub use crate::text::{ThemeTextColor, ThemeTextType};
200
201/// The overlay-portal authoring surface: the slot a widget that floats its own
202/// surface holds, what that surface is anchored to, and the placement geometry
203/// every anchored pattern needs.
204///
205/// Promoted here rather than left to each design system because all three of
206/// them had derived the same geometry independently: a design system's popover,
207/// menu, tooltip and context menu place a surface through [`place`] and host it
208/// through [`OverlaySlot`], reaching both through `frust::authoring` alone.
209pub use crate::overlay::{
210 OverlayAlign, OverlayAnchor, OverlayPlacement, OverlaySide, OverlaySlot, place,
211};
212
213/// The overlay registry's own vocabulary, re-exported from `frust-core` so a
214/// design system naming a band, an input class, a light-dismiss policy or a
215/// surface identity does it through this one module: an
216/// [`OverlaySlot`]'s setters take exactly these types, and a widget that cannot
217/// name them cannot configure one.
218pub use frust_core::{OutsideTap, OverlayBand, OverlayInput, OverlayKey};
219
220/// Pressed-state overlay opacity (source: androidx Compose Material3
221/// `StateTokens` v0_210, retrieved 2026-07-17 — supersedes material-web
222/// v0.192's 12%).
223///
224/// Lives here, not with the rest of the M3 state-layer table, because it is the
225/// one interaction opacity a *language-neutral* widget needs: a press overlay is
226/// a mechanism (how a control acknowledges a touch), not a Material design
227/// decision. A design system's own state-layer table is free to differ; this is
228/// the value the baseline widgets and the built-in catalogs share.
229pub const PRESSED_OPACITY: f32 = 0.10;
230
231/// A widget-held, `State`-erased app-state callback adapter (see
232/// [`erase_callback`]).
233pub type ErasedCallback = Box<dyn FnMut(&mut EventCtx)>;
234
235/// A widget-held, `State`-erased callback adapter carrying one value argument
236/// (see [`erase_callback_arg`]).
237pub type ErasedArgCallback<A> = Box<dyn FnMut(&mut EventCtx, A)>;
238
239/// A view-held, typed callback carrying one value argument (Checkbox's `bool`,
240/// Slider's `f64`), erased to [`ErasedArgCallback`] on build.
241pub type TypedArgCallback<State, A> = std::rc::Rc<dyn Fn(&mut State, A)>;
242
243/// Erase a view-held `Rc<dyn Fn(&mut State)>` app-state callback into the
244/// widget-held [`ErasedCallback`] adapter the interactive widgets invoke during
245/// the event pass.
246///
247/// The closure recovers the concrete `State` from the type-erased [`EventCtx`]
248/// with [`EventCtx::state_mut`] (the downcast happens *inside* the adapter), so
249/// the widget itself stays non-generic over `State`. Closures aren't comparable,
250/// so `build`/`rebuild` reinstall the adapter unconditionally — it's cheap.
251pub fn erase_callback<State: 'static>(callback: &Rc<dyn Fn(&mut State)>) -> ErasedCallback {
252 let callback = callback.clone();
253 Box::new(move |ctx: &mut EventCtx| {
254 let state = ctx.state_mut::<State>();
255 callback(state);
256 })
257}
258
259/// Like [`erase_callback`], but for callbacks that also carry a value argument
260/// (Checkbox's `bool`, Slider's `f64`).
261pub fn erase_callback_arg<State: 'static, A: 'static>(
262 callback: &TypedArgCallback<State, A>,
263) -> ErasedArgCallback<A> {
264 let callback = callback.clone();
265 Box::new(move |ctx: &mut EventCtx, arg: A| {
266 let state = ctx.state_mut::<State>();
267 callback(state, arg);
268 })
269}
270
271/// Build a [`ChildPod`] wrapping a child view's element.
272///
273/// The pod stores whatever `V::Element` the view builds, boxed once as the
274/// pod's `Box<dyn Widget>`, so a later [`rebuild_child`]/[`teardown_child`]
275/// with the **same** `V` recovers it as `&mut V::Element`. Two shapes, one
276/// function:
277///
278/// - **A concrete child** (`V` is the child's own view type, or an
279/// [`Either`](crate::Either) / [`EitherOf3`](crate::EitherOf3) ..
280/// [`EitherOf8`](crate::EitherOf8) of several): the element is stored once,
281/// and the rebuild reconciles it in place with no erasure on the path. An
282/// enum child's arm swap still counts as a swap for the pod's bookkeeping
283/// (see [`rebuild_child`]). Choose this when the child's type is fixed by the
284/// container view's own type parameters (`struct Wrap<V> { child: V }`).
285/// - **An [`AnyView`] child**: the element is itself a `Box<dyn Widget>`, so the
286/// pod holds it double-boxed and the rebuild recovers `&mut Box<dyn Widget>`
287/// — the type `AnyView`'s `rebuild` needs to swap the widget on a
288/// concrete-type change. Choose this when the child's type varies at runtime
289/// (a stored view, more than eight arms) or the child sits in a mixed list.
290///
291/// Whichever `V` built the pod must be the `V` every later call on it names;
292/// a mismatch is a programming error and panics on the downcast.
293///
294/// The build runs with the focus chain closed
295/// ([`BuildCtx::with_focus_link`]`(false, …)`): the pod being built is brand new,
296/// so by construction it holds no focus link — the same composition
297/// [`rebuild_child`] performs, kept uniform so "descending into a pod always
298/// recomputes the chain" has no exceptions.
299pub fn build_child<State: 'static, V: View<State>>(view: &V, ctx: &mut BuildCtx<'_>) -> ChildPod {
300 ChildPod::new(Box::new(ctx.with_focus_link(false, |ctx| view.build(ctx))))
301}
302
303/// Reconcile one child in place through its `ChildPod` — `V` is the view type
304/// the pod was built from by [`build_child`].
305///
306/// A plain concrete child (`V` = the child's own view type) is rebuilt in
307/// place and never reports a swap: its element type cannot change between
308/// frames. An [`Either`](crate::Either) / [`EitherOf3`](crate::EitherOf3) ..
309/// [`EitherOf8`](crate::EitherOf8) child switching arms keeps its element type
310/// but replaces the widget inside it, and reports that
311/// ([`BuildCtx::mark_element_replaced`]), so it gets the same swap semantics as
312/// an [`AnyView`] concrete-type change.
313///
314/// A swapped child (see `rebuild_child_tracked`) that still holds a recorded
315/// capture has that capture dropped: `pod.set_active(false)`, no synthetic
316/// `Cancel`. The old armed widget was torn down inside the child's own
317/// `rebuild` (`AnyView`'s, or the enum's) — its state died with it — and the
318/// fresh widget in its place never saw the original `Down`, so there is
319/// nothing to unwind; this mirrors [`rebuild_children`]'s documented
320/// type-swap semantics for the single-child wrappers (`Padding`/`Align`/
321/// `SizedBox`, and the interactive widgets' own label/track children) that call
322/// this instead of `rebuild_children`.
323///
324/// **A type swap severs the recorded focus path exactly like it severs the
325/// capture path**, and is handled the same way the keyed reconciler handles its
326/// own swap arm: the pod's `focused` flag is dropped (the fresh widget never
327/// claimed focus — leaving the link set would route `Key`/`Ime` events into a
328/// widget that ignores them) and
329/// [`mark_focus_orphaned`](frust_core::mark_focus_orphaned) is raised *when the
330/// severed link was on the live focus chain* ([`mark_orphan_if_live`]), so
331/// `RenderRoot::rebuild` releases `focus_active`/`ime_state` before the frame
332/// ends. This is the `Padding(if editing { text_input } else { text })` shape:
333/// without the release the keyboard stays up over an idle screen and
334/// `is_focus_active()` keeps reporting a session whose owner no longer exists. A
335/// swap of a merely *stale* flag under an already-blurred ancestor marks
336/// nothing — it owned no session.
337pub fn rebuild_child<State: 'static, V: View<State>>(
338 prev: &V,
339 next: &V,
340 pod: &mut ChildPod,
341 ctx: &mut BuildCtx<'_>,
342) -> ChangeFlags {
343 // Read before the nested rebuild, exactly as `rebuild_child_tracked` reads
344 // its own `link_focused`: the gate describes the link this pod held *going
345 // into* the swap.
346 let link_focused = pod.is_focused();
347 let (flags, swapped) = rebuild_child_tracked(prev, next, pod, ctx);
348 if swapped {
349 if pod.is_active() {
350 pod.set_active(false);
351 }
352 if link_focused {
353 pod.set_focused(false);
354 }
355 // `ctx` carries the chain down to *this wrapper*; the pod's own flag is
356 // ANDed onto it inside, the same composition every other marking site
357 // uses.
358 mark_orphan_if_live(ctx, link_focused);
359 }
360 flags
361}
362
363/// Like [`rebuild_child`], but also reports whether the rebuild *replaced* the
364/// underlying widget (an [`AnyView`] concrete-type swap, or an enum view's arm
365/// swap) rather than mutating it in place.
366///
367/// The swap flag drives both callers' capture bookkeeping: [`rebuild_children`]'s
368/// (multi-child `Vec` containers — `Flex`/`Stack`, always `V` = [`AnyView`]) and
369/// [`rebuild_child`]'s (single-child wrappers). A fresh widget swapped in at a
370/// still-captured index/pod never saw the original `Down`, so its stale `active`
371/// path is dropped without a synthetic `Cancel` (there is nothing armed to
372/// unwind). Detection has two halves, ORed:
373///
374/// - the element's widget [`TypeId`](std::any::TypeId) compared across the
375/// rebuild (see `widget_type_id`): the *inner* widget's for an [`AnyView`]
376/// element, so an `AnyView` child reports exactly the swaps it always has, and
377/// `V::Element`'s own for a concrete child, which no rebuild can change;
378/// - the element-replaced signal, collected by running the nested rebuild as a
379/// [`BuildCtx::track_element_replaced`] scope: an
380/// [`Either`](crate::Either) / `EitherOfN` child (or one nested inline inside
381/// an `AnyView` or another enum) keeps its element type across an arm swap
382/// and reports the swap through [`BuildCtx::mark_element_replaced`] instead.
383/// This pod is the nearest scope, so it consumes the signal; a pod deeper in
384/// the swapped subtree ran its own scope and consumed its own.
385///
386/// **This is the descent that extends the focus chain.** The nested rebuild runs
387/// under [`BuildCtx::with_focus_link`]`(pod.is_focused(), …)`, so a container
388/// deeper in the tree sees `ctx.has_focus()` == "is the whole chain from the root
389/// to me focused" — the value the orphan-marking sites gate on. Every route into
390/// a child's `rebuild`/`teardown` funnels through here (or through
391/// [`teardown_child`]), which is what makes the chain complete through arbitrary
392/// nesting.
393fn rebuild_child_tracked<State: 'static, V: View<State>>(
394 prev: &V,
395 next: &V,
396 pod: &mut ChildPod,
397 ctx: &mut BuildCtx<'_>,
398) -> (ChangeFlags, bool) {
399 // Read before the `widget_mut` borrow below.
400 let link_focused = pod.is_focused();
401 // The non-marking accessor: this function marks the pod itself, below, from
402 // the rebuild's flags — `widget_mut` would dirty it on every rebuild.
403 let element = pod
404 .widget_mut_for_rebuild()
405 .downcast_mut::<V::Element>()
406 .expect("child pod element is the element type its view built (build_child)");
407 let before = widget_type_id(element);
408 let (flags, replaced) = ctx.track_element_replaced(|ctx| {
409 ctx.with_focus_link(link_focused, |ctx| next.rebuild(prev, element, ctx))
410 });
411 let after = widget_type_id(element);
412 let swapped = before != after || replaced;
413 // The layout-skip dirty source of the rebuild pass: a child whose rebuild
414 // reports `LAYOUT` (or whose widget was replaced) is laid out again on the
415 // next pass. PAINT-only does not dirty layout. Every reconciler route —
416 // `rebuild_child`, both `rebuild_children` paths, and `OverlaySlot` through
417 // `rebuild_child` — funnels through here, so this one mark covers them; the
418 // pods above are marked by their own rebuild sites from the flags this
419 // returns, which every container folds into its own. A PAINT-only rebuild
420 // marks just the pod's paint dirtiness (layout stays skippable) so a
421 // retained layer over it is repainted.
422 if swapped || flags.contains(ChangeFlags::LAYOUT) {
423 pod.mark_layout_dirty();
424 } else if flags.contains(ChangeFlags::PAINT) {
425 pod.mark_paint_dirty();
426 }
427 (flags, swapped)
428}
429
430/// The [`TypeId`](std::any::TypeId) of the widget an element *is*, for swap
431/// detection: the boxed widget's when the element is a `Box<dyn Widget>` (an
432/// [`AnyView`]'s element, whose contents a rebuild can replace), else the
433/// element's own static type — which no rebuild can change (an enum element's
434/// arm swap is reported through [`BuildCtx::mark_element_replaced`] instead).
435fn widget_type_id<E: Widget>(element: &E) -> std::any::TypeId {
436 match (element as &dyn Any).downcast_ref::<Box<dyn Widget>>() {
437 Some(boxed) => {
438 let any: &dyn Any = &**boxed;
439 any.type_id()
440 }
441 None => std::any::TypeId::of::<E>(),
442 }
443}
444
445/// Tear down one child through its `ChildPod` — `V` is the view type the pod
446/// was built from by [`build_child`], concrete or [`AnyView`].
447///
448/// A pod still holding an in-flight capture ([`ChildPod::is_active`]) is
449/// cancelled (a synthetic `Cancel`, see `cancel_pod`) before teardown, so an
450/// armed widget dropped mid-gesture (e.g. the active row truncated out of a
451/// shrinking list) unwinds its state machine instead of vanishing with no
452/// terminating `Up`/`Cancel`.
453///
454/// A pod still holding the recorded **focus** path ([`ChildPod::is_focused`])
455/// *on the live focus chain* ([`BuildCtx::has_focus`]) raises
456/// [`mark_focus_orphaned`](frust_core::mark_focus_orphaned) instead: the focused
457/// widget is about to stop existing, so the whole focus/IME session dies with it
458/// and `RenderRoot::rebuild` releases it before the frame ends (see
459/// [`cancel_active_children`]'s note for the full mechanism, and why the pod
460/// cannot do it itself). A pod whose flag is stale — set, but under a link some
461/// ancestor already cleared — is torn down silently: it owns no session to lose.
462pub fn teardown_child<State: 'static, V: View<State>>(
463 view: &V,
464 pod: &mut ChildPod,
465 ctx: &mut BuildCtx<'_>,
466) {
467 if pod.is_active() {
468 cancel_pod(pod);
469 pod.set_active(false);
470 }
471 let link_focused = pod.is_focused();
472 if link_focused {
473 // The caller drops the pod right after this returns (or, in
474 // `pattern_switcher`, keeps it only to paint its exit), so clearing the
475 // flag is bookkeeping hygiene, not the load-bearing part — the mark is.
476 pod.set_focused(false);
477 }
478 mark_orphan_if_live(ctx, link_focused);
479 if let Some(element) = pod.widget_mut().downcast_mut::<V::Element>() {
480 // Descend with the chain extended by the link this pod held *before* the
481 // clear above: a focused pod's subtree is still on the live chain while it
482 // is being torn down, so a focused descendant reports its own orphan.
483 ctx.with_focus_link(link_focused, |ctx| view.teardown(element, ctx));
484 }
485}
486
487/// Raise the orphan mark for a severed focus link, but **only when the link was
488/// live** — the one gate every marking site in this module shares
489/// ([`teardown_child`], [`cancel_active_children`], the keyed reconciler's
490/// type-swap arm, and [`rebuild_child`]'s).
491///
492/// `link_focused` is the dying pod's own [`ChildPod::is_focused`] flag and
493/// `ctx.has_focus()` is the chain above it, so the mark means exactly one thing:
494/// **a LIVE session just lost its owner.** Both halves are required. The flag
495/// alone is not evidence of a live session — a container-routed blur clears the
496/// focus link at the nearest common ancestor only, leaving flags deeper in the
497/// blurred branch legitimately set (see [`route_event`]) — and marking on the
498/// flag alone is what let a recycled list row, a filtered-out item, or a switched
499/// pattern release the *currently typed-into* field somewhere else in the tree.
500/// The chain alone is not evidence either: a live chain running past an unfocused
501/// sibling says nothing about that sibling.
502fn mark_orphan_if_live(ctx: &BuildCtx<'_>, link_focused: bool) {
503 if link_focused && ctx.has_focus() {
504 frust_core::mark_focus_orphaned();
505 }
506}
507
508/// Deliver a synthetic [`PointerPhase::Cancel`] to a captured child whose
509/// in-flight gesture a structural rebuild has invalidated, so its state machine
510/// unwinds instead of firing on a later `Up`.
511///
512/// # Cancel-during-rebuild contract
513///
514/// The rebuild pass runs over a [`BuildCtx`], not an [`EventCtx`] — there is no
515/// application state in scope. This is sound *only because a `Cancel` handler
516/// must never read application state* (`EventCtx::state_mut`): every
517/// interactive widget's `Cancel` arm only clears internal flags. That invariant
518/// lets this build a minimal [`EventCtx`] over a throwaway `()` state to drive
519/// the unwind; a `Cancel` handler that reached for real state would panic here
520/// on the `()` downcast — a deliberate tripwire, not a silent corruption.
521pub(crate) fn cancel_pod(pod: &mut ChildPod) {
522 let mut dummy_state = ();
523 let mut ctx = EventCtx::new(&mut dummy_state, pod.origin(), pod.size());
524 // Position is irrelevant to a `Cancel` (handlers never hit-test on it).
525 let cancel = InputEvent::Pointer(PointerEvent {
526 phase: PointerPhase::Cancel,
527 position: Point::ZERO,
528 button: PointerButton::Primary,
529 });
530 pod.event_child(&mut ctx, &cancel);
531}
532
533/// Cancel-and-clear the recorded interaction paths of the pods handed to it —
534/// both the capture (`active`) path and the focus (`focused`) path — after a
535/// structural change.
536///
537/// Called by [`rebuild_children_positional`] on the **tail past the stable
538/// prefix** (indices `≥ k`, whose widget identity may have changed): a swapped
539/// slot, a shifted pod, or the grown tail. The stable prefix (indices `< k`)
540/// keeps the same live widget across the rebuild, so its recorded paths stay
541/// valid and are *not* passed here — that is Flutter's focus/IME retention
542/// invariant (see [`rebuild_children_positional`]). The keyed reconciler does not
543/// call this at all: a key-matched pod relocates with its flags intact, and only
544/// a torn-down or type-swapped pod (identity broken) is cleared inline there.
545///
546/// For each pod handed in, any surviving armed widget is unwound via [`cancel_pod`]
547/// and its `active` flag dropped, and any focused child has its `focused` flag
548/// dropped. `ctx` carries the focus chain down to the *container*
549/// ([`BuildCtx::has_focus`]), which each pod's own flag is ANDed onto to decide
550/// whether the clear severed a live session (see [`mark_orphan_if_live`]).
551///
552/// # Focus vs. capture: why one synthesizes a `Cancel` and the other does not
553///
554/// Capture state lives *inside* the widget (an `armed`/`pressed` flag its own
555/// event arms), so dropping the recorded path requires a synthetic [`Cancel`]
556/// ([`cancel_pod`]) to unwind that internal machine — otherwise it would fire on
557/// a later hit-tested `Up`. Focus state, by contrast, is *reflected* from the pod
558/// flag into the widget each event (`EventCtx::has_focus`, threaded through
559/// [`ChildPod::event_child`]) rather than latched internally, so clearing the pod
560/// flag is enough — there is no widget-internal blur to drive, and a `Cancel`
561/// handler must not touch app state anyway (`docs/CODE_STANDARDS.md`).
562///
563/// # RenderRoot notification: the orphan mark
564///
565/// Clearing `focused` here cannot notify [`RenderRoot`](frust_core::RenderRoot)
566/// directly — the rebuild pass runs over a [`BuildCtx`], with no `RenderRoot` in
567/// scope, exactly as the capture-cancel path above cannot reset
568/// `RenderRoot::pointer_captured`. It instead raises the thread-local
569/// [`mark_focus_orphaned`](frust_core::mark_focus_orphaned) flag, which
570/// `RenderRoot::rebuild` drains at the end of the same rebuild and turns into a
571/// full focus/IME session release (`focus_active` cleared, the shell-facing
572/// surface dropped, one edge on the focus/IME generation).
573///
574/// **Why the mark and not convergence.** Leaving the root's
575/// `focus_active`/`ime_state` stale for the next event pass to self-correct is
576/// fine while the user keeps touching the screen and wrong the moment they
577/// stop: on an idle screen no next event arrives, so the root
578/// keeps reporting a live focus session for a widget that no longer exists —
579/// which strands `ime_state()` at the shell and (measured on a Xiaomi 12) held
580/// the mobile frame gate's focus input up permanently after a navigator pop.
581/// A session must die with its owner, not with the next tap.
582///
583/// **Why a thread-local and not a tree walk.** Validating the root's mirror
584/// against reality after the diff would need to ask "does any pod in the tree
585/// still hold focus?", and there is no such iterator: `Widget` exposes no
586/// children, so nothing can walk the retained tree generically. The mark is the
587/// only channel available from inside a `BuildCtx` pass, and it mirrors
588/// `mark_pending_result_flush`'s shape exactly (data-free, idempotent,
589/// UI-thread-affine).
590///
591/// **A stale flag deep in a blurred branch is harmless by construction.** A
592/// container-routed blur clears the focus link at the nearest common ancestor
593/// only, so `focused` flags *below* that link stay set until focus next enters
594/// the subtree (see [`route_event`]). Such a pod is not evidence of a session:
595/// the mark is gated on the whole chain ([`mark_orphan_if_live`]), which a
596/// cleared ancestor link forces to `false` for the entire subtree below it —
597/// exactly as the same composition already hides those flags from paint
598/// (`ChildPod::paint_child`'s `self.focused && ctx.has_focus()`) and as
599/// focus-path routing already refuses to deliver into them. Tearing down a stale
600/// branch — a recycled list row, a filtered-out item, a switched pattern —
601/// therefore leaves the session of whatever field is *actually* focused
602/// untouched, instead of dropping the keyboard mid-typing somewhere unrelated.
603fn cancel_active_children(pods: &mut [ChildPod], ctx: &BuildCtx<'_>) {
604 for pod in pods.iter_mut() {
605 if pod.is_active() {
606 cancel_pod(pod);
607 pod.set_active(false);
608 }
609 let link_focused = pod.is_focused();
610 if link_focused {
611 pod.set_focused(false);
612 }
613 mark_orphan_if_live(ctx, link_focused);
614 }
615}
616
617/// Diff a child list against its live `ChildPod`s, extracting each child's
618/// [`AnyView`] through `view_of` (identity for a plain `Vec<AnyView>`, `|c|
619/// &c.view` for Flex's `FlexChild` wrapper) and its optional
620/// [`ChildKey`] through `key_of` (`|_| None` for keyless containers, `|c| c.key`
621/// for Flex) — the one shared reconciler for every multi-child container.
622///
623/// **Positional vs. keyed.** With *no* child carrying a key this is a positional
624/// diff (see `rebuild_children_positional`); with keys present it matches
625/// old↔new by key so reorders/inserts preserve widget identity and state (see
626/// `rebuild_children_keyed`). Keys are all-or-nothing per list: a list that
627/// mixes keyed and unkeyed children, or repeats a key, trips a `debug_assert`
628/// and falls back to the positional path (correct, just identity-blind).
629///
630/// **A structural change preserves focus/capture for unchanged siblings
631/// (Flutter's invariant).** A structural edit among siblings only cancels the
632/// recorded capture/focus paths of children whose own identity actually changed:
633/// the positional path preserves its stable prefix and cancels only the tail past
634/// the first type swap; the keyed path relocates a key-matched child's paths
635/// intact and cancels only a torn-down or type-swapped child. A
636/// structural-change-free rebuild leaves every recorded path untouched, so an
637/// ordinary every-frame rebuild never breaks a captured drag or dismisses the
638/// keyboard for an unchanged child. A "type swap" here is whatever
639/// `rebuild_child_tracked` reports: an [`AnyView`] concrete-type change, or an
640/// [`Either`](crate::Either) / `EitherOfN` arm swap inside the child (reported
641/// through [`BuildCtx::mark_element_replaced`]) — both replace the widget.
642///
643/// **A child that loses a LIVE focus path takes the root's session with it.**
644/// Every route that severs a recorded focus path here — a torn-down child
645/// ([`teardown_child`]), a cleared tail ([`cancel_active_children`]), a
646/// key-reused type swap — raises
647/// [`mark_focus_orphaned`](frust_core::mark_focus_orphaned) *when the severed
648/// link was on the live focus chain* ([`mark_orphan_if_live`]), and
649/// `RenderRoot::rebuild` releases `focus_active`/`ime_state` before the frame
650/// ends rather than leaving them standing over a widget that no longer exists. A
651/// severed link that was merely a stale flag deep in an already-blurred branch
652/// marks nothing: it owned no session. See [`cancel_active_children`] for the
653/// mechanism.
654pub fn rebuild_children<State: 'static, C>(
655 prev: &[C],
656 next: &[C],
657 pods: &mut Vec<ChildPod>,
658 ctx: &mut BuildCtx<'_>,
659 view_of: impl Fn(&C) -> &AnyView<State>,
660 key_of: impl Fn(&C) -> Option<ChildKey>,
661) -> ChangeFlags {
662 let any_keyed = prev.iter().chain(next.iter()).any(|c| key_of(c).is_some());
663 if !any_keyed {
664 return rebuild_children_positional(prev, next, pods, ctx, view_of);
665 }
666 // v1 keys are all-or-nothing per list: a mixed list has no well-defined
667 // match for its unkeyed members, so fall back to positional (identity-blind
668 // but correct) rather than guess. A debug build flags the misuse loudly.
669 let all_keyed = prev.iter().chain(next.iter()).all(|c| key_of(c).is_some());
670 if !all_keyed {
671 debug_assert!(
672 false,
673 "keyed child list mixes keyed and unkeyed children; \
674 falling back to positional reconciliation"
675 );
676 return rebuild_children_positional(prev, next, pods, ctx, view_of);
677 }
678 rebuild_children_keyed(prev, next, pods, ctx, view_of, key_of)
679}
680
681/// The positional reconciler: common indices rebuild in place, a grown tail is
682/// built, a shrunk tail is torn down and dropped. Length changes signal
683/// `LAYOUT | PAINT`.
684///
685/// **Focus/capture survive a sibling structural change (Flutter's invariant).**
686/// A structural edit among SIBLINGS must not clear focus/IME (or an in-flight
687/// capture) for a child whose own identity is unchanged. Positional matching
688/// rebuilds each common index `i` in place against the *same* live widget, so a
689/// recorded focus/capture path to it stays valid as long as that slot was not an
690/// in-place type swap. We therefore compute the **stable prefix** `k` — the
691/// largest `k ≤ min(prev.len, next.len)` such that no index `< k` type-swapped
692/// (the first swap index, or `common` if none) — and preserve focus AND capture
693/// for pods `< k`. Only the tail from `k` onward has its recorded paths
694/// cancelled/cleared via [`cancel_active_children`]: a swapped slot (fresh widget
695/// that never saw `Down`), a shifted pod that may now hold different logical
696/// content, and the grown tail (fresh pods, nothing to unwind). A truncated
697/// active pod is cancelled earlier in [`teardown_child`]. A structural-change-free
698/// rebuild (same length, no swap) leaves every path untouched, so an ordinary
699/// every-frame rebuild never breaks a captured drag *or* dismisses the keyboard
700/// for an unchanged sibling (the huddle search-field bug this fixes).
701///
702/// This is Flutter's focus/IME retention behavior: only a child whose
703/// identity actually changes loses focus.
704/// Positional matching cannot distinguish a same-typed prepend from a
705/// content-change-plus-append — an index `< k` that positionally kept its widget
706/// but semantically moved keeps its recorded path (the documented positional
707/// limitation; a caller wanting identity across reorders uses `keyed`).
708fn rebuild_children_positional<State: 'static, C>(
709 prev: &[C],
710 next: &[C],
711 pods: &mut Vec<ChildPod>,
712 ctx: &mut BuildCtx<'_>,
713 view_of: impl Fn(&C) -> &AnyView<State>,
714) -> ChangeFlags {
715 let mut flags = ChangeFlags::NONE;
716 let common = prev.len().min(next.len());
717 // The stable prefix ends at the first in-place type swap (or at `common` if
718 // there is none): every index before it keeps the same live widget across
719 // the rebuild, so a recorded focus/capture path to it stays valid.
720 let mut first_swap: Option<usize> = None;
721 for i in 0..common {
722 let (child_flags, child_swapped) =
723 rebuild_child_tracked(view_of(&prev[i]), view_of(&next[i]), &mut pods[i], ctx);
724 flags |= child_flags;
725 if child_swapped {
726 // Fresh widget at this slot: drop the stale capture, nothing to cancel.
727 pods[i].set_active(false);
728 if first_swap.is_none() {
729 first_swap = Some(i);
730 }
731 }
732 }
733 if next.len() > prev.len() {
734 for child in &next[prev.len()..] {
735 pods.push(build_child(view_of(child), ctx));
736 }
737 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
738 } else if next.len() < prev.len() {
739 for (offset, child) in prev[next.len()..].iter().enumerate() {
740 teardown_child(view_of(child), &mut pods[next.len() + offset], ctx);
741 }
742 pods.truncate(next.len());
743 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
744 }
745 // On a structural change (length shift or in-place type swap), preserve the
746 // stable prefix (`< k`, same widget identity) and cancel/clear only the tail
747 // from `k` onward. On a pure length change with no swap, `k == common`, so the
748 // prefix (every surviving common pod) is preserved and only the grown tail —
749 // fresh pods with no recorded path — is "cancelled" (a no-op); a truncated
750 // active pod was already cancelled in `teardown_child`.
751 if prev.len() != next.len() || first_swap.is_some() {
752 let k = first_swap.unwrap_or(common);
753 // `ctx` carries the chain down to *this container*; each pod's own flag
754 // is ANDed onto it inside.
755 cancel_active_children(&mut pods[k..], ctx);
756 }
757 flags
758}
759
760/// The keyed reconciler: match old↔new children by [`ChildKey`] so reorders and
761/// inserts preserve each surviving child's live widget (and thus its internal
762/// state) instead of rebuilding whatever happens to sit at the same index.
763///
764/// Matched children are *relocated* into the new order — their `ChildPod` (boxed
765/// widget + geometry) is moved, then rebuilt in place against its own previous
766/// view. Unmatched new keys are built fresh; unmatched old keys are torn down
767/// (with [`teardown_child`]'s cancel-if-active). A duplicate key (old or new)
768/// trips a `debug_assert` and falls back to the positional path.
769///
770/// **A key-matched child keeps its focus/capture across a move (Flutter's
771/// invariant).** A reorder or insert relocates a matched child's whole
772/// `ChildPod` — including its `focused`/`active` bookkeeping — so its recorded
773/// path stays valid: focus/capture routing scans for the pod by its flag
774/// ([`ChildPod::is_focused`]/[`is_active`](ChildPod::is_active)), so a `Key`/`Ime`
775/// event (or a captured `Move`/`Up`) still reaches the relocated child at its new
776/// index with nothing to update in a parent index. Only a child whose identity
777/// actually breaks loses its path: a torn-down (removed) key is cancelled in
778/// [`teardown_child`], and a key reused for a different concrete type (or whose
779/// enum child switched arms) is a swap (the old widget died inside the child's
780/// `rebuild`) whose stale `active`/`focused` flags are dropped inline below. A same-keys, same-order rebuild is likewise a
781/// content-only rebuild that leaves every path untouched.
782fn rebuild_children_keyed<State: 'static, C>(
783 prev: &[C],
784 next: &[C],
785 pods: &mut Vec<ChildPod>,
786 ctx: &mut BuildCtx<'_>,
787 view_of: impl Fn(&C) -> &AnyView<State>,
788 key_of: impl Fn(&C) -> Option<ChildKey>,
789) -> ChangeFlags {
790 // Old key -> old index, flagging any duplicate.
791 let mut old_by_key: HashMap<ChildKey, usize> = HashMap::with_capacity(prev.len());
792 let mut duplicate = false;
793 for (i, child) in prev.iter().enumerate() {
794 let key = key_of(child).expect("all-keyed list checked by caller");
795 if old_by_key.insert(key, i).is_some() {
796 duplicate = true;
797 }
798 }
799 // Duplicate new keys are equally ambiguous (two children claim one identity).
800 let mut seen_new: HashSet<ChildKey> = HashSet::with_capacity(next.len());
801 for child in next {
802 let key = key_of(child).expect("all-keyed list checked by caller");
803 if !seen_new.insert(key) {
804 duplicate = true;
805 }
806 }
807 if duplicate {
808 debug_assert!(
809 false,
810 "keyed child list has duplicate keys; falling back to positional reconciliation"
811 );
812 return rebuild_children_positional(prev, next, pods, ctx, view_of);
813 }
814
815 // Take ownership of the old pods so matched ones can be relocated by `take`.
816 let mut old_pods: Vec<Option<ChildPod>> = pods.drain(..).map(Some).collect();
817 let mut new_pods: Vec<ChildPod> = Vec::with_capacity(next.len());
818 let mut flags = ChangeFlags::NONE;
819
820 for child in next {
821 let key = key_of(child).expect("all-keyed list checked by caller");
822 if let Some(&old_index) = old_by_key.get(&key) {
823 let mut pod = old_pods[old_index]
824 .take()
825 .expect("each old key matches at most one new child (no duplicates)");
826 let (child_flags, child_swapped) =
827 rebuild_child_tracked(view_of(&prev[old_index]), view_of(child), &mut pod, ctx);
828 flags |= child_flags;
829 if child_swapped {
830 // A key reused for a different concrete type (or an enum arm
831 // swap): the old widget was torn down inside the child's
832 // rebuild, so its identity broke — drop
833 // the stale capture/focus paths (nothing armed to unwind). A
834 // key-matched non-swap relocates its pod (and thus its recorded
835 // focus/capture flags) intact, so no clearing happens there.
836 pod.set_active(false);
837 // The focused widget died inside the swap: the recorded focus
838 // path is severed, so the root's session goes with it — but only
839 // if this pod's link was on the LIVE chain; a stale flag under a
840 // blurred ancestor owns no session (see `mark_orphan_if_live` and
841 // `cancel_active_children`'s note).
842 let link_focused = pod.is_focused();
843 if link_focused {
844 pod.set_focused(false);
845 }
846 mark_orphan_if_live(ctx, link_focused);
847 }
848 new_pods.push(pod);
849 } else {
850 // A brand-new key: build a fresh child.
851 new_pods.push(build_child(view_of(child), ctx));
852 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
853 }
854 }
855
856 // Any old pod never taken is a removed key: tear it down (cancelling first if
857 // it held an in-flight capture, and dropping its focus flag with it).
858 for (old_index, slot) in old_pods.iter_mut().enumerate() {
859 if let Some(mut pod) = slot.take() {
860 teardown_child(view_of(&prev[old_index]), &mut pod, ctx);
861 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
862 }
863 }
864
865 *pods = new_pods;
866 flags
867}
868
869/// A field shape a container can hold children in, visitable in declaration
870/// order — the traversal half of the [`visit_children!`] seam.
871///
872/// Implemented for [`ChildPod`] and, generically, for `Option<T>`/`Vec<T>`/
873/// `[T]` over anything visitable, so the three shapes containers actually use
874/// (`child: ChildPod`, `icon: Option<ChildPod>`, `children: Vec<ChildPod>`) are
875/// covered by one impl each. A container holding pods inside its own row/slot
876/// struct implements this for that struct and stays on the same seam.
877pub trait VisitPods {
878 /// Hand each pod this value holds to `visitor`, in declaration order.
879 fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod));
880}
881
882impl VisitPods for ChildPod {
883 fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
884 visitor(self);
885 }
886}
887
888impl<T: VisitPods> VisitPods for Option<T> {
889 fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
890 if let Some(inner) = self {
891 inner.visit_pods(visitor);
892 }
893 }
894}
895
896impl<T: VisitPods> VisitPods for [T] {
897 fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
898 for item in self {
899 item.visit_pods(visitor);
900 }
901 }
902}
903
904impl<T: VisitPods> VisitPods for Vec<T> {
905 fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
906 self.as_slice().visit_pods(visitor);
907 }
908}
909
910/// Implement [`Widget::visit_children`](frust_core::Widget::visit_children) for
911/// the container whose `impl Widget` block this is written in, forwarding the
912/// named fields in the order given.
913///
914/// The read-only introspection seam that makes a container's retained subtree
915/// enumerable (`frust-core`'s `WidgetTree::inspect` is the consumer). One line
916/// per container, so the traversal lives here rather than being re-hand-written
917/// per widget:
918///
919/// ```
920/// # use frust_core::{BoxConstraints, ChildPod, LayoutCtx, PaintCtx, PaintScene, Widget};
921/// # use kurbo::Size;
922/// use frust_widgets::authoring::visit_children;
923///
924/// struct RowWidget {
925/// leading: Option<ChildPod>,
926/// children: Vec<ChildPod>,
927/// }
928///
929/// impl Widget for RowWidget {
930/// # fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { bc.max() }
931/// # fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
932/// // ...layout/paint/event/semantics as usual...
933/// visit_children!(leading, children);
934/// }
935/// ```
936///
937/// Each field's type only has to implement [`VisitPods`] — `ChildPod`,
938/// `Option<_>`, `Vec<_>`, or a container's own row struct. A widget whose
939/// children are reached some other way (a slot list behind an enum, a
940/// transition's stashed page) writes the method by hand instead; the contract is
941/// the same either way — visit every pod you own, in paint order, and nothing
942/// else.
943#[macro_export]
944macro_rules! visit_children {
945 ($($field:ident),* $(,)?) => {
946 fn visit_children(&self, visitor: &mut dyn FnMut(&$crate::authoring::ChildPod)) {
947 $( $crate::authoring::VisitPods::visit_pods(&self.$field, visitor); )*
948 }
949 };
950}
951
952pub use crate::visit_children;
953/// Re-exported so the [`visit_children!`] expansion can name the type without
954/// the call site importing it.
955pub use frust_core::ChildPod;
956
957/// Whether `event` is the phase that auto-releases a recorded capture
958/// (`Up`/`Cancel`) — shared by [`route_event`]/[`route_event_single`] and by
959/// [`OverlaySlot`](crate::overlay::OverlaySlot), which dispatches into its pod
960/// without going through either and owes the pod's `active` link the same
961/// lifetime every other container gives a captured child.
962///
963/// `frust-core`'s `component::route_child` keeps a copy of its own: that crate
964/// sits *below* this one, so it cannot name this function. It is the one
965/// remaining duplicate of this predicate in the workspace.
966pub(crate) fn releases_capture(event: &InputEvent) -> bool {
967 matches!(
968 event,
969 InputEvent::Pointer(p)
970 if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
971 )
972}
973
974/// Whether `p` carries a button that may begin a press.
975///
976/// A press/activation machine — a `pressed` visual, a pointer capture, an
977/// up-inside callback, a drag — starts on the **primary** button alone: the left
978/// mouse button, or any touch/pen contact (which every shell reports as
979/// [`PointerButton::Primary`] too). A secondary press is a context gesture: no
980/// baseline widget does anything with it, so it is left for a context-menu
981/// consumer instead of activating the control under the cursor.
982///
983/// Move/hover arms deliberately do **not** consult this: hover chrome and cursor
984/// shapes are position-driven, and a move carries no meaningful button. Neither
985/// do the focus-session arms — [`is_pointer_down`]'s blur-on-outside-tap, and a
986/// widget's own `request_focus`/IME publish — because the root reads *any* `Down`
987/// that bubbles no claim as a blur, whatever button carried it.
988///
989/// `frust_shadcn`'s `hit::presses` is the same predicate for that catalog; each
990/// catalog keeps its own copy rather than depending on a sibling crate.
991pub(crate) fn presses(p: &PointerEvent) -> bool {
992 p.button == PointerButton::Primary
993}
994
995/// Whether `event` is a pointer `Down` — the phase that both opens a capture and
996/// triggers blur-on-outside-tap evaluation in the routing helpers.
997fn is_pointer_down(event: &InputEvent) -> bool {
998 matches!(
999 event,
1000 InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
1001 )
1002}
1003
1004/// Route a pointer/scroll/keyboard/IME event — or a broadcast — to a
1005/// container's children.
1006///
1007/// **Broadcasts** ([`InputEvent::is_broadcast`], today
1008/// [`InputEvent::Housekeeping`]) are checked **first**, ahead of the capture,
1009/// focus, and hit-test branches: every child receives one unconditionally, in
1010/// order, and the container always reports [`EventResult::Ignored`] so no
1011/// first-handler-wins short-circuit can hide a subtree. This is what carries a
1012/// deferred, state-bearing callback flush (a navigator's queued `on_result`) to
1013/// wherever it was queued, on the frame that queued it, with no user input — see
1014/// [`InputEvent::Housekeeping`].
1015///
1016/// **Focus-routed events** ([`InputEvent::Key`]/[`InputEvent::Ime`]) bypass hit
1017/// testing entirely: they go straight to the child holding a focus link **on the
1018/// live session** ([`ChildPod::holds_live_focus`]), or are ignored if none does.
1019/// This is the focus mirror of the capture fast-path.
1020///
1021/// The liveness half is not a refinement, it is the question. Two children can
1022/// carry a recorded link at the same time — the blur sweep below runs on a
1023/// hit-tested `Down`, and a claim made from a broadcast (an overlay surface's
1024/// own press, which reaches no container's hit test) never triggers it — so a
1025/// container that answered "which child is focused?" with the first flagged one
1026/// delivered the keyboard by child order rather than by who holds the session.
1027/// A stamp comparison answers it by who actually claimed it last.
1028///
1029/// For **pointer/scroll**: a captured gesture goes straight to the recorded
1030/// active child (capture auto-releases on `Up`/`Cancel`). Otherwise the children
1031/// are hit-tested in reverse paint order — topmost (last-painted) first — and the
1032/// first child that both contains the point and reports [`EventResult::Handled`]
1033/// consumes it. This is the z-order convention Flex establishes and Stack shares.
1034///
1035/// **Blur-on-outside-tap:** a pointer `Down` that does not (re)establish focus on
1036/// the child it hits clears every focused child in this container. At the nearest
1037/// common ancestor of a stale focus branch and the tapped branch, this breaks the
1038/// recorded focus chain (a `Down` inside the still-focused child keeps it — that
1039/// child stays focused and is not cleared).
1040///
1041/// **Deeper stale flags below a cleared link are harmless by construction**, not
1042/// merely "corrected later". Nothing reads a `focused` flag on its own: every
1043/// consumer composes it with the chain above it, so a cleared link forces the
1044/// whole subtree below to read as unfocused. Focus-routed events stop at the
1045/// cleared link (this function finds no live-linked child to forward to); paint
1046/// ANDs the same way (`ChildPod::paint_child` composes the link, its stamp and
1047/// the chain); and the rebuild pass ANDs the same way too
1048/// ([`BuildCtx::has_focus`]), so tearing a stale branch down marks no orphan and
1049/// cannot release the session of the field that really is focused
1050/// ([`mark_orphan_if_live`]). The flags themselves are still cleaned up the next
1051/// time focus enters that subtree (a focus request re-records the whole chain).
1052pub fn route_event(
1053 children: &mut [ChildPod],
1054 ctx: &mut EventCtx<'_>,
1055 event: &InputEvent,
1056) -> EventResult {
1057 if event.is_broadcast() {
1058 for pod in children.iter_mut() {
1059 // Results are discarded on purpose: a broadcast is never consumed,
1060 // so every child gets it regardless of what an earlier one returned.
1061 pod.event_child(ctx, event);
1062 }
1063 return EventResult::Ignored;
1064 }
1065 if event.is_focus_routed() {
1066 if let Some(pod) = children.iter_mut().find(|p| p.holds_live_focus()) {
1067 return pod.event_child(ctx, event);
1068 }
1069 return EventResult::Ignored;
1070 }
1071 if let Some(pod) = children.iter_mut().find(|p| p.is_active()) {
1072 return route_event_single(pod, ctx, event);
1073 }
1074 let position = event.position();
1075 let mut handled = EventResult::Ignored;
1076 // The index of the hit child *if* it holds focus after dispatch — the one
1077 // focused child blur-on-Down must preserve.
1078 let mut kept_focus: Option<usize> = None;
1079 let n = children.len();
1080 for i in (0..n).rev() {
1081 if children[i].contains(position)
1082 && children[i].event_child(ctx, event) == EventResult::Handled
1083 {
1084 if children[i].holds_live_focus() {
1085 kept_focus = Some(i);
1086 }
1087 handled = EventResult::Handled;
1088 break;
1089 }
1090 }
1091 if is_pointer_down(event) {
1092 for (i, pod) in children.iter_mut().enumerate() {
1093 if Some(i) != kept_focus && pod.is_focused() {
1094 pod.set_focused(false);
1095 }
1096 }
1097 }
1098 handled
1099}
1100
1101/// Route a pointer/scroll event — or a broadcast — to a container's single
1102/// child.
1103///
1104/// Mirrors [`route_event`] for the one-child wrappers (`Padding`/`Align`/
1105/// `SizedBox`): a broadcast ([`InputEvent::is_broadcast`]) reaches the child
1106/// unconditionally and is never consumed (checked first, ahead of everything
1107/// else); a captured gesture is forwarded to `pod` unconditionally —
1108/// regardless of whether the event's position still falls within the child's
1109/// bounds — with the active path cleared on `Up`/`Cancel`; otherwise the child
1110/// only receives the event if it contains the point. Re-hit-testing
1111/// `pod.contains()` on every event instead of consulting [`ChildPod::is_active`]
1112/// is the bug this helper exists to prevent — see `ChildPod::contains`'s docs.
1113pub fn route_event_single(
1114 pod: &mut ChildPod,
1115 ctx: &mut EventCtx<'_>,
1116 event: &InputEvent,
1117) -> EventResult {
1118 if event.is_broadcast() {
1119 // Never consumed: forward, discard the result.
1120 pod.event_child(ctx, event);
1121 return EventResult::Ignored;
1122 }
1123 if event.is_focus_routed() {
1124 // Focus-routed events go to the child only if its recorded link belongs
1125 // to the live session — see [`route_event`] for why the liveness half is
1126 // load-bearing rather than cosmetic.
1127 if pod.holds_live_focus() {
1128 return pod.event_child(ctx, event);
1129 }
1130 return EventResult::Ignored;
1131 }
1132 if pod.is_active() {
1133 let result = pod.event_child(ctx, event);
1134 if releases_capture(event) {
1135 pod.set_active(false);
1136 }
1137 return result;
1138 }
1139 let inside = pod.contains(event.position());
1140 let result = if inside {
1141 pod.event_child(ctx, event)
1142 } else {
1143 EventResult::Ignored
1144 };
1145 // Blur-on-outside-tap: a `Down` that lands *outside* the (single) focused
1146 // child drops its recorded focus path. A `Down` inside the child keeps focus
1147 // (the child re-requests it, or simply stays the focused widget).
1148 if is_pointer_down(event) && !inside && pod.is_focused() {
1149 pod.set_focused(false);
1150 }
1151 result
1152}
1153
1154/// Compile-time proof that the whole authoring toolkit is reachable through the
1155/// **public** `crate::authoring::` path, not just the crate-root re-export the
1156/// in-crate call sites use.
1157///
1158/// Every item is named through `crate::authoring::…` and bound to an explicit
1159/// type, so an accidental re-privatization (or a signature change) fails the
1160/// build here rather than silently breaking a downstream design-system crate.
1161/// The module doc's worked example is the companion check: rustdoc compiles it as
1162/// a genuinely external crate.
1163#[cfg(test)]
1164mod authoring_surface_tests {
1165 use frust_core::{AnyView, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent};
1166
1167 #[test]
1168 // The point of every binding below is the spelled-out type: it is what makes
1169 // a re-privatization or a signature change fail the build here.
1170 #[allow(clippy::type_complexity)]
1171 fn every_promoted_item_is_reachable_through_the_authoring_path() {
1172 // Callback erasure: the two adapters, the typed input, the two erasers.
1173 let _erased: Option<crate::authoring::ErasedCallback> = None;
1174 let _erased_arg: Option<crate::authoring::ErasedArgCallback<f64>> = None;
1175 let _typed: Option<crate::authoring::TypedArgCallback<(), f64>> = None;
1176 let _erase: fn(&std::rc::Rc<dyn Fn(&mut ())>) -> crate::authoring::ErasedCallback =
1177 crate::authoring::erase_callback::<()>;
1178 let _erase_arg: fn(
1179 &crate::authoring::TypedArgCallback<(), f64>,
1180 ) -> crate::authoring::ErasedArgCallback<f64> =
1181 crate::authoring::erase_callback_arg::<(), f64>;
1182
1183 // Child plumbing: build/rebuild/teardown one child, reconcile many.
1184 let _build: fn(&AnyView<()>, &mut BuildCtx<'_>) -> ChildPod =
1185 crate::authoring::build_child::<(), AnyView<()>>;
1186 let _rebuild: fn(
1187 &AnyView<()>,
1188 &AnyView<()>,
1189 &mut ChildPod,
1190 &mut BuildCtx<'_>,
1191 ) -> ChangeFlags = crate::authoring::rebuild_child::<(), AnyView<()>>;
1192 let _teardown: fn(&AnyView<()>, &mut ChildPod, &mut BuildCtx<'_>) =
1193 crate::authoring::teardown_child::<(), AnyView<()>>;
1194 // `rebuild_children` takes `impl Fn` closures, which cannot be
1195 // turbofished into a `fn` pointer like the others — call it instead
1196 // (empty lists: the point is the path, not the reconciliation).
1197 let mut counter = 0u64;
1198 let mut build_ctx = BuildCtx::new(&mut counter);
1199 let mut pods: Vec<ChildPod> = Vec::new();
1200 let empty: Vec<AnyView<()>> = Vec::new();
1201 crate::authoring::rebuild_children(
1202 &empty,
1203 &empty,
1204 &mut pods,
1205 &mut build_ctx,
1206 |v: &AnyView<()>| v,
1207 |_: &AnyView<()>| None::<crate::ChildKey>,
1208 );
1209 assert!(pods.is_empty());
1210
1211 // Event routing: multi-child and single-child.
1212 let _route: fn(&mut [ChildPod], &mut EventCtx<'_>, &InputEvent) -> EventResult =
1213 crate::authoring::route_event;
1214 let _route_single: fn(&mut ChildPod, &mut EventCtx<'_>, &InputEvent) -> EventResult =
1215 crate::authoring::route_event_single;
1216
1217 // Themed text roles (color and family): each enum here, each builder
1218 // method on `TextView`.
1219 let _role: crate::authoring::ThemeTextColor = crate::authoring::ThemeTextColor::OnSurface;
1220 let _themed_role: fn(crate::TextView, crate::authoring::ThemeTextColor) -> crate::TextView =
1221 crate::TextView::themed_role;
1222 let _type: crate::authoring::ThemeTextType = crate::authoring::ThemeTextType::BodyLarge;
1223 let _themed_family: fn(
1224 crate::TextView,
1225 crate::authoring::ThemeTextType,
1226 ) -> crate::TextView = crate::TextView::themed_family;
1227
1228 // The shared press-overlay opacity.
1229 let _pressed: f32 = crate::authoring::PRESSED_OPACITY;
1230
1231 // The overlay portal: the placement math, the request it takes, the
1232 // slot an owner hosts its floated surface in, and the registry
1233 // vocabulary that slot is configured with.
1234 let _place: fn(
1235 kurbo::Rect,
1236 kurbo::Size,
1237 kurbo::Rect,
1238 crate::authoring::OverlayPlacement,
1239 ) -> kurbo::Rect = crate::authoring::place;
1240 let _side: crate::authoring::OverlaySide = crate::authoring::OverlaySide::Top;
1241 let _align: crate::authoring::OverlayAlign = crate::authoring::OverlayAlign::Center;
1242 let _anchor: crate::authoring::OverlayAnchor = crate::authoring::OverlayAnchor::Owner;
1243 let slot: crate::authoring::OverlaySlot<()> = crate::authoring::OverlaySlot::new();
1244 let _key: crate::authoring::OverlayKey = slot.key();
1245 let _band: crate::authoring::OverlayBand = crate::authoring::OverlayBand::Floating;
1246 let _input: crate::authoring::OverlayInput = crate::authoring::OverlayInput::Interactive;
1247 let _tap: crate::authoring::OutsideTap =
1248 crate::authoring::OutsideTap::Notify { consume: true };
1249
1250 // Child visitation: the trait, its three shape impls, and the macro
1251 // that writes the `Widget::visit_children` body from them.
1252 let _visit: fn(&ChildPod, &mut dyn FnMut(&ChildPod)) =
1253 <ChildPod as crate::authoring::VisitPods>::visit_pods;
1254 let _visit_opt: fn(&Option<ChildPod>, &mut dyn FnMut(&ChildPod)) =
1255 <Option<ChildPod> as crate::authoring::VisitPods>::visit_pods;
1256 let _visit_vec: fn(&Vec<ChildPod>, &mut dyn FnMut(&ChildPod)) =
1257 <Vec<ChildPod> as crate::authoring::VisitPods>::visit_pods;
1258 // The macro's own reachability is proven by its doc example, which
1259 // rustdoc compiles as a genuinely external crate.
1260 }
1261}
1262
1263/// Mechanism-level tests for [`rebuild_child`]'s type-swap capture *and focus*
1264/// handling, plus the orphan-marking sites of the multi-child reconcilers:
1265/// every single-child container (`Padding`/`Align`/
1266/// `SizedBox`, interactive widgets' labels) reconciles its child through this
1267/// helper, so the clear-on-swap behavior is proven once here at the shared
1268/// substrate, and each container's own test module (see `padding`/`align`/
1269/// `sized`) proves it end to end through real event routing/geometry.
1270#[cfg(test)]
1271mod tests {
1272 use super::*;
1273 use crate::test_support::{leaf, leaf_any, swap_leaf};
1274 use frust_core::{BoxConstraints, BuildCtx, LayoutCtx, PaintCtx, PaintScene, any};
1275 use kurbo::Size;
1276
1277 /// A context on the **live** focus chain — what a diff under a real focused
1278 /// root sees (`BuildCtx::new`'s "unknown, assume live" default).
1279 fn ctx(counter: &mut u64) -> BuildCtx<'_> {
1280 BuildCtx::new(counter)
1281 }
1282
1283 /// A context whose focus chain was broken by an ancestor: the diff of a
1284 /// subtree hanging below a link a container-routed blur already cleared.
1285 fn blurred_ctx(counter: &mut u64) -> BuildCtx<'_> {
1286 let mut ctx = BuildCtx::new(counter);
1287 ctx.set_has_focus(false);
1288 ctx
1289 }
1290
1291 /// A minimal multi-child container **view/widget pair** — the smallest thing
1292 /// that reconciles a child list through [`rebuild_children`] — so the focus
1293 /// chain can be exercised through genuine nesting (outer container → inner
1294 /// container → leaf) rather than one level of pods.
1295 struct NestView {
1296 children: Vec<AnyView<()>>,
1297 }
1298
1299 struct NestWidget {
1300 children: Vec<ChildPod>,
1301 }
1302
1303 impl View<()> for NestView {
1304 type Element = NestWidget;
1305
1306 fn build(&self, ctx: &mut BuildCtx<'_>) -> NestWidget {
1307 NestWidget {
1308 children: self.children.iter().map(|c| build_child(c, ctx)).collect(),
1309 }
1310 }
1311
1312 fn rebuild(
1313 &self,
1314 prev: &Self,
1315 element: &mut NestWidget,
1316 ctx: &mut BuildCtx<'_>,
1317 ) -> ChangeFlags {
1318 rebuild_children(
1319 &prev.children,
1320 &self.children,
1321 &mut element.children,
1322 ctx,
1323 |v: &AnyView<()>| v,
1324 |_: &AnyView<()>| None::<ChildKey>,
1325 )
1326 }
1327
1328 fn teardown(&self, element: &mut NestWidget, ctx: &mut BuildCtx<'_>) {
1329 for (view, pod) in self.children.iter().zip(element.children.iter_mut()) {
1330 teardown_child(view, pod, ctx);
1331 }
1332 }
1333 }
1334
1335 impl Widget for NestWidget {
1336 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1337 bc.constrain(Size::new(10.0, 10.0))
1338 }
1339 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1340 }
1341
1342 /// Reach the [`NestWidget`] inside a pod built from a [`NestView`] (double-boxed
1343 /// by [`build_child`]), so a test can set a pod flag deep in the tree.
1344 fn nest_in(pod: &mut ChildPod) -> &mut NestWidget {
1345 let boxed = pod
1346 .widget_mut()
1347 .downcast_mut::<Box<dyn Widget>>()
1348 .expect("pod holds a boxed AnyView widget");
1349 (**boxed)
1350 .downcast_mut::<NestWidget>()
1351 .expect("the boxed widget is the NestWidget")
1352 }
1353
1354 #[test]
1355 fn rebuild_child_clears_active_on_type_swap() {
1356 // A type swap (Leaf -> SwapLeaf, different concrete types) tears down
1357 // the old widget inside AnyView::rebuild and builds a fresh one in its
1358 // place; a still-active pod must have its stale capture dropped so a
1359 // later event isn't misrouted into the fresh widget.
1360 let mut counter = 0u64;
1361 let prev: AnyView<()> = leaf_any(10.0, 10.0);
1362 let mut pod = build_child(&prev, &mut ctx(&mut counter));
1363 pod.set_active(true);
1364
1365 let next: AnyView<()> = swap_leaf().into_any();
1366 rebuild_child::<(), _>(&prev, &next, &mut pod, &mut ctx(&mut counter));
1367
1368 assert!(
1369 !pod.is_active(),
1370 "a type-swapped child's stale capture must be dropped"
1371 );
1372 }
1373
1374 #[test]
1375 fn rebuild_child_preserves_active_on_same_type_rebuild() {
1376 // Negative guard against over-clearing: a same-type, content-only
1377 // rebuild (no AnyView type swap) must NOT touch an in-flight capture —
1378 // mirrors rebuild_children's content_only_rebuild_preserves_captured_drag.
1379 let mut counter = 0u64;
1380 let prev: AnyView<()> = leaf_any(10.0, 10.0);
1381 let mut pod = build_child(&prev, &mut ctx(&mut counter));
1382 pod.set_active(true);
1383
1384 let next: AnyView<()> = leaf_any(20.0, 20.0);
1385 rebuild_child::<(), _>(&prev, &next, &mut pod, &mut ctx(&mut counter));
1386
1387 assert!(
1388 pod.is_active(),
1389 "a content-only rebuild must not clear an in-flight capture"
1390 );
1391 }
1392
1393 /// [`rebuild_child`]'s type-swap marking site — the single-child wrappers
1394 /// (`Padding`/`Align`/`SizedBox`/`GestureDetector`/`Scroll`, the card/dialog
1395 /// slot helpers, `ListView`'s surviving rows) — pinned in both directions.
1396 /// `Padding(if editing { text_input } else { text })` is the shipped shape:
1397 /// the focused widget dies inside the swap, so a live session dies with it,
1398 /// while the same swap over a stale flag under a blurred ancestor owns no
1399 /// session to lose.
1400 #[test]
1401 fn a_wrapper_type_swap_marks_only_on_a_live_chain() {
1402 fn swap_wrapped_child(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
1403 let _ = frust_core::take_focus_orphaned();
1404 let mut counter = 0u64;
1405 let prev: AnyView<()> = leaf_any(10.0, 10.0);
1406 let mut pod = build_child(&prev, &mut ctx(&mut counter));
1407 // The wrapper's single child holds the recorded focus path.
1408 pod.set_focused(true);
1409
1410 // Leaf -> SwapLeaf: a concrete-type change, so the old widget dies
1411 // inside `AnyView::rebuild` and a fresh one takes its pod.
1412 let next: AnyView<()> = swap_leaf().into_any();
1413 rebuild_child::<(), _>(&prev, &next, &mut pod, &mut build(&mut counter));
1414 assert!(
1415 !pod.is_focused(),
1416 "a swapped-in widget must not inherit the old focus link"
1417 );
1418 frust_core::take_focus_orphaned()
1419 }
1420
1421 assert!(
1422 swap_wrapped_child(&mut ctx),
1423 "a live focus path dying inside a wrapper's type swap releases the session"
1424 );
1425 assert!(
1426 !swap_wrapped_child(&mut blurred_ctx),
1427 "the same swap over a stale flag below a cleared link marks nothing"
1428 );
1429 }
1430
1431 /// The negative guard for the site above: a same-type, content-only rebuild
1432 /// through a wrapper keeps the focus path (Flutter's retention invariant),
1433 /// so it must neither clear the flag nor mark — a mark here would drop the
1434 /// keyboard on every frame a padded field re-renders.
1435 #[test]
1436 fn a_wrapper_content_only_rebuild_keeps_focus_and_marks_nothing() {
1437 let _ = frust_core::take_focus_orphaned();
1438 let mut counter = 0u64;
1439 let prev: AnyView<()> = leaf_any(10.0, 10.0);
1440 let mut pod = build_child(&prev, &mut ctx(&mut counter));
1441 pod.set_focused(true);
1442
1443 let next: AnyView<()> = leaf_any(20.0, 20.0);
1444 rebuild_child::<(), _>(&prev, &next, &mut pod, &mut ctx(&mut counter));
1445
1446 assert!(
1447 pod.is_focused(),
1448 "a content-only rebuild must not clear the recorded focus path"
1449 );
1450 assert!(
1451 !frust_core::take_focus_orphaned(),
1452 "an unchanged wrapper child must not orphan the focus session"
1453 );
1454 }
1455
1456 /// The producer half of the generic-unmount focus release: a reconciler that
1457 /// severs a recorded focus path raises the orphan mark, which
1458 /// `RenderRoot::rebuild` drains into a full session release (the consumer
1459 /// half is pinned in `frust-core`'s
1460 /// `generic_unmount_orphan_releases_the_whole_session`).
1461 #[test]
1462 fn a_truncated_focused_child_marks_the_focus_orphan() {
1463 let _ = frust_core::take_focus_orphaned();
1464 let mut counter = 0u64;
1465 let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
1466 let mut pods: Vec<ChildPod> = prev
1467 .iter()
1468 .map(|v| build_child(v, &mut ctx(&mut counter)))
1469 .collect();
1470 // The second child holds the recorded focus path.
1471 pods[1].set_focused(true);
1472 assert!(
1473 !frust_core::take_focus_orphaned(),
1474 "building a list orphans nothing"
1475 );
1476
1477 // Shrink the list: the focused child is torn down and dropped.
1478 let next: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0)];
1479 rebuild_children(
1480 &prev,
1481 &next,
1482 &mut pods,
1483 &mut ctx(&mut counter),
1484 |v: &AnyView<()>| v,
1485 |_: &AnyView<()>| None::<ChildKey>,
1486 );
1487 assert_eq!(pods.len(), 1);
1488 assert!(
1489 frust_core::take_focus_orphaned(),
1490 "the focused child's unmount must be reported to the render root"
1491 );
1492 }
1493
1494 /// The cross-branch guard for the [`teardown_child`] marking site: the same
1495 /// truncation as above, but with the container's own link to the root already
1496 /// cleared (a blur moved focus to another branch, leaving this pod's flag
1497 /// stale). The stale pod owns no session, so its unmount must mark nothing —
1498 /// marking here is what dropped the keyboard out of an unrelated, still-live
1499 /// field when a list recycled a row.
1500 #[test]
1501 fn a_truncated_stale_focused_child_under_a_blurred_link_marks_nothing() {
1502 let _ = frust_core::take_focus_orphaned();
1503 let mut counter = 0u64;
1504 let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
1505 let mut pods: Vec<ChildPod> = prev
1506 .iter()
1507 .map(|v| build_child(v, &mut ctx(&mut counter)))
1508 .collect();
1509 // Stale: the flag is set, but the chain above this container is broken.
1510 pods[1].set_focused(true);
1511
1512 let next: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0)];
1513 rebuild_children(
1514 &prev,
1515 &next,
1516 &mut pods,
1517 &mut blurred_ctx(&mut counter),
1518 |v: &AnyView<()>| v,
1519 |_: &AnyView<()>| None::<ChildKey>,
1520 );
1521 assert_eq!(pods.len(), 1);
1522 assert!(
1523 !frust_core::take_focus_orphaned(),
1524 "a stale flag below a cleared link owns no session — releasing here \
1525 would kill the focus of whatever field really is focused"
1526 );
1527 }
1528
1529 /// The [`cancel_active_children`] marking site (the cleared tail past the
1530 /// first type swap), pinned in both directions: it marks on a live chain and
1531 /// stays silent under a blurred one.
1532 #[test]
1533 fn a_cleared_tail_marks_only_on_a_live_chain() {
1534 // Index 0 type-swaps (Leaf -> SwapLeaf), so the tail from 0 onward has
1535 // its recorded paths cleared; index 1 holds the focus flag.
1536 fn shrink_tail(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
1537 let _ = frust_core::take_focus_orphaned();
1538 let mut counter = 0u64;
1539 let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
1540 let mut pods: Vec<ChildPod> = prev
1541 .iter()
1542 .map(|v| build_child(v, &mut ctx(&mut counter)))
1543 .collect();
1544 pods[1].set_focused(true);
1545 let next: Vec<AnyView<()>> = vec![swap_leaf().into_any(), leaf_any(10.0, 10.0)];
1546 rebuild_children(
1547 &prev,
1548 &next,
1549 &mut pods,
1550 &mut build(&mut counter),
1551 |v: &AnyView<()>| v,
1552 |_: &AnyView<()>| None::<ChildKey>,
1553 );
1554 assert!(!pods[1].is_focused(), "the cleared tail drops the flag");
1555 frust_core::take_focus_orphaned()
1556 }
1557
1558 assert!(
1559 shrink_tail(&mut ctx),
1560 "a live focus link cleared by the tail sweep releases the session"
1561 );
1562 assert!(
1563 !shrink_tail(&mut blurred_ctx),
1564 "the same sweep over a stale flag below a cleared link marks nothing"
1565 );
1566 }
1567
1568 /// The keyed reconciler's type-swap marking site, pinned in both directions:
1569 /// a key reused for a different concrete type breaks identity, so a *live*
1570 /// focus link dies with it — but a stale one under a blurred ancestor does
1571 /// not.
1572 #[test]
1573 fn a_keyed_type_swap_marks_only_on_a_live_chain() {
1574 fn swap_keyed(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
1575 let _ = frust_core::take_focus_orphaned();
1576 let mut counter = 0u64;
1577 let key = ChildKey::new(1u32);
1578 let prev: Vec<(AnyView<()>, ChildKey)> = vec![(leaf_any(10.0, 10.0), key)];
1579 let mut pods: Vec<ChildPod> = prev
1580 .iter()
1581 .map(|c| build_child(&c.0, &mut ctx(&mut counter)))
1582 .collect();
1583 pods[0].set_focused(true);
1584 // Same key, different concrete view type: a swap, not a relocation.
1585 let next: Vec<(AnyView<()>, ChildKey)> = vec![(swap_leaf().into_any(), key)];
1586 rebuild_children(
1587 &prev,
1588 &next,
1589 &mut pods,
1590 &mut build(&mut counter),
1591 |c: &(AnyView<()>, ChildKey)| &c.0,
1592 |c: &(AnyView<()>, ChildKey)| Some(c.1),
1593 );
1594 assert!(!pods[0].is_focused(), "a swapped-away pod drops the flag");
1595 frust_core::take_focus_orphaned()
1596 }
1597
1598 assert!(
1599 swap_keyed(&mut ctx),
1600 "a live focus path dying inside a keyed swap releases the session"
1601 );
1602 assert!(
1603 !swap_keyed(&mut blurred_ctx),
1604 "a stale flag dying inside a keyed swap releases nothing"
1605 );
1606 }
1607
1608 /// The chain must compose through **nesting**, root → container → child: the
1609 /// blur clears the link at the nearest common ancestor (the outer container's
1610 /// pod), and the leaf's own flag two levels down stays stale. Tearing that
1611 /// leaf out must mark nothing — while the identical teardown with the outer
1612 /// link intact must still mark.
1613 #[test]
1614 fn the_focus_chain_composes_through_nested_containers() {
1615 fn drop_inner_leaf(outer_link_focused: bool) -> bool {
1616 let _ = frust_core::take_focus_orphaned();
1617 let mut counter = 0u64;
1618 let prev: Vec<AnyView<()>> = vec![any(NestView {
1619 children: vec![leaf_any(10.0, 10.0)],
1620 })];
1621 let mut pods: Vec<ChildPod> = prev
1622 .iter()
1623 .map(|v| build_child(v, &mut ctx(&mut counter)))
1624 .collect();
1625 // The leaf deep inside holds the focus flag either way; only the
1626 // OUTER link differs — that is the whole experiment.
1627 nest_in(&mut pods[0]).children[0].set_focused(true);
1628 pods[0].set_focused(outer_link_focused);
1629
1630 // The inner container loses its only child (a filtered/recycled row).
1631 let next: Vec<AnyView<()>> = vec![any(NestView { children: vec![] })];
1632 rebuild_children(
1633 &prev,
1634 &next,
1635 &mut pods,
1636 &mut ctx(&mut counter),
1637 |v: &AnyView<()>| v,
1638 |_: &AnyView<()>| None::<ChildKey>,
1639 );
1640 assert!(
1641 nest_in(&mut pods[0]).children.is_empty(),
1642 "the inner child was torn down"
1643 );
1644 frust_core::take_focus_orphaned()
1645 }
1646
1647 assert!(
1648 drop_inner_leaf(true),
1649 "an unbroken chain root→container→leaf is a live session; its unmount releases it"
1650 );
1651 assert!(
1652 !drop_inner_leaf(false),
1653 "a cleared link at the nearest common ancestor makes every flag below it inert"
1654 );
1655 }
1656
1657 /// The negative guard: an ordinary content-only rebuild keeps the focus path
1658 /// (Flutter's retention invariant) and must therefore orphan nothing — a mark
1659 /// raised here would drop the keyboard on every frame.
1660 #[test]
1661 fn a_content_only_rebuild_marks_no_focus_orphan() {
1662 let _ = frust_core::take_focus_orphaned();
1663 let mut counter = 0u64;
1664 let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
1665 let mut pods: Vec<ChildPod> = prev
1666 .iter()
1667 .map(|v| build_child(v, &mut ctx(&mut counter)))
1668 .collect();
1669 pods[1].set_focused(true);
1670
1671 let next: Vec<AnyView<()>> = vec![leaf_any(20.0, 20.0), leaf_any(20.0, 20.0)];
1672 rebuild_children(
1673 &prev,
1674 &next,
1675 &mut pods,
1676 &mut ctx(&mut counter),
1677 |v: &AnyView<()>| v,
1678 |_: &AnyView<()>| None::<ChildKey>,
1679 );
1680 assert!(pods[1].is_focused(), "the focus path survives intact");
1681 assert!(
1682 !frust_core::take_focus_orphaned(),
1683 "an unchanged child must not orphan the focus session"
1684 );
1685 }
1686
1687 /// An erased two-arm child for the list tests: `Leaf` on the left,
1688 /// `SwapLeaf` on the right. The `AnyView`'s concrete type is the same
1689 /// `Either` on both arms, so only the enum's own report reveals a swap.
1690 fn either_any(right: bool) -> AnyView<()> {
1691 type Arms = crate::Either<crate::test_support::Leaf, crate::test_support::SwapLeaf>;
1692 if right {
1693 any(Arms::Right(swap_leaf()))
1694 } else {
1695 any(Arms::Left(leaf(10.0, 10.0)))
1696 }
1697 }
1698
1699 /// The positional reconciler treats an `Either` arm swap at index `i` like an
1700 /// `AnyView` type swap there: the swapped slot's capture is dropped, the
1701 /// stable prefix before it keeps its paths, and every later pod's recorded
1702 /// capture/focus is cancelled.
1703 #[test]
1704 fn a_positional_either_arm_swap_cancels_from_its_index_on() {
1705 let _ = frust_core::take_focus_orphaned();
1706 let mut counter = 0u64;
1707 let prev: Vec<AnyView<()>> = vec![
1708 leaf_any(10.0, 10.0),
1709 either_any(false),
1710 leaf_any(10.0, 10.0),
1711 ];
1712 let mut pods: Vec<ChildPod> = prev
1713 .iter()
1714 .map(|v| build_child(v, &mut ctx(&mut counter)))
1715 .collect();
1716 pods[0].set_focused(true);
1717 pods[1].set_active(true);
1718 pods[2].set_active(true);
1719 pods[2].set_focused(true);
1720
1721 let next: Vec<AnyView<()>> =
1722 vec![leaf_any(10.0, 10.0), either_any(true), leaf_any(10.0, 10.0)];
1723 let flags = rebuild_children(
1724 &prev,
1725 &next,
1726 &mut pods,
1727 &mut ctx(&mut counter),
1728 |v: &AnyView<()>| v,
1729 |_: &AnyView<()>| None::<ChildKey>,
1730 );
1731 assert!(flags.needs_layout());
1732 assert!(
1733 pods[0].is_focused(),
1734 "the stable prefix keeps its focus path"
1735 );
1736 assert!(
1737 !pods[1].is_active(),
1738 "the swapped slot's capture is dropped"
1739 );
1740 assert!(!pods[2].is_active(), "a later active child is cancelled");
1741 assert!(!pods[2].is_focused(), "a later focused child is cleared");
1742 assert!(
1743 frust_core::take_focus_orphaned(),
1744 "the cleared tail severed a live focus path"
1745 );
1746 }
1747
1748 /// The keyed reconciler treats a key-matched `Either` arm swap like a key
1749 /// reused for a different concrete type: identity broke, so the pod's
1750 /// capture and focus links are dropped.
1751 #[test]
1752 fn a_keyed_either_arm_swap_clears_the_pod() {
1753 let _ = frust_core::take_focus_orphaned();
1754 let mut counter = 0u64;
1755 let key = ChildKey::new(1u32);
1756 let prev: Vec<(AnyView<()>, ChildKey)> = vec![(either_any(false), key)];
1757 let mut pods: Vec<ChildPod> = prev
1758 .iter()
1759 .map(|c| build_child(&c.0, &mut ctx(&mut counter)))
1760 .collect();
1761 pods[0].set_active(true);
1762 pods[0].set_focused(true);
1763
1764 let next: Vec<(AnyView<()>, ChildKey)> = vec![(either_any(true), key)];
1765 rebuild_children(
1766 &prev,
1767 &next,
1768 &mut pods,
1769 &mut ctx(&mut counter),
1770 |c: &(AnyView<()>, ChildKey)| &c.0,
1771 |c: &(AnyView<()>, ChildKey)| Some(c.1),
1772 );
1773 assert!(
1774 !pods[0].is_active(),
1775 "the arm-swapped pod's capture is dropped"
1776 );
1777 assert!(
1778 !pods[0].is_focused(),
1779 "the arm-swapped pod's focus is dropped"
1780 );
1781 assert!(frust_core::take_focus_orphaned());
1782
1783 // The same key on the same arm is a relocation-free content rebuild.
1784 pods[0].set_active(true);
1785 pods[0].set_focused(true);
1786 let again: Vec<(AnyView<()>, ChildKey)> = vec![(either_any(true), key)];
1787 rebuild_children(
1788 &next,
1789 &again,
1790 &mut pods,
1791 &mut ctx(&mut counter),
1792 |c: &(AnyView<()>, ChildKey)| &c.0,
1793 |c: &(AnyView<()>, ChildKey)| Some(c.1),
1794 );
1795 assert!(pods[0].is_active() && pods[0].is_focused());
1796 assert!(!frust_core::take_focus_orphaned());
1797 }
1798
1799 /// An `Either` swap inside a column-like container's child, inside an outer
1800 /// pod: the container's own reconciler consumes the report and clears the
1801 /// inner pod; the outer pod (whose widget, the container, survived) keeps
1802 /// its capture and focus.
1803 #[test]
1804 fn an_either_swap_inside_a_container_child_clears_only_the_inner_pod() {
1805 let _ = frust_core::take_focus_orphaned();
1806 let mut counter = 0u64;
1807 let prev: Vec<AnyView<()>> = vec![any(NestView {
1808 children: vec![either_any(false)],
1809 })];
1810 let mut pods: Vec<ChildPod> = prev
1811 .iter()
1812 .map(|v| build_child(v, &mut ctx(&mut counter)))
1813 .collect();
1814 pods[0].set_active(true);
1815 pods[0].set_focused(true);
1816 let inner = &mut nest_in(&mut pods[0]).children[0];
1817 inner.set_active(true);
1818 inner.set_focused(true);
1819
1820 let next: Vec<AnyView<()>> = vec![any(NestView {
1821 children: vec![either_any(true)],
1822 })];
1823 rebuild_children(
1824 &prev,
1825 &next,
1826 &mut pods,
1827 &mut ctx(&mut counter),
1828 |v: &AnyView<()>| v,
1829 |_: &AnyView<()>| None::<ChildKey>,
1830 );
1831 assert!(pods[0].is_active(), "the outer pod keeps its capture");
1832 assert!(pods[0].is_focused(), "the outer pod keeps its focus");
1833 let inner = &nest_in(&mut pods[0]).children[0];
1834 assert!(
1835 !inner.is_active(),
1836 "the inner swapped pod's capture is dropped"
1837 );
1838 assert!(
1839 !inner.is_focused(),
1840 "the inner swapped pod's focus is dropped"
1841 );
1842 assert!(frust_core::take_focus_orphaned());
1843 }
1844}
1845
1846/// Typed child slots: [`build_child`]/[`rebuild_child`]/[`teardown_child`]
1847/// driven with a concrete `V` through the hand-written `Wrap<V> { child: V }`
1848/// container — a concrete leaf (stored once, reconciled in place), an
1849/// [`Either`](crate::Either) / `EitherOf3` (an arm swap reported to the pod, so
1850/// it gets the `AnyView` swap semantics), and an [`AnyView`] (the documented
1851/// swap semantics, unchanged).
1852#[cfg(test)]
1853mod typed_child_tests {
1854 use std::cell::RefCell;
1855 use std::rc::Rc;
1856
1857 use super::*;
1858 use crate::test_support::{leaf_any, swap_leaf};
1859 use crate::{Either, EitherOf3};
1860 use frust_core::{
1861 BoxConstraints, DiscardScene, FrameTime, LayoutCtx, PaintCtx, PaintScene, RenderRoot,
1862 take_focus_orphaned,
1863 };
1864 use kurbo::Size;
1865
1866 type Log = Rc<RefCell<Vec<String>>>;
1867
1868 /// A one-child container generic over its child's view type — the shape a
1869 /// typed child slot exists for: no erasure anywhere on the child's path.
1870 struct Wrap<V> {
1871 child: V,
1872 }
1873
1874 struct WrapWidget {
1875 child: ChildPod,
1876 }
1877
1878 impl<State: 'static, V: View<State>> View<State> for Wrap<V> {
1879 type Element = WrapWidget;
1880
1881 fn build(&self, ctx: &mut BuildCtx<'_>) -> WrapWidget {
1882 WrapWidget {
1883 child: build_child(&self.child, ctx),
1884 }
1885 }
1886
1887 fn rebuild(
1888 &self,
1889 prev: &Self,
1890 element: &mut WrapWidget,
1891 ctx: &mut BuildCtx<'_>,
1892 ) -> ChangeFlags {
1893 rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
1894 }
1895
1896 fn teardown(&self, element: &mut WrapWidget, ctx: &mut BuildCtx<'_>) {
1897 teardown_child(&self.child, &mut element.child, ctx);
1898 }
1899 }
1900
1901 impl Widget for WrapWidget {
1902 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1903 self.child.layout_child(ctx, bc)
1904 }
1905
1906 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1907 self.child.paint_child(ctx, scene);
1908 }
1909
1910 crate::authoring::visit_children!(child);
1911 }
1912
1913 /// A fixed-size leaf that logs build / teardown / drop / paint under its tag,
1914 /// with a paint counter that only survives an in-place rebuild.
1915 struct Probe {
1916 tag: &'static str,
1917 log: Log,
1918 }
1919
1920 struct ProbeWidget {
1921 tag: &'static str,
1922 log: Log,
1923 painted: u32,
1924 }
1925
1926 impl Drop for ProbeWidget {
1927 fn drop(&mut self) {
1928 self.log.borrow_mut().push(format!("drop {}", self.tag));
1929 }
1930 }
1931
1932 impl View<()> for Probe {
1933 type Element = ProbeWidget;
1934
1935 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
1936 self.log.borrow_mut().push(format!("build {}", self.tag));
1937 ProbeWidget {
1938 tag: self.tag,
1939 log: Rc::clone(&self.log),
1940 painted: 0,
1941 }
1942 }
1943
1944 fn rebuild(
1945 &self,
1946 _prev: &Self,
1947 _element: &mut ProbeWidget,
1948 _ctx: &mut BuildCtx<'_>,
1949 ) -> ChangeFlags {
1950 ChangeFlags::NONE
1951 }
1952
1953 fn teardown(&self, _element: &mut ProbeWidget, _ctx: &mut BuildCtx<'_>) {
1954 self.log.borrow_mut().push(format!("teardown {}", self.tag));
1955 }
1956 }
1957
1958 impl Widget for ProbeWidget {
1959 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1960 bc.constrain(Size::new(10.0, 10.0))
1961 }
1962
1963 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
1964 self.painted += 1;
1965 self.log
1966 .borrow_mut()
1967 .push(format!("paint {} #{}", self.tag, self.painted));
1968 }
1969 }
1970
1971 fn probe(tag: &'static str, log: &Log) -> Probe {
1972 Probe {
1973 tag,
1974 log: Rc::clone(log),
1975 }
1976 }
1977
1978 fn new_log() -> Log {
1979 Rc::new(RefCell::new(Vec::new()))
1980 }
1981
1982 fn drain(log: &Log) -> Vec<String> {
1983 std::mem::take(&mut *log.borrow_mut())
1984 }
1985
1986 /// Reach the [`WrapWidget`] inside a pod built from a `Wrap<_>` — a concrete
1987 /// view, so the pod holds it boxed **once**.
1988 fn wrap_in(pod: &mut ChildPod) -> &mut WrapWidget {
1989 pod.widget_mut()
1990 .downcast_mut::<WrapWidget>()
1991 .expect("a concrete view's element is stored single-boxed")
1992 }
1993
1994 #[test]
1995 fn a_concrete_child_is_stored_once_and_rebuilt_in_place() {
1996 let log = new_log();
1997 let mut root: RenderRoot<(), Wrap<Probe>> = RenderRoot::new();
1998 let mut state = ();
1999 let mut logic = |_: &mut ()| Wrap {
2000 child: probe("a", &log),
2001 };
2002 root.rebuild(&mut logic, &mut state);
2003 root.layout(Size::new(100.0, 100.0));
2004 root.paint(&mut DiscardScene, FrameTime::from_nanos(0));
2005 root.paint(&mut DiscardScene, FrameTime::from_nanos(1));
2006 assert_eq!(drain(&log), ["build a", "paint a #1", "paint a #2"]);
2007
2008 // Same concrete child: reconciled in place, the paint counter survives.
2009 root.rebuild(&mut logic, &mut state);
2010 root.layout(Size::new(100.0, 100.0));
2011 root.paint(&mut DiscardScene, FrameTime::from_nanos(2));
2012 assert_eq!(drain(&log), ["paint a #3"]);
2013
2014 // Tooling sees the leaf's own type under the wrapper.
2015 let nodes = root.inspect();
2016 let child = nodes
2017 .iter()
2018 .find(|n| n.depth == 1)
2019 .expect("the wrapper publishes its child pod");
2020 assert!(
2021 child.type_name.ends_with("ProbeWidget"),
2022 "inspect reports the leaf's own type, got {}",
2023 child.type_name
2024 );
2025 }
2026
2027 #[test]
2028 fn a_concrete_child_pod_holds_the_element_single_boxed_and_tears_it_down() {
2029 let log = new_log();
2030 let mut counter = 0u64;
2031 let mut ctx = BuildCtx::new(&mut counter);
2032 let view = Wrap {
2033 child: probe("a", &log),
2034 };
2035 let mut pod = build_child(&view, &mut ctx);
2036 let wrap = wrap_in(&mut pod);
2037 assert!(
2038 wrap.child
2039 .widget_mut()
2040 .downcast_mut::<ProbeWidget>()
2041 .is_some(),
2042 "the leaf's element sits directly in its pod, not behind a second box"
2043 );
2044 assert!(wrap.child.type_name().ends_with("ProbeWidget"));
2045
2046 // A concrete rebuild never reports a swap.
2047 let next = Wrap {
2048 child: probe("a", &log),
2049 };
2050 let (flags, swapped) = rebuild_child_tracked(&view, &next, &mut pod, &mut ctx);
2051 assert_eq!(flags, ChangeFlags::NONE);
2052 assert!(!swapped, "a concrete child's element type cannot change");
2053
2054 teardown_child(&next, &mut pod, &mut ctx);
2055 assert_eq!(drain(&log), ["build a", "teardown a"]);
2056 }
2057
2058 #[test]
2059 fn an_either_child_swaps_arms_through_either() {
2060 let log = new_log();
2061 let mut counter = 0u64;
2062 let mut ctx = BuildCtx::new(&mut counter);
2063 let _ = take_focus_orphaned();
2064
2065 // A pod whose typed child is the `Either` itself.
2066 let a: Either<Probe, Probe> = Either::Left(probe("a", &log));
2067 let mut pod = build_child(&a, &mut ctx);
2068 assert_eq!(drain(&log), ["build a"]);
2069 // The pod holds the recorded focus path, so the chain into `Either`'s
2070 // rebuild is live.
2071 pod.set_focused(true);
2072
2073 let b: Either<Probe, Probe> = Either::Right(probe("b", &log));
2074 let (flags, swapped) = rebuild_child_tracked(&a, &b, &mut pod, &mut ctx);
2075 assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
2076 assert!(
2077 swapped,
2078 "both arms share one element type, but Either reports the arm swap \
2079 (BuildCtx::mark_element_replaced), so the pod sees a swap like an AnyView one"
2080 );
2081 assert_eq!(drain(&log), ["teardown a", "build b", "drop a"]);
2082 assert!(
2083 take_focus_orphaned(),
2084 "Either raises the focus orphan itself on a live chain"
2085 );
2086
2087 teardown_child(&b, &mut pod, &mut ctx);
2088 assert_eq!(drain(&log), ["teardown b"]);
2089 }
2090
2091 /// What `rebuild_child` left on a `Wrap<V>` after one rebuild of its typed
2092 /// child: the wrapper pod's own focus link, the child pod's capture and focus
2093 /// links, and whether the focus orphan was raised.
2094 struct WrapOutcome {
2095 flags: ChangeFlags,
2096 outer_focused: bool,
2097 child_active: bool,
2098 child_focused: bool,
2099 orphaned: bool,
2100 }
2101
2102 /// Rebuild `Wrap { child: prev }` into `Wrap { child: next }` through
2103 /// `rebuild_child`, with the child pod holding a capture and the focus path;
2104 /// `outer_focused` decides whether the wrapper's own link (and so the chain
2105 /// into the child) is live.
2106 fn rebuild_wrapped<V: View<()>>(prev: V, next: V, outer_focused: bool) -> WrapOutcome {
2107 let _ = take_focus_orphaned();
2108 let mut counter = 0u64;
2109 let mut ctx = BuildCtx::new(&mut counter);
2110 let prev = Wrap { child: prev };
2111 let next = Wrap { child: next };
2112 let mut pod = build_child(&prev, &mut ctx);
2113 pod.set_focused(outer_focused);
2114 let child = &mut wrap_in(&mut pod).child;
2115 child.set_active(true);
2116 child.set_focused(true);
2117
2118 let flags = rebuild_child(&prev, &next, &mut pod, &mut ctx);
2119 let orphaned = take_focus_orphaned();
2120 let outer_focused = pod.is_focused();
2121 let child = &wrap_in(&mut pod).child;
2122 WrapOutcome {
2123 flags,
2124 outer_focused,
2125 child_active: child.is_active(),
2126 child_focused: child.is_focused(),
2127 orphaned,
2128 }
2129 }
2130
2131 /// Assert the swap semantics an `AnyView` concrete-type swap gets
2132 /// (`an_erased_child_keeps_the_swap_semantics`), on both chains.
2133 fn assert_swap_semantics(live: WrapOutcome, blurred: WrapOutcome, what: &str) {
2134 assert_eq!(
2135 live.flags,
2136 ChangeFlags::LAYOUT | ChangeFlags::PAINT,
2137 "{what}"
2138 );
2139 assert!(
2140 live.outer_focused,
2141 "{what}: the wrapper itself was not swapped, so its own link stands"
2142 );
2143 assert!(
2144 !live.child_active,
2145 "{what}: the fresh arm never saw the Down, so the capture is dropped"
2146 );
2147 assert!(
2148 !live.child_focused,
2149 "{what}: the fresh arm never claimed focus, so the link is dropped"
2150 );
2151 assert!(
2152 live.orphaned,
2153 "{what}: a live focus path died inside the swap"
2154 );
2155
2156 assert!(
2157 !blurred.child_active,
2158 "{what}: dropped on a blurred chain too"
2159 );
2160 assert!(
2161 !blurred.child_focused,
2162 "{what}: dropped on a blurred chain too"
2163 );
2164 assert!(
2165 !blurred.orphaned,
2166 "{what}: the same swap below a cleared link owns no session to lose"
2167 );
2168 }
2169
2170 /// Assert a same-arm rebuild kept every recorded path and marked nothing.
2171 fn assert_kept(outcome: WrapOutcome, what: &str) {
2172 assert!(
2173 outcome.child_active,
2174 "{what}: the in-flight capture survives"
2175 );
2176 assert!(outcome.child_focused, "{what}: the focus path survives");
2177 assert!(outcome.outer_focused, "{what}: the wrapper's link survives");
2178 assert!(
2179 !outcome.orphaned,
2180 "{what}: an in-place rebuild orphans nothing"
2181 );
2182 }
2183
2184 #[test]
2185 fn an_either_arm_swap_drops_the_child_pod_capture_and_focus() {
2186 let log = new_log();
2187 let swap = |outer_focused| {
2188 rebuild_wrapped::<Either<Probe, Probe>>(
2189 Either::Left(probe("a", &log)),
2190 Either::Right(probe("b", &log)),
2191 outer_focused,
2192 )
2193 };
2194 assert_swap_semantics(swap(true), swap(false), "Either Left -> Right");
2195 }
2196
2197 #[test]
2198 fn an_either_of_3_arm_swap_drops_the_child_pod_capture_and_focus() {
2199 let log = new_log();
2200 let swap = |outer_focused| {
2201 rebuild_wrapped::<EitherOf3<Probe, Probe, Probe>>(
2202 EitherOf3::A(probe("a", &log)),
2203 EitherOf3::C(probe("c", &log)),
2204 outer_focused,
2205 )
2206 };
2207 assert_swap_semantics(swap(true), swap(false), "EitherOf3 A -> C");
2208 }
2209
2210 #[test]
2211 fn a_same_arm_rebuild_keeps_the_child_pod_capture_and_focus() {
2212 let log = new_log();
2213 let kept = rebuild_wrapped::<Either<Probe, Probe>>(
2214 Either::Left(probe("a", &log)),
2215 Either::Left(probe("a", &log)),
2216 true,
2217 );
2218 assert_eq!(kept.flags, ChangeFlags::NONE);
2219 assert_kept(kept, "Either Left -> Left");
2220
2221 let kept = rebuild_wrapped::<EitherOf3<Probe, Probe, Probe>>(
2222 EitherOf3::B(probe("b", &log)),
2223 EitherOf3::B(probe("b", &log)),
2224 true,
2225 );
2226 assert_eq!(kept.flags, ChangeFlags::NONE);
2227 assert_kept(kept, "EitherOf3 B -> B");
2228
2229 // The tracked rebuild itself reports no swap for a same-arm rebuild.
2230 let mut counter = 0u64;
2231 let mut ctx = BuildCtx::new(&mut counter);
2232 let a: Either<Probe, Probe> = Either::Left(probe("a", &log));
2233 let mut pod = build_child(&a, &mut ctx);
2234 let (_, swapped) = rebuild_child_tracked(&a, &a, &mut pod, &mut ctx);
2235 assert!(!swapped, "a same-arm rebuild keeps the widget");
2236 }
2237
2238 #[test]
2239 fn an_inline_nested_either_swap_reaches_the_enclosing_pod() {
2240 // `Either<Either<A, B>, C>`: the outer enum stays on `Left` and the inner
2241 // one swaps. No pod sits between them, so the inner report propagates
2242 // through the outer enum's same-arm rebuild to the pod holding both.
2243 let log = new_log();
2244 type Nested = Either<Either<Probe, Probe>, Probe>;
2245 let swap = |outer_focused| {
2246 rebuild_wrapped::<Nested>(
2247 Either::Left(Either::Left(probe("a", &log))),
2248 Either::Left(Either::Right(probe("b", &log))),
2249 outer_focused,
2250 )
2251 };
2252 assert_swap_semantics(swap(true), swap(false), "inline inner arm swap");
2253 }
2254
2255 #[test]
2256 fn an_either_swap_inside_a_nested_pod_clears_only_that_pod() {
2257 // `Wrap<Wrap<Either>>`: the Either sits in the INNER wrapper's pod, which
2258 // consumes its own report; the outer wrapper's child pod (holding the
2259 // inner wrapper, whose widget survived) keeps its flags.
2260 let log = new_log();
2261 let _ = take_focus_orphaned();
2262 let mut counter = 0u64;
2263 let mut ctx = BuildCtx::new(&mut counter);
2264 let prev: Wrap<Wrap<Either<Probe, Probe>>> = Wrap {
2265 child: Wrap {
2266 child: Either::Left(probe("a", &log)),
2267 },
2268 };
2269 let next: Wrap<Wrap<Either<Probe, Probe>>> = Wrap {
2270 child: Wrap {
2271 child: Either::Right(probe("b", &log)),
2272 },
2273 };
2274 let mut pod = build_child(&prev, &mut ctx);
2275 pod.set_focused(true);
2276 let outer_child = &mut wrap_in(&mut pod).child;
2277 outer_child.set_active(true);
2278 outer_child.set_focused(true);
2279 let inner_child = &mut wrap_in(outer_child).child;
2280 inner_child.set_active(true);
2281 inner_child.set_focused(true);
2282
2283 let (flags, swapped) = rebuild_child_tracked(&prev, &next, &mut pod, &mut ctx);
2284 assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
2285 assert!(
2286 !swapped,
2287 "the inner pod consumed the report; the outermost widget survived"
2288 );
2289 assert!(pod.is_focused());
2290 let outer_child = &mut wrap_in(&mut pod).child;
2291 assert!(
2292 outer_child.is_active(),
2293 "the outer child pod keeps its capture"
2294 );
2295 assert!(
2296 outer_child.is_focused(),
2297 "the outer child pod keeps its focus"
2298 );
2299 let inner_child = &wrap_in(outer_child).child;
2300 assert!(
2301 !inner_child.is_active(),
2302 "the swapped pod's capture is dropped"
2303 );
2304 assert!(
2305 !inner_child.is_focused(),
2306 "the swapped pod's focus is dropped"
2307 );
2308 assert!(
2309 take_focus_orphaned(),
2310 "a live focus path died inside the swap"
2311 );
2312 }
2313
2314 /// Swap `Wrap<AnyView<()>>`'s child Leaf -> SwapLeaf with a capture and the
2315 /// focus path recorded on the child pod; `outer_focused` decides whether the
2316 /// wrapper's own link (and so the chain into the child) is live.
2317 fn swap_an_erased_child(outer_focused: bool) -> (ChangeFlags, ChildPod, bool) {
2318 let _ = take_focus_orphaned();
2319 let mut counter = 0u64;
2320 let mut ctx = BuildCtx::new(&mut counter);
2321 let prev: Wrap<AnyView<()>> = Wrap {
2322 child: leaf_any(10.0, 10.0),
2323 };
2324 let mut pod = build_child(&prev, &mut ctx);
2325 pod.set_focused(outer_focused);
2326 let inner = &mut wrap_in(&mut pod).child;
2327 inner.set_active(true);
2328 inner.set_focused(true);
2329
2330 let next: Wrap<AnyView<()>> = Wrap {
2331 child: swap_leaf().into_any(),
2332 };
2333 let flags = rebuild_child(&prev, &next, &mut pod, &mut ctx);
2334 (flags, pod, take_focus_orphaned())
2335 }
2336
2337 #[test]
2338 fn an_erased_child_keeps_the_swap_semantics() {
2339 let (flags, mut pod, orphaned) = swap_an_erased_child(true);
2340 assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
2341 assert!(
2342 pod.is_focused(),
2343 "the wrapper itself was not swapped, so its own link stands"
2344 );
2345 let inner = &wrap_in(&mut pod).child;
2346 assert!(!inner.is_active(), "the swapped child's capture is dropped");
2347 assert!(
2348 !inner.is_focused(),
2349 "the swapped child's focus link is dropped"
2350 );
2351 assert!(orphaned, "a live focus path died inside the swap");
2352
2353 let (flags, mut pod, orphaned) = swap_an_erased_child(false);
2354 assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
2355 assert!(!wrap_in(&mut pod).child.is_active());
2356 assert!(
2357 !orphaned,
2358 "the same swap below a cleared link owns no session to lose"
2359 );
2360 }
2361}
2362
2363/// Focus-path routing tests for [`route_event`]/[`route_event_single`]: Key/IME
2364/// events reach only the focused child (including through a nested container),
2365/// blur-on-outside-tap breaks the chain, a second focus request moves focus,
2366/// `ApplyEditingState` routes to the focused child, a published IME surface
2367/// bubbles up, and the capture and focus paths stay independent.
2368#[cfg(test)]
2369mod focus_tests {
2370 use super::*;
2371 use frust_core::{
2372 BoxConstraints, EditingState, EventCtx, ImeEvent, ImeState, Key, KeyEvent, LayoutCtx,
2373 Modifiers, PaintCtx, PaintScene,
2374 };
2375 use kurbo::{Point, Rect, Size};
2376
2377 /// A leaf that: on a pointer `Down` optionally requests focus and records
2378 /// `id + 100`; on a `Move` (capture path) records `id + 200`; on a Key/IME
2379 /// event records `id` and optionally publishes an IME surface. All state is a
2380 /// shared `Vec<u32>` so tests can assert *which* leaf saw *what*.
2381 struct KeyLeaf {
2382 id: u32,
2383 takes_focus: bool,
2384 publish_ime: bool,
2385 }
2386
2387 impl Widget for KeyLeaf {
2388 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2389 bc.max()
2390 }
2391 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2392 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2393 match event {
2394 InputEvent::Pointer(p) => match p.phase {
2395 PointerPhase::Down => {
2396 if self.takes_focus {
2397 ctx.request_focus();
2398 }
2399 ctx.state_mut::<Vec<u32>>().push(self.id + 100);
2400 EventResult::Handled
2401 }
2402 PointerPhase::Move => {
2403 ctx.state_mut::<Vec<u32>>().push(self.id + 200);
2404 EventResult::Handled
2405 }
2406 _ => EventResult::Handled,
2407 },
2408 InputEvent::Key(_) | InputEvent::Ime(_) => {
2409 ctx.state_mut::<Vec<u32>>().push(self.id);
2410 if self.publish_ime {
2411 ctx.publish_ime_state(ImeState {
2412 active: true,
2413 editing: EditingState {
2414 text: format!("leaf{}", self.id),
2415 selection_base: 1,
2416 selection_extent: 1,
2417 composing_base: -1,
2418 composing_extent: -1,
2419 },
2420 caret: Some(Rect::new(0.0, 0.0, 1.0, 10.0)),
2421 content_type: Default::default(),
2422 suppress_soft_keyboard: false,
2423 });
2424 }
2425 EventResult::Handled
2426 }
2427 _ => EventResult::Ignored,
2428 }
2429 }
2430 }
2431
2432 /// A minimal multi-child container: stacks its children vertically (each 10
2433 /// tall at the given width) and routes events through [`route_event`] — the
2434 /// same helper the real `Flex`/`Stack` use — so focus routing is exercised
2435 /// through a nested container.
2436 struct Nest {
2437 children: Vec<ChildPod>,
2438 }
2439
2440 impl Widget for Nest {
2441 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2442 let w = bc.max().width;
2443 for (i, pod) in self.children.iter_mut().enumerate() {
2444 pod.set_origin(Point::new(0.0, i as f64 * 10.0));
2445 pod.layout_child(ctx, &BoxConstraints::tight(Size::new(w, 10.0)));
2446 }
2447 Size::new(w, self.children.len() as f64 * 10.0)
2448 }
2449 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2450 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2451 route_event(&mut self.children, ctx, event)
2452 }
2453 }
2454
2455 fn leaf_pod(id: u32, takes_focus: bool, publish_ime: bool) -> ChildPod {
2456 ChildPod::new(Box::new(KeyLeaf {
2457 id,
2458 takes_focus,
2459 publish_ime,
2460 }))
2461 }
2462
2463 /// Lay out a slice of pods vertically (10 tall each at width 100) so hit
2464 /// testing distinguishes them: child `i` occupies `y ∈ [i*10, i*10+10)`.
2465 fn lay_out_vertically(pods: &mut [ChildPod]) {
2466 let mut lctx = LayoutCtx::new();
2467 for (i, pod) in pods.iter_mut().enumerate() {
2468 pod.set_origin(Point::new(0.0, i as f64 * 10.0));
2469 pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 10.0)));
2470 }
2471 }
2472
2473 fn down(y: f64) -> InputEvent {
2474 InputEvent::Pointer(PointerEvent {
2475 phase: PointerPhase::Down,
2476 position: Point::new(5.0, y),
2477 button: PointerButton::Primary,
2478 })
2479 }
2480
2481 fn mv(y: f64) -> InputEvent {
2482 InputEvent::Pointer(PointerEvent {
2483 phase: PointerPhase::Move,
2484 position: Point::new(5.0, y),
2485 button: PointerButton::Primary,
2486 })
2487 }
2488
2489 fn key() -> InputEvent {
2490 InputEvent::Key(KeyEvent {
2491 key: Key::Character("a".to_string()),
2492 modifiers: Modifiers::default(),
2493 repeat: false,
2494 })
2495 }
2496
2497 fn ime_apply() -> InputEvent {
2498 InputEvent::Ime(ImeEvent::ApplyEditingState(EditingState {
2499 text: "sync".to_string(),
2500 selection_base: 4,
2501 selection_extent: 4,
2502 composing_base: -1,
2503 composing_extent: -1,
2504 }))
2505 }
2506
2507 // `state` must be a concrete `&mut Vec<u32>`: it is erased to `&mut dyn Any`
2508 // and the leaves recover it with `state_mut::<Vec<u32>>()`, so a slice would
2509 // fail the downcast.
2510 #[allow(clippy::ptr_arg)]
2511 fn route(children: &mut [ChildPod], state: &mut Vec<u32>, event: &InputEvent) -> EventResult {
2512 let mut ctx = EventCtx::new(state, Point::ZERO, Size::new(100.0, 100.0));
2513 route_event(children, &mut ctx, event)
2514 }
2515
2516 #[test]
2517 fn key_routes_only_to_focused_child() {
2518 let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
2519 lay_out_vertically(&mut children);
2520 let mut state = Vec::new();
2521
2522 // Tap child 2 (y in [10,20)) → it requests focus.
2523 route(&mut children, &mut state, &down(15.0));
2524 assert!(children[1].is_focused());
2525 assert!(!children[0].is_focused());
2526 state.clear();
2527
2528 // A Key event reaches ONLY the focused child (id 2), never child 1.
2529 route(&mut children, &mut state, &key());
2530 assert_eq!(state, vec![2]);
2531 }
2532
2533 #[test]
2534 fn key_reaches_focused_leaf_through_nested_container() {
2535 // Outer container holds one Nest; the Nest holds two leaves.
2536 let inner = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
2537 let mut nest = Nest { children: inner };
2538 // Lay out the nest so its inner children get real geometry.
2539 {
2540 let mut lctx = LayoutCtx::new();
2541 nest.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 20.0)));
2542 }
2543 let mut outer = vec![ChildPod::new(Box::new(nest))];
2544 outer[0].set_origin(Point::ZERO);
2545 {
2546 let mut lctx = LayoutCtx::new();
2547 outer[0].layout_child(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 20.0)));
2548 }
2549 let mut state = Vec::new();
2550
2551 // Tap the second inner leaf (y in [10,20)) → focus chain nest→leaf2.
2552 route(&mut outer, &mut state, &down(15.0));
2553 assert!(outer[0].is_focused(), "the nest records the focus path");
2554 state.clear();
2555
2556 // Key routes outer→nest→leaf2 only.
2557 route(&mut outer, &mut state, &key());
2558 assert_eq!(state, vec![2]);
2559 }
2560
2561 #[test]
2562 fn blur_on_outside_tap_clears_focus_chain() {
2563 // Child 1 takes focus; child 2 does NOT (a non-editable widget).
2564 let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, false, false)];
2565 lay_out_vertically(&mut children);
2566 let mut state = Vec::new();
2567
2568 route(&mut children, &mut state, &down(5.0)); // focus child 1
2569 assert!(children[0].is_focused());
2570
2571 // Tap child 2 (does not take focus) → blur clears the focus chain.
2572 route(&mut children, &mut state, &down(15.0));
2573 assert!(
2574 !children[0].is_focused(),
2575 "outside tap blurs the focused child"
2576 );
2577 assert!(!children[1].is_focused());
2578 state.clear();
2579
2580 // A Key event now reaches nobody.
2581 let result = route(&mut children, &mut state, &key());
2582 assert_eq!(result, EventResult::Ignored);
2583 assert!(state.is_empty());
2584 }
2585
2586 #[test]
2587 fn tap_inside_focused_widget_keeps_focus() {
2588 // A focused widget re-tapped keeps focus (Down inside focused widget).
2589 let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, false, false)];
2590 lay_out_vertically(&mut children);
2591 let mut state = Vec::new();
2592
2593 route(&mut children, &mut state, &down(5.0)); // focus child 1
2594 route(&mut children, &mut state, &down(5.0)); // tap child 1 again
2595 assert!(
2596 children[0].is_focused(),
2597 "a tap inside the focused widget keeps focus"
2598 );
2599 state.clear();
2600 route(&mut children, &mut state, &key());
2601 assert_eq!(state, vec![1]);
2602 }
2603
2604 #[test]
2605 fn second_focus_request_moves_focus() {
2606 let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
2607 lay_out_vertically(&mut children);
2608 let mut state = Vec::new();
2609
2610 route(&mut children, &mut state, &down(5.0)); // focus child 1
2611 assert!(children[0].is_focused());
2612 route(&mut children, &mut state, &down(15.0)); // focus child 2
2613 assert!(children[1].is_focused());
2614 assert!(!children[0].is_focused(), "focus moved off child 1");
2615 state.clear();
2616
2617 route(&mut children, &mut state, &key());
2618 assert_eq!(
2619 state,
2620 vec![2],
2621 "key now reaches only the newly focused child"
2622 );
2623 }
2624
2625 #[test]
2626 fn ime_apply_routes_to_focused_child() {
2627 let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
2628 lay_out_vertically(&mut children);
2629 let mut state = Vec::new();
2630
2631 route(&mut children, &mut state, &down(15.0)); // focus child 2
2632 state.clear();
2633 route(&mut children, &mut state, &ime_apply());
2634 assert_eq!(state, vec![2]);
2635 }
2636
2637 #[test]
2638 fn capture_and_focus_paths_are_independent() {
2639 // Child 1 holds the capture (active) path; child 2 holds the focus path.
2640 let mut children = vec![leaf_pod(1, false, false), leaf_pod(2, false, false)];
2641 lay_out_vertically(&mut children);
2642 children[0].set_active(true);
2643 children[1].set_focused(true);
2644 let mut state = Vec::new();
2645
2646 // A Move routes down the capture path → child 1 only (id+200).
2647 route(&mut children, &mut state, &mv(999.0));
2648 assert_eq!(state, vec![201]);
2649 state.clear();
2650
2651 // A Key routes down the focus path → child 2 only (id).
2652 route(&mut children, &mut state, &key());
2653 assert_eq!(state, vec![2]);
2654
2655 // Both paths survive intact.
2656 assert!(children[0].is_active());
2657 assert!(children[1].is_focused());
2658 }
2659}
2660
2661/// The layout-skip dirty source of the reconcilers: a child whose rebuild
2662/// reports `LAYOUT` (or swaps its widget) leaves its pod layout-dirty; a
2663/// `PAINT`-only or no-op rebuild leaves it clean.
2664#[cfg(test)]
2665mod layout_skip_tests {
2666 use super::*;
2667 use frust_core::{BoxConstraints, BuildCtx, LayoutCtx, PaintCtx, PaintScene, any};
2668 use kurbo::Size;
2669
2670 /// A leaf whose rebuild reports exactly the flags its view carries.
2671 struct FlagView(ChangeFlags);
2672
2673 struct FlagWidget;
2674
2675 impl View<()> for FlagView {
2676 type Element = FlagWidget;
2677
2678 fn build(&self, _ctx: &mut BuildCtx<'_>) -> FlagWidget {
2679 FlagWidget
2680 }
2681
2682 fn rebuild(
2683 &self,
2684 _prev: &Self,
2685 _element: &mut FlagWidget,
2686 _ctx: &mut BuildCtx<'_>,
2687 ) -> ChangeFlags {
2688 self.0
2689 }
2690 }
2691
2692 impl Widget for FlagWidget {
2693 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2694 bc.constrain(Size::new(10.0, 10.0))
2695 }
2696 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2697 }
2698
2699 /// A different concrete type, for a swap.
2700 struct OtherView;
2701
2702 impl View<()> for OtherView {
2703 type Element = FlagWidget;
2704
2705 fn build(&self, _ctx: &mut BuildCtx<'_>) -> FlagWidget {
2706 FlagWidget
2707 }
2708
2709 fn rebuild(
2710 &self,
2711 _prev: &Self,
2712 _element: &mut FlagWidget,
2713 _ctx: &mut BuildCtx<'_>,
2714 ) -> ChangeFlags {
2715 ChangeFlags::NONE
2716 }
2717 }
2718
2719 /// Lay `pod` out once through a plain context, which clears its dirty flag.
2720 fn clean(pod: &mut ChildPod) {
2721 pod.layout_child(
2722 &mut LayoutCtx::new(),
2723 &BoxConstraints::loose(Size::new(50.0, 50.0)),
2724 );
2725 assert!(!pod.is_layout_dirty());
2726 }
2727
2728 #[test]
2729 fn rebuild_child_marks_the_pod_on_layout_only() {
2730 let mut counter = 0;
2731 let mut ctx = BuildCtx::new(&mut counter);
2732 let mut pod = build_child(&FlagView(ChangeFlags::NONE), &mut ctx);
2733 assert!(pod.is_layout_dirty(), "a freshly built pod starts dirty");
2734 clean(&mut pod);
2735
2736 let none = FlagView(ChangeFlags::NONE);
2737 let paint = FlagView(ChangeFlags::PAINT);
2738 let layout = FlagView(ChangeFlags::LAYOUT);
2739 rebuild_child(&none, &none, &mut pod, &mut ctx);
2740 assert!(!pod.is_layout_dirty(), "a no-op rebuild leaves it clean");
2741 rebuild_child(&none, &paint, &mut pod, &mut ctx);
2742 assert!(!pod.is_layout_dirty(), "PAINT alone leaves it clean");
2743 rebuild_child(&paint, &layout, &mut pod, &mut ctx);
2744 assert!(pod.is_layout_dirty(), "LAYOUT dirties the pod");
2745 }
2746
2747 #[test]
2748 fn rebuild_child_marks_paint_only_on_a_paint_rebuild() {
2749 use frust_core::DiscardScene;
2750 let mut counter = 0;
2751 let mut ctx = BuildCtx::new(&mut counter);
2752 let mut pod = build_child(&FlagView(ChangeFlags::NONE), &mut ctx);
2753 clean(&mut pod);
2754 pod.paint_child(
2755 &mut PaintCtx::new(kurbo::Point::ZERO, Size::new(50.0, 50.0)),
2756 &mut DiscardScene,
2757 );
2758 assert!(!pod.is_paint_dirty(), "a painted pod is paint-clean");
2759
2760 let none = FlagView(ChangeFlags::NONE);
2761 let paint = FlagView(ChangeFlags::PAINT);
2762 let layout = FlagView(ChangeFlags::LAYOUT);
2763 rebuild_child(&none, &none, &mut pod, &mut ctx);
2764 assert!(!pod.is_paint_dirty(), "a no-op rebuild leaves paint clean");
2765 rebuild_child(&none, &paint, &mut pod, &mut ctx);
2766 assert!(pod.is_paint_dirty(), "PAINT dirties paint");
2767 assert!(!pod.is_layout_dirty(), "PAINT leaves layout clean");
2768
2769 clean(&mut pod);
2770 pod.paint_child(
2771 &mut PaintCtx::new(kurbo::Point::ZERO, Size::new(50.0, 50.0)),
2772 &mut DiscardScene,
2773 );
2774 rebuild_child(&paint, &layout, &mut pod, &mut ctx);
2775 assert!(pod.is_layout_dirty() && pod.is_paint_dirty());
2776 }
2777
2778 #[test]
2779 fn rebuild_children_marks_only_the_children_reporting_layout() {
2780 let mut counter = 0;
2781 let mut ctx = BuildCtx::new(&mut counter);
2782 let prev: Vec<AnyView<()>> = vec![
2783 any(FlagView(ChangeFlags::NONE)),
2784 any(FlagView(ChangeFlags::NONE)),
2785 any(FlagView(ChangeFlags::NONE)),
2786 ];
2787 let mut pods: Vec<ChildPod> = prev.iter().map(|v| build_child(v, &mut ctx)).collect();
2788 pods.iter_mut().for_each(clean);
2789
2790 let next: Vec<AnyView<()>> = vec![
2791 any(FlagView(ChangeFlags::PAINT)),
2792 any(FlagView(ChangeFlags::LAYOUT)),
2793 // A concrete-type swap: a fresh widget, laid out from scratch.
2794 any(OtherView),
2795 ];
2796 let flags = rebuild_children(
2797 &prev,
2798 &next,
2799 &mut pods,
2800 &mut ctx,
2801 |v: &AnyView<()>| v,
2802 |_: &AnyView<()>| None::<ChildKey>,
2803 );
2804 assert!(flags.contains(ChangeFlags::LAYOUT));
2805 let dirty: Vec<bool> = pods.iter().map(ChildPod::is_layout_dirty).collect();
2806 assert_eq!(dirty, [false, true, true]);
2807 }
2808}