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