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 [`AnyView`] child, and [`rebuild_children`] for a `Vec` of them
12//!   (positional or [`ChildKey`]-keyed reconciliation, with the focus/capture
13//!   retention rules a hand-rolled diff gets wrong).
14//! - **Event routing**: [`route_event`] (multi-child containers, reverse paint
15//!   order + capture/focus fast paths) and [`route_event_single`] (one-child
16//!   wrappers).
17//! - **Callback erasure**: [`erase_callback`]/[`erase_callback_arg`], turning a
18//!   view-held `Rc<dyn Fn(&mut State)>` into the [`ErasedCallback`]/
19//!   [`ErasedArgCallback`] adapter a non-generic widget holds, so the retained
20//!   widget never becomes generic over the application-state type.
21//! - **Child visitation**: [`visit_children!`] over [`VisitPods`], writing a
22//!   container's [`Widget::visit_children`] body from its child fields — the
23//!   read-only seam that lets tooling walk into a container's retained subtree
24//!   (`frust-core`'s `WidgetTree::inspect`). One line per container; a leaf
25//!   needs nothing.
26//! - **Themed text roles**: [`ThemeTextColor`] (re-exported here) and
27//!   [`TextView::themed_role`](crate::TextView::themed_role) — how a widget labels
28//!   a child [`text`](crate::text) run with the themed color role it should
29//!   default to, instead of hardcoding a color; and [`ThemeTextType`]
30//!   (re-exported here) with
31//!   [`TextView::themed_family`](crate::TextView::themed_family) — how it opts
32//!   the run's font family into the active theme's type-scale role, resolved
33//!   at layout so a live theme swap re-shapes it, instead of shaping in
34//!   `TextStyle::default()`'s system UI font.
35//!
36//! # Hover: claim it, latch it, then correct it at paint time
37//!
38//! There is no hover phase and no Enter/Leave event. A widget that wants hover
39//! chrome opts in with three things, each covering a case the others cannot:
40//!
41//! - In its **uncaptured** [`PointerPhase::Move`] arm, hit-test the event position
42//!   against its own bounds and call
43//!   [`EventCtx::claim_hover`](frust_core::EventCtx::claim_hover) when it is
44//!   inside — on *every* such move, not just on entry (the claim is per-pass, not
45//!   sticky).
46//! - Latch that same hit test into an internal hover flag, and gate
47//!   `request_redraw` on the flag actually *changing*. This is the frame source
48//!   for hover gain (and for the link moving from a sibling onto this widget):
49//!   `claim_hover` asks for no redraw, and the pipeline manufactures one only when
50//!   a hover ends with nothing taking it. Gating on the change is also what keeps
51//!   a pointer wandering inside one widget from repainting per event.
52//! - In `paint`, read
53//!   [`PaintCtx::is_hovered`](frust_core::PaintCtx::is_hovered) as the
54//!   authoritative value and self-correct the flag from it. This is not optional
55//!   belt-and-braces: a pointer leaving the widget routes its next move onto
56//!   whatever it moved *onto*, so the widget it left never gets an event saying so.
57//!
58//! Everything else is the pipeline's job. `frust-core` stamps the claim down the
59//! pod chain and strands the previous claimant by advancing a hover epoch, so a
60//! container needs no hover bookkeeping at all — [`route_event`] and
61//! [`route_event_single`] carry it for free, and a hand-rolled router that
62//! forwards through [`ChildPod::event_child`] does too. Because the claim is
63//! recorded as a *path*, an enclosing container reads hovered while the pointer is
64//! over a claiming child (CSS `:hover` semantics), and every off-path widget reads
65//! `false`.
66//!
67//! A container that wants hover chrome of its **own** has one extra rule: it
68//! claims *after* routing the move to its children, never before. Only one claim
69//! per pass is recorded and the first one recorded wins, so an ancestor that claims
70//! before it forwards makes every descendant ineligible for the pass — the child
71//! under the pointer never reads hovered, while its latched flag keeps flipping and
72//! asking for a frame on every move. Claiming after routing makes the container's
73//! claim a fallback: a child's claim wins and the container still reads hovered
74//! through the path, and when no child claims the container's own claim is what
75//! records.
76//!
77//! A **captured** pointer can never create hover, so a drag never paints hover
78//! under the finger and a widget that captures its own gesture is hover-free for
79//! the length of it. Nothing distinguishes a touch contact from a mouse, though: a
80//! touch drag that captured nothing is an ordinary hover pass, so a non-capturing
81//! consumer can tint transiently under a finger — the `Up` at lift ends the link
82//! (see `docs/LIMITATIONS.md`'s `hover-window-leave-standing`).
83//!
84//! **Application code should prefer `frust::authoring`**, which re-exports
85//! everything below plus the `frust-core` trait vocabulary and `kurbo`/`peniko`
86//! geometry — so an app depends on `frust` alone. The example spells its imports
87//! the long way only because this crate cannot name `frust` without a
88//! dev-dependency cycle; `frust::authoring`'s own module docs carry the
89//! facade-spelled version.
90//!
91//! **Stability: pre-1.0, and a real supported public API** — not `#[doc(hidden)]`
92//! plumbing, so a change to any signature here is a **breaking change**, released
93//! as such. The three catalogs shipping with this crate (`material`, `cupertino`,
94//! `glyph`) consume exactly this surface and nothing more, so a third-party design
95//! system authored against it is at parity with the built-ins by construction.
96//!
97//! # Example: a one-child container widget
98//!
99//! A container that offsets its single child, wired through the full lifecycle —
100//! build, rebuild, teardown, layout, paint, event routing, semantics forwarding:
101//!
102//! ```
103//! use frust_core::{
104//!     AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult,
105//!     InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
106//! };
107//! use frust_widgets::authoring::{build_child, rebuild_child, route_event_single, teardown_child};
108//! use kurbo::{Point, Size};
109//!
110//! /// The declarative half: a child plus the offset to apply to it.
111//! struct OffsetView<State: 'static> {
112//!     offset: Point,
113//!     child: AnyView<State>,
114//! }
115//!
116//! /// The view-fn app code calls; `any` erases the child because the view
117//! /// *stores* it in a field (storing is the one place erasure is needed).
118//! fn offset<State: 'static, V: View<State>>(offset: Point, child: V) -> OffsetView<State> {
119//!     OffsetView { offset, child: any(child) }
120//! }
121//!
122//! /// The retained half: the live child pod plus the applied offset.
123//! struct OffsetWidget {
124//!     offset: Point,
125//!     child: ChildPod,
126//! }
127//!
128//! impl<State: 'static> View<State> for OffsetView<State> {
129//!     type Element = OffsetWidget;
130//!
131//!     fn build(&self, ctx: &mut BuildCtx<'_>) -> OffsetWidget {
132//!         OffsetWidget { offset: self.offset, child: build_child(&self.child, ctx) }
133//!     }
134//!
135//!     fn rebuild(
136//!         &self,
137//!         prev: &Self,
138//!         element: &mut OffsetWidget,
139//!         ctx: &mut BuildCtx<'_>,
140//!     ) -> ChangeFlags {
141//!         let mut flags = ChangeFlags::NONE;
142//!         if prev.offset != self.offset {
143//!             element.offset = self.offset;
144//!             flags |= ChangeFlags::LAYOUT;
145//!         }
146//!         flags | rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
147//!     }
148//!
149//!     fn teardown(&self, element: &mut OffsetWidget, ctx: &mut BuildCtx<'_>) {
150//!         teardown_child(&self.child, &mut element.child, ctx);
151//!     }
152//! }
153//!
154//! impl Widget for OffsetWidget {
155//!     fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
156//!         let size = self.child.layout_child(ctx, bc);
157//!         self.child.set_origin(self.offset);
158//!         bc.constrain(size)
159//!     }
160//!
161//!     fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
162//!         self.child.paint_child(ctx, scene);
163//!     }
164//!
165//!     fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
166//!         // Never re-hit-test a captured child by hand — this helper owns the
167//!         // capture/focus fast paths and the blur-on-outside-tap rule.
168//!         route_event_single(&mut self.child, ctx, event)
169//!     }
170//!
171//!     fn semantics(&self, ctx: &mut SemanticsCtx) {
172//!         // A transparent wrapper still MUST forward, or the child's whole
173//!         // subtree drops out of the accessibility tree.
174//!         self.child.semantics_child(ctx);
175//!     }
176//!
177//!     // The read-only tooling seam: publish every pod this widget owns.
178//!     frust_widgets::authoring::visit_children!(child);
179//! }
180//! # fn main() {}
181//! ```
182
183use std::any::Any;
184use std::collections::{HashMap, HashSet};
185use std::rc::Rc;
186
187use frust_core::{
188    AnyView, BuildCtx, ChangeFlags, EventCtx, EventResult, InputEvent, PointerButton, PointerEvent,
189    PointerPhase, View, Widget,
190};
191use kurbo::Point;
192
193use crate::ChildKey;
194
195pub use crate::image::ImageSource;
196pub use crate::text::{ThemeTextColor, ThemeTextType};
197
198/// The overlay-portal authoring surface: the slot a widget that floats its own
199/// surface holds, what that surface is anchored to, and the placement geometry
200/// every anchored pattern needs.
201///
202/// Promoted here rather than left to each design system because all three of
203/// them had derived the same geometry independently: a design system's popover,
204/// menu, tooltip and context menu place a surface through [`place`] and host it
205/// through [`OverlaySlot`], reaching both through `frust::authoring` alone.
206pub use crate::overlay::{
207    OverlayAlign, OverlayAnchor, OverlayPlacement, OverlaySide, OverlaySlot, place,
208};
209
210/// The overlay registry's own vocabulary, re-exported from `frust-core` so a
211/// design system naming a band, an input class, a light-dismiss policy or a
212/// surface identity does it through this one module: an
213/// [`OverlaySlot`]'s setters take exactly these types, and a widget that cannot
214/// name them cannot configure one.
215pub use frust_core::{OutsideTap, OverlayBand, OverlayInput, OverlayKey};
216
217/// Pressed-state overlay opacity (source: androidx Compose Material3
218/// `StateTokens` v0_210, retrieved 2026-07-17 — supersedes material-web
219/// v0.192's 12%).
220///
221/// Lives here, not with the rest of the M3 state-layer table, because it is the
222/// one interaction opacity a *language-neutral* widget needs: a press overlay is
223/// a mechanism (how a control acknowledges a touch), not a Material design
224/// decision. A design system's own state-layer table is free to differ; this is
225/// the value the baseline widgets and the built-in catalogs share.
226pub const PRESSED_OPACITY: f32 = 0.10;
227
228/// A widget-held, `State`-erased app-state callback adapter (see
229/// [`erase_callback`]).
230pub type ErasedCallback = Box<dyn FnMut(&mut EventCtx)>;
231
232/// A widget-held, `State`-erased callback adapter carrying one value argument
233/// (see [`erase_callback_arg`]).
234pub type ErasedArgCallback<A> = Box<dyn FnMut(&mut EventCtx, A)>;
235
236/// A view-held, typed callback carrying one value argument (Checkbox's `bool`,
237/// Slider's `f64`), erased to [`ErasedArgCallback`] on build.
238pub type TypedArgCallback<State, A> = std::rc::Rc<dyn Fn(&mut State, A)>;
239
240/// Erase a view-held `Rc<dyn Fn(&mut State)>` app-state callback into the
241/// widget-held [`ErasedCallback`] adapter the interactive widgets invoke during
242/// the event pass.
243///
244/// The closure recovers the concrete `State` from the type-erased [`EventCtx`]
245/// with [`EventCtx::state_mut`] (the downcast happens *inside* the adapter), so
246/// the widget itself stays non-generic over `State`. Closures aren't comparable,
247/// so `build`/`rebuild` reinstall the adapter unconditionally — it's cheap.
248pub fn erase_callback<State: 'static>(callback: &Rc<dyn Fn(&mut State)>) -> ErasedCallback {
249    let callback = callback.clone();
250    Box::new(move |ctx: &mut EventCtx| {
251        let state = ctx.state_mut::<State>();
252        callback(state);
253    })
254}
255
256/// Like [`erase_callback`], but for callbacks that also carry a value argument
257/// (Checkbox's `bool`, Slider's `f64`).
258pub fn erase_callback_arg<State: 'static, A: 'static>(
259    callback: &TypedArgCallback<State, A>,
260) -> ErasedArgCallback<A> {
261    let callback = callback.clone();
262    Box::new(move |ctx: &mut EventCtx, arg: A| {
263        let state = ctx.state_mut::<State>();
264        callback(state, arg);
265    })
266}
267
268/// Build a [`ChildPod`] wrapping an [`AnyView`]'s element.
269///
270/// The element (`Box<dyn Widget>`) is stored double-boxed so a later
271/// [`rebuild_child`] can recover it as `&mut Box<dyn Widget>` — the type
272/// `AnyView`'s `rebuild` needs to swap the widget on a concrete-type change.
273///
274/// The build runs with the focus chain closed
275/// ([`BuildCtx::with_focus_link`]`(false, …)`): the pod being built is brand new,
276/// so by construction it holds no focus link — the same composition
277/// [`rebuild_child`] performs, kept uniform so "descending into a pod always
278/// recomputes the chain" has no exceptions.
279pub fn build_child<State: 'static>(view: &AnyView<State>, ctx: &mut BuildCtx<'_>) -> ChildPod {
280    let element: Box<dyn Widget> = ctx.with_focus_link(false, |ctx| view.build(ctx));
281    ChildPod::new(Box::new(element))
282}
283
284/// Reconcile one [`AnyView`] child in place through its `ChildPod`.
285///
286/// A type-swapped child (see `rebuild_child_tracked`) that still holds a
287/// recorded capture has that capture dropped: `pod.set_active(false)`, no
288/// synthetic `Cancel`. The old armed widget was torn down inside
289/// `AnyView::rebuild` — its state died with it — and the fresh widget in its
290/// place never saw the original `Down`, so there is nothing to unwind; this
291/// mirrors [`rebuild_children`]'s documented type-swap semantics for the
292/// single-child wrappers (`Padding`/`Align`/`SizedBox`, and the interactive
293/// widgets' own label/track children) that call this instead of
294/// `rebuild_children`.
295///
296/// **A type swap severs the recorded focus path exactly like it severs the
297/// capture path**, and is handled the same way the keyed reconciler handles its
298/// own swap arm: the pod's `focused` flag is dropped (the fresh widget never
299/// claimed focus — leaving the link set would route `Key`/`Ime` events into a
300/// widget that ignores them) and
301/// [`mark_focus_orphaned`](frust_core::mark_focus_orphaned) is raised *when the
302/// severed link was on the live focus chain* ([`mark_orphan_if_live`]), so
303/// `RenderRoot::rebuild` releases `focus_active`/`ime_state` before the frame
304/// ends. This is the `Padding(if editing { text_input } else { text })` shape:
305/// without the release the keyboard stays up over an idle screen and
306/// `is_focus_active()` keeps reporting a session whose owner no longer exists. A
307/// swap of a merely *stale* flag under an already-blurred ancestor marks
308/// nothing — it owned no session.
309pub fn rebuild_child<State: 'static>(
310    prev: &AnyView<State>,
311    next: &AnyView<State>,
312    pod: &mut ChildPod,
313    ctx: &mut BuildCtx<'_>,
314) -> ChangeFlags {
315    // Read before the nested rebuild, exactly as `rebuild_child_tracked` reads
316    // its own `link_focused`: the gate describes the link this pod held *going
317    // into* the swap.
318    let link_focused = pod.is_focused();
319    let (flags, swapped) = rebuild_child_tracked(prev, next, pod, ctx);
320    if swapped {
321        if pod.is_active() {
322            pod.set_active(false);
323        }
324        if link_focused {
325            pod.set_focused(false);
326        }
327        // `ctx` carries the chain down to *this wrapper*; the pod's own flag is
328        // ANDed onto it inside, the same composition every other marking site
329        // uses.
330        mark_orphan_if_live(ctx, link_focused);
331    }
332    flags
333}
334
335/// Like [`rebuild_child`], but also reports whether the rebuild *replaced* the
336/// underlying widget (an [`AnyView`] concrete-type swap) rather than mutating it
337/// in place.
338///
339/// The swap flag drives both callers' capture bookkeeping: [`rebuild_children`]'s
340/// (multi-child `Vec` containers — `Flex`/`Stack`) and [`rebuild_child`]'s
341/// (single-child wrappers). A fresh widget swapped in at a still-captured
342/// index/pod never saw the original `Down`, so its stale `active` path is
343/// dropped without a synthetic `Cancel` (there is nothing armed to unwind).
344/// Detection compares the boxed element's concrete
345/// [`TypeId`](std::any::TypeId) across the rebuild.
346///
347/// **This is the descent that extends the focus chain.** The nested rebuild runs
348/// under [`BuildCtx::with_focus_link`]`(pod.is_focused(), …)`, so a container
349/// deeper in the tree sees `ctx.has_focus()` == "is the whole chain from the root
350/// to me focused" — the value the orphan-marking sites gate on. Every route into
351/// a child's `rebuild`/`teardown` funnels through here (or through
352/// [`teardown_child`]), which is what makes the chain complete through arbitrary
353/// nesting.
354fn rebuild_child_tracked<State: 'static>(
355    prev: &AnyView<State>,
356    next: &AnyView<State>,
357    pod: &mut ChildPod,
358    ctx: &mut BuildCtx<'_>,
359) -> (ChangeFlags, bool) {
360    // Read before the `widget_mut` borrow below.
361    let link_focused = pod.is_focused();
362    let element = pod
363        .widget_mut()
364        .downcast_mut::<Box<dyn Widget>>()
365        .expect("layout-container child element is a boxed AnyView widget");
366    let before = {
367        let any: &dyn Any = &**element;
368        any.type_id()
369    };
370    let flags = ctx.with_focus_link(link_focused, |ctx| next.rebuild(prev, element, ctx));
371    let after = {
372        let any: &dyn Any = &**element;
373        any.type_id()
374    };
375    (flags, before != after)
376}
377
378/// Tear down one [`AnyView`] child through its `ChildPod`.
379///
380/// A pod still holding an in-flight capture ([`ChildPod::is_active`]) is
381/// cancelled (a synthetic `Cancel`, see `cancel_pod`) before teardown, so an
382/// armed widget dropped mid-gesture (e.g. the active row truncated out of a
383/// shrinking list) unwinds its state machine instead of vanishing with no
384/// terminating `Up`/`Cancel`.
385///
386/// A pod still holding the recorded **focus** path ([`ChildPod::is_focused`])
387/// *on the live focus chain* ([`BuildCtx::has_focus`]) raises
388/// [`mark_focus_orphaned`](frust_core::mark_focus_orphaned) instead: the focused
389/// widget is about to stop existing, so the whole focus/IME session dies with it
390/// and `RenderRoot::rebuild` releases it before the frame ends (see
391/// [`cancel_active_children`]'s note for the full mechanism, and why the pod
392/// cannot do it itself). A pod whose flag is stale — set, but under a link some
393/// ancestor already cleared — is torn down silently: it owns no session to lose.
394pub fn teardown_child<State: 'static>(
395    view: &AnyView<State>,
396    pod: &mut ChildPod,
397    ctx: &mut BuildCtx<'_>,
398) {
399    if pod.is_active() {
400        cancel_pod(pod);
401        pod.set_active(false);
402    }
403    let link_focused = pod.is_focused();
404    if link_focused {
405        // The pod is dropped by the caller right after this returns, so clearing
406        // the flag is bookkeeping hygiene, not the load-bearing part — the mark is.
407        pod.set_focused(false);
408    }
409    mark_orphan_if_live(ctx, link_focused);
410    if let Some(element) = pod.widget_mut().downcast_mut::<Box<dyn Widget>>() {
411        // Descend with the chain extended by the link this pod held *before* the
412        // clear above: a focused pod's subtree is still on the live chain while it
413        // is being torn down, so a focused descendant reports its own orphan.
414        ctx.with_focus_link(link_focused, |ctx| view.teardown(element, ctx));
415    }
416}
417
418/// Raise the orphan mark for a severed focus link, but **only when the link was
419/// live** — the one gate every marking site in this module shares
420/// ([`teardown_child`], [`cancel_active_children`], the keyed reconciler's
421/// type-swap arm, and [`rebuild_child`]'s).
422///
423/// `link_focused` is the dying pod's own [`ChildPod::is_focused`] flag and
424/// `ctx.has_focus()` is the chain above it, so the mark means exactly one thing:
425/// **a LIVE session just lost its owner.** Both halves are required. The flag
426/// alone is not evidence of a live session — a container-routed blur clears the
427/// focus link at the nearest common ancestor only, leaving flags deeper in the
428/// blurred branch legitimately set (see [`route_event`]) — and marking on the
429/// flag alone is what let a recycled list row, a filtered-out item, or a switched
430/// pattern release the *currently typed-into* field somewhere else in the tree.
431/// The chain alone is not evidence either: a live chain running past an unfocused
432/// sibling says nothing about that sibling.
433fn mark_orphan_if_live(ctx: &BuildCtx<'_>, link_focused: bool) {
434    if link_focused && ctx.has_focus() {
435        frust_core::mark_focus_orphaned();
436    }
437}
438
439/// Deliver a synthetic [`PointerPhase::Cancel`] to a captured child whose
440/// in-flight gesture a structural rebuild has invalidated, so its state machine
441/// unwinds instead of firing on a later `Up`.
442///
443/// # Cancel-during-rebuild contract
444///
445/// The rebuild pass runs over a [`BuildCtx`], not an [`EventCtx`] — there is no
446/// application state in scope. This is sound *only because a `Cancel` handler
447/// must never read application state* (`EventCtx::state_mut`): every
448/// interactive widget's `Cancel` arm only clears internal flags. That invariant
449/// lets this build a minimal [`EventCtx`] over a throwaway `()` state to drive
450/// the unwind; a `Cancel` handler that reached for real state would panic here
451/// on the `()` downcast — a deliberate tripwire, not a silent corruption.
452pub(crate) fn cancel_pod(pod: &mut ChildPod) {
453    let mut dummy_state = ();
454    let mut ctx = EventCtx::new(&mut dummy_state, pod.origin(), pod.size());
455    // Position is irrelevant to a `Cancel` (handlers never hit-test on it).
456    let cancel = InputEvent::Pointer(PointerEvent {
457        phase: PointerPhase::Cancel,
458        position: Point::ZERO,
459        button: PointerButton::Primary,
460    });
461    pod.event_child(&mut ctx, &cancel);
462}
463
464/// Cancel-and-clear the recorded interaction paths of the pods handed to it —
465/// both the capture (`active`) path and the focus (`focused`) path — after a
466/// structural change.
467///
468/// Called by [`rebuild_children_positional`] on the **tail past the stable
469/// prefix** (indices `≥ k`, whose widget identity may have changed): a swapped
470/// slot, a shifted pod, or the grown tail. The stable prefix (indices `< k`)
471/// keeps the same live widget across the rebuild, so its recorded paths stay
472/// valid and are *not* passed here — that is Flutter's focus/IME retention
473/// invariant (see [`rebuild_children_positional`]). The keyed reconciler does not
474/// call this at all: a key-matched pod relocates with its flags intact, and only
475/// a torn-down or type-swapped pod (identity broken) is cleared inline there.
476///
477/// For each pod handed in, any surviving armed widget is unwound via [`cancel_pod`]
478/// and its `active` flag dropped, and any focused child has its `focused` flag
479/// dropped. `ctx` carries the focus chain down to the *container*
480/// ([`BuildCtx::has_focus`]), which each pod's own flag is ANDed onto to decide
481/// whether the clear severed a live session (see [`mark_orphan_if_live`]).
482///
483/// # Focus vs. capture: why one synthesizes a `Cancel` and the other does not
484///
485/// Capture state lives *inside* the widget (an `armed`/`pressed` flag its own
486/// event arms), so dropping the recorded path requires a synthetic [`Cancel`]
487/// ([`cancel_pod`]) to unwind that internal machine — otherwise it would fire on
488/// a later hit-tested `Up`. Focus state, by contrast, is *reflected* from the pod
489/// flag into the widget each event (`EventCtx::has_focus`, threaded through
490/// [`ChildPod::event_child`]) rather than latched internally, so clearing the pod
491/// flag is enough — there is no widget-internal blur to drive, and a `Cancel`
492/// handler must not touch app state anyway (`docs/CODE_STANDARDS.md`).
493///
494/// # RenderRoot notification: the orphan mark
495///
496/// Clearing `focused` here cannot notify [`RenderRoot`](frust_core::RenderRoot)
497/// directly — the rebuild pass runs over a [`BuildCtx`], with no `RenderRoot` in
498/// scope, exactly as the capture-cancel path above cannot reset
499/// `RenderRoot::pointer_captured`. It instead raises the thread-local
500/// [`mark_focus_orphaned`](frust_core::mark_focus_orphaned) flag, which
501/// `RenderRoot::rebuild` drains at the end of the same rebuild and turns into a
502/// full focus/IME session release (`focus_active` cleared, the shell-facing
503/// surface dropped, one edge on the focus/IME generation).
504///
505/// **Why the mark and not convergence.** Leaving the root's
506/// `focus_active`/`ime_state` stale for the next event pass to self-correct is
507/// fine while the user keeps touching the screen and wrong the moment they
508/// stop: on an idle screen no next event arrives, so the root
509/// keeps reporting a live focus session for a widget that no longer exists —
510/// which strands `ime_state()` at the shell and (measured on a Xiaomi 12) held
511/// the mobile frame gate's focus input up permanently after a navigator pop.
512/// A session must die with its owner, not with the next tap.
513///
514/// **Why a thread-local and not a tree walk.** Validating the root's mirror
515/// against reality after the diff would need to ask "does any pod in the tree
516/// still hold focus?", and there is no such iterator: `Widget` exposes no
517/// children, so nothing can walk the retained tree generically. The mark is the
518/// only channel available from inside a `BuildCtx` pass, and it mirrors
519/// `mark_pending_result_flush`'s shape exactly (data-free, idempotent,
520/// UI-thread-affine).
521///
522/// **A stale flag deep in a blurred branch is harmless by construction.** A
523/// container-routed blur clears the focus link at the nearest common ancestor
524/// only, so `focused` flags *below* that link stay set until focus next enters
525/// the subtree (see [`route_event`]). Such a pod is not evidence of a session:
526/// the mark is gated on the whole chain ([`mark_orphan_if_live`]), which a
527/// cleared ancestor link forces to `false` for the entire subtree below it —
528/// exactly as the same composition already hides those flags from paint
529/// (`ChildPod::paint_child`'s `self.focused && ctx.has_focus()`) and as
530/// focus-path routing already refuses to deliver into them. Tearing down a stale
531/// branch — a recycled list row, a filtered-out item, a switched pattern —
532/// therefore leaves the session of whatever field is *actually* focused
533/// untouched, instead of dropping the keyboard mid-typing somewhere unrelated.
534fn cancel_active_children(pods: &mut [ChildPod], ctx: &BuildCtx<'_>) {
535    for pod in pods.iter_mut() {
536        if pod.is_active() {
537            cancel_pod(pod);
538            pod.set_active(false);
539        }
540        let link_focused = pod.is_focused();
541        if link_focused {
542            pod.set_focused(false);
543        }
544        mark_orphan_if_live(ctx, link_focused);
545    }
546}
547
548/// Diff a child list against its live `ChildPod`s, extracting each child's
549/// [`AnyView`] through `view_of` (identity for a plain `Vec<AnyView>`, `|c|
550/// &c.view` for Flex's `FlexChild` wrapper) and its optional
551/// [`ChildKey`] through `key_of` (`|_| None` for keyless containers, `|c| c.key`
552/// for Flex) — the one shared reconciler for every multi-child container.
553///
554/// **Positional vs. keyed.** With *no* child carrying a key this is a positional
555/// diff (see `rebuild_children_positional`); with keys present it matches
556/// old↔new by key so reorders/inserts preserve widget identity and state (see
557/// `rebuild_children_keyed`). Keys are all-or-nothing per list: a list that
558/// mixes keyed and unkeyed children, or repeats a key, trips a `debug_assert`
559/// and falls back to the positional path (correct, just identity-blind).
560///
561/// **A structural change preserves focus/capture for unchanged siblings
562/// (Flutter's invariant).** A structural edit among siblings only cancels the
563/// recorded capture/focus paths of children whose own identity actually changed:
564/// the positional path preserves its stable prefix and cancels only the tail past
565/// the first type swap; the keyed path relocates a key-matched child's paths
566/// intact and cancels only a torn-down or type-swapped child. A
567/// structural-change-free rebuild leaves every recorded path untouched, so an
568/// ordinary every-frame rebuild never breaks a captured drag or dismisses the
569/// keyboard for an unchanged child.
570///
571/// **A child that loses a LIVE focus path takes the root's session with it.**
572/// Every route that severs a recorded focus path here — a torn-down child
573/// ([`teardown_child`]), a cleared tail ([`cancel_active_children`]), a
574/// key-reused type swap — raises
575/// [`mark_focus_orphaned`](frust_core::mark_focus_orphaned) *when the severed
576/// link was on the live focus chain* ([`mark_orphan_if_live`]), and
577/// `RenderRoot::rebuild` releases `focus_active`/`ime_state` before the frame
578/// ends rather than leaving them standing over a widget that no longer exists. A
579/// severed link that was merely a stale flag deep in an already-blurred branch
580/// marks nothing: it owned no session. See [`cancel_active_children`] for the
581/// mechanism.
582pub fn rebuild_children<State: 'static, C>(
583    prev: &[C],
584    next: &[C],
585    pods: &mut Vec<ChildPod>,
586    ctx: &mut BuildCtx<'_>,
587    view_of: impl Fn(&C) -> &AnyView<State>,
588    key_of: impl Fn(&C) -> Option<ChildKey>,
589) -> ChangeFlags {
590    let any_keyed = prev.iter().chain(next.iter()).any(|c| key_of(c).is_some());
591    if !any_keyed {
592        return rebuild_children_positional(prev, next, pods, ctx, view_of);
593    }
594    // v1 keys are all-or-nothing per list: a mixed list has no well-defined
595    // match for its unkeyed members, so fall back to positional (identity-blind
596    // but correct) rather than guess. A debug build flags the misuse loudly.
597    let all_keyed = prev.iter().chain(next.iter()).all(|c| key_of(c).is_some());
598    if !all_keyed {
599        debug_assert!(
600            false,
601            "keyed child list mixes keyed and unkeyed children; \
602             falling back to positional reconciliation"
603        );
604        return rebuild_children_positional(prev, next, pods, ctx, view_of);
605    }
606    rebuild_children_keyed(prev, next, pods, ctx, view_of, key_of)
607}
608
609/// The positional reconciler: common indices rebuild in place, a grown tail is
610/// built, a shrunk tail is torn down and dropped. Length changes signal
611/// `LAYOUT | PAINT`.
612///
613/// **Focus/capture survive a sibling structural change (Flutter's invariant).**
614/// A structural edit among SIBLINGS must not clear focus/IME (or an in-flight
615/// capture) for a child whose own identity is unchanged. Positional matching
616/// rebuilds each common index `i` in place against the *same* live widget, so a
617/// recorded focus/capture path to it stays valid as long as that slot was not an
618/// in-place type swap. We therefore compute the **stable prefix** `k` — the
619/// largest `k ≤ min(prev.len, next.len)` such that no index `< k` type-swapped
620/// (the first swap index, or `common` if none) — and preserve focus AND capture
621/// for pods `< k`. Only the tail from `k` onward has its recorded paths
622/// cancelled/cleared via [`cancel_active_children`]: a swapped slot (fresh widget
623/// that never saw `Down`), a shifted pod that may now hold different logical
624/// content, and the grown tail (fresh pods, nothing to unwind). A truncated
625/// active pod is cancelled earlier in [`teardown_child`]. A structural-change-free
626/// rebuild (same length, no swap) leaves every path untouched, so an ordinary
627/// every-frame rebuild never breaks a captured drag *or* dismisses the keyboard
628/// for an unchanged sibling (the huddle search-field bug this fixes).
629///
630/// This is Flutter's focus/IME retention behavior: only a child whose
631/// identity actually changes loses focus.
632/// Positional matching cannot distinguish a same-typed prepend from a
633/// content-change-plus-append — an index `< k` that positionally kept its widget
634/// but semantically moved keeps its recorded path (the documented positional
635/// limitation; a caller wanting identity across reorders uses `keyed`).
636fn rebuild_children_positional<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) -> ChangeFlags {
643    let mut flags = ChangeFlags::NONE;
644    let common = prev.len().min(next.len());
645    // The stable prefix ends at the first in-place type swap (or at `common` if
646    // there is none): every index before it keeps the same live widget across
647    // the rebuild, so a recorded focus/capture path to it stays valid.
648    let mut first_swap: Option<usize> = None;
649    for i in 0..common {
650        let (child_flags, child_swapped) =
651            rebuild_child_tracked(view_of(&prev[i]), view_of(&next[i]), &mut pods[i], ctx);
652        flags |= child_flags;
653        if child_swapped {
654            // Fresh widget at this slot: drop the stale capture, nothing to cancel.
655            pods[i].set_active(false);
656            if first_swap.is_none() {
657                first_swap = Some(i);
658            }
659        }
660    }
661    if next.len() > prev.len() {
662        for child in &next[prev.len()..] {
663            pods.push(build_child(view_of(child), ctx));
664        }
665        flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
666    } else if next.len() < prev.len() {
667        for (offset, child) in prev[next.len()..].iter().enumerate() {
668            teardown_child(view_of(child), &mut pods[next.len() + offset], ctx);
669        }
670        pods.truncate(next.len());
671        flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
672    }
673    // On a structural change (length shift or in-place type swap), preserve the
674    // stable prefix (`< k`, same widget identity) and cancel/clear only the tail
675    // from `k` onward. On a pure length change with no swap, `k == common`, so the
676    // prefix (every surviving common pod) is preserved and only the grown tail —
677    // fresh pods with no recorded path — is "cancelled" (a no-op); a truncated
678    // active pod was already cancelled in `teardown_child`.
679    if prev.len() != next.len() || first_swap.is_some() {
680        let k = first_swap.unwrap_or(common);
681        // `ctx` carries the chain down to *this container*; each pod's own flag
682        // is ANDed onto it inside.
683        cancel_active_children(&mut pods[k..], ctx);
684    }
685    flags
686}
687
688/// The keyed reconciler: match old↔new children by [`ChildKey`] so reorders and
689/// inserts preserve each surviving child's live widget (and thus its internal
690/// state) instead of rebuilding whatever happens to sit at the same index.
691///
692/// Matched children are *relocated* into the new order — their `ChildPod` (boxed
693/// widget + geometry) is moved, then rebuilt in place against its own previous
694/// view. Unmatched new keys are built fresh; unmatched old keys are torn down
695/// (with [`teardown_child`]'s cancel-if-active). A duplicate key (old or new)
696/// trips a `debug_assert` and falls back to the positional path.
697///
698/// **A key-matched child keeps its focus/capture across a move (Flutter's
699/// invariant).** A reorder or insert relocates a matched child's whole
700/// `ChildPod` — including its `focused`/`active` bookkeeping — so its recorded
701/// path stays valid: focus/capture routing scans for the pod by its flag
702/// ([`ChildPod::is_focused`]/[`is_active`](ChildPod::is_active)), so a `Key`/`Ime`
703/// event (or a captured `Move`/`Up`) still reaches the relocated child at its new
704/// index with nothing to update in a parent index. Only a child whose identity
705/// actually breaks loses its path: a torn-down (removed) key is cancelled in
706/// [`teardown_child`], and a key reused for a different concrete type is a swap
707/// (the old widget died inside `AnyView::rebuild`) whose stale `active`/`focused`
708/// flags are dropped inline below. A same-keys, same-order rebuild is likewise a
709/// content-only rebuild that leaves every path untouched.
710fn rebuild_children_keyed<State: 'static, C>(
711    prev: &[C],
712    next: &[C],
713    pods: &mut Vec<ChildPod>,
714    ctx: &mut BuildCtx<'_>,
715    view_of: impl Fn(&C) -> &AnyView<State>,
716    key_of: impl Fn(&C) -> Option<ChildKey>,
717) -> ChangeFlags {
718    // Old key -> old index, flagging any duplicate.
719    let mut old_by_key: HashMap<ChildKey, usize> = HashMap::with_capacity(prev.len());
720    let mut duplicate = false;
721    for (i, child) in prev.iter().enumerate() {
722        let key = key_of(child).expect("all-keyed list checked by caller");
723        if old_by_key.insert(key, i).is_some() {
724            duplicate = true;
725        }
726    }
727    // Duplicate new keys are equally ambiguous (two children claim one identity).
728    let mut seen_new: HashSet<ChildKey> = HashSet::with_capacity(next.len());
729    for child in next {
730        let key = key_of(child).expect("all-keyed list checked by caller");
731        if !seen_new.insert(key) {
732            duplicate = true;
733        }
734    }
735    if duplicate {
736        debug_assert!(
737            false,
738            "keyed child list has duplicate keys; falling back to positional reconciliation"
739        );
740        return rebuild_children_positional(prev, next, pods, ctx, view_of);
741    }
742
743    // Take ownership of the old pods so matched ones can be relocated by `take`.
744    let mut old_pods: Vec<Option<ChildPod>> = pods.drain(..).map(Some).collect();
745    let mut new_pods: Vec<ChildPod> = Vec::with_capacity(next.len());
746    let mut flags = ChangeFlags::NONE;
747
748    for child in next {
749        let key = key_of(child).expect("all-keyed list checked by caller");
750        if let Some(&old_index) = old_by_key.get(&key) {
751            let mut pod = old_pods[old_index]
752                .take()
753                .expect("each old key matches at most one new child (no duplicates)");
754            let (child_flags, child_swapped) =
755                rebuild_child_tracked(view_of(&prev[old_index]), view_of(child), &mut pod, ctx);
756            flags |= child_flags;
757            if child_swapped {
758                // A key reused for a different concrete type: the old widget was
759                // torn down inside AnyView::rebuild, so its identity broke — drop
760                // the stale capture/focus paths (nothing armed to unwind). A
761                // key-matched non-swap relocates its pod (and thus its recorded
762                // focus/capture flags) intact, so no clearing happens there.
763                pod.set_active(false);
764                // The focused widget died inside the swap: the recorded focus
765                // path is severed, so the root's session goes with it — but only
766                // if this pod's link was on the LIVE chain; a stale flag under a
767                // blurred ancestor owns no session (see `mark_orphan_if_live` and
768                // `cancel_active_children`'s note).
769                let link_focused = pod.is_focused();
770                if link_focused {
771                    pod.set_focused(false);
772                }
773                mark_orphan_if_live(ctx, link_focused);
774            }
775            new_pods.push(pod);
776        } else {
777            // A brand-new key: build a fresh child.
778            new_pods.push(build_child(view_of(child), ctx));
779            flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
780        }
781    }
782
783    // Any old pod never taken is a removed key: tear it down (cancelling first if
784    // it held an in-flight capture, and dropping its focus flag with it).
785    for (old_index, slot) in old_pods.iter_mut().enumerate() {
786        if let Some(mut pod) = slot.take() {
787            teardown_child(view_of(&prev[old_index]), &mut pod, ctx);
788            flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
789        }
790    }
791
792    *pods = new_pods;
793    flags
794}
795
796/// A field shape a container can hold children in, visitable in declaration
797/// order — the traversal half of the [`visit_children!`] seam.
798///
799/// Implemented for [`ChildPod`] and, generically, for `Option<T>`/`Vec<T>`/
800/// `[T]` over anything visitable, so the three shapes containers actually use
801/// (`child: ChildPod`, `icon: Option<ChildPod>`, `children: Vec<ChildPod>`) are
802/// covered by one impl each. A container holding pods inside its own row/slot
803/// struct implements this for that struct and stays on the same seam.
804pub trait VisitPods {
805    /// Hand each pod this value holds to `visitor`, in declaration order.
806    fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod));
807}
808
809impl VisitPods for ChildPod {
810    fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
811        visitor(self);
812    }
813}
814
815impl<T: VisitPods> VisitPods for Option<T> {
816    fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
817        if let Some(inner) = self {
818            inner.visit_pods(visitor);
819        }
820    }
821}
822
823impl<T: VisitPods> VisitPods for [T] {
824    fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
825        for item in self {
826            item.visit_pods(visitor);
827        }
828    }
829}
830
831impl<T: VisitPods> VisitPods for Vec<T> {
832    fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
833        self.as_slice().visit_pods(visitor);
834    }
835}
836
837/// Implement [`Widget::visit_children`](frust_core::Widget::visit_children) for
838/// the container whose `impl Widget` block this is written in, forwarding the
839/// named fields in the order given.
840///
841/// The read-only introspection seam that makes a container's retained subtree
842/// enumerable (`frust-core`'s `WidgetTree::inspect` is the consumer). One line
843/// per container, so the traversal lives here rather than being re-hand-written
844/// per widget:
845///
846/// ```
847/// # use frust_core::{BoxConstraints, ChildPod, LayoutCtx, PaintCtx, PaintScene, Widget};
848/// # use kurbo::Size;
849/// use frust_widgets::authoring::visit_children;
850///
851/// struct RowWidget {
852///     leading: Option<ChildPod>,
853///     children: Vec<ChildPod>,
854/// }
855///
856/// impl Widget for RowWidget {
857///     # fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { bc.max() }
858///     # fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
859///     // ...layout/paint/event/semantics as usual...
860///     visit_children!(leading, children);
861/// }
862/// ```
863///
864/// Each field's type only has to implement [`VisitPods`] — `ChildPod`,
865/// `Option<_>`, `Vec<_>`, or a container's own row struct. A widget whose
866/// children are reached some other way (a slot list behind an enum, a
867/// transition's stashed page) writes the method by hand instead; the contract is
868/// the same either way — visit every pod you own, in paint order, and nothing
869/// else.
870#[macro_export]
871macro_rules! visit_children {
872    ($($field:ident),* $(,)?) => {
873        fn visit_children(&self, visitor: &mut dyn FnMut(&$crate::authoring::ChildPod)) {
874            $( $crate::authoring::VisitPods::visit_pods(&self.$field, visitor); )*
875        }
876    };
877}
878
879pub use crate::visit_children;
880/// Re-exported so the [`visit_children!`] expansion can name the type without
881/// the call site importing it.
882pub use frust_core::ChildPod;
883
884/// Whether `event` is the phase that auto-releases a recorded capture
885/// (`Up`/`Cancel`) — shared by [`route_event`]/[`route_event_single`] and by
886/// [`OverlaySlot`](crate::overlay::OverlaySlot), which dispatches into its pod
887/// without going through either and owes the pod's `active` link the same
888/// lifetime every other container gives a captured child.
889///
890/// `frust-core`'s `component::route_child` keeps a copy of its own: that crate
891/// sits *below* this one, so it cannot name this function. It is the one
892/// remaining duplicate of this predicate in the workspace.
893pub(crate) fn releases_capture(event: &InputEvent) -> bool {
894    matches!(
895        event,
896        InputEvent::Pointer(p)
897            if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
898    )
899}
900
901/// Whether `p` carries a button that may begin a press.
902///
903/// A press/activation machine — a `pressed` visual, a pointer capture, an
904/// up-inside callback, a drag — starts on the **primary** button alone: the left
905/// mouse button, or any touch/pen contact (which every shell reports as
906/// [`PointerButton::Primary`] too). A secondary press is a context gesture: no
907/// baseline widget does anything with it, so it is left for a context-menu
908/// consumer instead of activating the control under the cursor.
909///
910/// Move/hover arms deliberately do **not** consult this: hover chrome and cursor
911/// shapes are position-driven, and a move carries no meaningful button. Neither
912/// do the focus-session arms — [`is_pointer_down`]'s blur-on-outside-tap, and a
913/// widget's own `request_focus`/IME publish — because the root reads *any* `Down`
914/// that bubbles no claim as a blur, whatever button carried it.
915///
916/// `frust_shadcn`'s `hit::presses` is the same predicate for that catalog; each
917/// catalog keeps its own copy rather than depending on a sibling crate.
918pub(crate) fn presses(p: &PointerEvent) -> bool {
919    p.button == PointerButton::Primary
920}
921
922/// Whether `event` is a pointer `Down` — the phase that both opens a capture and
923/// triggers blur-on-outside-tap evaluation in the routing helpers.
924fn is_pointer_down(event: &InputEvent) -> bool {
925    matches!(
926        event,
927        InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
928    )
929}
930
931/// Route a pointer/scroll/keyboard/IME event — or a broadcast — to a
932/// container's children.
933///
934/// **Broadcasts** ([`InputEvent::is_broadcast`], today
935/// [`InputEvent::Housekeeping`]) are checked **first**, ahead of the capture,
936/// focus, and hit-test branches: every child receives one unconditionally, in
937/// order, and the container always reports [`EventResult::Ignored`] so no
938/// first-handler-wins short-circuit can hide a subtree. This is what carries a
939/// deferred, state-bearing callback flush (a navigator's queued `on_result`) to
940/// wherever it was queued, on the frame that queued it, with no user input — see
941/// [`InputEvent::Housekeeping`].
942///
943/// **Focus-routed events** ([`InputEvent::Key`]/[`InputEvent::Ime`]) bypass hit
944/// testing entirely: they go straight to the child holding a focus link **on the
945/// live session** ([`ChildPod::holds_live_focus`]), or are ignored if none does.
946/// This is the focus mirror of the capture fast-path.
947///
948/// The liveness half is not a refinement, it is the question. Two children can
949/// carry a recorded link at the same time — the blur sweep below runs on a
950/// hit-tested `Down`, and a claim made from a broadcast (an overlay surface's
951/// own press, which reaches no container's hit test) never triggers it — so a
952/// container that answered "which child is focused?" with the first flagged one
953/// delivered the keyboard by child order rather than by who holds the session.
954/// A stamp comparison answers it by who actually claimed it last.
955///
956/// For **pointer/scroll**: a captured gesture goes straight to the recorded
957/// active child (capture auto-releases on `Up`/`Cancel`). Otherwise the children
958/// are hit-tested in reverse paint order — topmost (last-painted) first — and the
959/// first child that both contains the point and reports [`EventResult::Handled`]
960/// consumes it. This is the z-order convention Flex establishes and Stack shares.
961///
962/// **Blur-on-outside-tap:** a pointer `Down` that does not (re)establish focus on
963/// the child it hits clears every focused child in this container. At the nearest
964/// common ancestor of a stale focus branch and the tapped branch, this breaks the
965/// recorded focus chain (a `Down` inside the still-focused child keeps it — that
966/// child stays focused and is not cleared).
967///
968/// **Deeper stale flags below a cleared link are harmless by construction**, not
969/// merely "corrected later". Nothing reads a `focused` flag on its own: every
970/// consumer composes it with the chain above it, so a cleared link forces the
971/// whole subtree below to read as unfocused. Focus-routed events stop at the
972/// cleared link (this function finds no live-linked child to forward to); paint
973/// ANDs the same way (`ChildPod::paint_child` composes the link, its stamp and
974/// the chain); and the rebuild pass ANDs the same way too
975/// ([`BuildCtx::has_focus`]), so tearing a stale branch down marks no orphan and
976/// cannot release the session of the field that really is focused
977/// ([`mark_orphan_if_live`]). The flags themselves are still cleaned up the next
978/// time focus enters that subtree (a focus request re-records the whole chain).
979pub fn route_event(
980    children: &mut [ChildPod],
981    ctx: &mut EventCtx<'_>,
982    event: &InputEvent,
983) -> EventResult {
984    if event.is_broadcast() {
985        for pod in children.iter_mut() {
986            // Results are discarded on purpose: a broadcast is never consumed,
987            // so every child gets it regardless of what an earlier one returned.
988            pod.event_child(ctx, event);
989        }
990        return EventResult::Ignored;
991    }
992    if event.is_focus_routed() {
993        if let Some(pod) = children.iter_mut().find(|p| p.holds_live_focus()) {
994            return pod.event_child(ctx, event);
995        }
996        return EventResult::Ignored;
997    }
998    if let Some(pod) = children.iter_mut().find(|p| p.is_active()) {
999        return route_event_single(pod, ctx, event);
1000    }
1001    let position = event.position();
1002    let mut handled = EventResult::Ignored;
1003    // The index of the hit child *if* it holds focus after dispatch — the one
1004    // focused child blur-on-Down must preserve.
1005    let mut kept_focus: Option<usize> = None;
1006    let n = children.len();
1007    for i in (0..n).rev() {
1008        if children[i].contains(position)
1009            && children[i].event_child(ctx, event) == EventResult::Handled
1010        {
1011            if children[i].holds_live_focus() {
1012                kept_focus = Some(i);
1013            }
1014            handled = EventResult::Handled;
1015            break;
1016        }
1017    }
1018    if is_pointer_down(event) {
1019        for (i, pod) in children.iter_mut().enumerate() {
1020            if Some(i) != kept_focus && pod.is_focused() {
1021                pod.set_focused(false);
1022            }
1023        }
1024    }
1025    handled
1026}
1027
1028/// Route a pointer/scroll event — or a broadcast — to a container's single
1029/// child.
1030///
1031/// Mirrors [`route_event`] for the one-child wrappers (`Padding`/`Align`/
1032/// `SizedBox`): a broadcast ([`InputEvent::is_broadcast`]) reaches the child
1033/// unconditionally and is never consumed (checked first, ahead of everything
1034/// else); a captured gesture is forwarded to `pod` unconditionally —
1035/// regardless of whether the event's position still falls within the child's
1036/// bounds — with the active path cleared on `Up`/`Cancel`; otherwise the child
1037/// only receives the event if it contains the point. Re-hit-testing
1038/// `pod.contains()` on every event instead of consulting [`ChildPod::is_active`]
1039/// is the bug this helper exists to prevent — see `ChildPod::contains`'s docs.
1040pub fn route_event_single(
1041    pod: &mut ChildPod,
1042    ctx: &mut EventCtx<'_>,
1043    event: &InputEvent,
1044) -> EventResult {
1045    if event.is_broadcast() {
1046        // Never consumed: forward, discard the result.
1047        pod.event_child(ctx, event);
1048        return EventResult::Ignored;
1049    }
1050    if event.is_focus_routed() {
1051        // Focus-routed events go to the child only if its recorded link belongs
1052        // to the live session — see [`route_event`] for why the liveness half is
1053        // load-bearing rather than cosmetic.
1054        if pod.holds_live_focus() {
1055            return pod.event_child(ctx, event);
1056        }
1057        return EventResult::Ignored;
1058    }
1059    if pod.is_active() {
1060        let result = pod.event_child(ctx, event);
1061        if releases_capture(event) {
1062            pod.set_active(false);
1063        }
1064        return result;
1065    }
1066    let inside = pod.contains(event.position());
1067    let result = if inside {
1068        pod.event_child(ctx, event)
1069    } else {
1070        EventResult::Ignored
1071    };
1072    // Blur-on-outside-tap: a `Down` that lands *outside* the (single) focused
1073    // child drops its recorded focus path. A `Down` inside the child keeps focus
1074    // (the child re-requests it, or simply stays the focused widget).
1075    if is_pointer_down(event) && !inside && pod.is_focused() {
1076        pod.set_focused(false);
1077    }
1078    result
1079}
1080
1081/// Compile-time proof that the whole authoring toolkit is reachable through the
1082/// **public** `crate::authoring::` path, not just the crate-root re-export the
1083/// in-crate call sites use.
1084///
1085/// Every item is named through `crate::authoring::…` and bound to an explicit
1086/// type, so an accidental re-privatization (or a signature change) fails the
1087/// build here rather than silently breaking a downstream design-system crate.
1088/// The module doc's worked example is the companion check: rustdoc compiles it as
1089/// a genuinely external crate.
1090#[cfg(test)]
1091mod authoring_surface_tests {
1092    use frust_core::{AnyView, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent};
1093
1094    #[test]
1095    // The point of every binding below is the spelled-out type: it is what makes
1096    // a re-privatization or a signature change fail the build here.
1097    #[allow(clippy::type_complexity)]
1098    fn every_promoted_item_is_reachable_through_the_authoring_path() {
1099        // Callback erasure: the two adapters, the typed input, the two erasers.
1100        let _erased: Option<crate::authoring::ErasedCallback> = None;
1101        let _erased_arg: Option<crate::authoring::ErasedArgCallback<f64>> = None;
1102        let _typed: Option<crate::authoring::TypedArgCallback<(), f64>> = None;
1103        let _erase: fn(&std::rc::Rc<dyn Fn(&mut ())>) -> crate::authoring::ErasedCallback =
1104            crate::authoring::erase_callback::<()>;
1105        let _erase_arg: fn(
1106            &crate::authoring::TypedArgCallback<(), f64>,
1107        ) -> crate::authoring::ErasedArgCallback<f64> =
1108            crate::authoring::erase_callback_arg::<(), f64>;
1109
1110        // Child plumbing: build/rebuild/teardown one child, reconcile many.
1111        let _build: fn(&AnyView<()>, &mut BuildCtx<'_>) -> ChildPod =
1112            crate::authoring::build_child::<()>;
1113        let _rebuild: fn(
1114            &AnyView<()>,
1115            &AnyView<()>,
1116            &mut ChildPod,
1117            &mut BuildCtx<'_>,
1118        ) -> ChangeFlags = crate::authoring::rebuild_child::<()>;
1119        let _teardown: fn(&AnyView<()>, &mut ChildPod, &mut BuildCtx<'_>) =
1120            crate::authoring::teardown_child::<()>;
1121        // `rebuild_children` takes `impl Fn` closures, which cannot be
1122        // turbofished into a `fn` pointer like the others — call it instead
1123        // (empty lists: the point is the path, not the reconciliation).
1124        let mut counter = 0u64;
1125        let mut build_ctx = BuildCtx::new(&mut counter);
1126        let mut pods: Vec<ChildPod> = Vec::new();
1127        let empty: Vec<AnyView<()>> = Vec::new();
1128        crate::authoring::rebuild_children(
1129            &empty,
1130            &empty,
1131            &mut pods,
1132            &mut build_ctx,
1133            |v: &AnyView<()>| v,
1134            |_: &AnyView<()>| None::<crate::ChildKey>,
1135        );
1136        assert!(pods.is_empty());
1137
1138        // Event routing: multi-child and single-child.
1139        let _route: fn(&mut [ChildPod], &mut EventCtx<'_>, &InputEvent) -> EventResult =
1140            crate::authoring::route_event;
1141        let _route_single: fn(&mut ChildPod, &mut EventCtx<'_>, &InputEvent) -> EventResult =
1142            crate::authoring::route_event_single;
1143
1144        // Themed text roles (color and family): each enum here, each builder
1145        // method on `TextView`.
1146        let _role: crate::authoring::ThemeTextColor = crate::authoring::ThemeTextColor::OnSurface;
1147        let _themed_role: fn(crate::TextView, crate::authoring::ThemeTextColor) -> crate::TextView =
1148            crate::TextView::themed_role;
1149        let _type: crate::authoring::ThemeTextType = crate::authoring::ThemeTextType::BodyLarge;
1150        let _themed_family: fn(
1151            crate::TextView,
1152            crate::authoring::ThemeTextType,
1153        ) -> crate::TextView = crate::TextView::themed_family;
1154
1155        // The shared press-overlay opacity.
1156        let _pressed: f32 = crate::authoring::PRESSED_OPACITY;
1157
1158        // The overlay portal: the placement math, the request it takes, the
1159        // slot an owner hosts its floated surface in, and the registry
1160        // vocabulary that slot is configured with.
1161        let _place: fn(
1162            kurbo::Rect,
1163            kurbo::Size,
1164            kurbo::Rect,
1165            crate::authoring::OverlayPlacement,
1166        ) -> kurbo::Rect = crate::authoring::place;
1167        let _side: crate::authoring::OverlaySide = crate::authoring::OverlaySide::Top;
1168        let _align: crate::authoring::OverlayAlign = crate::authoring::OverlayAlign::Center;
1169        let _anchor: crate::authoring::OverlayAnchor = crate::authoring::OverlayAnchor::Owner;
1170        let slot: crate::authoring::OverlaySlot<()> = crate::authoring::OverlaySlot::new();
1171        let _key: crate::authoring::OverlayKey = slot.key();
1172        let _band: crate::authoring::OverlayBand = crate::authoring::OverlayBand::Floating;
1173        let _input: crate::authoring::OverlayInput = crate::authoring::OverlayInput::Interactive;
1174        let _tap: crate::authoring::OutsideTap =
1175            crate::authoring::OutsideTap::Notify { consume: true };
1176
1177        // Child visitation: the trait, its three shape impls, and the macro
1178        // that writes the `Widget::visit_children` body from them.
1179        let _visit: fn(&ChildPod, &mut dyn FnMut(&ChildPod)) =
1180            <ChildPod as crate::authoring::VisitPods>::visit_pods;
1181        let _visit_opt: fn(&Option<ChildPod>, &mut dyn FnMut(&ChildPod)) =
1182            <Option<ChildPod> as crate::authoring::VisitPods>::visit_pods;
1183        let _visit_vec: fn(&Vec<ChildPod>, &mut dyn FnMut(&ChildPod)) =
1184            <Vec<ChildPod> as crate::authoring::VisitPods>::visit_pods;
1185        // The macro's own reachability is proven by its doc example, which
1186        // rustdoc compiles as a genuinely external crate.
1187    }
1188}
1189
1190/// Mechanism-level tests for [`rebuild_child`]'s type-swap capture *and focus*
1191/// handling, plus the orphan-marking sites of the multi-child reconcilers:
1192/// every single-child container (`Padding`/`Align`/
1193/// `SizedBox`, interactive widgets' labels) reconciles its child through this
1194/// helper, so the clear-on-swap behavior is proven once here at the shared
1195/// substrate, and each container's own test module (see `padding`/`align`/
1196/// `sized`) proves it end to end through real event routing/geometry.
1197#[cfg(test)]
1198mod tests {
1199    use super::*;
1200    use crate::test_support::{leaf_any, swap_leaf};
1201    use frust_core::{BoxConstraints, BuildCtx, LayoutCtx, PaintCtx, PaintScene, any};
1202    use kurbo::Size;
1203
1204    /// A context on the **live** focus chain — what a diff under a real focused
1205    /// root sees (`BuildCtx::new`'s "unknown, assume live" default).
1206    fn ctx(counter: &mut u64) -> BuildCtx<'_> {
1207        BuildCtx::new(counter)
1208    }
1209
1210    /// A context whose focus chain was broken by an ancestor: the diff of a
1211    /// subtree hanging below a link a container-routed blur already cleared.
1212    fn blurred_ctx(counter: &mut u64) -> BuildCtx<'_> {
1213        let mut ctx = BuildCtx::new(counter);
1214        ctx.set_has_focus(false);
1215        ctx
1216    }
1217
1218    /// A minimal multi-child container **view/widget pair** — the smallest thing
1219    /// that reconciles a child list through [`rebuild_children`] — so the focus
1220    /// chain can be exercised through genuine nesting (outer container → inner
1221    /// container → leaf) rather than one level of pods.
1222    struct NestView {
1223        children: Vec<AnyView<()>>,
1224    }
1225
1226    struct NestWidget {
1227        children: Vec<ChildPod>,
1228    }
1229
1230    impl View<()> for NestView {
1231        type Element = NestWidget;
1232
1233        fn build(&self, ctx: &mut BuildCtx<'_>) -> NestWidget {
1234            NestWidget {
1235                children: self.children.iter().map(|c| build_child(c, ctx)).collect(),
1236            }
1237        }
1238
1239        fn rebuild(
1240            &self,
1241            prev: &Self,
1242            element: &mut NestWidget,
1243            ctx: &mut BuildCtx<'_>,
1244        ) -> ChangeFlags {
1245            rebuild_children(
1246                &prev.children,
1247                &self.children,
1248                &mut element.children,
1249                ctx,
1250                |v: &AnyView<()>| v,
1251                |_: &AnyView<()>| None::<ChildKey>,
1252            )
1253        }
1254
1255        fn teardown(&self, element: &mut NestWidget, ctx: &mut BuildCtx<'_>) {
1256            for (view, pod) in self.children.iter().zip(element.children.iter_mut()) {
1257                teardown_child(view, pod, ctx);
1258            }
1259        }
1260    }
1261
1262    impl Widget for NestWidget {
1263        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1264            bc.constrain(Size::new(10.0, 10.0))
1265        }
1266        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1267    }
1268
1269    /// Reach the [`NestWidget`] inside a pod built from a [`NestView`] (double-boxed
1270    /// by [`build_child`]), so a test can set a pod flag deep in the tree.
1271    fn nest_in(pod: &mut ChildPod) -> &mut NestWidget {
1272        let boxed = pod
1273            .widget_mut()
1274            .downcast_mut::<Box<dyn Widget>>()
1275            .expect("pod holds a boxed AnyView widget");
1276        (**boxed)
1277            .downcast_mut::<NestWidget>()
1278            .expect("the boxed widget is the NestWidget")
1279    }
1280
1281    #[test]
1282    fn rebuild_child_clears_active_on_type_swap() {
1283        // A type swap (Leaf -> SwapLeaf, different concrete types) tears down
1284        // the old widget inside AnyView::rebuild and builds a fresh one in its
1285        // place; a still-active pod must have its stale capture dropped so a
1286        // later event isn't misrouted into the fresh widget.
1287        let mut counter = 0u64;
1288        let prev: AnyView<()> = leaf_any(10.0, 10.0);
1289        let mut pod = build_child(&prev, &mut ctx(&mut counter));
1290        pod.set_active(true);
1291
1292        let next: AnyView<()> = swap_leaf().into_any();
1293        rebuild_child::<()>(&prev, &next, &mut pod, &mut ctx(&mut counter));
1294
1295        assert!(
1296            !pod.is_active(),
1297            "a type-swapped child's stale capture must be dropped"
1298        );
1299    }
1300
1301    #[test]
1302    fn rebuild_child_preserves_active_on_same_type_rebuild() {
1303        // Negative guard against over-clearing: a same-type, content-only
1304        // rebuild (no AnyView type swap) must NOT touch an in-flight capture —
1305        // mirrors rebuild_children's content_only_rebuild_preserves_captured_drag.
1306        let mut counter = 0u64;
1307        let prev: AnyView<()> = leaf_any(10.0, 10.0);
1308        let mut pod = build_child(&prev, &mut ctx(&mut counter));
1309        pod.set_active(true);
1310
1311        let next: AnyView<()> = leaf_any(20.0, 20.0);
1312        rebuild_child::<()>(&prev, &next, &mut pod, &mut ctx(&mut counter));
1313
1314        assert!(
1315            pod.is_active(),
1316            "a content-only rebuild must not clear an in-flight capture"
1317        );
1318    }
1319
1320    /// [`rebuild_child`]'s type-swap marking site — the single-child wrappers
1321    /// (`Padding`/`Align`/`SizedBox`/`GestureDetector`/`Scroll`, the card/dialog
1322    /// slot helpers, `ListView`'s surviving rows) — pinned in both directions.
1323    /// `Padding(if editing { text_input } else { text })` is the shipped shape:
1324    /// the focused widget dies inside the swap, so a live session dies with it,
1325    /// while the same swap over a stale flag under a blurred ancestor owns no
1326    /// session to lose.
1327    #[test]
1328    fn a_wrapper_type_swap_marks_only_on_a_live_chain() {
1329        fn swap_wrapped_child(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
1330            let _ = frust_core::take_focus_orphaned();
1331            let mut counter = 0u64;
1332            let prev: AnyView<()> = leaf_any(10.0, 10.0);
1333            let mut pod = build_child(&prev, &mut ctx(&mut counter));
1334            // The wrapper's single child holds the recorded focus path.
1335            pod.set_focused(true);
1336
1337            // Leaf -> SwapLeaf: a concrete-type change, so the old widget dies
1338            // inside `AnyView::rebuild` and a fresh one takes its pod.
1339            let next: AnyView<()> = swap_leaf().into_any();
1340            rebuild_child::<()>(&prev, &next, &mut pod, &mut build(&mut counter));
1341            assert!(
1342                !pod.is_focused(),
1343                "a swapped-in widget must not inherit the old focus link"
1344            );
1345            frust_core::take_focus_orphaned()
1346        }
1347
1348        assert!(
1349            swap_wrapped_child(&mut ctx),
1350            "a live focus path dying inside a wrapper's type swap releases the session"
1351        );
1352        assert!(
1353            !swap_wrapped_child(&mut blurred_ctx),
1354            "the same swap over a stale flag below a cleared link marks nothing"
1355        );
1356    }
1357
1358    /// The negative guard for the site above: a same-type, content-only rebuild
1359    /// through a wrapper keeps the focus path (Flutter's retention invariant),
1360    /// so it must neither clear the flag nor mark — a mark here would drop the
1361    /// keyboard on every frame a padded field re-renders.
1362    #[test]
1363    fn a_wrapper_content_only_rebuild_keeps_focus_and_marks_nothing() {
1364        let _ = frust_core::take_focus_orphaned();
1365        let mut counter = 0u64;
1366        let prev: AnyView<()> = leaf_any(10.0, 10.0);
1367        let mut pod = build_child(&prev, &mut ctx(&mut counter));
1368        pod.set_focused(true);
1369
1370        let next: AnyView<()> = leaf_any(20.0, 20.0);
1371        rebuild_child::<()>(&prev, &next, &mut pod, &mut ctx(&mut counter));
1372
1373        assert!(
1374            pod.is_focused(),
1375            "a content-only rebuild must not clear the recorded focus path"
1376        );
1377        assert!(
1378            !frust_core::take_focus_orphaned(),
1379            "an unchanged wrapper child must not orphan the focus session"
1380        );
1381    }
1382
1383    /// The producer half of the generic-unmount focus release: a reconciler that
1384    /// severs a recorded focus path raises the orphan mark, which
1385    /// `RenderRoot::rebuild` drains into a full session release (the consumer
1386    /// half is pinned in `frust-core`'s
1387    /// `generic_unmount_orphan_releases_the_whole_session`).
1388    #[test]
1389    fn a_truncated_focused_child_marks_the_focus_orphan() {
1390        let _ = frust_core::take_focus_orphaned();
1391        let mut counter = 0u64;
1392        let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
1393        let mut pods: Vec<ChildPod> = prev
1394            .iter()
1395            .map(|v| build_child(v, &mut ctx(&mut counter)))
1396            .collect();
1397        // The second child holds the recorded focus path.
1398        pods[1].set_focused(true);
1399        assert!(
1400            !frust_core::take_focus_orphaned(),
1401            "building a list orphans nothing"
1402        );
1403
1404        // Shrink the list: the focused child is torn down and dropped.
1405        let next: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0)];
1406        rebuild_children(
1407            &prev,
1408            &next,
1409            &mut pods,
1410            &mut ctx(&mut counter),
1411            |v: &AnyView<()>| v,
1412            |_: &AnyView<()>| None::<ChildKey>,
1413        );
1414        assert_eq!(pods.len(), 1);
1415        assert!(
1416            frust_core::take_focus_orphaned(),
1417            "the focused child's unmount must be reported to the render root"
1418        );
1419    }
1420
1421    /// The cross-branch guard for the [`teardown_child`] marking site: the same
1422    /// truncation as above, but with the container's own link to the root already
1423    /// cleared (a blur moved focus to another branch, leaving this pod's flag
1424    /// stale). The stale pod owns no session, so its unmount must mark nothing —
1425    /// marking here is what dropped the keyboard out of an unrelated, still-live
1426    /// field when a list recycled a row.
1427    #[test]
1428    fn a_truncated_stale_focused_child_under_a_blurred_link_marks_nothing() {
1429        let _ = frust_core::take_focus_orphaned();
1430        let mut counter = 0u64;
1431        let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
1432        let mut pods: Vec<ChildPod> = prev
1433            .iter()
1434            .map(|v| build_child(v, &mut ctx(&mut counter)))
1435            .collect();
1436        // Stale: the flag is set, but the chain above this container is broken.
1437        pods[1].set_focused(true);
1438
1439        let next: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0)];
1440        rebuild_children(
1441            &prev,
1442            &next,
1443            &mut pods,
1444            &mut blurred_ctx(&mut counter),
1445            |v: &AnyView<()>| v,
1446            |_: &AnyView<()>| None::<ChildKey>,
1447        );
1448        assert_eq!(pods.len(), 1);
1449        assert!(
1450            !frust_core::take_focus_orphaned(),
1451            "a stale flag below a cleared link owns no session — releasing here \
1452             would kill the focus of whatever field really is focused"
1453        );
1454    }
1455
1456    /// The [`cancel_active_children`] marking site (the cleared tail past the
1457    /// first type swap), pinned in both directions: it marks on a live chain and
1458    /// stays silent under a blurred one.
1459    #[test]
1460    fn a_cleared_tail_marks_only_on_a_live_chain() {
1461        // Index 0 type-swaps (Leaf -> SwapLeaf), so the tail from 0 onward has
1462        // its recorded paths cleared; index 1 holds the focus flag.
1463        fn shrink_tail(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
1464            let _ = frust_core::take_focus_orphaned();
1465            let mut counter = 0u64;
1466            let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
1467            let mut pods: Vec<ChildPod> = prev
1468                .iter()
1469                .map(|v| build_child(v, &mut ctx(&mut counter)))
1470                .collect();
1471            pods[1].set_focused(true);
1472            let next: Vec<AnyView<()>> = vec![swap_leaf().into_any(), leaf_any(10.0, 10.0)];
1473            rebuild_children(
1474                &prev,
1475                &next,
1476                &mut pods,
1477                &mut build(&mut counter),
1478                |v: &AnyView<()>| v,
1479                |_: &AnyView<()>| None::<ChildKey>,
1480            );
1481            assert!(!pods[1].is_focused(), "the cleared tail drops the flag");
1482            frust_core::take_focus_orphaned()
1483        }
1484
1485        assert!(
1486            shrink_tail(&mut ctx),
1487            "a live focus link cleared by the tail sweep releases the session"
1488        );
1489        assert!(
1490            !shrink_tail(&mut blurred_ctx),
1491            "the same sweep over a stale flag below a cleared link marks nothing"
1492        );
1493    }
1494
1495    /// The keyed reconciler's type-swap marking site, pinned in both directions:
1496    /// a key reused for a different concrete type breaks identity, so a *live*
1497    /// focus link dies with it — but a stale one under a blurred ancestor does
1498    /// not.
1499    #[test]
1500    fn a_keyed_type_swap_marks_only_on_a_live_chain() {
1501        fn swap_keyed(build: &mut dyn FnMut(&mut u64) -> BuildCtx<'_>) -> bool {
1502            let _ = frust_core::take_focus_orphaned();
1503            let mut counter = 0u64;
1504            let key = ChildKey::new(1u32);
1505            let prev: Vec<(AnyView<()>, ChildKey)> = vec![(leaf_any(10.0, 10.0), key)];
1506            let mut pods: Vec<ChildPod> = prev
1507                .iter()
1508                .map(|c| build_child(&c.0, &mut ctx(&mut counter)))
1509                .collect();
1510            pods[0].set_focused(true);
1511            // Same key, different concrete view type: a swap, not a relocation.
1512            let next: Vec<(AnyView<()>, ChildKey)> = vec![(swap_leaf().into_any(), key)];
1513            rebuild_children(
1514                &prev,
1515                &next,
1516                &mut pods,
1517                &mut build(&mut counter),
1518                |c: &(AnyView<()>, ChildKey)| &c.0,
1519                |c: &(AnyView<()>, ChildKey)| Some(c.1),
1520            );
1521            assert!(!pods[0].is_focused(), "a swapped-away pod drops the flag");
1522            frust_core::take_focus_orphaned()
1523        }
1524
1525        assert!(
1526            swap_keyed(&mut ctx),
1527            "a live focus path dying inside a keyed swap releases the session"
1528        );
1529        assert!(
1530            !swap_keyed(&mut blurred_ctx),
1531            "a stale flag dying inside a keyed swap releases nothing"
1532        );
1533    }
1534
1535    /// The chain must compose through **nesting**, root → container → child: the
1536    /// blur clears the link at the nearest common ancestor (the outer container's
1537    /// pod), and the leaf's own flag two levels down stays stale. Tearing that
1538    /// leaf out must mark nothing — while the identical teardown with the outer
1539    /// link intact must still mark.
1540    #[test]
1541    fn the_focus_chain_composes_through_nested_containers() {
1542        fn drop_inner_leaf(outer_link_focused: bool) -> bool {
1543            let _ = frust_core::take_focus_orphaned();
1544            let mut counter = 0u64;
1545            let prev: Vec<AnyView<()>> = vec![any(NestView {
1546                children: vec![leaf_any(10.0, 10.0)],
1547            })];
1548            let mut pods: Vec<ChildPod> = prev
1549                .iter()
1550                .map(|v| build_child(v, &mut ctx(&mut counter)))
1551                .collect();
1552            // The leaf deep inside holds the focus flag either way; only the
1553            // OUTER link differs — that is the whole experiment.
1554            nest_in(&mut pods[0]).children[0].set_focused(true);
1555            pods[0].set_focused(outer_link_focused);
1556
1557            // The inner container loses its only child (a filtered/recycled row).
1558            let next: Vec<AnyView<()>> = vec![any(NestView { children: vec![] })];
1559            rebuild_children(
1560                &prev,
1561                &next,
1562                &mut pods,
1563                &mut ctx(&mut counter),
1564                |v: &AnyView<()>| v,
1565                |_: &AnyView<()>| None::<ChildKey>,
1566            );
1567            assert!(
1568                nest_in(&mut pods[0]).children.is_empty(),
1569                "the inner child was torn down"
1570            );
1571            frust_core::take_focus_orphaned()
1572        }
1573
1574        assert!(
1575            drop_inner_leaf(true),
1576            "an unbroken chain root→container→leaf is a live session; its unmount releases it"
1577        );
1578        assert!(
1579            !drop_inner_leaf(false),
1580            "a cleared link at the nearest common ancestor makes every flag below it inert"
1581        );
1582    }
1583
1584    /// The negative guard: an ordinary content-only rebuild keeps the focus path
1585    /// (Flutter's retention invariant) and must therefore orphan nothing — a mark
1586    /// raised here would drop the keyboard on every frame.
1587    #[test]
1588    fn a_content_only_rebuild_marks_no_focus_orphan() {
1589        let _ = frust_core::take_focus_orphaned();
1590        let mut counter = 0u64;
1591        let prev: Vec<AnyView<()>> = vec![leaf_any(10.0, 10.0), leaf_any(10.0, 10.0)];
1592        let mut pods: Vec<ChildPod> = prev
1593            .iter()
1594            .map(|v| build_child(v, &mut ctx(&mut counter)))
1595            .collect();
1596        pods[1].set_focused(true);
1597
1598        let next: Vec<AnyView<()>> = vec![leaf_any(20.0, 20.0), leaf_any(20.0, 20.0)];
1599        rebuild_children(
1600            &prev,
1601            &next,
1602            &mut pods,
1603            &mut ctx(&mut counter),
1604            |v: &AnyView<()>| v,
1605            |_: &AnyView<()>| None::<ChildKey>,
1606        );
1607        assert!(pods[1].is_focused(), "the focus path survives intact");
1608        assert!(
1609            !frust_core::take_focus_orphaned(),
1610            "an unchanged child must not orphan the focus session"
1611        );
1612    }
1613}
1614
1615/// Focus-path routing tests for [`route_event`]/[`route_event_single`]: Key/IME
1616/// events reach only the focused child (including through a nested container),
1617/// blur-on-outside-tap breaks the chain, a second focus request moves focus,
1618/// `ApplyEditingState` routes to the focused child, a published IME surface
1619/// bubbles up, and the capture and focus paths stay independent.
1620#[cfg(test)]
1621mod focus_tests {
1622    use super::*;
1623    use frust_core::{
1624        BoxConstraints, EditingState, EventCtx, ImeEvent, ImeState, Key, KeyEvent, LayoutCtx,
1625        Modifiers, PaintCtx, PaintScene,
1626    };
1627    use kurbo::{Point, Rect, Size};
1628
1629    /// A leaf that: on a pointer `Down` optionally requests focus and records
1630    /// `id + 100`; on a `Move` (capture path) records `id + 200`; on a Key/IME
1631    /// event records `id` and optionally publishes an IME surface. All state is a
1632    /// shared `Vec<u32>` so tests can assert *which* leaf saw *what*.
1633    struct KeyLeaf {
1634        id: u32,
1635        takes_focus: bool,
1636        publish_ime: bool,
1637    }
1638
1639    impl Widget for KeyLeaf {
1640        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1641            bc.max()
1642        }
1643        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1644        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1645            match event {
1646                InputEvent::Pointer(p) => match p.phase {
1647                    PointerPhase::Down => {
1648                        if self.takes_focus {
1649                            ctx.request_focus();
1650                        }
1651                        ctx.state_mut::<Vec<u32>>().push(self.id + 100);
1652                        EventResult::Handled
1653                    }
1654                    PointerPhase::Move => {
1655                        ctx.state_mut::<Vec<u32>>().push(self.id + 200);
1656                        EventResult::Handled
1657                    }
1658                    _ => EventResult::Handled,
1659                },
1660                InputEvent::Key(_) | InputEvent::Ime(_) => {
1661                    ctx.state_mut::<Vec<u32>>().push(self.id);
1662                    if self.publish_ime {
1663                        ctx.publish_ime_state(ImeState {
1664                            active: true,
1665                            editing: EditingState {
1666                                text: format!("leaf{}", self.id),
1667                                selection_base: 1,
1668                                selection_extent: 1,
1669                                composing_base: -1,
1670                                composing_extent: -1,
1671                            },
1672                            caret: Some(Rect::new(0.0, 0.0, 1.0, 10.0)),
1673                            content_type: Default::default(),
1674                            suppress_soft_keyboard: false,
1675                        });
1676                    }
1677                    EventResult::Handled
1678                }
1679                _ => EventResult::Ignored,
1680            }
1681        }
1682    }
1683
1684    /// A minimal multi-child container: stacks its children vertically (each 10
1685    /// tall at the given width) and routes events through [`route_event`] — the
1686    /// same helper the real `Flex`/`Stack` use — so focus routing is exercised
1687    /// through a nested container.
1688    struct Nest {
1689        children: Vec<ChildPod>,
1690    }
1691
1692    impl Widget for Nest {
1693        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1694            let w = bc.max().width;
1695            for (i, pod) in self.children.iter_mut().enumerate() {
1696                pod.set_origin(Point::new(0.0, i as f64 * 10.0));
1697                pod.layout_child(ctx, &BoxConstraints::tight(Size::new(w, 10.0)));
1698            }
1699            Size::new(w, self.children.len() as f64 * 10.0)
1700        }
1701        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1702        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1703            route_event(&mut self.children, ctx, event)
1704        }
1705    }
1706
1707    fn leaf_pod(id: u32, takes_focus: bool, publish_ime: bool) -> ChildPod {
1708        ChildPod::new(Box::new(KeyLeaf {
1709            id,
1710            takes_focus,
1711            publish_ime,
1712        }))
1713    }
1714
1715    /// Lay out a slice of pods vertically (10 tall each at width 100) so hit
1716    /// testing distinguishes them: child `i` occupies `y ∈ [i*10, i*10+10)`.
1717    fn lay_out_vertically(pods: &mut [ChildPod]) {
1718        let mut lctx = LayoutCtx::new();
1719        for (i, pod) in pods.iter_mut().enumerate() {
1720            pod.set_origin(Point::new(0.0, i as f64 * 10.0));
1721            pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 10.0)));
1722        }
1723    }
1724
1725    fn down(y: f64) -> InputEvent {
1726        InputEvent::Pointer(PointerEvent {
1727            phase: PointerPhase::Down,
1728            position: Point::new(5.0, y),
1729            button: PointerButton::Primary,
1730        })
1731    }
1732
1733    fn mv(y: f64) -> InputEvent {
1734        InputEvent::Pointer(PointerEvent {
1735            phase: PointerPhase::Move,
1736            position: Point::new(5.0, y),
1737            button: PointerButton::Primary,
1738        })
1739    }
1740
1741    fn key() -> InputEvent {
1742        InputEvent::Key(KeyEvent {
1743            key: Key::Character("a".to_string()),
1744            modifiers: Modifiers::default(),
1745            repeat: false,
1746        })
1747    }
1748
1749    fn ime_apply() -> InputEvent {
1750        InputEvent::Ime(ImeEvent::ApplyEditingState(EditingState {
1751            text: "sync".to_string(),
1752            selection_base: 4,
1753            selection_extent: 4,
1754            composing_base: -1,
1755            composing_extent: -1,
1756        }))
1757    }
1758
1759    // `state` must be a concrete `&mut Vec<u32>`: it is erased to `&mut dyn Any`
1760    // and the leaves recover it with `state_mut::<Vec<u32>>()`, so a slice would
1761    // fail the downcast.
1762    #[allow(clippy::ptr_arg)]
1763    fn route(children: &mut [ChildPod], state: &mut Vec<u32>, event: &InputEvent) -> EventResult {
1764        let mut ctx = EventCtx::new(state, Point::ZERO, Size::new(100.0, 100.0));
1765        route_event(children, &mut ctx, event)
1766    }
1767
1768    #[test]
1769    fn key_routes_only_to_focused_child() {
1770        let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
1771        lay_out_vertically(&mut children);
1772        let mut state = Vec::new();
1773
1774        // Tap child 2 (y in [10,20)) → it requests focus.
1775        route(&mut children, &mut state, &down(15.0));
1776        assert!(children[1].is_focused());
1777        assert!(!children[0].is_focused());
1778        state.clear();
1779
1780        // A Key event reaches ONLY the focused child (id 2), never child 1.
1781        route(&mut children, &mut state, &key());
1782        assert_eq!(state, vec![2]);
1783    }
1784
1785    #[test]
1786    fn key_reaches_focused_leaf_through_nested_container() {
1787        // Outer container holds one Nest; the Nest holds two leaves.
1788        let inner = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
1789        let mut nest = Nest { children: inner };
1790        // Lay out the nest so its inner children get real geometry.
1791        {
1792            let mut lctx = LayoutCtx::new();
1793            nest.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 20.0)));
1794        }
1795        let mut outer = vec![ChildPod::new(Box::new(nest))];
1796        outer[0].set_origin(Point::ZERO);
1797        {
1798            let mut lctx = LayoutCtx::new();
1799            outer[0].layout_child(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 20.0)));
1800        }
1801        let mut state = Vec::new();
1802
1803        // Tap the second inner leaf (y in [10,20)) → focus chain nest→leaf2.
1804        route(&mut outer, &mut state, &down(15.0));
1805        assert!(outer[0].is_focused(), "the nest records the focus path");
1806        state.clear();
1807
1808        // Key routes outer→nest→leaf2 only.
1809        route(&mut outer, &mut state, &key());
1810        assert_eq!(state, vec![2]);
1811    }
1812
1813    #[test]
1814    fn blur_on_outside_tap_clears_focus_chain() {
1815        // Child 1 takes focus; child 2 does NOT (a non-editable widget).
1816        let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, false, false)];
1817        lay_out_vertically(&mut children);
1818        let mut state = Vec::new();
1819
1820        route(&mut children, &mut state, &down(5.0)); // focus child 1
1821        assert!(children[0].is_focused());
1822
1823        // Tap child 2 (does not take focus) → blur clears the focus chain.
1824        route(&mut children, &mut state, &down(15.0));
1825        assert!(
1826            !children[0].is_focused(),
1827            "outside tap blurs the focused child"
1828        );
1829        assert!(!children[1].is_focused());
1830        state.clear();
1831
1832        // A Key event now reaches nobody.
1833        let result = route(&mut children, &mut state, &key());
1834        assert_eq!(result, EventResult::Ignored);
1835        assert!(state.is_empty());
1836    }
1837
1838    #[test]
1839    fn tap_inside_focused_widget_keeps_focus() {
1840        // A focused widget re-tapped keeps focus (Down inside focused widget).
1841        let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, false, false)];
1842        lay_out_vertically(&mut children);
1843        let mut state = Vec::new();
1844
1845        route(&mut children, &mut state, &down(5.0)); // focus child 1
1846        route(&mut children, &mut state, &down(5.0)); // tap child 1 again
1847        assert!(
1848            children[0].is_focused(),
1849            "a tap inside the focused widget keeps focus"
1850        );
1851        state.clear();
1852        route(&mut children, &mut state, &key());
1853        assert_eq!(state, vec![1]);
1854    }
1855
1856    #[test]
1857    fn second_focus_request_moves_focus() {
1858        let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
1859        lay_out_vertically(&mut children);
1860        let mut state = Vec::new();
1861
1862        route(&mut children, &mut state, &down(5.0)); // focus child 1
1863        assert!(children[0].is_focused());
1864        route(&mut children, &mut state, &down(15.0)); // focus child 2
1865        assert!(children[1].is_focused());
1866        assert!(!children[0].is_focused(), "focus moved off child 1");
1867        state.clear();
1868
1869        route(&mut children, &mut state, &key());
1870        assert_eq!(
1871            state,
1872            vec![2],
1873            "key now reaches only the newly focused child"
1874        );
1875    }
1876
1877    #[test]
1878    fn ime_apply_routes_to_focused_child() {
1879        let mut children = vec![leaf_pod(1, true, false), leaf_pod(2, true, false)];
1880        lay_out_vertically(&mut children);
1881        let mut state = Vec::new();
1882
1883        route(&mut children, &mut state, &down(15.0)); // focus child 2
1884        state.clear();
1885        route(&mut children, &mut state, &ime_apply());
1886        assert_eq!(state, vec![2]);
1887    }
1888
1889    #[test]
1890    fn capture_and_focus_paths_are_independent() {
1891        // Child 1 holds the capture (active) path; child 2 holds the focus path.
1892        let mut children = vec![leaf_pod(1, false, false), leaf_pod(2, false, false)];
1893        lay_out_vertically(&mut children);
1894        children[0].set_active(true);
1895        children[1].set_focused(true);
1896        let mut state = Vec::new();
1897
1898        // A Move routes down the capture path → child 1 only (id+200).
1899        route(&mut children, &mut state, &mv(999.0));
1900        assert_eq!(state, vec![201]);
1901        state.clear();
1902
1903        // A Key routes down the focus path → child 2 only (id).
1904        route(&mut children, &mut state, &key());
1905        assert_eq!(state, vec![2]);
1906
1907        // Both paths survive intact.
1908        assert!(children[0].is_active());
1909        assert!(children[1].is_focused());
1910    }
1911}