Skip to main content

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}