Skip to main content

frust_core/
app.rs

1//! The render root: the object each platform shell drives each frame.
2//!
3//! It owns the widget [`WidgetTree`] and the previous [`View`], and exposes the
4//! three framework passes in Masonry order (the subset relevant to v0):
5//!
6//! * [`RenderRoot::rebuild`] — run the root build closure, diff against the previous view,
7//!   producing/mutating the retained widget.
8//! * [`RenderRoot::layout`] — hand the root widget window-sized constraints and
9//!   record the size it returns.
10//! * [`RenderRoot::paint`] — emit the root widget's draw commands into a scene.
11//!
12//! v0 is single-root: the root component's build closure returns one `impl View<State>` whose concrete
13//! type is fixed, so the root's previous view and element are stored typed.
14//! ViewSequence / multiple children are explicitly out of scope for now.
15
16use std::any::Any;
17use std::cell::Cell;
18use std::num::NonZeroU64;
19use std::sync::atomic::{AtomicU64, Ordering};
20
21use kurbo::{Point, Rect, Size};
22
23use crate::anim::FrameTime;
24use crate::event::{
25    ContactFrame, ContactPass, CursorIcon, EventCtx, EventOutcome, EventResult, FileDropEvent,
26    FileDropPhase, ImeState, InputEvent, OverlayEvent, OverlayEventKind, PointerButton,
27    PointerEvent, PointerId, PointerPhase, RequestPass,
28};
29use crate::insets::WindowInsets;
30use crate::layout::BoxConstraints;
31use crate::overlay::{
32    OutsideTap, OverlayEntry, OverlayHit, OverlayInput, OverlayKey, OverlayPaintPass,
33    sort_into_paint_order,
34};
35use crate::selection_toolbar::{
36    SelectionToolbarActions, SelectionToolbarPass, SelectionToolbarRequest,
37};
38use crate::semantics::{ROOT_NODE_ID, SemanticsCtx, SemanticsUpdate};
39use crate::tree::{InspectNode, WidgetPod, WidgetTree};
40use crate::view::{BuildCtx, ChangeFlags, View, WidgetId};
41use crate::widget::{LayoutCtx, PaintCtx, PaintOutcome, PaintScene, PlatformViewFrame};
42
43/// The window's shape and platform-occlusion state, delivered to app code as a
44/// plain [`provide_context`](reactive_graph::owner::provide_context)-carried
45/// value — logical size, device-pixel scale, a
46/// [derived](Orientation::from_size) orientation, and the current
47/// [`WindowInsets`].
48///
49/// # Plain value, not a signal
50///
51/// `WindowMetrics` is delivered exactly like `Theme` and [`WindowInsets`]
52/// already are: a shell calls `provide_context` with a freshly-built value on
53/// change, and app code recovers it with `use_context::<WindowMetrics>()`
54/// inside `Component::build`. It is **not** an `RwSignal` — only `deep_link`
55/// and `back` are true signals in `frust-reactive`; every other host-signal
56/// carrier (theme, insets, and now this) is a re-provided plain value.
57///
58/// # Alongside `WindowInsets`, not superseding it
59///
60/// `WindowInsets` already reaches `Component::build` on Android and iOS today
61/// (each shell's `push_insets` calls `provide_context(insets)` independently
62/// of anything here — desktop has no such arm for either value yet).
63/// `WindowMetrics` is additive: a shell that starts providing it keeps
64/// providing the standalone `WindowInsets` context too, so an existing
65/// `use_context::<WindowInsets>()` call site never breaks. `insets` on this
66/// type is a **copy** of that same value for convenience (a widget laying
67/// itself out around window shape wants size/scale/orientation/insets
68/// together), not a replacement for the independent context.
69///
70/// # Orientation is derived, not platform-sourced
71///
72/// No platform callback in either mobile shell carries an orientation enum —
73/// Android's `nativeOnSurfaceChanged` and iOS's `frust_resize` each hand the
74/// shell only a `(width, height, scale)` triple. [`Orientation`] is therefore
75/// always computed from `size` via [`Orientation::from_size`]
76/// (portrait when `height >= width`, so an exact square reads as portrait);
77/// it never tracks a device orientation-lock setting or a platform rotation
78/// event directly.
79///
80/// # Context is not reactive
81///
82/// `provide_context` is a plain insert into the owner's context map — it
83/// notifies nothing — and `use_context` inside `Component::build` (or the
84/// root build closure) creates no subscription, so re-providing a changed
85/// `WindowMetrics` does not itself mark anything dirty or wake a frame. A new
86/// value becomes visible only on the next rebuild, which the resize or inset
87/// change that produced it already drives; do not write a shell that assumes
88/// a `provide_context` write triggers one. A shell wiring this up (see
89/// `docs/SHELLS_ARCHITECTURE.md`) must still re-provide `WindowMetrics` only
90/// on an actual change (mirroring `RenderRoot::set_insets`'s
91/// `PartialEq`-guarded no-op) — the reason is cost at the FFI boundary (a
92/// lock write plus an allocation every frame), not a rebuild storm.
93/// Separately, there is no per-component rebuild skipping in this framework
94/// (a component always re-runs `build` on any rebuild it does take part in),
95/// which is affordable only because builds are cheap by construction.
96#[derive(Clone, Copy, Debug, PartialEq)]
97pub struct WindowMetrics {
98    /// The window's logical (density-independent) size.
99    pub size: Size,
100    /// The device-pixel scale factor (logical → physical px multiplier).
101    pub scale: f64,
102    /// The orientation derived from `size` — see the type's doc for why this
103    /// is computed, never platform-sourced.
104    pub orientation: Orientation,
105    /// A copy of the window's current insets — see the type's doc for why
106    /// this does not replace the standalone `WindowInsets` context.
107    pub insets: WindowInsets,
108}
109
110impl WindowMetrics {
111    /// Construct a [`WindowMetrics`] from its transported fields, deriving
112    /// [`orientation`](Self::orientation) from `size` rather than accepting it
113    /// as an input — see the type's doc for why orientation is never
114    /// platform-sourced.
115    pub fn new(size: Size, scale: f64, insets: WindowInsets) -> Self {
116        Self {
117            size,
118            scale,
119            orientation: Orientation::from_size(size),
120            insets,
121        }
122    }
123}
124
125/// A window's derived portrait/landscape orientation.
126///
127/// Always computed from a [`WindowMetrics::size`] via [`Orientation::from_size`]
128/// — see [`WindowMetrics`]'s doc for why no platform callback carries this as
129/// an enum directly.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum Orientation {
132    /// `size.height >= size.width`, including the exact-square case.
133    Portrait,
134    /// `size.height < size.width`.
135    Landscape,
136}
137
138impl Orientation {
139    /// Derives orientation from a logical window size: portrait when
140    /// `height >= width` (an exact square reads as portrait), landscape
141    /// otherwise.
142    pub fn from_size(size: Size) -> Self {
143        if size.height >= size.width {
144            Orientation::Portrait
145        } else {
146            Orientation::Landscape
147        }
148    }
149}
150
151/// How many [`InputEvent::Housekeeping`] flush passes one
152/// [`RenderRoot::rebuild`] will run before deferring the rest to the next frame.
153///
154/// A flushed pop-result callback may itself push or pop, queueing another
155/// callback — so the flush/re-diff cycle has to be allowed to iterate, but it
156/// must never be allowed to spin: a pair of callbacks that push each other would
157/// otherwise hang the frame. Three passes covers every shape observed in
158/// practice (a result that navigates once, and that page's own result), while
159/// keeping the worst case at four build-closure runs per frame — the closure is
160/// cheap by construction (see [`RenderRoot::rebuild`]).
161///
162/// Past the cap the mark stays raised and one more frame is requested, so the
163/// remaining work lands next frame instead of being lost.
164const MAX_PENDING_RESULT_FLUSH_PASSES: usize = 3;
165
166/// Change-guarded write of the shell-facing IME surface: replaces `slot` and
167/// bumps `generation` **only** when the value actually moves ([`ImeState`] is
168/// `PartialEq`).
169///
170/// A free function over the two fields rather than a `&mut self` method, so the
171/// change guard has exactly one implementation whatever borrows its caller
172/// happens to hold; [`RenderRoot::store_ime_state`] is the `&mut self` form, and
173/// is what every current caller goes through.
174///
175/// The change guard is load-bearing, not an optimisation: the paint pass
176/// re-publishes the focused widget's IME surface every frame, so an
177/// unconditional bump would make the shell's `focus_or_ime_changed` edge fire on
178/// every vsync for the whole life of a focus session — the level-input behavior
179/// the generation exists to replace.
180fn store_ime_state_in(slot: &mut Option<ImeState>, generation: &mut u64, next: Option<ImeState>) {
181    if *slot != next {
182        *slot = next;
183        *generation = generation.wrapping_add(1);
184    }
185}
186
187/// Release the whole focus/IME session: drop `focus_active` **and** the
188/// shell-facing surface together, bumping `generation` **exactly once** if
189/// either actually moved.
190///
191/// The paired form of [`RenderRoot::set_focus_active`]`(false)` +
192/// [`RenderRoot::store_ime_state`]`(None)`, and the single primitive every
193/// release site goes through — the blur-on-outside-tap `Down`, an explicit
194/// [`EventCtx::release_focus`](crate::event::EventCtx::release_focus), a widget
195/// publishing an *inactive* surface (see [`RenderRoot::paint`]), and the
196/// generic-unmount orphan drain in [`RenderRoot::rebuild`]. Keeping them on one
197/// primitive is what makes "a session ends" mean the same thing everywhere,
198/// rather than four hand-assembled pairs that can drift apart.
199///
200/// **One release is one edge.** The two field writers bump on each field's own
201/// change, so calling them in sequence would move
202/// [`focus_ime_generation`](RenderRoot::focus_ime_generation) *twice* for the
203/// ordinary release (focus `true`→`false` and surface `Some`→`None`). A shell
204/// only ever compares the value, so two bumps and one bump raise the same single
205/// `focus_or_ime_changed` edge — but a counter that moves once per observable
206/// transition is the contract the field doc states, and is what the release
207/// tests pin. The change guard itself is unchanged: an already-released root
208/// writes the same values back and moves nothing.
209///
210/// **A release ends the session's identity too.** On an actual move it advances
211/// `focus_epoch` and republishes it, which strands every recorded focus link at
212/// once — the chain the session ran through, and a floated surface's link that
213/// no container's blur sweep can reach. That is why the release has to own the
214/// epoch rather than leave it to the caller: a session cleared without moving
215/// its identity leaves links behind that still name it.
216///
217/// A free function over the fields (not a `&mut self` method) for the same
218/// reason [`store_ime_state_in`] is — one implementation of the contract,
219/// whatever borrows the caller holds. [`RenderRoot::release_focus_session`] is
220/// the method form, and is what every current caller goes through.
221fn release_focus_session_in(
222    focus_active: &mut bool,
223    ime_slot: &mut Option<ImeState>,
224    generation: &mut u64,
225    focus_epoch: &mut u64,
226    root_identity: u64,
227) {
228    let moved = *focus_active || ime_slot.is_some();
229    *focus_active = false;
230    *ime_slot = None;
231    if moved {
232        *generation = generation.wrapping_add(1);
233        // A session that ends strands every link recorded against it, wherever
234        // in (or off) the tree it sits — the one clearing sweep no container can
235        // be asked to run. Only on an actual release: a `Down` on already-blurred
236        // chrome is the commonest event there is and must move nothing.
237        *focus_epoch = advance_focus_epoch(*focus_epoch);
238        crate::widget::set_live_focus_session(root_identity, *focus_epoch, *focus_epoch);
239    }
240}
241
242/// The allocator behind [`RenderRoot::root_identity`], handing every root a
243/// value no other root shares.
244///
245/// Starts at `1` so `0` stays available as "no root" (a pod that has never held a
246/// claim, and the at-rest published hover link — see
247/// `crate::event::set_live_hover_link`).
248///
249/// A module-level static rather than an associated const/`static` inside the
250/// generic `impl`: the latter is monomorphized per `<State, V>` pair, which would
251/// hand two roots of different concrete types the same identity — exactly the
252/// collision this counter exists to remove. `Relaxed` is enough because the value
253/// is only ever compared for equality, never used to order anything.
254static NEXT_ROOT_IDENTITY: AtomicU64 = AtomicU64::new(1);
255
256/// The next focus epoch after `epoch`, skipping `0`.
257///
258/// `0` is reserved twice over — it is a never-claimed
259/// [`ChildPod`](crate::widget::ChildPod)'s stamp, and it is the epoch half of
260/// the `(0, 0)` pair a dispatch driven with no root at all compares against — so
261/// a root that wrapped onto it would hand every unclaimed pod in the tree a live
262/// link at once.
263fn advance_focus_epoch(epoch: u64) -> u64 {
264    match epoch.wrapping_add(1) {
265        0 => 1,
266        next => next,
267    }
268}
269
270/// What [`RenderRoot`]'s overlay pre-pass decided about one incoming event.
271///
272/// The pre-pass runs before anything else [`RenderRoot::event`] does, and has
273/// exactly two answers: the event belonged to a floated surface (or was swallowed
274/// by a modal light-dismiss) and the main tree must not see it, or it did not and
275/// today's dispatch continues. Both arms carry an [`EventOutcome`], because even
276/// the "continue" answer may already have produced one — an
277/// [`OutsideTap::Notify`]`{ consume: false }` surface is told about the press
278/// *and* lets it through, and the redraw that notification asked for must not be
279/// dropped on the floor when the main dispatch's own outcome replaces it.
280enum OverlayRoute {
281    /// The overlay layer consumed the event; return this outcome unchanged.
282    Consumed(EventOutcome),
283    /// The event continues into today's dispatch; merge this outcome into
284    /// whatever that produces.
285    Continue(EventOutcome),
286}
287
288/// Owns the retained tree and drives the rebuild/layout/paint passes for a
289/// single-root application.
290///
291/// Generic over the application `State` and the concrete root view type `V`
292/// returned by the build closure.
293pub struct RenderRoot<State: 'static, V: View<State>> {
294    tree: WidgetTree,
295    root_id: Option<WidgetId>,
296    /// The previous view, retained to diff against on the next rebuild.
297    prev_view: Option<V>,
298    /// Monotonic widget-id counter, borrowed by each `BuildCtx`.
299    next_id: u64,
300    window_size: Size,
301    /// The **claimant** of the pointer capture in flight, if any: the contact
302    /// whose `Down` requested capture. Set on that `Down`, cleared only by the
303    /// claimant's own `Up`/`Cancel` — another contact's release never touches
304    /// it (rule (c) of [`InputEvent::PointerContact`]'s multi-contact contract).
305    /// Root-level mirror of the per-container `active` path bookkeeping, which
306    /// is keyed on the same claimant (see [`crate::widget::ChildPod::set_active`]).
307    capture_claimant: Option<PointerId>,
308    /// Whether the live capture's captor opted into the gesture's other
309    /// contacts ([`EventCtx::capture_contacts`]) on the `Down` it captured
310    /// with. Meaningless — and kept `false` — while nothing is captured.
311    /// Cleared early when a container takes the gesture over from that captor
312    /// ([`EventCtx::release_captured_child`]).
313    capture_contacts: bool,
314    /// Whether the live opt-in was made by the root widget itself rather than
315    /// by a pod below it — in which case a non-claimant contact is handed to the
316    /// root widget directly instead of walking the active path (see
317    /// [`crate::widget::ChildPod::event_child`]). Same lifetime as
318    /// `capture_contacts`.
319    contacts_captor_is_root: bool,
320    /// Whether some widget in the tree currently holds focus. Root-level mirror of
321    /// the per-container `focused` path bookkeeping (the focus analog of
322    /// `capture_claimant`): set when a dispatch requested focus, cleared by a
323    /// session release — a blur-on-outside-tap `Down`, an explicit focus release,
324    /// a widget publishing an inactive IME surface, or the generic-unmount orphan
325    /// drain in [`RenderRoot::rebuild`] (see [`release_focus_session_in`]).
326    focus_active: bool,
327    /// Which branch the live focus/IME session belongs to: `None` for the main
328    /// tree, `Some(key)` for the floated surface whose pod holds the recorded
329    /// focus path. The identity `focus_active` deliberately does not carry — one
330    /// bool cannot say *whose* session it is, and a surface's chain and the main
331    /// tree's are not siblings any container's blur sweep can reach across.
332    ///
333    /// **Resolved from links that can be shown to be live.** The only
334    /// authoritative view of a pod's recorded link the root ever gets is the pod
335    /// itself, which it holds for exactly the length of
336    /// [`RenderRoot::paint_overlays`] — so that pass writes this, from
337    /// [`crate::widget::ChildPod::holds_live_focus`], and the event pass only ever
338    /// *clears* it (a hit-tested claim is the main tree's by construction, and a
339    /// release ends the session outright). An overlay-pass focus request cannot
340    /// be attributed at the root: the bubble is a bare flag, and a field in the
341    /// main tree re-claiming its own session through a floated toolbar raises
342    /// exactly the same one as an editable inside the surface claiming it for the
343    /// first time.
344    ///
345    /// The liveness half is what makes the record worth keeping. A pod's raw
346    /// `focused` flag survives the session moving away from it — nothing visits
347    /// an abandoned branch to clear one — so a record resolved from the flag
348    /// alone latched on the first surface that ever took focus and never let go.
349    /// Resolved from the stamp instead, it answers a question the code can
350    /// falsify, and it names nobody the moment the session leaves every surface.
351    ///
352    /// **It no longer gates the tree's paint seed**, which is the other half of
353    /// the same correction: seeding is per-link, against `focus_epoch`, so a
354    /// branch proves its own claim rather than the root vouching for it from one
355    /// frame behind. What is left here is *provenance* — whether an overlay
356    /// dispatch's IME publish is the session owner's — plus the cross-surface
357    /// retirement in [`RenderRoot::event`], both of which genuinely need a name
358    /// rather than a per-link answer.
359    focus_surface: Option<OverlayKey>,
360    /// The identity of the live focus session — what a
361    /// [`ChildPod`](crate::widget::ChildPod) stamps beside its recorded focus
362    /// link, and the only thing that tells a link on the session the root has
363    /// now from one a moved session left behind.
364    ///
365    /// The focus analog of `hover_epoch`, with one difference that follows from
366    /// focus having no per-pass rhythm: this advances **around a dispatch**
367    /// rather than at the end of one. [`RenderRoot::event`] moves it forward
368    /// before it dispatches and puts it back afterwards unless the dispatch
369    /// actually recorded a claim — so a claim is stamped with a value nothing
370    /// older carries, and a pass that moved no focus leaves every standing link
371    /// exactly as it found it. Published to the pods through
372    /// [`crate::widget::set_live_focus_session`], which is where a container
373    /// deciding routing, and a pod's own destructor, read it.
374    ///
375    /// Starts at `1`, not `0`: a freshly built pod's stamp is `0`, and `(0, 0)`
376    /// is what a dispatch driven with no root at all sees, so a real root must
377    /// never publish that pair.
378    ///
379    /// Wrapping is deliberate and harmless — the value is only ever compared for
380    /// equality, never ordered — but it skips `0` on the way round (see
381    /// [`advance_focus_epoch`]).
382    focus_epoch: u64,
383    /// Whether the last completed hover pass left some widget in the tree holding
384    /// the hover link. Root-level mirror of the per-pod hover stamp (the hover
385    /// analog of `focus_active`), seeded into every event/paint pass so nothing
386    /// below can read as hovered while the root says nothing is.
387    hover_active: bool,
388    /// The live hover epoch: the identity of the most recent completed hover pass.
389    ///
390    /// Advanced by exactly one per hover pass — an **uncaptured**
391    /// [`PointerPhase::Move`] (which may record a claim), or the `Down`/`Up`/
392    /// `Cancel` that ends a hover outright (which may not) — and by nothing else,
393    /// so a scroll, key, IME, or housekeeping pass leaves a live hover standing.
394    /// A [`crate::widget::ChildPod`]'s recorded stamp counts as hovered only while
395    /// it equals this, which is what strands the previous claimant's path with no
396    /// container having to clear it (see the [`crate::event`] module docs).
397    ///
398    /// Starts at `1`, not `0`: a freshly built pod's stamp is `0`, and starting the
399    /// epoch past it means a never-claimed pod cannot match the live epoch by
400    /// accident before the first hover pass ever runs.
401    hover_epoch: u64,
402    /// This root's process-unique identity, assigned once at construction from
403    /// [`NEXT_ROOT_IDENTITY`] and never reused.
404    ///
405    /// It exists for exactly one comparison: the hover-orphan channel
406    /// (`crate::event`'s `mark_hover_orphaned`/`take_hover_orphaned`) is a
407    /// thread-local a *destructor* writes, so a second root driving passes on the
408    /// same thread can otherwise see a mark that is none of its business.
409    /// `hover_epoch` cannot tell them apart — every root's counter starts at `1`
410    /// and advances per hover pass, so two roots hold colliding integers as a rule
411    /// rather than as a fluke. Publishing and draining `(identity, epoch)` is what
412    /// keeps one root's unmounting claimant from ending another's live hover.
413    root_identity: u64,
414    /// The cursor the last cursor pass resolved — hover's sibling channel, and
415    /// the value a desktop shell reads through [`RenderRoot::cursor`].
416    ///
417    /// Deliberately **not** derived from `hover_active`: that mirror is
418    /// identity-free (it knows *that* something is hovered, not which widget or
419    /// what shape it wants), so a request travels its own pass-scoped slot
420    /// ([`EventCtx::set_cursor`]) and is resolved here.
421    ///
422    /// Re-resolved on every pointer [`PointerPhase::Move`], captured or not:
423    /// whatever the pass requested, or [`CursorIcon::Default`] when it requested
424    /// nothing. Every other pass leaves it standing — see [`RenderRoot::event`]
425    /// for why a `Down`/`Up` must not reset it. There is no generation counter
426    /// beside it: the shell compares the value it last applied (see
427    /// [`RenderRoot::cursor`]).
428    cursor: CursorIcon,
429    /// The text the tree last asked the shell to put on the host clipboard, or
430    /// `None` once drained — the cursor's write-only sibling, resolved from the
431    /// same kind of per-pass slot ([`EventCtx::write_clipboard`]) by the same
432    /// [`RequestPass`] bracket.
433    ///
434    /// **One-shot, unlike [`cursor`](RenderRoot::cursor).** A cursor is a *level*
435    /// (a standing shape a shell re-applies when it differs); a clipboard write
436    /// is an *edge* (a thing to do once), so the accessor
437    /// [`RenderRoot::take_clipboard_write`] drains it and a shell that forgets to
438    /// call it merely delays the write rather than repeating it.
439    ///
440    /// A pass that writes replaces whatever stood here undrained — the newest
441    /// copy is the one the user meant, and the shell is expected to drain after
442    /// every dispatch — while a pass that writes nothing leaves it alone rather
443    /// than silently discarding a write nobody has taken yet.
444    pending_clipboard_write: Option<String>,
445    /// Whether the tree has asked the shell to read the host clipboard back to it
446    /// ([`EventCtx::request_paste`]), until drained by
447    /// [`RenderRoot::take_paste_request`].
448    ///
449    /// The data-free twin of [`pending_clipboard_write`](RenderRoot::pending_clipboard_write),
450    /// and one-shot for the same reason. Raised by any pass in which a widget
451    /// asked and lowered only by the drain, so a shell that skips a drain answers
452    /// late rather than losing the paste.
453    pending_paste_request: bool,
454    /// The IME surface the focused widget last published (via
455    /// [`EventCtx::publish_ime_state`]), surfaced to the shell by
456    /// [`RenderRoot::ime_state`]. Persists across rebuilds/events until refreshed
457    /// by a new publish or dropped by a release (a blur, a focus release, an
458    /// inactive publish, or a generic-unmount orphan drain — see
459    /// [`release_focus_session_in`]).
460    ///
461    /// Only ever `None` or an **active** surface: an inactive publish is a
462    /// release, never a stored value (see [`RenderRoot::ime_state`]).
463    ime_state: Option<ImeState>,
464    /// A monotonically-increasing generation bumped on every **actual** change
465    /// of `focus_active` or `ime_state` — the focus/IME session's edge signal,
466    /// read by a shell through [`RenderRoot::focus_ime_generation`].
467    ///
468    /// The mobile frame gate turns this into an *edge* input
469    /// (`FrameInputs::focus_or_ime_changed`): a shell caches the last value it
470    /// saw and runs a frame when it moves. A *level* input ("something holds
471    /// focus") forced a frame every vsync for as long as a field stayed focused,
472    /// which made caret pacing unreachable — measured at 62–120 fps on a static
473    /// screen whose only live input was focus (Xiaomi 12).
474    ///
475    /// Same-value writes deliberately do **not** bump it (see
476    /// [`RenderRoot::set_focus_active`]/[`RenderRoot::store_ime_state`]): the
477    /// paint pass republishes the focused widget's IME surface on *every* frame,
478    /// so bumping on write rather than on change would re-create exactly the
479    /// per-vsync forcing this edge exists to remove.
480    focus_ime_gen: u64,
481    /// The [`PlatformViewFrame`]s the tree published during the most recent
482    /// [`RenderRoot::paint`], surfaced to the shell via
483    /// [`RenderRoot::platform_view_frames`]. Unlike `ime_state` above, this is
484    /// REPLACED wholesale every pass (never merged with the previous one), so
485    /// a pass that publishes none yields an empty `Vec` — a culled/removed
486    /// slot from the prior frame does not linger as a stale frame. Core stays
487    /// dumb here: the shell's differ owns absent-means-hide/dispose semantics.
488    platform_view_frames: Vec<PlatformViewFrame>,
489    /// The z-shield rects the tree reported during the most recent
490    /// [`RenderRoot::paint`] (via [`crate::widget::PaintCtx::report_input_shield`]),
491    /// surfaced to the shell via [`RenderRoot::input_shields`].
492    ///
493    /// Exactly the `platform_view_frames` discipline above — REPLACED wholesale
494    /// every pass, so a pass whose shields stopped painting reports none. Core
495    /// stays dumb: it never associates a shield with a slot, that is the
496    /// shell-side differ's job.
497    input_shields: Vec<Rect>,
498    /// Dirtiness accumulated since the last [`RenderRoot::take_change_flags`] —
499    /// merged from each rebuild so a shell can decide, in one place, whether a
500    /// frame needs layout/paint at all.
501    pending: ChangeFlags,
502    /// The app's active theme, stored type-erased so `frust-core` needs no
503    /// `frust-theme` dependency (the concrete `Theme` is boxed by the shell —
504    /// see [`RenderRoot::set_theme`]). Lent as `Option<&dyn Any>` into each
505    /// [`LayoutCtx`]/[`PaintCtx`]; `None` until a shell sets one (a supported
506    /// state — bare-core tests and pre-theme apps run without a theme).
507    theme: Option<Box<dyn Any>>,
508    /// The window's insets ([`WindowInsets`]), delivered by the shell via
509    /// [`RenderRoot::set_insets`] and threaded into every subsequent
510    /// layout/paint pass (recovered by widgets through
511    /// [`crate::widget::LayoutCtx::window_insets`]/
512    /// [`crate::widget::PaintCtx::window_insets`]). Unlike the theme this is a
513    /// concrete core-owned type (only `f64` scalars), stored by value — no
514    /// `Box<dyn Any>` erasure needed. Defaults to the zero inset until a shell
515    /// pushes one (a supported state — bare-core tests and pre-insets apps).
516    insets: WindowInsets,
517    /// The shell's running count of frames the render thread has actually
518    /// presented, threaded into every subsequent paint pass and recovered by
519    /// widgets through [`crate::widget::PaintCtx::presented_frames`]. A plain
520    /// `u64` core stores by value (like the insets). `None` until a shell pushes
521    /// one via [`RenderRoot::set_presented_frames`] — a supported state
522    /// (bare-core tests and pre-wiring shells run without it), so widgets can
523    /// fall back to a paint-cadence measure. Unlike the theme/insets this is a
524    /// pure observation: [`RenderRoot::set_presented_frames`] deliberately marks
525    /// NO [`ChangeFlags`] and bumps NO semantics generation (see its doc), so a
526    /// ticking presented count never forces a relayout or feeds the mobile frame
527    /// gate.
528    presented_frames: Option<u64>,
529    /// Whether the shell created a translucent (alpha-channel, "Mode B") GPU
530    /// surface, threaded into every subsequent paint pass and recovered by
531    /// widgets through [`crate::widget::PaintCtx::is_translucent`]. A plain
532    /// `bool` core stores by value (like the insets); `false` (opaque, "Mode A")
533    /// until a shell pushes one via [`RenderRoot::set_surface_translucent`] — the
534    /// supported default for every desktop app and bare-core test. The
535    /// platform-view hole-punch is the sole reader: a slot clears its rect only
536    /// on a translucent surface (see `frust-widgets`' `PlatformViewWidget`).
537    surface_translucent: bool,
538    /// The persistent, never-reused per-pod semantics base-id allocator's next
539    /// value. Seeded at `2` (ids `0`/`1` reserved: `0` keeps
540    /// `NonZeroU64` valid, `1` is the [`ROOT_NODE_ID`] window node), advanced as
541    /// [`ChildPod`](crate::widget::ChildPod)s are assigned bases on their first
542    /// semantics visit, and carried across passes so a pod that first appears on a
543    /// later frame never collides with an already-assigned one. A `Cell` because
544    /// [`RenderRoot::semantics`] runs behind `&self`.
545    semantics_alloc: Cell<u64>,
546    /// The root widget's stable semantics base id (the root pod is arena-backed,
547    /// not a [`ChildPod`](crate::widget::ChildPod), so it caches its base here
548    /// rather than in a pod). Lazily assigned on the first semantics pass.
549    root_semantics_id: Cell<Option<NonZeroU64>>,
550    /// A monotonically-increasing generation bumped whenever a rebuild or theme
551    /// swap could have changed the semantics tree, so a shell can cheaply skip
552    /// re-pulling + re-pushing an unchanged accessibility tree (the semantics
553    /// dirty gate — see [`RenderRoot::semantics_if_changed`]). v1 recompute is
554    /// acceptable; this is the seam a shell gates on.
555    semantics_gen: u64,
556    /// Set by [`RenderRoot::rebuild`] when the deferred-callback flush owes the
557    /// shell a frame, and folded into the next [`RenderRoot::paint`]'s
558    /// [`PaintOutcome::needs_frame`] (then cleared). Two raisers, both in the
559    /// flush loop: hitting [`MAX_PENDING_RESULT_FLUSH_PASSES`] with work still
560    /// owed, and a dispatched [`InputEvent::Housekeeping`] whose
561    /// [`EventOutcome::needs_redraw`] came back set.
562    ///
563    /// The frame-request half of the deferral: `pending |= PAINT` already tells
564    /// the mobile frame gate to run its next tick, but the desktop loop is
565    /// dirty-driven (`ControlFlow::Wait`) and schedules off `needs_frame`, so the
566    /// deferral has to surface there too — otherwise the remaining flush would
567    /// wait for whatever input happens to arrive next, which is the exact
568    /// failure this whole mechanism exists to remove.
569    deferred_frame: bool,
570    /// The routing half of the overlay entries the **last** [`RenderRoot::paint`]
571    /// registered, in paint order (`Floating` band then `Tooltip`, registration
572    /// order within each) — what [`RenderRoot::event`]'s overlay pre-pass
573    /// hit-tests before the main tree ever sees a pointer.
574    ///
575    /// Replaced wholesale every paint, exactly like `platform_view_frames`: an
576    /// owner keeps a surface routable by registering it again each frame, so a
577    /// surface whose owner stopped registering (or was unmounted) stops taking
578    /// input after the next paint with nothing to unregister.
579    ///
580    /// Carries **no pod handle** by construction (see
581    /// [`OverlayHit`](crate::overlay::OverlayHit)): the owner owns the pod, and a
582    /// root holding a clone of it between passes would both outlive the owner and
583    /// invite a borrow held across a pass boundary.
584    ///
585    /// One frame of lag is inherent and intended: input is routed against where
586    /// the surfaces were painted, which is the only place the user could have
587    /// seen them.
588    overlay_hits: Vec<OverlayHit>,
589    /// The selection-toolbar request the focused field published during the most
590    /// recent [`RenderRoot::paint`], surfaced to the shell through
591    /// [`RenderRoot::selection_toolbar`] for the platform edit-menu route.
592    ///
593    /// Resolved per paint pass: a pass in which nothing published clears it, which
594    /// is what puts the menu away when a selection collapses. A session release
595    /// clears it too (see [`RenderRoot::release_focus_session`]), so the menu can
596    /// never outlive the focus the selection belonged to — the event pass's blur
597    /// lands a whole frame before the paint that would otherwise notice.
598    selection_toolbar: Option<SelectionToolbarRequest>,
599    /// A monotonically-increasing generation bumped on every **actual** change of
600    /// `selection_toolbar` — the same change-guarded edge signal
601    /// `focus_ime_gen` is, and for the same reason: the publishing field
602    /// re-publishes an unchanged request every single frame its selection stands,
603    /// so bumping on write rather than on change would ask the shell to re-present
604    /// the platform menu on every vsync.
605    ///
606    /// Kept beside the value rather than inside it so the counter survives a
607    /// clear: a shell diffs the generation to notice the menu went *away* just as
608    /// much as to notice it appeared.
609    selection_toolbar_gen: u64,
610    _state: core::marker::PhantomData<fn(&mut State)>,
611}
612
613impl<State: 'static, V: View<State>> RenderRoot<State, V> {
614    /// Create an empty render root with no widget yet built.
615    pub fn new() -> Self {
616        Self {
617            tree: WidgetTree::new(),
618            root_id: None,
619            prev_view: None,
620            next_id: 0,
621            window_size: Size::ZERO,
622            capture_claimant: None,
623            capture_contacts: false,
624            contacts_captor_is_root: false,
625            focus_active: false,
626            focus_surface: None,
627            // Past a fresh pod's `0` stamp, and past the `(0, 0)` a rootless
628            // dispatch sees — see the field doc.
629            focus_epoch: 1,
630            hover_active: false,
631            // Past a fresh pod's `0` stamp — see the field doc.
632            hover_epoch: 1,
633            root_identity: NEXT_ROOT_IDENTITY.fetch_add(1, Ordering::Relaxed),
634            cursor: CursorIcon::Default,
635            pending_clipboard_write: None,
636            pending_paste_request: false,
637            ime_state: None,
638            focus_ime_gen: 0,
639            platform_view_frames: Vec::new(),
640            input_shields: Vec::new(),
641            pending: ChangeFlags::NONE,
642            theme: None,
643            insets: WindowInsets::default(),
644            presented_frames: None,
645            surface_translucent: false,
646            // Ids 0 and 1 are reserved (see the field doc); pods start at 2.
647            semantics_alloc: Cell::new(2),
648            root_semantics_id: Cell::new(None),
649            semantics_gen: 0,
650            deferred_frame: false,
651            overlay_hits: Vec::new(),
652            selection_toolbar: None,
653            selection_toolbar_gen: 0,
654            _state: core::marker::PhantomData,
655        }
656    }
657
658    /// Store the app's active theme, threaded into every subsequent
659    /// layout/paint pass as `Option<&dyn Any>` and recovered by widgets via
660    /// [`crate::widget::PaintCtx::theme_as`]/[`crate::widget::LayoutCtx::theme_as`].
661    ///
662    /// The theme is boxed **type-erased** (`Box<dyn Any>`) so this crate stays
663    /// independent of `frust-theme`; the shell boxes the concrete `Theme`
664    /// (and re-boxes it on a live appearance change, e.g. dark-mode toggle).
665    /// Calling again replaces the stored theme.
666    ///
667    /// Marks `LAYOUT | PAINT` pending (drained by
668    /// [`RenderRoot::take_change_flags`]): a theme swap can change baked-in
669    /// paint state a widget resolves at layout time (e.g. `Text`'s themed
670    /// glyph color, cached into its `TextLayout` — see
671    /// `frust-widgets::text`), so a shell that later gates layout/paint on
672    /// this seam must still see a bare `set_theme` as dirty even though no
673    /// view changed.
674    pub fn set_theme(&mut self, theme: Box<dyn Any>) {
675        self.theme = Some(theme);
676        self.pending |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
677        // A theme swap can change semantics-visible state (e.g. a relabelled or
678        // re-bounded node once layout re-runs); treat it as semantics-dirty too.
679        self.semantics_gen = self.semantics_gen.wrapping_add(1);
680    }
681
682    /// Store the window's insets ([`WindowInsets`]), threaded into every
683    /// subsequent layout/paint pass and recovered by widgets via
684    /// [`crate::widget::LayoutCtx::window_insets`]/
685    /// [`crate::widget::PaintCtx::window_insets`].
686    ///
687    /// Mirrors [`RenderRoot::set_theme`]'s dirty-tracking contract: a change
688    /// marks `LAYOUT | PAINT` pending (drained by
689    /// [`RenderRoot::take_change_flags`]) so a shell gating layout/paint on that
690    /// seam still relayouts when the insets move — a `SafeArea` widget resolves
691    /// its inset at layout time, so the mobile layout-skip gate must see a bare
692    /// `set_insets` as dirty even though no view changed (the same reasoning as
693    /// the theme swap — see `docs/ARCHITECTURE.md`'s Theme delivery and Frame
694    /// gate). A change also bumps the semantics generation, since a moved inset
695    /// shifts laid-out node bounds.
696    ///
697    /// No-op guarded by [`WindowInsets`]'s `PartialEq`: pushing the current
698    /// value marks nothing dirty, so a shell that polls the platform insets
699    /// every frame and forwards unconditionally never forces a needless
700    /// relayout. (A shell may also skip the call itself by comparing first —
701    /// this is the same guard, held on the core side.)
702    pub fn set_insets(&mut self, insets: WindowInsets) {
703        if self.insets == insets {
704            return;
705        }
706        self.insets = insets;
707        self.pending |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
708        // A moved inset shifts laid-out node bounds once layout re-runs; treat
709        // it as semantics-dirty too (mirrors `set_theme`).
710        self.semantics_gen = self.semantics_gen.wrapping_add(1);
711    }
712
713    /// The window's insets currently threaded into the layout/paint passes.
714    pub fn insets(&self) -> WindowInsets {
715        self.insets
716    }
717
718    /// Store the shell's running count of frames the render thread has actually
719    /// presented, threaded into every subsequent paint pass and recovered by
720    /// widgets via [`crate::widget::PaintCtx::presented_frames`]. A shell loads
721    /// the atomic its render side increments (once per presented frame) and
722    /// pushes it here once per UI frame, before `paint`.
723    ///
724    /// **Deliberately dirties nothing.** Unlike [`RenderRoot::set_theme`] and
725    /// [`RenderRoot::set_insets`] — which mark `LAYOUT | PAINT` pending because a
726    /// widget bakes their value in at layout time — this setter marks NO
727    /// [`ChangeFlags`] and bumps NO semantics generation. The presented count is
728    /// a paint-only *observation* a widget reads live every paint (never baked at
729    /// layout), so treating it as dirty would be wrong twice over: it would force
730    /// a needless relayout, and — critically — on the mobile shells a
731    /// monotonically ticking counter would keep the frame gate's pending-flags
732    /// input perpetually true, so the menu would never idle (the 32s-idle
733    /// behavior must survive). Keeping this setter dirt-free
734    /// is exactly what keeps the frame gate unaware of it (see
735    /// `docs/ARCHITECTURE.md`'s Frame gate).
736    pub fn set_presented_frames(&mut self, presented: u64) {
737        self.presented_frames = Some(presented);
738    }
739
740    /// The presented-frame count currently threaded into the paint pass, or
741    /// `None` if no shell has pushed one.
742    pub fn presented_frames(&self) -> Option<u64> {
743        self.presented_frames
744    }
745
746    /// Store whether the shell's GPU surface is translucent (alpha-channel,
747    /// "Mode B"), threaded into every subsequent paint pass and recovered by
748    /// widgets through [`crate::widget::PaintCtx::is_translucent`]. A shell
749    /// pushes the surface's **resolved** translucency here — what the GPU
750    /// backend reports after the surface is installed, not what the app
751    /// requested via `frust-shell-common::surface_mode`'s latch: a translucency
752    /// request the platform refuses must degrade to the opaque contract, or
753    /// every `platform_view` slot punches a hole in an opaque swapchain
754    /// (black rectangles). Every desktop app leaves the default `false`
755    /// (opaque, "Mode A").
756    ///
757    /// Marks `PAINT` pending on an actual change (`PartialEq`-guarded, mirroring
758    /// [`RenderRoot::set_insets`]'s no-op guard): translucency is read purely at
759    /// paint time (the hole-punch runs in `paint`, never baked at layout), so a
760    /// flip must repaint but need not relayout. A flip is rare but **real**: a
761    /// surface (re)install can resolve differently from the previous one, and
762    /// both mobile shells re-push this every frame (the no-op-if-unchanged
763    /// guard is what makes that free).
764    pub fn set_surface_translucent(&mut self, translucent: bool) {
765        if self.surface_translucent == translucent {
766            return;
767        }
768        self.surface_translucent = translucent;
769        self.pending |= ChangeFlags::PAINT;
770    }
771
772    /// Whether the shell's GPU surface is currently marked translucent.
773    pub fn is_surface_translucent(&self) -> bool {
774        self.surface_translucent
775    }
776
777    /// Whether a captured pointer gesture is currently in flight.
778    pub fn is_pointer_captured(&self) -> bool {
779        self.capture_claimant.is_some()
780    }
781
782    /// The contact that claimed the pointer capture in flight — the only one
783    /// whose `Up`/`Cancel` can end it — or `None` while nothing is captured.
784    pub fn pointer_capture_claimant(&self) -> Option<PointerId> {
785        self.capture_claimant
786    }
787
788    /// Whether the capture in flight routes the gesture's **other** contacts to
789    /// its captor — the captor opted in with [`EventCtx::capture_contacts`] on
790    /// the `Down` it captured with, and no container has since taken the
791    /// gesture over from it ([`EventCtx::release_captured_child`]). `false`
792    /// while nothing is captured.
793    pub fn pointer_capture_contacts(&self) -> bool {
794        self.capture_contacts
795    }
796
797    /// Whether some widget in the tree currently holds keyboard/IME focus.
798    pub fn is_focus_active(&self) -> bool {
799        self.focus_active
800    }
801
802    /// Whether some widget in the tree currently holds the hover link — i.e.
803    /// whether the last hover pass (an uncaptured [`PointerPhase::Move`]) left the
804    /// pointer over a widget that claimed it.
805    ///
806    /// The hover analog of [`RenderRoot::is_focus_active`], and a level accessor
807    /// like it: hover is not a session (nothing has to be released), so there is no
808    /// generation counterpart. `false` for any app whose widgets never call
809    /// [`EventCtx::claim_hover`](crate::event::EventCtx::claim_hover). A touch app
810    /// can still see it go `true` transiently: nothing distinguishes a touch
811    /// contact from a mouse here, so an uncaptured touch drag over a
812    /// non-capturing claimant is an ordinary hover pass — ended by the `Up` at
813    /// lift (see `docs/LIMITATIONS.md`'s `hover-window-leave-standing`).
814    ///
815    /// A [`RenderRoot::rebuild`] that removes the claimant ends the link too, so
816    /// this never reports a hover held by a widget that no longer exists — the
817    /// hover counterpart of the unmount focus release (see that method).
818    pub fn is_hover_active(&self) -> bool {
819        self.hover_active
820    }
821
822    /// The cursor the tree last asked the host to show — what a desktop shell
823    /// pushes to its window (`frust-shell-desktop` maps it onto winit's own
824    /// cursor icons).
825    ///
826    /// A **level** accessor like [`RenderRoot::is_hover_active`], not an edge one:
827    /// the value re-resolves on every pointer [`PointerPhase::Move`] and stands
828    /// unchanged through every other pass, so a shell caches what it last applied
829    /// and calls the platform only when this differs. There is deliberately no
830    /// generation counter — a cursor is a *value*, not a session, and an unmoved
831    /// cursor is indistinguishable from one re-resolved to the same shape.
832    ///
833    /// [`CursorIcon::Default`] before the first `Move`, and after any `Move` in
834    /// which no widget called
835    /// [`EventCtx::set_cursor`](crate::event::EventCtx::set_cursor) — so any app
836    /// whose widgets never request a cursor reads `Default` forever, and the mobile
837    /// shells never read this at all regardless of what resolves here.
838    ///
839    /// **Residual:** a widget that is torn down (or moves out from under a
840    /// stationary pointer) while its request stands leaves the last shape in
841    /// place until the next `Move` re-resolves it — the same self-correction
842    /// window hover has, and for the same reason: nothing re-resolves without
843    /// pointer motion.
844    pub fn cursor(&self) -> CursorIcon {
845        self.cursor
846    }
847
848    /// Take (and clear) the text the tree asked the shell to put on the host
849    /// clipboard — the drain a shell performs immediately after every
850    /// [`RenderRoot::event`], beside [`cursor()`](RenderRoot::cursor) and
851    /// [`ime_state()`](RenderRoot::ime_state).
852    ///
853    /// `Some` exactly when some widget called
854    /// [`EventCtx::write_clipboard`](crate::event::EventCtx::write_clipboard)
855    /// during a pass since the last drain (answering a
856    /// [`EditCommand::Copy`](crate::event::EditCommand::Copy)/[`Cut`](crate::event::EditCommand::Cut),
857    /// or a chord the widget decoded itself). The shell hands the text to its host
858    /// clipboard — winit's `arboard` on desktop, `ClipboardManager` on Android,
859    /// `UIPasteboard` on iOS — and does nothing at all on `None`.
860    ///
861    /// **Destructive**, unlike [`cursor()`](RenderRoot::cursor): a clipboard write
862    /// is an edge, not a standing level, so a caller that drains and drops the
863    /// result loses that write. Draining twice after one pass yields `None` the
864    /// second time.
865    ///
866    /// A widget that never copies leaves this `None` forever, so a shell with no
867    /// clipboard (the mobile shells before their own clipboard work lands) may
868    /// call it and discard the result, or not call it at all.
869    pub fn take_clipboard_write(&mut self) -> Option<String> {
870        self.pending_clipboard_write.take()
871    }
872
873    /// Take (and clear) whether the tree asked the shell to read the host
874    /// clipboard back to it — drained beside
875    /// [`take_clipboard_write`](RenderRoot::take_clipboard_write) after every
876    /// [`RenderRoot::event`].
877    ///
878    /// `true` exactly when some widget called
879    /// [`EventCtx::request_paste`](crate::event::EventCtx::request_paste) during a
880    /// pass since the last drain. The shell answers by reading its host clipboard
881    /// and dispatching
882    /// [`InputEvent::EditCommand`]`(`[`EditCommand::Paste`](crate::event::EditCommand::Paste)`(text))`
883    /// — a *new* dispatch, because the read may be asynchronous and the pass that
884    /// asked is over. That answer carries no identity of its own and is
885    /// focus-routed to whoever holds focus when it lands: a release in between
886    /// drops it harmlessly, but a focus *move* in between lands it in the new
887    /// field rather than the one that asked. A synchronous read has no such
888    /// window; an asynchronous one snapshots
889    /// [`focus_epoch`](RenderRoot::focus_epoch) at this drain and discards an
890    /// answer whose epoch no longer matches — not
891    /// [`focus_ime_generation`](RenderRoot::focus_ime_generation), which also
892    /// moves within a single session.
893    ///
894    /// **Destructive**, for [`take_clipboard_write`](RenderRoot::take_clipboard_write)'s
895    /// reason. A pass may both write and request (a cut that immediately re-reads,
896    /// or a widget answering two chords) — the two drains are independent.
897    pub fn take_paste_request(&mut self) -> bool {
898        std::mem::take(&mut self.pending_paste_request)
899    }
900
901    /// The IME surface the focused widget published, for the shell to drive the
902    /// platform input method (winit `set_ime_cursor_area`, Android
903    /// `updateSelection`, iOS `inputDelegate`). `None` when nothing is focused or
904    /// the focused widget publishes no IME surface.
905    ///
906    /// Written by the focused widget through [`EventCtx::publish_ime_state`] during
907    /// the event pass and refreshed on every event; it survives a rebuild (so the
908    /// shell can query it between frames) and is cleared when focus is lost.
909    ///
910    /// # `None` is the only "no session" form — an inactive surface is never stored
911    ///
912    /// A widget publishing `ImeState { active: false, .. }` is ending the session,
913    /// not describing it, so both publish paths turn that into a full release
914    /// (see [`RenderRoot::paint`]) and this returns `None` rather than
915    /// `Some(inactive)`. A shell therefore never has to distinguish the two, and
916    /// `is_some()` means "a live IME session" with no second check.
917    ///
918    /// **The platform still sees the keyboard-hide.** All three shells already
919    /// map `None` onto the inactive form on the way out, so the observable wire
920    /// behavior is unchanged: `frust-shell-android`'s `ime_state_to_json` returns
921    /// `ImeJsonState::default()` (`active:false`, empty text, `-1` indices, null
922    /// caret, `"normal"`) and `frust-shell-ios`' returns the byte-identical
923    /// `ime_state_json(false, "", -1, -1, -1, -1, None, "normal")` — exactly what
924    /// the navigator's own cleared surface serialised to before. Kotlin's
925    /// `pollImeAfterDispatch` and Swift's `syncImeFocus` both branch on `active`
926    /// alone (an inactive surface's text/caret/content-type are ignored), and the
927    /// desktop shell's `sync_ime` reads `is_some_and(|s| s.active)`. Dropping the
928    /// inactive surface's payload also stops a disabled *secret* field's text
929    /// riding to the platform after its session ended.
930    pub fn ime_state(&self) -> Option<ImeState> {
931        self.ime_state.clone()
932    }
933
934    /// The focus/IME session generation — bumped on every **actual** change of
935    /// [`is_focus_active`](RenderRoot::is_focus_active) or
936    /// [`ime_state`](RenderRoot::ime_state), and on nothing else.
937    ///
938    /// The *edge* counterpart of those two level accessors, for a shell that
939    /// needs "did the focus/IME session move since I last looked?" rather than
940    /// "is something focused?". A shell caches the value it last saw and
941    /// compares (mirroring [`semantics_generation`](RenderRoot::semantics_generation)'s
942    /// cheap dirty gate) — that comparison is the mobile frame gate's
943    /// `FrameInputs::focus_or_ime_changed` input.
944    ///
945    /// A same-value write never moves it: re-publishing an identical IME
946    /// surface (which the paint pass does on every frame a field stays focused)
947    /// or re-blurring an already-blurred root is not an edge. Wrapping is
948    /// deliberate and harmless — a comparison, never an ordering.
949    ///
950    /// # Not the session's identity
951    ///
952    /// This counts *changes to the published surface*, not *sessions*, and the
953    /// two come apart in both directions — see
954    /// [`focus_epoch`](RenderRoot::focus_epoch), which is what to reach for when
955    /// the question is "is this still the same focus session?". Answering that
956    /// one from this counter is wrong whenever focus moves between two fields
957    /// without the published value changing.
958    pub fn focus_ime_generation(&self) -> u64 {
959        self.focus_ime_gen
960    }
961
962    /// The live focus session's **identity** — advanced once per honoured focus
963    /// claim and once per session release, and by nothing else.
964    ///
965    /// The neighbour of [`focus_ime_generation`](RenderRoot::focus_ime_generation)
966    /// and easy to mistake for it, so: that one counts *changes to the published
967    /// surface* (the focus flag, or the [`ImeState`] value), this one counts
968    /// *sessions*. They come apart in both directions, which is why both exist:
969    ///
970    /// * Focus moving from one field to another moves this one and can leave
971    ///   that one completely still. Claiming focus while some field already
972    ///   holds it writes `true` over `true`, and the surface the new field
973    ///   publishes may compare equal to the old field's ([`ImeState`] is
974    ///   `{active, editing, caret, content_type}` and names no widget) — or may
975    ///   not be published at all, since a widget is free to take focus and
976    ///   publish nothing, which leaves the previous field's surface standing.
977    /// * An edit landing, a caret moving, or the field being repositioned under
978    ///   the user moves that one and leaves this one still: the session is the
979    ///   same session throughout.
980    ///
981    /// So a caller binding an asynchronous answer to the session that asked for
982    /// it wants this one; a caller asking "must I run a frame, or re-sync the
983    /// platform IME?" wants that one.
984    ///
985    /// **Never `0`.** The counter is built at `1` and steps *past* `0` on wrap,
986    /// because `0` is a never-claimed [`ChildPod`](crate::widget::ChildPod)'s
987    /// stamp and a root publishing it would hand every unclaimed pod in the tree
988    /// a live link. A caller is therefore free to use `0` as its own "no root /
989    /// no answer" sentinel with no risk of colliding with a live value. Wrapping
990    /// is otherwise deliberate and harmless: the value is compared for equality,
991    /// never ordered.
992    pub fn focus_epoch(&self) -> u64 {
993        self.focus_epoch
994    }
995
996    /// Set the root's focus flag, bumping [`RenderRoot::focus_ime_generation`]
997    /// only when the value actually moves.
998    ///
999    /// One of the two writers of `focus_active` outside construction (the other
1000    /// is [`release_focus_session_in`], which clears it together with the IME
1001    /// surface as one edge): every focus/blur arm of [`RenderRoot::event`] goes
1002    /// through one of them, so the edge generation cannot drift from the state it
1003    /// describes. In practice this one only ever *sets* focus — a clear is always
1004    /// a session release.
1005    fn set_focus_active(&mut self, active: bool) {
1006        if self.focus_active != active {
1007            self.focus_active = active;
1008            self.focus_ime_gen = self.focus_ime_gen.wrapping_add(1);
1009        }
1010    }
1011
1012    /// Store (or clear) the shell-facing IME surface, bumping
1013    /// [`RenderRoot::focus_ime_generation`] only when the stored value actually
1014    /// moves — the `&mut self` form of [`store_ime_state_in`], for the event
1015    /// pass (the paint pass holds disjoint field borrows and calls that
1016    /// function directly).
1017    fn store_ime_state(&mut self, ime: Option<ImeState>) {
1018        store_ime_state_in(&mut self.ime_state, &mut self.focus_ime_gen, ime);
1019    }
1020
1021    /// End the focus/IME session: clear `focus_active` and drop the shell-facing
1022    /// surface together, moving [`RenderRoot::focus_ime_generation`] exactly once
1023    /// if either was set. The `&mut self` form of [`release_focus_session_in`]
1024    /// (whose doc carries the full contract), for the event and rebuild passes;
1025    /// the paint pass holds disjoint field borrows and calls that function
1026    /// directly.
1027    ///
1028    /// Idempotent: releasing an already-released root writes the same values
1029    /// back and fires no edge.
1030    fn release_focus_session(&mut self) {
1031        release_focus_session_in(
1032            &mut self.focus_active,
1033            &mut self.ime_state,
1034            &mut self.focus_ime_gen,
1035            &mut self.focus_epoch,
1036            self.root_identity,
1037        );
1038        // No session, no owner. A paint pass that releases re-resolves the owner
1039        // from the pods before it ends anyway (see
1040        // `RenderRoot::resolve_session_surface`), so this write is the event and
1041        // rebuild passes' own.
1042        self.focus_surface = None;
1043        // A selection toolbar describes the *focused* field's selection, so the
1044        // session ending is the toolbar ending — and it must end on the event
1045        // pass that blurred, not a frame later when the next paint happens to
1046        // publish nothing. Change-guarded like every other edge here: releasing
1047        // an already-toolbarless root moves no generation.
1048        if self.selection_toolbar.take().is_some() {
1049            self.selection_toolbar_gen = self.selection_toolbar_gen.wrapping_add(1);
1050        }
1051    }
1052
1053    /// Publish this root's live focus session so the pods can compare their own
1054    /// stamps against it — the focus counterpart of the `(root, epoch)` pair
1055    /// `crate::event::set_live_hover_link` publishes for hover.
1056    ///
1057    /// Called at the head of every pass, not only when the session moves: the
1058    /// channel mirrors one root, so a second root driving passes on the same
1059    /// thread would otherwise leave this one's pods comparing against a session
1060    /// that is none of their business. Re-publishing an unchanged triple costs a
1061    /// `Cell` write and notifies nothing.
1062    ///
1063    /// Publishes the session "at rest" — the live epoch and the epoch a claim
1064    /// would take are the same value. [`RenderRoot::event`] publishes the two
1065    /// apart for the length of its dispatch; see that method.
1066    fn publish_focus_session(&self) {
1067        crate::widget::set_live_focus_session(
1068            self.root_identity,
1069            self.focus_epoch,
1070            self.focus_epoch,
1071        );
1072    }
1073
1074    /// End the standing hover link outright, outside any hover pass: advance the
1075    /// epoch (which strands every stamp in the tree at once, so no container has
1076    /// to be told) and clear the mirror.
1077    ///
1078    /// Hover's analog of [`RenderRoot::release_focus_session`], and idempotent in
1079    /// the same way — ending a hover nothing holds writes the same mirror back and
1080    /// costs one epoch. There is no generation counter to move: hover is not a
1081    /// session a shell mirrors (see [`RenderRoot::is_hover_active`]).
1082    ///
1083    /// The one caller is [`RenderRoot::rebuild`]'s severed-claimant drain; a hover
1084    /// pass ends its own link inline, where it also decides the *new* one.
1085    fn end_hover_link(&mut self) {
1086        self.hover_epoch = self.hover_epoch.wrapping_add(1);
1087        self.hover_active = false;
1088        crate::event::set_live_hover_link(self.root_identity, 0);
1089    }
1090
1091    /// The [`PlatformViewFrame`]s published during the most recent
1092    /// [`RenderRoot::paint`], in paint order.
1093    ///
1094    /// Replaced wholesale every pass (see the `platform_view_frames` field
1095    /// doc), so a pass with no publishers yields an empty slice — a shell
1096    /// never sees a stale frame for a slot that stopped painting.
1097    pub fn platform_view_frames(&self) -> &[PlatformViewFrame] {
1098        &self.platform_view_frames
1099    }
1100
1101    /// The z-shield rects reported during the most recent [`RenderRoot::paint`]
1102    /// (see [`crate::widget::PaintCtx::report_input_shield`]), in paint order.
1103    ///
1104    /// Replaced wholesale every pass, exactly like
1105    /// [`RenderRoot::platform_view_frames`] — a shell feeds both into the same
1106    /// differ ingest call, and the differ intersects these against each
1107    /// interactive slot's rect.
1108    pub fn input_shields(&self) -> &[Rect] {
1109        &self.input_shields
1110    }
1111
1112    /// Drain the slot ids whose `platform_view` widgets were torn down since the
1113    /// last call (`View::teardown` ran on them — see
1114    /// [`crate::widget::report_retired_slot`]).
1115    ///
1116    /// The prompt-teardown channel: a shell calls this once per frame, right
1117    /// after its rebuild, and retires each id in its platform-view differ
1118    /// (`PlatformViewState::retire`) so a disposed slot's native view goes away
1119    /// immediately instead of waiting out the differ's missing-streak
1120    /// heuristic. Draining is destructive, mirroring
1121    /// [`RenderRoot::take_change_flags`]: an id is reported exactly once, so a
1122    /// shell that drains and drops the result loses the prompt path (the
1123    /// missing-streak backstop still covers it).
1124    ///
1125    /// A merely *culled* slot (scrolled offscreen, a parent skipping paint)
1126    /// never appears here — culling doesn't run `teardown` — which is what
1127    /// keeps the camera keep-alive contract intact.
1128    pub fn take_retired_platform_views(&mut self) -> Vec<u64> {
1129        crate::widget::take_retired_slots()
1130    }
1131
1132    /// Take (and clear) the dirtiness accumulated since the last call.
1133    ///
1134    /// A shell can consult this to skip the layout/paint passes when nothing has
1135    /// changed and no redraw was requested (a desktop optimisation; the mobile
1136    /// continuous-loop shells may ignore it and repaint every tick). Each
1137    /// [`RenderRoot::rebuild`] merges its result here; this drains it.
1138    pub fn take_change_flags(&mut self) -> ChangeFlags {
1139        let flags = self.pending;
1140        self.pending = ChangeFlags::NONE;
1141        flags
1142    }
1143
1144    /// Non-draining peek at the dirtiness accumulated since the last
1145    /// [`RenderRoot::take_change_flags`] — `true` when any `LAYOUT`/`PAINT`
1146    /// bit is pending, without clearing it.
1147    ///
1148    /// Complements [`take_change_flags`](RenderRoot::take_change_flags) for a
1149    /// shell frame gate: the gate reads this as one of its
1150    /// "should this frame run" inputs *before* deciding, so a frame it chooses
1151    /// to skip leaves `pending` intact for the next non-skipped frame to drain
1152    /// and act on. Draining stays the job of `take_change_flags`, called only
1153    /// on a frame that actually runs its layout/paint passes. No behavioral
1154    /// change to rebuild/layout/paint.
1155    pub fn has_pending_change_flags(&self) -> bool {
1156        !self.pending.is_empty()
1157    }
1158
1159    /// The root widget id, once built.
1160    pub fn root_id(&self) -> Option<WidgetId> {
1161        self.root_id
1162    }
1163
1164    /// Shared access to the retained tree (for the shell / tests).
1165    pub fn tree(&self) -> &WidgetTree {
1166        &self.tree
1167    }
1168
1169    /// A read-only, pre-order snapshot of the retained tree for tooling: per
1170    /// node an id, its parent and children, the concrete widget's type name, an
1171    /// optional debug label, and its absolute border box in logical px.
1172    ///
1173    /// Computed on demand in O(nodes) and takes `&self` — no per-frame
1174    /// bookkeeping, no mutation, and nothing here participates in
1175    /// build/layout/paint. Bounds reflect the **last layout pass**, so call it
1176    /// after one (before the first, every rect is zero-sized).
1177    ///
1178    /// Scope: the walk covers the [`WidgetTree`] arena *and* the
1179    /// [`ChildPod`](crate::widget::ChildPod)s containers own, reached through
1180    /// [`Widget::visit_children`](crate::widget::Widget::visit_children) — so it
1181    /// is the real retained hierarchy, not just the arena (which holds little
1182    /// more than the root pod). A container that leaves that seam defaulted
1183    /// reads as a leaf.
1184    pub fn inspect(&self) -> Vec<InspectNode> {
1185        self.tree.inspect()
1186    }
1187
1188    /// Run the build closure, then build (first call) or rebuild (subsequent calls)
1189    /// the root widget, returning what changed.
1190    ///
1191    /// the build closure is expected to be cheap and re-entrant: it is
1192    /// re-run in full every rebuild.
1193    ///
1194    /// # Deferred-callback flush
1195    ///
1196    /// The view diff itself is state-free (`rebuild_view` below takes no
1197    /// `State`), so a widget applying a structural op there — the navigator
1198    /// draining its queued `push`/`pop` is the shipped case — cannot run an app
1199    /// callback that needs `&mut State`. It instead queues the callback and calls
1200    /// [`mark_pending_result_flush`](crate::event::mark_pending_result_flush);
1201    /// this method drains that flag and dispatches an
1202    /// [`InputEvent::Housekeeping`] broadcast through the ordinary
1203    /// [`event`](RenderRoot::event) plumbing, where `state` *is* in scope. This
1204    /// is the only unconditional per-frame pass that holds `&mut State`, which is
1205    /// why the dispatch lives here and not in a shell (flushing on the next
1206    /// real input meant waiting seconds for a touch, or forever when the next
1207    /// touch went to chrome outside the navigator).
1208    ///
1209    /// A flushed callback mutates `State`, so the view built before it ran is
1210    /// stale — the build closure + `rebuild_view` cycle therefore re-runs after
1211    /// each flush, and the same frame shows the result. Results can queue further
1212    /// nav ops, so the loop is **bounded**; past the cap the flag is left standing
1213    /// and one more frame is requested rather than spinning (see
1214    /// `MAX_PENDING_RESULT_FLUSH_PASSES`, this module's private cap constant).
1215    ///
1216    /// The broadcast's [`EventOutcome`] is propagated, not discarded: a
1217    /// `needs_redraw` coming back from the dispatch folds into this rebuild's
1218    /// [`ChangeFlags::PAINT`] and the deferred frame request, so a callback
1219    /// whose only effect is [`EventCtx::request_redraw`]
1220    /// — invisible to the re-diff, since no view-visible state changed — still
1221    /// wakes both the mobile frame gate and the desktop `Wait` loop.
1222    pub fn rebuild(
1223        &mut self,
1224        build: &mut impl FnMut(&mut State) -> V,
1225        state: &mut State,
1226    ) -> ChangeFlags {
1227        // Republish this root's focus session before the diff runs: a pod
1228        // severed by it compares its own stamp against the channel from its
1229        // destructor, and the channel mirrors one root at a time.
1230        self.publish_focus_session();
1231        let view = build(state);
1232        let mut flags = self.rebuild_view(view);
1233
1234        // Deferred-callback convergence loop (see the method doc). Each pass:
1235        // drain the flag, run the queued callbacks against real state, then
1236        // re-diff so this frame reflects them.
1237        let mut passes = 0usize;
1238        while crate::event::take_pending_result_flush() {
1239            if passes >= MAX_PENDING_RESULT_FLUSH_PASSES {
1240                // Cap reached. Put the flag back — the work is still owed — and
1241                // ask for one more frame instead of spinning inside this one.
1242                // `pending |= PAINT` is what the mobile frame gate reads
1243                // (`has_pending_change_flags`); `deferred_frame` is what surfaces
1244                // on the next `paint` as `needs_frame`, which is how the desktop
1245                // `ControlFlow::Wait` loop learns to wake.
1246                crate::event::mark_pending_result_flush();
1247                flags |= ChangeFlags::PAINT;
1248                self.deferred_frame = true;
1249                break;
1250            }
1251            // The dispatch's own outcome is load-bearing, not noise: a flushed
1252            // callback whose *only* effect is `EventCtx::request_redraw` (no
1253            // signal write, no state the next build-closure run reads) leaves the
1254            // re-diff below reporting `ChangeFlags::NONE`, so nothing else in
1255            // this method would ever mark the frame dirty and the requested
1256            // redraw would be dropped on the floor. Fold it into exactly the
1257            // wake the cap branch above raises: `PAINT` reaches `self.pending`,
1258            // which is what the mobile frame gate reads
1259            // (`has_pending_change_flags`), and `deferred_frame` surfaces on the
1260            // next `paint` as `needs_frame`, which is how the desktop
1261            // `ControlFlow::Wait` loop learns to schedule a frame. Both
1262            // Housekeeping producers need it (a navigator pop-result callback
1263            // and `frust-widgets`' gesture long-press latch), and without it a
1264            // redraw-only effect waits for whatever input happens to arrive
1265            // next — exactly the failure this mechanism exists to remove.
1266            //
1267            // Non-empty flags also bump the semantics generation below, which
1268            // is correct: the callback just mutated real `State` through a live
1269            // `EventCtx`, so the accessibility tree may genuinely have changed,
1270            // and every other paint-class path here bumps it the same way (a
1271            // spurious bump costs one recompute of an unchanged tree, a missed
1272            // one strands a stale tree).
1273            let outcome = self.event(state, &InputEvent::Housekeeping);
1274            if outcome.needs_redraw {
1275                flags |= ChangeFlags::PAINT;
1276                self.deferred_frame = true;
1277            }
1278            let view = build(state);
1279            flags |= self.rebuild_view(view);
1280            passes += 1;
1281        }
1282
1283        // Generic-unmount focus release. A reconciler that tears down (or
1284        // type-swaps, or clears the `focused` flag of) a child pod holding the
1285        // recorded focus path *on the live focus chain* has severed that path,
1286        // but runs over a `BuildCtx` with no `RenderRoot` in scope — so it raises
1287        // `mark_focus_orphaned` and this drain performs the release the
1288        // reconciler could not. "On the live chain" is what `rebuild_view`'s seed
1289        // buys: a mark means a live session lost its owner, never that some stale
1290        // flag deep in an already-blurred branch went away (see
1291        // `mark_focus_orphaned`). Without it the root's mirror stays standing over
1292        // a widget that no longer exists: `is_focus_active()` keeps reporting
1293        // true and `ime_state()` keeps handing the shell a surface for a dead
1294        // field, self-correcting only on the next event pass — which never
1295        // arrives on a screen the user has stopped touching (the pop-into-idle
1296        // case this whole seam exists for).
1297        //
1298        // Drained *after* the flush loop so one release covers every pass: a
1299        // flushed callback that navigates re-diffs, and either diff may orphan
1300        // the focus. `Housekeeping` claims no focus of its own (its root arm is
1301        // inert), so nothing the loop dispatched can be undone here.
1302        //
1303        // The release marks no `ChangeFlags` of its own: the structural change
1304        // that severed the path already flagged `LAYOUT | PAINT`, and the
1305        // generation bump is what wakes the mobile frame gate's
1306        // `focus_or_ime_changed` edge for the one repaint the release needs.
1307        if crate::event::take_focus_orphaned() {
1308            self.release_focus_session();
1309        }
1310
1311        // Generic-unmount hover release, the same shape one channel over: a diff
1312        // that dropped the `ChildPod` holding the live hover link has severed a
1313        // path the epoch mechanism cannot strand, because stranding needs a hover
1314        // pass and the dead claimant will never see another one. Without this the
1315        // mirror stands over a widget that no longer exists — `is_hover_active()`
1316        // reporting a link nothing holds — and every surviving ancestor of the
1317        // claimant keeps painting hover chrome off its own still-matching stamp
1318        // until some later `Move` re-derives, which never comes on a pointer the
1319        // user has stopped moving.
1320        //
1321        // The mark is raised by the pod's destructor rather than by the
1322        // reconcilers (the stamp has no setter for a container to cooperate
1323        // through — see `mark_hover_orphaned`), which is what makes this cover
1324        // every removal route, including hand-rolled containers outside this
1325        // workspace. That reach is also why the mark is qualified by
1326        // `root_identity`: a destructor fires whenever a pod happens to die, so
1327        // an unqualified mark could be a second root's on this thread. Drained
1328        // after the flush loop for the focus release's reason: any pass of the
1329        // loop may re-diff, and one end covers them all.
1330        //
1331        // Unlike that release this one flags `PAINT` of its own. The reconciler
1332        // that dropped the claimant usually reported `LAYOUT | PAINT` already,
1333        // but "usually" is not a contract this drain can rest on: the destructor
1334        // route deliberately covers containers outside this workspace (that is
1335        // its whole reason for existing), and one of those can drop a pod while
1336        // reporting whatever flags it likes. Ending a hover always changes what
1337        // paints, so the correction states its own need for the frame it rides
1338        // on — idempotent where the reconciler already said so.
1339        if crate::event::take_hover_orphaned(self.root_identity) && self.hover_active {
1340            self.end_hover_link();
1341            flags |= ChangeFlags::PAINT;
1342        }
1343
1344        self.pending |= flags;
1345        // A rebuild that changed layout/paint could have changed the semantics
1346        // tree (added/removed/relabelled nodes); bump the dirty gate a shell polls
1347        // via `semantics_if_changed`.
1348        if !flags.is_empty() {
1349            self.semantics_gen = self.semantics_gen.wrapping_add(1);
1350        }
1351        flags
1352    }
1353
1354    /// The rebuild body, split out so [`RenderRoot::rebuild`] can accumulate the
1355    /// result into [`RenderRoot::pending`] in one place.
1356    ///
1357    /// # Seeding the diff's focus chain
1358    ///
1359    /// The root is where the effective focus chain ([`BuildCtx::has_focus`])
1360    /// starts: the root widget sits in no `ChildPod`, so its "link above" is the
1361    /// root's own session mirror. A reconciler deep in the diff ANDs its pod's
1362    /// `focused` flag onto this seed and marks an orphan only if the whole chain
1363    /// holds — which is why the seed is "is there a session to lose" rather than
1364    /// `focus_active` alone: an active surface parked without the flag is still a
1365    /// live session `release_focus_session` would move. With neither set there is
1366    /// nothing to release, so the seed is `false` and the diff marks nothing.
1367    fn rebuild_view(&mut self, view: V) -> ChangeFlags {
1368        // Read before the `&mut self.next_id` borrow below (disjoint fields, but
1369        // spelled out for the reader).
1370        let session_live = self.focus_active || self.ime_state.is_some();
1371        match (self.root_id, self.prev_view.take()) {
1372            // Reconcile against the previous view of the same type.
1373            (Some(root_id), Some(prev)) => {
1374                let mut ctx = BuildCtx::new(&mut self.next_id);
1375                ctx.set_has_focus(session_live);
1376                let flags = {
1377                    let pod = self
1378                        .tree
1379                        .pod_mut(root_id)
1380                        .expect("root pod present when root_id is set");
1381                    let element = pod
1382                        .widget_mut()
1383                        .downcast_mut::<V::Element>()
1384                        .expect("root widget type matches its originating view");
1385                    view.rebuild(&prev, element, &mut ctx)
1386                };
1387                if let Some(pod) = self.tree.pod_mut(root_id) {
1388                    pod.merge_flags(flags);
1389                }
1390                self.prev_view = Some(view);
1391                flags
1392            }
1393            // First build: materialise the widget and insert it as the root.
1394            _ => {
1395                let mut ctx = BuildCtx::new(&mut self.next_id);
1396                // A first build tears nothing down, so the seed is moot — set it
1397                // anyway so the rule is "the root always seeds the chain", with no
1398                // arm exempt.
1399                ctx.set_has_focus(session_live);
1400                let id = ctx.alloc_id();
1401                let element = view.build(&mut ctx);
1402                // `new_typed` boxes the element exactly like `new` would, and
1403                // additionally records `V::Element`'s type name for
1404                // introspection — the concrete type is only nameable here.
1405                let pod = WidgetPod::new_typed(id, element);
1406                let root_id = self.tree.insert_root(pod);
1407                self.root_id = Some(root_id);
1408                self.prev_view = Some(view);
1409                ChangeFlags::LAYOUT | ChangeFlags::PAINT
1410            }
1411        }
1412    }
1413
1414    /// Lay out the root widget against `window_size` and record its geometry.
1415    ///
1416    /// The root receives loose constraints (zero up to the window size) and is
1417    /// placed at the origin. Returns the size the root chose. No text context is
1418    /// threaded in (use [`RenderRoot::layout_with_text`] when the tree contains
1419    /// text widgets); the stored theme, if any, is still threaded down.
1420    pub fn layout(&mut self, window_size: Size) -> Size {
1421        self.layout_inner(window_size, None)
1422    }
1423
1424    /// Lay out the root widget, threading a shared text-shaping context down to
1425    /// text widgets.
1426    ///
1427    /// `text_ctx` is the shell-owned `frust_text::TextContext`, passed
1428    /// type-erased so this crate needs no `frust-text` dependency. Text
1429    /// widgets recover it via [`crate::widget::LayoutCtx::text_context`]. The
1430    /// stored theme, if any, is threaded down alongside it.
1431    pub fn layout_with_text(&mut self, window_size: Size, text_ctx: &mut dyn Any) -> Size {
1432        self.layout_inner(window_size, Some(text_ctx))
1433    }
1434
1435    /// Shared layout body: hands the root loose window constraints, lends the
1436    /// optional text context and the stored theme into a [`LayoutCtx`], and
1437    /// records the size the root returns.
1438    fn layout_inner(&mut self, window_size: Size, text_ctx: Option<&mut dyn Any>) -> Size {
1439        self.window_size = window_size;
1440        let Some(root_id) = self.root_id else {
1441            return Size::ZERO;
1442        };
1443        let bc = BoxConstraints::loose(window_size);
1444        // Disjoint field borrows: the theme (immut) and the tree (mut) are
1445        // different fields of `self`, so both borrows coexist through the layout.
1446        let theme = self.theme.as_deref();
1447        // Copied out before the `&mut self.tree` borrow below (a disjoint,
1448        // `Copy` field read).
1449        let insets = self.insets;
1450        let Some(pod) = self.tree.pod_mut(root_id) else {
1451            return Size::ZERO;
1452        };
1453        let mut ctx = LayoutCtx::with_resources(text_ctx, theme);
1454        // Thread the window insets down; one layout context reaches the whole
1455        // tree, so the global insets are set once here (see `crate::insets`).
1456        ctx.set_window_insets(insets);
1457        // Thread the window's own size down the same way — global and
1458        // origin-independent like the insets. A widget floating an overlay pod
1459        // lays it out against this rather than against its own constraints (see
1460        // `LayoutCtx::window_size`).
1461        ctx.set_window_size(window_size);
1462        let size = pod.widget_mut().layout(&mut ctx, &bc);
1463        pod.set_layout(Point::ZERO, size);
1464        size
1465    }
1466
1467    /// Paint the root widget into `scene`, returning whether the tree wants
1468    /// another frame to continue an animation.
1469    ///
1470    /// A widget whose paint advances animation state (e.g. a scroll fling) signals
1471    /// [`PaintCtx::request_frame`]; that flag bubbles up through the container
1472    /// [`ChildPod`](crate::widget::ChildPod)s and out here as
1473    /// [`PaintOutcome::needs_frame`], which the shell honors by scheduling the next
1474    /// frame (desktop `window.request_redraw()`; the mobile continuous loops
1475    /// already do so). Mirrors how [`RenderRoot::event`] surfaces `needs_redraw`.
1476    ///
1477    /// A widget whose animation changes its *layout* signals
1478    /// [`PaintCtx::request_layout`] instead (or as well); that bubbles up the same
1479    /// way and is folded here into the render root's pending [`ChangeFlags`]
1480    /// (`LAYOUT`), so the *next* frame's
1481    /// [`take_change_flags`](RenderRoot::take_change_flags)`().needs_layout()`
1482    /// reports it and the mobile intra-frame layout skip relayouts while the
1483    /// animation is in flight. It is also surfaced on the returned
1484    /// [`PaintOutcome::needs_layout`].
1485    ///
1486    /// `frame_time` is the shell's shared monotonic clock for this frame
1487    /// (time enters `frust-core` from the shell, never `Instant::now()` here). It
1488    /// is seeded onto the root [`PaintCtx`] and threaded unchanged to every child
1489    /// ([`crate::widget::ChildPod::paint_child`]), so an animating widget advances
1490    /// against one consistent timestamp — see [`PaintCtx::frame_time`].
1491    ///
1492    /// # The overlay post-pass
1493    ///
1494    /// Painting the main tree is only the first half. Widgets registering a
1495    /// floated surface during that walk ([`PaintCtx::register_overlay`]) are
1496    /// drained here and painted **after** it, in band order — which is the only
1497    /// way a popover, menu or tooltip escapes its owner's paint order and every
1498    /// ancestor's clip. Their routing rects are retained (see
1499    /// `RenderRoot::overlay_hits`) for the next event pass to hit-test first, and
1500    /// their paint outcomes merge into this pass's own, so an animating overlay
1501    /// keeps the frames coming exactly like an animating widget in the tree.
1502    pub fn paint(&mut self, scene: &mut dyn PaintScene, frame_time: FrameTime) -> PaintOutcome {
1503        // Open the two paint-pass channels for the whole pass. Entering CLEARS
1504        // each slot, which is what makes "the registry is empty at the start of
1505        // every paint" true by construction rather than by everyone remembering
1506        // to unregister; `Drop` hands an enclosing pass its own back.
1507        let overlay_pass = OverlayPaintPass::enter();
1508        let toolbar_pass = SelectionToolbarPass::enter();
1509        // Republish this root's focus session before anything reads it: the
1510        // channel mirrors one root, and the pods about to be visited must
1511        // compare their stamps against *this* root's session.
1512        self.publish_focus_session();
1513
1514        let (mut outcome, main_tree_ime) = self.paint_main_tree(scene, frame_time);
1515
1516        // Drain what the main tree registered and paint it above everything.
1517        // Sorting is stable, so the band decides and registration order breaks
1518        // ties within a band (see `crate::overlay::sort_into_paint_order`).
1519        let mut entries = overlay_pass.take();
1520        sort_into_paint_order(&mut entries);
1521        // Retain the routing half — never the pods — for the next event pass.
1522        self.overlay_hits = entries.iter().map(OverlayHit::of).collect();
1523        let (holder, holder_ime) = self.paint_overlays(&entries, scene, frame_time, &mut outcome);
1524        // Release the owners' pod clones before the pass ends: the root holds no
1525        // overlay pod at rest, so a pod's lifetime stays exactly its owner's.
1526        drop(entries);
1527
1528        // Both halves have now spoken, so the one question neither of them can
1529        // answer alone — whose surface the shell is configured with — is settled
1530        // in one place, with both answers in hand.
1531        self.resolve_session_surface(main_tree_ime, holder, holder_ime);
1532
1533        // Resolve the selection-toolbar publish last, so a field that published
1534        // while painting *inside* a floated pod (a text input hosted in a
1535        // popover) is resolved by the same rule as one in the main tree.
1536        self.resolve_selection_toolbar(toolbar_pass.take());
1537
1538        outcome
1539    }
1540
1541    /// Settle which branch's IME surface the shell is configured with, from the
1542    /// two halves of the paint pass: what the main tree published, and what the
1543    /// floated surface holding the live focus link published (`holder`/
1544    /// `holder_ime`, both resolved from the pods' own stamps — see
1545    /// [`RenderRoot::paint_overlays`]).
1546    ///
1547    /// # Why it is decided here and not where the publish happens
1548    ///
1549    /// A publish is a claim about *the* session, and the session has exactly one
1550    /// owner. The main tree paints first, so at the moment it publishes, nothing
1551    /// yet knows whether a floated surface is about to prove that the session is
1552    /// no longer the tree's. Storing it there and correcting later is what
1553    /// produced a frame of lag with a secure field's surface standing in it —
1554    /// and the record the correction had to be driven from was one nothing could
1555    /// falsify. Deferring the decision by the width of one pass removes both.
1556    ///
1557    /// # The rules
1558    ///
1559    /// * the owner's own publish wins, whether that owner is a surface or the
1560    ///   tree; a publish from anywhere else was already dropped by the half that
1561    ///   collected it;
1562    /// * an **inactive** publish from the owner is the session ending, not a
1563    ///   value — the same reading every other release site gives it;
1564    /// * an owner that published **nothing** leaves a standing surface standing,
1565    ///   *unless* ownership moved this pass: then what stands belongs to the
1566    ///   branch that just lost the session, and goes with it rather than
1567    ///   remaining as the shell's idea of a live one. This is the rule that stops
1568    ///   a popover taking focus from leaving the platform keyboard configured for
1569    ///   the secure field underneath it — in both directions, since the session
1570    ///   coming back to the tree moves ownership just as much as it leaving.
1571    fn resolve_session_surface(
1572        &mut self,
1573        main_tree_ime: Option<ImeState>,
1574        holder: Option<OverlayKey>,
1575        holder_ime: Option<ImeState>,
1576    ) {
1577        let published = if holder.is_some() {
1578            holder_ime
1579        } else {
1580            main_tree_ime
1581        };
1582        let ownership_moved = holder != self.focus_surface;
1583        match published {
1584            Some(ime) if ime.active => self.store_ime_state(Some(ime)),
1585            Some(_) => self.release_focus_session(),
1586            None if ownership_moved => self.store_ime_state(None),
1587            None => {}
1588        }
1589        // Resolved from the links themselves, never from the focus request that
1590        // opened the session — see the field's doc.
1591        self.focus_surface = if self.focus_active { holder } else { None };
1592    }
1593
1594    /// Paint the main widget tree — everything [`RenderRoot::paint`] does before
1595    /// the floated overlay pods get their turn.
1596    ///
1597    /// Split out from [`RenderRoot::paint`] purely for borrow scoping: this body
1598    /// holds `&mut self.tree` (beside disjoint borrows of the theme) for its whole
1599    /// length, while painting an overlay pod needs the root's fields again to
1600    /// merge outcomes and extend the per-pass channels. Nothing about the pass
1601    /// itself changed when it moved here.
1602    ///
1603    /// Reports, beside the outcome, the IME surface the tree published this pass
1604    /// (if any) — handed back rather than stored, because whether the tree still
1605    /// owns the session is not knowable until the floated pods have been visited
1606    /// (see [`RenderRoot::resolve_session_surface`]).
1607    fn paint_main_tree(
1608        &mut self,
1609        scene: &mut dyn PaintScene,
1610        frame_time: FrameTime,
1611    ) -> (PaintOutcome, Option<ImeState>) {
1612        let Some(root_id) = self.root_id else {
1613            return (PaintOutcome::default(), None);
1614        };
1615        // Disjoint field borrows: the theme (immut) vs the tree (mut).
1616        let theme = self.theme.as_deref();
1617        // Copied out before the `&mut self.tree` borrow (a disjoint `Copy` read).
1618        let insets = self.insets;
1619        // Same disjoint `Copy` read: the presented-frame count threaded to widgets.
1620        let presented_frames = self.presented_frames;
1621        // Same disjoint `Copy` read: the surface-translucency flag the
1622        // platform-view hole-punch reads (see `PaintCtx::is_translucent`).
1623        let surface_translucent = self.surface_translucent;
1624        if let Some(pod) = self.tree.pod_mut(root_id) {
1625            let mut ctx = PaintCtx::new(pod.origin(), pod.size());
1626            // Seed the shared shell clock so the whole paint pass sees one time.
1627            ctx.set_frame_time(frame_time);
1628            // Lend the stored theme (type-erased) into the paint pass; widgets
1629            // recover it via `PaintCtx::theme_as`.
1630            ctx.set_theme(theme);
1631            // Thread the window insets down (global — see `crate::insets`).
1632            ctx.set_window_insets(insets);
1633            // Thread the shell's presented-frame count down (global; a widget
1634            // measuring FPS differences it — see `PaintCtx::presented_frames`).
1635            ctx.set_presented_frames(presented_frames);
1636            // Thread the surface-translucency flag down (global; the
1637            // platform-view hole-punch gates its rect-clear on it — see
1638            // `PaintCtx::is_translucent`).
1639            ctx.set_translucent(surface_translucent);
1640            // Seed the root widget's paint-time focus from the session mirror so
1641            // a leaf-root editable observes its own focus, and thread the live
1642            // session's identity down beside it: deeper focus is resolved
1643            // per-pod by `ChildPod::paint_child`, which counts a recorded link
1644            // only while its stamp names this session.
1645            //
1646            // The mirror alone, deliberately. Narrowing the seed by *which
1647            // branch* the root believes owns the session was the previous shape,
1648            // and it de-seeded the whole tree off a record no pass could
1649            // falsify: a link a floated pod recorded is reached by no container's
1650            // blur sweep, so once one existed the tree never got seeded again.
1651            // The epoch answers the same question where it can actually be
1652            // answered — at each link, against the session that link was
1653            // recorded for.
1654            //
1655            // The arena root has no recorded link of its own (it is a
1656            // `WidgetPod`, not a `ChildPod`, and carries no stamp), so a
1657            // *leaf-root* editable is still seeded from the bare mirror. Every
1658            // real tree puts a container there, and the first `ChildPod` below it
1659            // composes the stamp back in.
1660            ctx.set_has_focus(self.focus_active);
1661            ctx.set_focus_epoch(self.focus_epoch);
1662            // Thread the hover mirror + live epoch the same way: the root widget's
1663            // own hover comes from the mirror (a leaf root can claim hover itself),
1664            // and deeper links are resolved per-pod by `ChildPod::paint_child`
1665            // against this epoch.
1666            ctx.set_hovered(self.hover_active);
1667            ctx.set_hover_epoch(self.hover_epoch);
1668            pod.widget_mut().paint(&mut ctx, scene);
1669            pod.clear_flags();
1670            // A focused editable republishes its IME surface during paint (which
1671            // runs after every rebuild), so a controlled change applied by the
1672            // rebuild — e.g. a submit clearing the field — refreshes the
1673            // shell-facing `ime_state` that the event pass alone would leave
1674            // stale.
1675            //
1676            // Collected, not stored: what the tree published is only the
1677            // session's if the session is still the tree's, and the pods that
1678            // could say otherwise have not been visited yet.
1679            // `resolve_session_surface` decides once both halves have spoken,
1680            // and routes the store through the change guard so an *unchanged*
1681            // republish — the overwhelmingly common case, a focused field
1682            // re-publishing the same surface frame after frame — moves no
1683            // generation and therefore fires no `focus_or_ime_changed` edge at
1684            // the shell.
1685            //
1686            // An **inactive** publish is not a surface refresh at all: it is the
1687            // publishing widget saying "this session is over" — the navigator's
1688            // post-pop `cleared_ime_state`, `PatternSwitcher`'s equivalent, and
1689            // a `TextInput` turned disabled/read-only under a live focus are the
1690            // three shipped producers, and every one of them is an unmount or a
1691            // de-focus. Storing it as `Some(inactive)` and leaving `focus_active`
1692            // standing is what leaked the session after a pop: the shells' gate
1693            // saw `ime_state().is_some()`, `is_focus_active()` kept lying, and the
1694            // next real focus interaction started from a corrupt baseline. So the
1695            // resolver takes the *full* release instead — the same one a
1696            // blur-on-outside-tap `Down` performs, firing exactly one edge.
1697            //
1698            // The `self.focus_active` guard stays and is applied here, at the
1699            // point of collection: a publish arriving when no session is live
1700            // describes nothing, so a widget whose pod focus was just cleared by
1701            // a container-routed blur (but whose internal flag lags one frame)
1702            // can never resurrect the `ime_state` that blur dropped — even
1703            // before it observes the blur via `PaintCtx::has_focus`.
1704            //
1705            // Provenance below that guard is the chain's own job now: a link
1706            // reads focused only while its stamp names the live session, so a
1707            // branch the session has left publishes nothing to collect.
1708            let main_tree_ime = if self.focus_active {
1709                ctx.take_ime_state()
1710            } else {
1711                None
1712            };
1713            // Replace (never merge) the whole platform-view collection with
1714            // whatever this pass published — unlike `ime_state` above there is
1715            // no single "the" published instance to guard behind a focus
1716            // check, and a pass that publishes none must clear out every
1717            // stale frame from the previous one (see the field's doc comment).
1718            self.platform_view_frames = ctx.take_platform_views();
1719            // Same replace-per-pass discipline for the z-shield channel: a pass
1720            // whose shields stopped painting reports none, so a stale shield can
1721            // never keep stealing input from an interactive slot (see
1722            // `PaintCtx::report_input_shield`).
1723            self.input_shields = ctx.take_input_shields();
1724            // Fold a bubbled `request_layout` into `pending` so the *next* frame
1725            // relayouts. `pending` survives to the next frame and feeds both the
1726            // frame gate (`has_pending_change_flags`) and the Android layout-skip
1727            // (`take_change_flags().needs_layout()`), so no shell change is needed
1728            // on any platform. Deliberately opt-in: `request_frame` alone never
1729            // sets LAYOUT, keeping paint-only animations layout-free.
1730            let needs_layout = ctx.needs_layout();
1731            if needs_layout {
1732                self.pending |= ChangeFlags::LAYOUT;
1733            }
1734            // A rebuild that ran out of flush passes owes one more frame; surface
1735            // it here (and clear it) so a dirty-driven shell schedules the frame
1736            // that finishes the flush — see the `deferred_frame` field doc.
1737            let deferred_frame = std::mem::take(&mut self.deferred_frame);
1738            let outcome = PaintOutcome {
1739                needs_frame: ctx.needs_frame() || deferred_frame,
1740                needs_layout,
1741                // Aggregate tick class: paced-only iff a frame was requested and
1742                // every request was CosmeticLoop-class. The mobile frame gate
1743                // may throttle such a frame; any Transition request
1744                // (including the LAYOUT-implying `request_layout` above) leaves
1745                // this false so the frame runs every vsync.
1746                needs_frame_paced_only: ctx.needs_frame_paced_only(),
1747                // ...and, when it IS paceable, how fast it asked to be re-run:
1748                // the MIN over every paced request this pass (`Duration::ZERO`
1749                // / `None` meaning the theme's own cosmetic rate). The gate
1750                // resolves it against the live theme's cap — see
1751                // `PaintCtx::request_frame_paced_at`.
1752                paced_interval: ctx.paced_interval(),
1753            };
1754            (outcome, main_tree_ime)
1755        } else {
1756            (PaintOutcome::default(), None)
1757        }
1758    }
1759
1760    /// Paint the pods registered during this pass, above the main tree, and fold
1761    /// each one's paint outcome back into `outcome`.
1762    ///
1763    /// `entries` arrives in paint order (`Floating` band first, then `Tooltip`,
1764    /// registration order within each). Each pod is painted through a
1765    /// [`PaintCtx`] whose absolute origin is its own registered
1766    /// [`window_rect`](crate::overlay::OverlayEntry::window_rect) — not its
1767    /// owner's origin, which is the whole point of floating — carrying the same
1768    /// clock, theme, insets, presented count, translucency and focus/hover
1769    /// seeding the root pod's context carries, so a widget inside a pod cannot
1770    /// tell it is not in the tree.
1771    ///
1772    /// The pod borrow is taken per entry and released before the next: the root
1773    /// must never hold one across a pass boundary, nor across another entry's
1774    /// paint (two entries may belong to the same owner).
1775    ///
1776    /// It is also the one pass that can act on a floated pod's focus link at
1777    /// all, because it is the one pass holding the pods. Two things follow, and
1778    /// both happen here:
1779    ///
1780    /// * it **retires** a link the live session has already stranded
1781    ///   ([`ChildPod::retire_stale_focus_link`](crate::widget::ChildPod::retire_stale_focus_link)),
1782    ///   so the raw flag consumers that cannot consult an epoch — an owner's own
1783    ///   `is_focused()` read, a hand-written container's routing — stop seeing a
1784    ///   record of a session that has moved on;
1785    /// * it **resolves** which surface, if any, holds a link on the live session,
1786    ///   and collects that one surface's IME publish. Every other pod's publish
1787    ///   is dropped where it is taken: paint descends into every pod
1788    ///   unconditionally and the bubble up `paint_child` carries a published
1789    ///   surface whatever the publisher's link says, so without this the last pod
1790    ///   painted would decide what the shell is configured with — a surface
1791    ///   belonging to a field it has nothing to do with, secure-text
1792    ///   configuration and all.
1793    ///
1794    /// Returns `(holder, holder's publish)` for
1795    /// [`RenderRoot::resolve_session_surface`] to settle against the main tree's.
1796    /// **Two holders resolve to none:** a second live link is a contradiction the
1797    /// mechanism is supposed to make impossible (one claim, one chain, one
1798    /// stamp), and answering it by picking the topmost would let a surface speak
1799    /// for a session on evidence that has already failed. Reporting no holder
1800    /// instead means neither surface's publish is carried out of this pass —
1801    /// the resolver then reads the pass exactly as it reads one where nothing is
1802    /// floated at all.
1803    fn paint_overlays(
1804        &mut self,
1805        entries: &[OverlayEntry],
1806        scene: &mut dyn PaintScene,
1807        frame_time: FrameTime,
1808        outcome: &mut PaintOutcome,
1809    ) -> (Option<OverlayKey>, Option<ImeState>) {
1810        if entries.is_empty() {
1811            // Nothing is floated, so nothing floated owns the session.
1812            return (None, None);
1813        }
1814        // The same disjoint field borrows the main pass takes, for the same
1815        // reason: the theme is lent immutably into each context while other
1816        // fields of `self` are written.
1817        let theme = self.theme.as_deref();
1818        let presented_frames = self.presented_frames;
1819        let surface_translucent = self.surface_translucent;
1820        let hover_active = self.hover_active;
1821        let hover_epoch = self.hover_epoch;
1822        let focus_active = self.focus_active;
1823        let focus_epoch = self.focus_epoch;
1824
1825        // The surface whose pod holds a link on the LIVE session, resolved as the
1826        // pods go past, together with the surface that pod published — one pair,
1827        // so the publish can never be attributed to an entry that did not make
1828        // it. A second live holder sets `contested` and the pair is discarded.
1829        let mut holder: Option<(OverlayKey, Option<ImeState>)> = None;
1830        let mut contested = false;
1831
1832        for entry in entries {
1833            let mut ctx = PaintCtx::new(entry.window_rect.origin(), entry.window_rect.size());
1834            ctx.set_frame_time(frame_time);
1835            ctx.set_theme(theme);
1836            ctx.set_window_insets(entry.insets);
1837            ctx.set_presented_frames(presented_frames);
1838            ctx.set_translucent(surface_translucent);
1839            // Seeded from the root's own mirrors exactly as the root pod's
1840            // context is, so a focused editable inside a floated pod observes its
1841            // focus (and a hovered one its hover) through the ordinary
1842            // `ChildPod::paint_child` composition.
1843            //
1844            // The mirror alone, deliberately: the pod's own link is ANDed onto it
1845            // inside `paint_child`, which is the same composition that decides
1846            // the main tree's, so a pod that holds no link reads unfocused
1847            // whatever the mirror says.
1848            ctx.set_has_focus(focus_active);
1849            ctx.set_focus_epoch(focus_epoch);
1850            ctx.set_hovered(hover_active);
1851            ctx.set_hover_epoch(hover_epoch);
1852            // Retired and read before the paint, in the same borrow: nothing in a
1853            // paint pass moves the focus path, and the answer is what decides
1854            // whether this pod may describe the session below.
1855            let pod_holds_focus = {
1856                let mut pod = entry.pod.borrow_mut();
1857                pod.retire_stale_focus_link();
1858                let holds = pod.holds_live_focus();
1859                pod.paint_child(&mut ctx, scene);
1860                holds
1861            };
1862
1863            // Fold this pod's continuation-frame request into the frame's outcome
1864            // on the two lattices the tree's own aggregation uses: `needs_frame`
1865            // ORs, the class is a max-lattice (any unpaced request makes the whole
1866            // frame unpaced), and the paced interval is a MIN-lattice. The
1867            // standing aggregate's class is recovered from the outcome itself —
1868            // `needs_frame && !needs_frame_paced_only` is precisely "something
1869            // unpaced asked" — which also preserves the deferred-flush frame the
1870            // main pass may have folded in.
1871            let stood_unpaced = outcome.needs_frame && !outcome.needs_frame_paced_only;
1872            let entry_unpaced = ctx.needs_frame() && !ctx.needs_frame_paced_only();
1873            outcome.needs_frame |= ctx.needs_frame();
1874            outcome.needs_frame_paced_only =
1875                outcome.needs_frame && !(stood_unpaced || entry_unpaced);
1876            outcome.paced_interval = match (outcome.paced_interval, ctx.paced_interval()) {
1877                (Some(standing), Some(asked)) => Some(standing.min(asked)),
1878                (standing, asked) => standing.or(asked),
1879            };
1880            // A layout-animating widget inside a pod relayouts the next frame the
1881            // same way one in the tree does.
1882            if ctx.needs_layout() {
1883                outcome.needs_layout = true;
1884                self.pending |= ChangeFlags::LAYOUT;
1885            }
1886            // A focused editable inside a pod republishes its IME surface on every
1887            // paint, exactly like one in the tree, so the same rules apply
1888            // verbatim: collect a publish only while a session is actually active
1889            // (never resurrect a surface a blur cleared), and only from the pod
1890            // that holds a link on THAT session — the provenance a child of the
1891            // tree gets for free from its chain. A publish taken from any other
1892            // pod is dropped here, which is why the take is unconditional: the
1893            // per-entry context is about to be discarded either way, and leaving
1894            // a surface in it would only invite a later reader to trust it.
1895            //
1896            // An *inactive* publish is refused on exactly the same terms rather
1897            // than treated as a release: ending a session is a claim about it
1898            // too, and a pod that does not hold it makes neither.
1899            let published = ctx.take_ime_state();
1900            if focus_active && pod_holds_focus {
1901                if holder.is_some() {
1902                    // Two pods claiming one session. See the method doc: the
1903                    // contradiction is answered by attributing the session to
1904                    // nobody, not by ranking the claimants.
1905                    contested = true;
1906                } else {
1907                    holder = Some((entry.key, published));
1908                }
1909            }
1910            // EXTEND the two replace-per-pass channels rather than replacing them:
1911            // `paint_main_tree` already put this pass's tree-published frames and
1912            // shields there, and a platform-view slot or z-shield that happens to
1913            // paint inside a floated pod must survive beside them (see
1914            // `PaintCtx::publish_platform_view`).
1915            self.platform_view_frames.extend(ctx.take_platform_views());
1916            self.input_shields.extend(ctx.take_input_shields());
1917        }
1918
1919        match holder {
1920            Some((key, published)) if !contested => (Some(key), published),
1921            _ => (None, None),
1922        }
1923    }
1924
1925    /// Resolve this paint pass's selection-toolbar publish into the shell-facing
1926    /// slot, moving [`RenderRoot::selection_toolbar_generation`] only on a
1927    /// **menu edge**.
1928    ///
1929    /// The request carries two shapes of fact, and they are resolved differently
1930    /// (see [`SelectionToolbarRequest`]):
1931    ///
1932    /// * The whole request is stored as a **level**, newest wins. A shell reads
1933    ///   [`SelectionToolbarRequest::anchor`] on every tick it has a menu on
1934    ///   screen, so the stored anchor has to be the current one, not the one the
1935    ///   generation last moved for.
1936    /// * The generation moves on the **menu-significant** part alone:
1937    ///   [`SelectionToolbarRequest::present_menu`] and
1938    ///   [`SelectionToolbarRequest::actions`], with an absent request reading as
1939    ///   "no menu, no verbs" so appearing and disappearing are edges on the same
1940    ///   comparison. An anchor that merely moved is deliberately **not** an edge:
1941    ///   a focused field recomputes its anchor every painted frame, and a
1942    ///   selection dragged wider moves it on every touch sample — bumping there
1943    ///   would ask the platform to re-present its menu per sample.
1944    ///
1945    /// The publish is refused outright while no focus session is active, mirroring
1946    /// `paint`'s refusal to let a paint-time publish resurrect a cleared IME
1947    /// surface: the request describes the focused field, so one arriving after the
1948    /// blur describes a field that no longer holds anything.
1949    fn resolve_selection_toolbar(&mut self, published: Option<SelectionToolbarRequest>) {
1950        let next = if self.focus_active { published } else { None };
1951        // "Nothing published" is the same statement as "no menu wanted, no verbs
1952        // enabled" — which is what lets one comparison cover a change between two
1953        // requests, a first appearance, and a clearing alike.
1954        let menu_edge = |request: &Option<SelectionToolbarRequest>| {
1955            request.map_or((false, SelectionToolbarActions::default()), |request| {
1956                (request.present_menu, request.actions)
1957            })
1958        };
1959        if menu_edge(&self.selection_toolbar) != menu_edge(&next) {
1960            self.selection_toolbar_gen = self.selection_toolbar_gen.wrapping_add(1);
1961        }
1962        self.selection_toolbar = next;
1963    }
1964
1965    /// The selection-toolbar request the focused field published during the most
1966    /// recent [`RenderRoot::paint`], or `None` when no field is focused at all.
1967    ///
1968    /// The shell half of the platform edit-menu route
1969    /// ([`SelectionToolbarPolicy::Native`](crate::selection_toolbar::SelectionToolbarPolicy::Native)):
1970    /// a shell reads it beside [`RenderRoot::ime_state`], answers "may I offer
1971    /// this verb?" from [`SelectionToolbarRequest::actions`] whenever the platform
1972    /// asks, and presents the host's own menu at
1973    /// [`SelectionToolbarRequest::anchor`] when
1974    /// [`SelectionToolbarRequest::present_menu`] says so. A **level**, not an edge
1975    /// — re-read it as often as you like; pair it with
1976    /// [`RenderRoot::selection_toolbar_generation`] to notice the changes worth
1977    /// presenting or dismissing for.
1978    ///
1979    /// Present for a focused field with no selection at all, which is not a
1980    /// wasted answer: paste applies to a bare caret, and a platform asking
1981    /// whether it may offer one needs a reply before any bar exists.
1982    ///
1983    /// A field under the framework policy publishes this too (it floats its own
1984    /// toolbar through [`crate::overlay`] as well), so a shell that drives the
1985    /// platform menu must decide on the policy, not on the presence of a request.
1986    pub fn selection_toolbar(&self) -> Option<SelectionToolbarRequest> {
1987        self.selection_toolbar
1988    }
1989
1990    /// A monotonically-increasing generation bumped on every change to the
1991    /// **menu-significant** part of [`RenderRoot::selection_toolbar`] — its
1992    /// [`present_menu`](SelectionToolbarRequest::present_menu) flag and its
1993    /// [`actions`](SelectionToolbarRequest::actions) — including the clearing that
1994    /// a blur produces, so a shell sees the menu going away as an edge too.
1995    ///
1996    /// A moved [`anchor`](SelectionToolbarRequest::anchor) is **not** an edge: it
1997    /// is republished (and recomputed) every painted frame, so a shell re-reads it
1998    /// from [`RenderRoot::selection_toolbar`] rather than waiting for this to move
1999    /// — bumping on it would re-present a menu on every touch sample of a drag
2000    /// that widens a selection.
2001    ///
2002    /// The `focus_ime_generation` contract one channel over: a shell caches the
2003    /// last value it acted on and acts only when it moves, which is what keeps a
2004    /// standing selection — republished every single frame — from asking the
2005    /// platform to re-present its menu on every vsync.
2006    pub fn selection_toolbar_generation(&self) -> u64 {
2007        self.selection_toolbar_gen
2008    }
2009
2010    /// Collect the accessibility tree for the current frame,
2011    /// returning a [`SemanticsUpdate`] a platform adapter (`accesskit_*`)
2012    /// can consume.
2013    ///
2014    /// Pull-based and stateless: the shell calls this when a platform a11y client
2015    /// asks for the tree (or after a change), *never* per frame — this crate owns
2016    /// no scheduling. Must run **after** [`RenderRoot::layout`], since node bounds
2017    /// come from the pods' post-layout geometry.
2018    ///
2019    /// The result is always rooted at a synthetic [`accesskit::Role::Window`]
2020    /// node covering the window, whose children are whatever the root widget
2021    /// contributed. An unbuilt tree yields a bare window node with no children.
2022    pub fn semantics(&self) -> SemanticsUpdate {
2023        let mut ctx = SemanticsCtx::new(self.window_size, self.semantics_alloc.get());
2024        let window = self.window_size;
2025        let root_pod = self.root_id.and_then(|id| self.tree.pod(id));
2026        // The root pod is arena-backed (not a `ChildPod`), so it caches its stable
2027        // base id in `root_semantics_id` rather than in a pod — assigned on first
2028        // pass and reused thereafter, exactly like `ChildPod::semantics_base`.
2029        let root_widget_base = match self.root_semantics_id.get() {
2030            Some(id) => id,
2031            None => {
2032                let id = ctx.alloc_base();
2033                self.root_semantics_id.set(Some(id));
2034                id
2035            }
2036        };
2037        let root_node = ctx.push_container_with_id(
2038            ROOT_NODE_ID,
2039            accesskit::Role::Window,
2040            |node| {
2041                node.set_bounds(accesskit::Rect {
2042                    x0: 0.0,
2043                    y0: 0.0,
2044                    x1: window.width,
2045                    y1: window.height,
2046                });
2047            },
2048            |ctx| {
2049                if let Some(pod) = root_pod {
2050                    // The root pod sits at its recorded origin (ZERO today) with
2051                    // its laid-out size; descend into that geometry and its stable
2052                    // id scope, mirroring `ChildPod::semantics_child`.
2053                    ctx.descend_into_pod(
2054                        root_widget_base,
2055                        pod.origin().to_vec2(),
2056                        pod.size(),
2057                        |ctx| {
2058                            pod.widget().semantics(ctx);
2059                        },
2060                    );
2061                }
2062            },
2063        );
2064        // Persist the allocator's high-water mark so the next pass keeps handing
2065        // out fresh, never-reused bases to pods that first appear later.
2066        self.semantics_alloc.set(ctx.next_base());
2067        ctx.finish(root_node)
2068    }
2069
2070    /// The current semantics generation — bumped by every rebuild/theme swap that
2071    /// could have changed the accessibility tree (the semantics dirty gate).
2072    ///
2073    /// A shell records the value it last pushed and compares; see
2074    /// [`RenderRoot::semantics_if_changed`].
2075    pub fn semantics_generation(&self) -> u64 {
2076        self.semantics_gen
2077    }
2078
2079    /// Pull a fresh [`SemanticsUpdate`] **only if** the semantics tree may have
2080    /// changed since generation `last_seen`.
2081    ///
2082    /// Returns `None` when nothing relevant changed, letting a shell skip both the
2083    /// tree walk and the platform `accesskit_*` push. Call it post-layout (bounds
2084    /// must be valid). A shell threads its stored generation in and, on `Some`,
2085    /// updates it from [`RenderRoot::semantics_generation`]. v1 pushes the whole
2086    /// tree when it does recompute (stable ids make that valid); finer-grained
2087    /// diffing is a later optimization.
2088    pub fn semantics_if_changed(&self, last_seen: u64) -> Option<SemanticsUpdate> {
2089        (self.semantics_gen != last_seen).then(|| self.semantics())
2090    }
2091
2092    /// Deliver an input event to the widget tree, returning what happened.
2093    ///
2094    /// Builds a root [`EventCtx`] over the (type-erased) `state`, dispatches to
2095    /// the root widget — which routes the event down through its container
2096    /// children — and folds the result into an [`EventOutcome`]. The outcome's
2097    /// `needs_redraw` is set whenever a widget consumed the event or explicitly
2098    /// requested a redraw; the shell turns that into a `window.request_redraw()`.
2099    ///
2100    /// Root capture bookkeeping mirrors the per-container `active`-child model: a
2101    /// `Down` whose dispatch requested capture marks a gesture in flight and
2102    /// latches the contact that sent it as the gesture's claimant; the
2103    /// claimant's `Up` and `Cancel` release it (never a window-leave, and never
2104    /// another contact's release).
2105    ///
2106    /// Pointer contacts are gated **first**, before anything else runs: an
2107    /// [`InputEvent::PointerContact`] is unwrapped into the plain
2108    /// [`InputEvent::Pointer`] every widget matches on, with
2109    /// [`EventCtx::pointer_id`](crate::event::EventCtx::pointer_id) reporting its
2110    /// id, and a contact the multi-contact contract does not route — an
2111    /// additional contact with nothing captured, or one the captor did not opt
2112    /// into — is dropped here with an empty outcome (see that variant's
2113    /// *Multi-contact contract*). A bare `InputEvent::Pointer` is the mouse.
2114    ///
2115    /// Root hover bookkeeping is the third recorded path, and the one this pass
2116    /// *derives* rather than merely mirrors: an **uncaptured** `Move` opens a hover
2117    /// pass (widgets on the hit-tested path may claim it — see
2118    /// [`EventCtx::claim_hover`](crate::event::EventCtx::claim_hover)), a
2119    /// `Down`/`Up`/`Cancel` ends whatever hover stood, and every other event leaves
2120    /// it alone. There is nothing to release and no generation to bump: the epoch
2121    /// advance strands the previous claimant's path by itself, and the outcome's
2122    /// `needs_redraw` carries the one repaint **no widget can ask for** — a hover
2123    /// that ended with nothing taking it. A hover that *begins* or *moves from one
2124    /// claimant to another* is repainted by the new claimant's own change-gated
2125    /// `request_redraw`, which is why keeping an internal hover flag is part of the
2126    /// consumer contract rather than an optimization (see `claim_hover`).
2127    /// **No shell change is required for hover** — the desktop shell already
2128    /// dispatches a `Move` on every cursor move.
2129    ///
2130    /// The cursor is hover's sibling channel and the fourth thing this pass
2131    /// resolves: any pointer `Move` (captured included) re-resolves
2132    /// [`RenderRoot::cursor`] from the pass's last
2133    /// [`EventCtx::set_cursor`](crate::event::EventCtx::set_cursor), defaulting to
2134    /// [`CursorIcon::Default`] when nothing asked. It deliberately does **not**
2135    /// fold into the outcome's `needs_redraw`: applying a cursor is a platform
2136    /// call a desktop shell makes straight after this pass returns, with no frame
2137    /// involved, and folding it in would repaint the tree on every hover move.
2138    ///
2139    /// The **clipboard channel** rides the same bracket and is the fifth thing
2140    /// this pass resolves: whatever the dispatch asked for through
2141    /// [`EventCtx::write_clipboard`](crate::event::EventCtx::write_clipboard) and
2142    /// [`EventCtx::request_paste`](crate::event::EventCtx::request_paste) lands in
2143    /// [`RenderRoot::take_clipboard_write`] / [`RenderRoot::take_paste_request`],
2144    /// which a shell drains immediately after this returns, beside
2145    /// [`RenderRoot::cursor`] and [`RenderRoot::ime_state`]. Unlike the cursor,
2146    /// both commit on **every** pass rather than on a pointer `Move` alone — a
2147    /// copy can be answered from a key chord, a context-menu tap, or an
2148    /// [`InputEvent::EditCommand`] — and both are one-shot drains rather than
2149    /// standing levels. Like the cursor, neither folds into `needs_redraw`:
2150    /// talking to the host clipboard paints nothing (a `Cut` that mutates the
2151    /// document asks for its own redraw, for the mutation).
2152    ///
2153    /// # Reentrancy
2154    ///
2155    /// This pass **never rebuilds or repaints**. Event handlers mutate `state`
2156    /// synchronously through the context; the shell is expected to run a single
2157    /// [`RenderRoot::rebuild`] (then layout/paint) *after* the event pass returns,
2158    /// driven by the outcome. Rebuilding re-entrantly here would invalidate the
2159    /// widget references the dispatch still holds and turn the event→state→view
2160    /// feedback into recursion.
2161    ///
2162    /// [`RenderRoot::rebuild`] calls this itself with
2163    /// [`InputEvent::Housekeeping`] to flush deferred state-bearing callbacks.
2164    /// That is *sequential*, not re-entrant — the dispatch fully returns before
2165    /// the next diff starts — so the rule above is intact.
2166    ///
2167    /// # File-drag follow-up
2168    ///
2169    /// Every [`InputEvent::FileDrop`] whose phase is [`FileDropPhase::Drop`]
2170    /// or [`FileDropPhase::Cancel`] is followed, within this same call and
2171    /// whether or not its hit-tested dispatch was handled, by a
2172    /// [`FileDropPhase::Ended`] broadcast to the whole tree; the returned
2173    /// outcome folds both. A `Hover` and an `Ended` dispatched directly get
2174    /// no follow-up. See [`FileDropEvent`]'s *Broadcast follow-up* for why.
2175    pub fn event(&mut self, state: &mut State, event: &InputEvent) -> EventOutcome {
2176        let Some(root_id) = self.root_id else {
2177            return EventOutcome::default();
2178        };
2179
2180        // The multi-contact gate (`InputEvent::PointerContact`'s contract). A
2181        // contact is unwrapped into the plain `Pointer` every widget matches on,
2182        // so everything below sees one pointer shape; a bare `Pointer` is the
2183        // mouse. Any other event dispatches under the contact the enclosing pass
2184        // carries (the overlay pre-pass re-enters this method with a broadcast
2185        // that must keep its gesture's id), or the mouse at the top level.
2186        let unwrapped;
2187        let (event, pointer_id) = match event {
2188            InputEvent::PointerContact {
2189                pointer_id,
2190                event: pointer,
2191            } => {
2192                unwrapped = InputEvent::Pointer(*pointer);
2193                (&unwrapped, *pointer_id)
2194            }
2195            InputEvent::Pointer(_) => (event, PointerId::MOUSE),
2196            _ => (event, crate::event::current_pointer_id()),
2197        };
2198        let secondary = if matches!(event, InputEvent::Pointer(_)) {
2199            match self.contact_route(pointer_id) {
2200                Some(secondary) => secondary,
2201                // Rule (b), or rule (c) for a captor that did not opt in: the
2202                // contact reaches nothing and moves no root state.
2203                None => return EventOutcome::default(),
2204            }
2205        } else {
2206            false
2207        };
2208        // Published for the whole dispatch: `EventCtx::new` seeds
2209        // `pointer_id()` from it (so the id survives a component boundary),
2210        // `capture_contacts` records its opt-in into it, and `ChildPod::set_active`
2211        // reads its `secondary` mark to keep every container's active link on
2212        // the claimant. Restored on drop, so a nested pass scopes its own.
2213        let contact_pass = ContactPass::enter(pointer_id, secondary);
2214
2215        // The overlay pre-pass runs before every other thing this method does —
2216        // before the hover derivation, before the cursor bracket, before the
2217        // dispatch — because a pointer over a floated surface must reach none of
2218        // them: not the main tree's hit test, not its hover pass, and above all
2219        // not its blur rule. See `route_overlay`.
2220        let carried = match self.route_overlay(state, event) {
2221            OverlayRoute::Consumed(outcome) => return outcome,
2222            OverlayRoute::Continue(outcome) => outcome,
2223        };
2224
2225        // Open a candidate focus session for this dispatch, BEFORE anything is
2226        // routed. A claim recorded below is stamped with this new epoch, which
2227        // nothing older carries — so the branch the session is leaving is
2228        // stranded by arithmetic rather than by a clearing sweep that would have
2229        // to visit it, and a floated pod no container owns is retired on exactly
2230        // the same terms as a field in the tree.
2231        //
2232        // Candidate, not committed: `self.focus_epoch` is left alone and the
2233        // pass settles it below, because whether this dispatch recorded anything
2234        // is only known once it has run. A `Move` over a focused field, a
2235        // housekeeping broadcast, a press inside a surface that claims nothing —
2236        // none of those may disturb a standing link.
2237        //
2238        // Both epochs are published because a claim recorded on the way back up
2239        // has to be observable to the container still unwinding around it (a
2240        // blur sweep asking which child kept focus, a portal noticing its surface
2241        // took the session) while a link recorded *before* this dispatch still
2242        // has to read live. See `crate::widget::set_live_focus_session`.
2243        //
2244        // After `route_overlay`, deliberately: an event that belongs to a floated
2245        // surface is re-dispatched through the front door and opens its own
2246        // candidate session there, and this call returns that outcome untouched.
2247        let live_focus_epoch = self.focus_epoch;
2248        let candidate_focus_epoch = advance_focus_epoch(live_focus_epoch);
2249        crate::widget::set_live_focus_session(
2250            self.root_identity,
2251            live_focus_epoch,
2252            candidate_focus_epoch,
2253        );
2254        let focus_active_before = self.focus_active;
2255
2256        let Some(pod) = self.tree.pod_mut(root_id) else {
2257            // Nothing to dispatch into, but an outside-tap notification may
2258            // already have produced an outcome; returning it rather than the
2259            // default keeps that redraw.
2260            self.publish_focus_session();
2261            return carried;
2262        };
2263
2264        // Hover is derived per **uncaptured** pointer `Move`: that pass, and only
2265        // that pass, may record a claim, so a captured drag can never paint hover
2266        // under the pointer. Every other pointer phase — `Down`, `Up`, `Cancel` —
2267        // is an epoch-advancing pass that *ends* whatever hover stood without
2268        // opening a new one: a press is not a hover, a touch `Down` must not
2269        // inherit one, and a lift is the only signal a touch contact leaving the
2270        // screen ever produces (no further `Move` follows it, so a tint claimed
2271        // during an uncaptured touch drag would otherwise stand indefinitely).
2272        // Ending on `Up` costs a mouse the hover tint between a click's release
2273        // and its next motion — the same standing-until-next-move class as the
2274        // press-then-hold-still gap, and traded deliberately for a touch link that
2275        // cannot outlive the finger (`docs/LIMITATIONS.md`'s
2276        // `hover-window-leave-standing`). A scroll, key, IME, or the housekeeping
2277        // broadcast leaves a live hover exactly as it was.
2278        //
2279        // A non-claimant contact delivered down a live capture (`secondary`) is
2280        // neither: it is not a gesture of its own, so it leaves hover exactly as
2281        // the claimant's gesture had it.
2282        let hover_pass = matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Move)
2283            && self.capture_claimant.is_none();
2284        let hover_ends = !secondary
2285            && matches!(
2286                event,
2287                InputEvent::Pointer(p)
2288                    if matches!(
2289                        p.phase,
2290                        PointerPhase::Down | PointerPhase::Up | PointerPhase::Cancel
2291                    )
2292            );
2293        let hover_epoch = self.hover_epoch;
2294        let hover_was_active = self.hover_active;
2295        let root_identity = self.root_identity;
2296
2297        // The cursor pass is hover's pass widened by one case: **any** pointer
2298        // `Move`, captured included, because a captured `Move` routes only to the
2299        // capturing widget and that is exactly how a drag keeps its own cursor
2300        // while the pointer is outside its bounds. Every other pass leaves the
2301        // resolved cursor standing — notably `Down`/`Up`, whose handlers have no
2302        // reason to restate a cursor and whose reset would blink the shape back to
2303        // `Default` for the length of a click.
2304        //
2305        // The slot is cleared here rather than trusted to be empty: a `set_cursor`
2306        // from a dispatch no root drove (a reconciler's synthesized `Cancel`)
2307        // must not leak into this pass's resolution. The clear and the drain below
2308        // are one bracket (`CursorPass`) rather than two bare calls, so a dispatch
2309        // that re-entered this method could not silently eat the enclosing pass's
2310        // request — see that guard.
2311        //
2312        // The claimant's moves only: a second finger moving under a captured
2313        // drag says nothing about the drag's own cursor.
2314        let cursor_pass =
2315            !secondary && matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Move);
2316        // The same bracket carries the clipboard channel (a write and a paste
2317        // request), which differs only in when it commits: every pass, not the
2318        // pointer-move subset, since a copy can be answered from a key chord or an
2319        // `EditCommand` that never moved a pointer. See `RequestPass`.
2320        let request_slot = RequestPass::enter();
2321
2322        // Whether the root widget itself holds the live opt-in (rather than a
2323        // pod below it), and whether it opted in during this dispatch.
2324        let contacts_captor_is_root = self.capture_claimant.is_some()
2325            && self.capture_contacts
2326            && self.contacts_captor_is_root;
2327        let root_opted_in;
2328
2329        let (handled, needs_redraw, captured, hover_claimed, focus_req, focus_rel, ime) = {
2330            let state_any: &mut dyn Any = state;
2331            let mut ctx = EventCtx::new(state_any, pod.origin(), pod.size());
2332            // Seed the root widget's focus flag so a leaf-root editable that holds
2333            // focus can observe `has_focus()`; deeper focus is threaded per-pod.
2334            ctx.set_has_focus(self.focus_active);
2335            // Same for the hover link, plus the live epoch every pod compares its
2336            // stamp against and the eligibility gate that decides whether a claim
2337            // is recordable at all this pass.
2338            ctx.set_hovered(hover_was_active);
2339            ctx.set_hover_epoch(hover_epoch);
2340            ctx.set_hover_eligible(hover_pass);
2341            // Stamped onto whichever pod records a claim, so that pod's
2342            // destructor can tell this root's link from another root's
2343            // identically-numbered epoch (see `root_identity`).
2344            ctx.set_hover_root(root_identity);
2345            // The root widget's own contact frame: attributes a
2346            // `capture_contacts` made by the root widget itself (not by a pod
2347            // below it) to the root, and marks the dispatch as running under the
2348            // live opt-in's holder when the root widget is that holder.
2349            let frame = ContactFrame::enter(contacts_captor_is_root);
2350            let result = if secondary && !contacts_captor_is_root {
2351                // A non-claimant contact walks the recorded active path
2352                // forward-only (see `ChildPod::event_child`): the root widget is
2353                // handed the inert carrier and the real event rides beside it,
2354                // so only the captor that opted in runs its pointer handling. A
2355                // walk that never reached a pod on the path (the root widget
2356                // does not forward broadcasts to its captured child) falls back
2357                // to the ordinary delivery.
2358                let widget = pod.widget_mut();
2359                let carrier = crate::event::secondary_walk_carrier();
2360                let (_, delivered) = crate::event::run_secondary_walk(event.clone(), || {
2361                    widget.event(&mut ctx, &carrier)
2362                });
2363                match delivered {
2364                    Some(result) => result,
2365                    None => crate::event::without_secondary_walk(|| widget.event(&mut ctx, event)),
2366                }
2367            } else {
2368                pod.widget_mut().event(&mut ctx, event)
2369            };
2370            root_opted_in = frame.close().0;
2371            let handled = matches!(result, EventResult::Handled);
2372            (
2373                handled,
2374                ctx.needs_redraw() || handled,
2375                ctx.is_pointer_captured(),
2376                ctx.is_hover_claimed(),
2377                ctx.is_focus_requested(),
2378                ctx.is_focus_released(),
2379                ctx.take_ime_state(),
2380            )
2381        };
2382
2383        // A published IME surface refreshes the stored one (persists past this
2384        // event, survives rebuild) until a blur clears it below. `store_ime_state`
2385        // bumps the focus/IME edge generation only if the surface actually moved
2386        // (a keystroke that changes nothing observable is not an edge).
2387        //
2388        // An **inactive** publish carries the same release intent here as it does
2389        // in `paint` (see that method's take path): a widget that publishes
2390        // `active: false` is ending the session, not describing it, so it takes
2391        // the full release. Reachable from this pass too — every paint-time
2392        // producer of an inactive surface is a container/widget whose `event` arm
2393        // can run first — and the two passes must not disagree about what an
2394        // inactive surface means. No `focus_active` guard is needed (unlike
2395        // `paint`, which must refuse to *resurrect* a cleared surface): releasing
2396        // an already-released root moves nothing and fires no edge.
2397        //
2398        // Ordering: this runs before the focus match below, so a dispatch that
2399        // both published an inactive surface and requested focus still ends up
2400        // focused — the later, more specific claim wins.
2401        //
2402        // The provenance rule `paint_overlays` applies to a paint-time publish,
2403        // mirrored onto this pass — the two must not disagree about who is
2404        // allowed to speak for the session, so a change to either belongs in
2405        // both.
2406        //
2407        // What the root can attribute here is bounded by how the event was
2408        // routed. A hit-tested or focus-routed dispatch reaches a publisher
2409        // through the tree, and the tree's own chain is what gated it (a
2410        // well-behaved editable publishes only while `has_focus`, which is now
2411        // stamp-gated) — so the rule for those is the liveness half alone: a
2412        // session must exist, or be opening in this very dispatch, for a publish
2413        // to describe anything. An `InputEvent::Overlay` is broadcast to the
2414        // whole tree and reaches every floated pod whether or not it holds
2415        // anything, so there the addressed surface must be the recorded owner of
2416        // the session, or be claiming it now — the same "the publisher holds the
2417        // link" question `paint_overlays` answers from the pods themselves.
2418        //
2419        // The *inactive* publish is refused on the same terms and for the same
2420        // reason it is at paint: ending a session is a claim about it too, and a
2421        // branch that owns none makes neither.
2422        let publish_attributable = match event {
2423            InputEvent::Overlay(overlay) => focus_req || self.focus_surface == Some(overlay.key),
2424            _ => self.focus_active || focus_req,
2425        };
2426        match ime {
2427            Some(_) if !publish_attributable => {}
2428            Some(ime) if !ime.active => {
2429                self.release_focus_session();
2430            }
2431            Some(ime) => self.store_ime_state(Some(ime)),
2432            None => {}
2433        }
2434
2435        // Root-level capture path: a captured `Down` opens a gesture; `Up`/`Cancel`
2436        // close it. `Move` leaves the flag untouched so it survives the drag.
2437        //
2438        // Root-level focus path (the capture mirror): a `Down` that requested
2439        // focus opens the focus session; a `Down` that did not is a
2440        // blur-on-outside-tap and closes it (the per-container `focused` flags are
2441        // cleared by the routing helpers). Key/Ime/Scroll only adjust focus if the
2442        // dispatch explicitly requested or released it.
2443        //
2444        // Every arm mutates through `set_focus_active`/`store_ime_state` or the
2445        // paired `release_focus_session`, the change-guarded writers that own the
2446        // focus/IME edge generation: a `Down` on already-blurred chrome (the
2447        // commonest event of all) writes the same values back and must therefore
2448        // NOT fire an edge.
2449        //
2450        // Whether an arm below actually *honoured* a focus claim — the one thing
2451        // that makes the candidate epoch opened above the real one. Deliberately
2452        // not `focus_req` itself: the housekeeping arm ignores a claim by
2453        // contract, and a claim it ignored must leave the standing session (and
2454        // therefore the standing epoch) exactly where it was.
2455        let mut focus_claim_honoured = false;
2456        match event {
2457            // A non-claimant contact delivered down a live capture (rule (c) of
2458            // `InputEvent::PointerContact`'s contract): not a gesture of its own,
2459            // so it opens, moves and releases no capture and never blurs — its
2460            // `Up` must not end the claimant's gesture, and its `Down` is not a
2461            // tap outside anything. An explicit focus request or release from
2462            // its handler is honoured exactly as the scroll arm below honours
2463            // one.
2464            InputEvent::Pointer(_) if secondary => {
2465                if focus_req {
2466                    focus_claim_honoured = true;
2467                    self.set_focus_active(true);
2468                }
2469                if focus_rel {
2470                    self.release_focus_session();
2471                }
2472            }
2473            // Never reached: the gate at the top unwrapped every contact into a
2474            // plain `Pointer`, and routed it by the arms around this one.
2475            InputEvent::PointerContact { .. } => {}
2476            InputEvent::Pointer(pointer) => match pointer.phase {
2477                PointerPhase::Down => {
2478                    if captured {
2479                        // Rule (a): the contact whose `Down` captured is the
2480                        // claimant, and only its release ends the gesture. The
2481                        // opt-in is read from the pass rather than the root
2482                        // context's bubble so a captor below a component
2483                        // boundary (which mirrors capture but not the opt-in)
2484                        // is still heard.
2485                        self.capture_claimant = Some(pointer_id);
2486                        self.capture_contacts = contact_pass.contacts_requested();
2487                        self.contacts_captor_is_root = root_opted_in;
2488                    }
2489                    if focus_req {
2490                        focus_claim_honoured = true;
2491                        self.set_focus_active(true);
2492                        // A hit-tested press reached the claimant through the
2493                        // containers, so the session is the main tree's — the one
2494                        // attribution the event pass can make without help, and
2495                        // what makes the next paint seed the tree's links again
2496                        // the moment focus comes back to it (see `focus_surface`).
2497                        self.focus_surface = None;
2498                    } else {
2499                        // Blur: no widget on the tapped path took focus. The
2500                        // canonical release — flag and surface drop together, as
2501                        // one edge (see `release_focus_session_in`).
2502                        self.release_focus_session();
2503                    }
2504                }
2505                // Only ever the claimant's (or an uncaptured contact's) release:
2506                // any other contact's was routed to the `secondary` arm above or
2507                // dropped at the gate.
2508                PointerPhase::Up | PointerPhase::Cancel => {
2509                    self.capture_claimant = None;
2510                    self.capture_contacts = false;
2511                    self.contacts_captor_is_root = false;
2512                }
2513                PointerPhase::Move => {}
2514            },
2515            // A file drop is hit-tested exactly like `Scroll`/`Scale` above —
2516            // no capture, no claimant bookkeeping — so it joins their bucket
2517            // rather than getting one of its own: a target that calls
2518            // `request_focus`/`release_focus` from its `FileDrop` handler is
2519            // honoured on the same terms a scroll's would be, even though no
2520            // shipped drop target does today. Its `Ended` broadcast follow-up
2521            // lands here too: like an overlay event it is a broadcast that
2522            // stands for a real user gesture, so an explicit request is
2523            // honoured and nothing blurs (only a pointer `Down` blurs).
2524            InputEvent::Scroll { .. }
2525            | InputEvent::Scale(_)
2526            | InputEvent::FileDrop(_)
2527            | InputEvent::Key(_)
2528            | InputEvent::Ime(_)
2529            | InputEvent::EditCommand(_) => {
2530                if focus_req {
2531                    // The candidate epoch opened above becomes this claim's, so
2532                    // a focus move driven from the keyboard retires the branch it
2533                    // supersedes on exactly the terms a press does — including a
2534                    // floated surface's link, which this arm could otherwise
2535                    // never reach. `focus_surface` is deliberately left alone:
2536                    // which branch a focus-routed claim came from is not
2537                    // something the root can attribute, so the next paint
2538                    // re-resolves it from the pods' own live links.
2539                    focus_claim_honoured = true;
2540                    self.set_focus_active(true);
2541                }
2542                if focus_rel {
2543                    self.release_focus_session();
2544                }
2545            }
2546            // A broadcast is not user input: it opens no gesture, claims no
2547            // focus, and blurs nothing. Deliberately inert here — a housekeeping
2548            // pass that moved the root's capture/focus bookkeeping would change
2549            // what the *next* real event does, which is exactly what this
2550            // mechanism must not do (see `InputEvent::Housekeeping`).
2551            //
2552            // The one thing a broadcast *can* still move is the IME surface, via
2553            // the publish handling above (which is pass-agnostic by design, and
2554            // was before this arm existed): a widget that publishes while
2555            // flushing has said something about its session either way, and an
2556            // inactive publish releases it. No shipped widget does — `TextInput`
2557            // ignores a broadcast outright, and both cleared-surface publishers
2558            // are paint-time — so this is a contract note, not live behavior.
2559            InputEvent::Housekeeping => {}
2560            // A floated surface's own input: a broadcast at the root, but a real
2561            // user gesture underneath, so it moves *some* of what a hit-tested
2562            // event moves and deliberately none of the rest.
2563            InputEvent::Overlay(overlay) => {
2564                // Honoured: a text field inside a popover may claim focus, and the
2565                // session it opens is an ordinary one.
2566                //
2567                // What it must not do is stack on top of the session it
2568                // supersedes. A hit-tested press has the containers' blur sweep
2569                // under it, which clears every focused child but the one the
2570                // press kept; a press inside a floated surface reaches no
2571                // container's hit test, so nothing retires the chain the claim
2572                // replaces and two branches go on believing they are focused —
2573                // one of them still describing its own IME surface to the shell.
2574                // The root retires it in the two places it has standing to: here,
2575                // when its record already names a *different* surface as the
2576                // owner, and in `paint_overlays`, which resolves that record from
2577                // the pods themselves and stops seeding the main tree's links the
2578                // pass after the session leaves it.
2579                //
2580                // The other repair — running the blur sweep for this event, with
2581                // the claiming surface's owner kept — was not taken: that sweep
2582                // belongs to the containers, which run it over the children they
2583                // hold when a pointer `Down` passes through them. The root has no
2584                // mutable route to a `focused` link below its own pod, so from
2585                // here it is not a sweep at all but a change to how every
2586                // container routes a broadcast.
2587                //
2588                // The record is deliberately not *written* here: an overlay-pass
2589                // focus request cannot be attributed at the root (see
2590                // `focus_surface`) — a field in the tree re-claiming its own
2591                // session through a floated toolbar raises exactly the flag an
2592                // editable inside the surface raises to take it away.
2593                if focus_req {
2594                    if self.focus_surface.is_some_and(|owner| owner != overlay.key) {
2595                        // A session another surface owns, which *is* attributable:
2596                        // retire it whole rather than let the new claim inherit
2597                        // the surface the old one published.
2598                        self.release_focus_session();
2599                    }
2600                    focus_claim_honoured = true;
2601                    self.set_focus_active(true);
2602                }
2603                // Honoured: an **explicit** `EventCtx::release_focus` from inside
2604                // the surface ends the session it asked to end — the same rule the
2605                // `Scroll`/`Key`/`Ime` arm applies.
2606                if focus_rel {
2607                    self.release_focus_session();
2608                }
2609                // NEVER the blur branch. A `Down` that claims no focus blurs the
2610                // tree only when it was hit-tested *in* the tree; a press inside a
2611                // floated surface is the one press that must not, or tapping a
2612                // selection toolbar would drop the very selection the toolbar acts
2613                // on — which is the whole reason these two are routed apart.
2614                //
2615                // Nor does it advance the hover epoch: `hover_pass`/`hover_ends`
2616                // above match `InputEvent::Pointer` only, so a live hover in the
2617                // main tree survives an overlay pass by construction rather than by
2618                // a check here.
2619                //
2620                // A capture requested from inside the surface IS honoured, exactly
2621                // as a hit-tested `Down`'s is: the gesture then owns every
2622                // follow-up, and because a live capture short-circuits the overlay
2623                // pre-pass those follow-ups arrive as ordinary `Pointer` events
2624                // routed by the capture path — which is what lets a drag begun
2625                // inside a surface continue outside it, and what closes it on the
2626                // `Up` through the arm above.
2627                //
2628                // Whatever phase claimed it, not `Down` alone. The pods record
2629                // their own `active` path on any phase, so a capture claimed on a
2630                // `Move` left the surface latched and the root's mirror clear:
2631                // the pre-pass kept hit-testing, the follow-ups kept missing the
2632                // latched surface, and the `Up` that would have released it never
2633                // arrived — leaving the surface free to divert every later
2634                // pointer event its owner is reached by. Mirroring the claim on
2635                // any phase is what routes those follow-ups, and that `Up`, back
2636                // down the capture path to the surface that opened it.
2637                //
2638                // The claimant is the contact this overlay event was routed for —
2639                // the pre-pass re-enters this method under the gesture's own
2640                // contact pass, so `pointer_id` is that gesture's id.
2641                if captured {
2642                    self.capture_claimant = Some(pointer_id);
2643                    self.capture_contacts = contact_pass.contacts_requested();
2644                    self.contacts_captor_is_root = root_opted_in;
2645                }
2646                // ...and released on the phase that ends the gesture, in the
2647                // same door it was claimed through. The claim is mirrored on any
2648                // phase (see above), so the release has to be too: an `Up` or
2649                // `Cancel` that arrives *as an overlay event* — which is what
2650                // happens when the surface was not holding the capture when the
2651                // gesture began, so the pre-pass kept hit-testing it — would
2652                // otherwise leave the root's latch standing on a gesture that is
2653                // over, and a standing latch short-circuits the pre-pass for
2654                // every floated surface until some unrelated pointer release
2655                // happens along.
2656                //
2657                // A gesture whose claim *did* short-circuit the pre-pass ends on
2658                // the ordinary pointer arm above instead, because that is the
2659                // door its follow-ups come in by; the pod's own `active` link is
2660                // released by whichever of the two the release arrived through
2661                // (see `frust-widgets`' `OverlaySlot::forward`).
2662                if let OverlayEventKind::Pointer(pointer) = &overlay.kind
2663                    && matches!(pointer.phase, PointerPhase::Up | PointerPhase::Cancel)
2664                    && self
2665                        .capture_claimant
2666                        .is_none_or(|claimant| claimant == pointer_id)
2667                {
2668                    self.capture_claimant = None;
2669                    self.capture_contacts = false;
2670                    self.contacts_captor_is_root = false;
2671                }
2672            }
2673        }
2674
2675        // A takeover during the claimant's own dispatch: a container cancelled
2676        // the widget that opted into the gesture's other contacts and released
2677        // it from the active path (`EventCtx::release_captured_child`). The
2678        // opt-in goes with it — unless the container that took over opted in
2679        // itself afterwards, which the pass records — so the other contacts are
2680        // dropped from here on rather than routed to a captor that is gone. The
2681        // claimant latch is deliberately kept: the gesture is still the
2682        // claimant's, now held by the container that took it over (still on the
2683        // active path, as the released child's ancestor), and only the
2684        // claimant's own `Up`/`Cancel` ends it. Never on a non-claimant
2685        // contact's dispatch, in which no container can clear a recorded link
2686        // (`ChildPod::set_active`), so nothing could have left the path.
2687        if !secondary && self.capture_claimant.is_some() && contact_pass.capture_released() {
2688            self.capture_contacts = contact_pass.contacts_requested();
2689        }
2690
2691        // Settle the session's identity — the one write that decides which
2692        // recorded links survive this dispatch, in one place, from what the
2693        // dispatch actually did.
2694        //
2695        // * A **live session that ended** strands everything: the chain it ran
2696        //   through, a floated surface's link, and anything this very pass
2697        //   stamped (a claim the same dispatch then released). Past the candidate,
2698        //   therefore, not merely onto it — this is the clearing sweep no
2699        //   container can be asked to run.
2700        // * A **claim the root honoured** commits the candidate, so the chain it
2701        //   stamped is the only one that reads live and every older link is
2702        //   stranded by arithmetic.
2703        // * Anything else leaves the session exactly where it was, which is what
2704        //   makes a `Move`, a housekeeping broadcast, or a press inside a surface
2705        //   that claimed nothing incapable of disturbing a standing link — and
2706        //   what strands a claim an arm declined to honour, since the candidate
2707        //   it was stamped with is then never published again.
2708        let session_ended = focus_active_before && !self.focus_active;
2709        self.focus_epoch = if session_ended {
2710            advance_focus_epoch(candidate_focus_epoch)
2711        } else if focus_claim_honoured {
2712            candidate_focus_epoch
2713        } else {
2714            live_focus_epoch
2715        };
2716        self.publish_focus_session();
2717
2718        // Close the hover pass: advance the epoch (which strands every stamp this
2719        // pass did not renew, wherever in the tree it sits) and refresh the mirror.
2720        // A claim only counts on a `hover_pass` — an ineligible pass records none
2721        // anyway, but stating it here keeps the mirror true by construction rather
2722        // than by the eligibility gate alone.
2723        if hover_pass || hover_ends {
2724            self.hover_epoch = self.hover_epoch.wrapping_add(1);
2725            self.hover_active = hover_pass && hover_claimed;
2726            // Republish the link a dropping pod checks its stamp against, so a
2727            // rebuild that removes the claimant can report the severance the
2728            // epoch alone cannot strand (see `ChildPod`'s `Drop`). Epoch `0` while
2729            // nothing holds a link, which is what makes a stale stamp's drop —
2730            // the common case — cost one comparison and mark nothing. The
2731            // identity rides with it because epoch integers are per-root and
2732            // collide by construction (see `root_identity`).
2733            crate::event::set_live_hover_link(
2734                self.root_identity,
2735                if self.hover_active {
2736                    self.hover_epoch
2737                } else {
2738                    0
2739                },
2740            );
2741        }
2742
2743        // Close the cursor pass: the last request of the pass wins, and its
2744        // absence resolves to `Default` — which is what makes the request
2745        // stateless (a widget that stops asking needs no clearing) and what a
2746        // pointer moving off every requesting widget resolves to. Drained
2747        // unconditionally so a request made on a non-cursor pass cannot survive
2748        // into the next one; only a cursor pass commits it.
2749        let requested = request_slot.take();
2750        if cursor_pass {
2751            self.cursor = requested.cursor.unwrap_or_default();
2752        }
2753        // The clipboard half of the same drain, committed on **every** pass: a copy
2754        // is answered from whatever event decoded it, and there is no
2755        // "clipboard pass" the way there is a cursor pass. Both fields are
2756        // one-shot (see their docs) — a write from this pass supersedes one still
2757        // standing undrained, and a request is raised but never lowered here, so a
2758        // shell that skipped a drain answers late rather than losing the paste.
2759        if let Some(text) = requested.clipboard_write {
2760            self.pending_clipboard_write = Some(text);
2761        }
2762        self.pending_paste_request |= requested.paste_request;
2763        // A hover that ended with *nothing* taking it needs one repaint no widget
2764        // can ask for: the pointer moved onto empty chrome (or a press/lift/cancel
2765        // cleared the link), so the old claimant's `event()` was never called and
2766        // the new state has no claimant to speak for it. Every other edge is the
2767        // consumer's own: a hover that *began* or *moved from one claimant to
2768        // another* is repainted by the new claimant's change-gated
2769        // `request_redraw`, and because a repaint is global, that one frame is also
2770        // what lets the widget losing the link drop its overlay from
2771        // `PaintCtx::is_hovered`. This mirror is identity-free, so an A→B handoff
2772        // is `true`→`true` here and manufactures nothing — which is exactly why the
2773        // consumer's internal flag is normative (see `EventCtx::claim_hover`).
2774        let needs_redraw = needs_redraw || (hover_was_active && !self.hover_active);
2775
2776        let mut outcome = EventOutcome {
2777            // Merge whatever the overlay pre-pass already produced: a
2778            // pass-through outside-tap notification ran before this dispatch and
2779            // its redraw is owed just as much as the dispatch's own.
2780            handled: handled || carried.handled,
2781            needs_redraw: needs_redraw || carried.needs_redraw,
2782        };
2783
2784        // An OS file drag that just ended (`Drop`/`Cancel`) is followed by the
2785        // broadcast form, handled or not, so every widget holding state the
2786        // drag opened — a drop target's external session — hears the drag is
2787        // over: one whose release resolved over a region with no drop target
2788        // in it, and one whose release a *different* widget handled (two
2789        // independent drop-target groups the drag hovered in turn). See
2790        // `FileDropEvent`'s *Broadcast follow-up*. Re-entered through the
2791        // front door for the reason `route_overlay` re-enters: the follow-up
2792        // gets the same request bracket and outcome folding as any dispatch.
2793        // Shallow by construction: `Ended` is not one of the two phases that
2794        // trigger it.
2795        if let InputEvent::FileDrop(drop) = event
2796            && matches!(drop.phase, FileDropPhase::Drop | FileDropPhase::Cancel)
2797        {
2798            let ended = InputEvent::FileDrop(FileDropEvent {
2799                phase: FileDropPhase::Ended,
2800                position: Point::ZERO,
2801                paths: Vec::new(),
2802            });
2803            let follow_up = self.event(state, &ended);
2804            outcome.handled |= follow_up.handled;
2805            outcome.needs_redraw |= follow_up.needs_redraw;
2806        }
2807        outcome
2808    }
2809
2810    /// Decide what one pointer contact does — the root half of
2811    /// [`InputEvent::PointerContact`]'s multi-contact contract.
2812    ///
2813    /// `Some(false)` routes it as the gesture's own pointer (rule (a): slot `0`
2814    /// with nothing captured, hit-tested; or the claimant of a live capture,
2815    /// down the captured path). `Some(true)` routes it as a **non-claimant**
2816    /// contact down a live capture's path, which happens only while the captor's
2817    /// opt-in stands (rule (c)): the dispatch then walks the recorded active
2818    /// path forward-only, so only the captor's own handler sees it (see
2819    /// [`crate::widget::ChildPod::event_child`]). `None` drops it: an additional
2820    /// contact with nothing captured (rule (b)), or one the captor did not opt
2821    /// into — or whose opt-in ended when a container took the gesture over from
2822    /// the captor ([`EventCtx::release_captured_child`]) — (rule (c)).
2823    fn contact_route(&self, pointer_id: PointerId) -> Option<bool> {
2824        match self.capture_claimant {
2825            None if pointer_id.slot == 0 => Some(false),
2826            None => None,
2827            Some(claimant) if claimant == pointer_id => Some(false),
2828            Some(_) if self.capture_contacts => Some(true),
2829            Some(_) => None,
2830        }
2831    }
2832
2833    /// Hit-test the surfaces the last paint floated, **before** the main tree
2834    /// sees an uncaptured pointer, scroll, or scale — the routing half of the
2835    /// overlay portal (see [`crate::overlay`]).
2836    ///
2837    /// # What it does
2838    ///
2839    /// Walks [`RenderRoot::overlay_hits`] topmost-first (the `Tooltip` band before
2840    /// `Floating`, later registration before earlier), skipping
2841    /// [`OverlayInput::Transparent`] entries, and on the first rect containing the
2842    /// event's position re-dispatches it as
2843    /// [`InputEvent::Overlay`] — a broadcast carrying the owner's key and a
2844    /// **window-space** payload — then returns
2845    /// [`OverlayRoute::Consumed`]. The main tree never sees the original event.
2846    ///
2847    /// If nothing was hit and the event is a primary `Down`, every `Interactive`
2848    /// entry registered [`OutsideTap::Notify`] is told, topmost-first, with
2849    /// [`OverlayEventKind::OutsideDown`]; the press is then consumed iff any of
2850    /// them asked to consume it, and otherwise continues into today's dispatch.
2851    ///
2852    /// # What it deliberately does not do
2853    ///
2854    /// A **live capture short-circuits it entirely**: a gesture that has captured
2855    /// the pointer owns every follow-up until it ends, and re-hit-testing a drag
2856    /// that wandered over a floated surface would hand it to the wrong widget
2857    /// mid-gesture. That is also what lets a drag *begun* inside a surface
2858    /// continue outside it — the capture the overlay `Down` opened routes the
2859    /// follow-ups by the ordinary captured path.
2860    ///
2861    /// Housekeeping, `Key`, `Ime`, `EditCommand` and an overlay event already
2862    /// being routed pass straight through: a broadcast and a focus-routed event
2863    /// each reach their target with no hit test, so there is nothing here to
2864    /// redirect.
2865    ///
2866    /// A `FileDrop` passes straight through too, but for a different reason:
2867    /// it is hit-tested (see [`InputEvent::FileDrop`]), yet there is no
2868    /// [`OverlayEventKind`] carrier for it to ride a redirect through. A
2869    /// native OS drag has no floated-surface concept on the platform side —
2870    /// winit reports it against the window, not a particular surface — so a
2871    /// drop target living inside a popover is unreachable by one in v1 (see
2872    /// `docs/LIMITATIONS.md`). Its [`FileDropPhase::Ended`] follow-up passes
2873    /// through as well, as the broadcast it is.
2874    fn route_overlay(&mut self, state: &mut State, event: &InputEvent) -> OverlayRoute {
2875        if self.capture_claimant.is_some() || self.overlay_hits.is_empty() {
2876            return OverlayRoute::Continue(EventOutcome::default());
2877        }
2878        let (position, kind) = match event {
2879            // A contact is routed for its position and phase exactly like a
2880            // plain pointer (the root's gate has already unwrapped one, so the
2881            // second pattern is for totality); the re-entered dispatch below
2882            // keeps its id through the contact pass it runs under.
2883            InputEvent::Pointer(pointer) | InputEvent::PointerContact { event: pointer, .. } => {
2884                (pointer.position, OverlayEventKind::Pointer(*pointer))
2885            }
2886            InputEvent::Scroll { position, delta } => (
2887                *position,
2888                OverlayEventKind::Scroll {
2889                    position: *position,
2890                    delta: *delta,
2891                },
2892            ),
2893            InputEvent::Scale(scale) => (
2894                scale.focal,
2895                OverlayEventKind::Scale {
2896                    focal: scale.focal,
2897                    phase: scale.phase,
2898                    scale_delta: scale.scale_delta,
2899                    velocity: scale.velocity,
2900                },
2901            ),
2902            InputEvent::FileDrop(_)
2903            | InputEvent::Key(_)
2904            | InputEvent::Ime(_)
2905            | InputEvent::EditCommand(_)
2906            | InputEvent::Housekeeping
2907            | InputEvent::Overlay(_) => {
2908                return OverlayRoute::Continue(EventOutcome::default());
2909            }
2910        };
2911
2912        // `overlay_hits` is in paint order, so walking it in reverse walks from
2913        // the surface painted last — the one the user sees on top — downward.
2914        // Cloned because each dispatch below needs `&mut self`; the list is one
2915        // small `Copy` struct per floated surface, and the clone never happens on
2916        // the overwhelmingly common no-overlays path (guarded above).
2917        let hits = self.overlay_hits.clone();
2918        for hit in hits.iter().rev() {
2919            // An `egui`-style transparent surface is painted above the app and
2920            // hit-tested by nothing: the pointer passes straight through to
2921            // whatever the main tree has underneath.
2922            if hit.input == OverlayInput::Transparent {
2923                continue;
2924            }
2925            if hit.contains(position) {
2926                let routed = InputEvent::Overlay(OverlayEvent {
2927                    key: hit.key,
2928                    kind: kind.clone(),
2929                });
2930                // Re-entering this method is deliberate and shallow: the routed
2931                // event is a broadcast, so it takes the branch above out
2932                // immediately and can never recurse further. Going back through
2933                // the front door is what gives the overlay dispatch the same
2934                // request bracket, focus bookkeeping and outcome folding every
2935                // other event gets, with one arm's worth of difference rather
2936                // than a second copy of the pass.
2937                return OverlayRoute::Consumed(self.event(state, &routed));
2938            }
2939        }
2940
2941        // Nothing floated was hit. Only a **primary** press is a light-dismiss
2942        // signal: a secondary press is a context gesture (see
2943        // `docs/CODE_STANDARDS.md`'s Interaction Semantics), and a `Move`, `Up` or
2944        // scroll outside a surface says nothing about dismissing it.
2945        let dismissing = matches!(
2946            event,
2947            InputEvent::Pointer(pointer) | InputEvent::PointerContact { event: pointer, .. }
2948                if pointer.phase == PointerPhase::Down
2949                    && pointer.button == PointerButton::Primary
2950        );
2951        if !dismissing {
2952            return OverlayRoute::Continue(EventOutcome::default());
2953        }
2954
2955        let mut carried = EventOutcome::default();
2956        let mut consumed = false;
2957        for hit in hits.iter().rev() {
2958            // A transparent surface takes no input at all, outside-taps included:
2959            // it is chrome the pointer does not know about.
2960            if hit.input == OverlayInput::Transparent {
2961                continue;
2962            }
2963            let OutsideTap::Notify { consume } = hit.outside_tap else {
2964                continue;
2965            };
2966            let routed = InputEvent::Overlay(OverlayEvent {
2967                key: hit.key,
2968                kind: OverlayEventKind::OutsideDown,
2969            });
2970            let outcome = self.event(state, &routed);
2971            carried.handled |= outcome.handled;
2972            carried.needs_redraw |= outcome.needs_redraw;
2973            // Every notified surface is told before any of them consumes:
2974            // dismissing one menu must not hide the press from a second surface
2975            // that also wanted to close.
2976            consumed |= consume;
2977        }
2978        if consumed {
2979            OverlayRoute::Consumed(carried)
2980        } else {
2981            OverlayRoute::Continue(carried)
2982        }
2983    }
2984
2985    /// Perform a platform accessibility action, returning the same
2986    /// [`EventOutcome`] the synthesized input produced.
2987    ///
2988    /// A platform `accesskit_*` adapter delivers an `ActionRequest(node_id,
2989    /// action)`; a shell forwards it here. v1 routes actions through the **normal
2990    /// event path** by synthesizing pointer events at the target node's absolute
2991    /// bounds center (recovered from a fresh semantics pass — the id → bounds map
2992    /// the pass produces), so *every* fire-on-up-inside widget is operable with
2993    /// **zero** widget-side changes:
2994    ///
2995    /// * [`accesskit::Action::Click`] → a `Down` then an `Up` at the center,
2996    ///   activating any button/switch/checkbox exactly as a real tap would.
2997    /// * [`accesskit::Action::Focus`] → a `Down` then a synthetic `Cancel` at
2998    ///   the center: the `Down` claims focus for a widget that opts in on `Down`
2999    ///   (the recorded-focus contract), and the `Cancel` releases the capture
3000    ///   that same `Down` opened without touching the recorded focus path — so
3001    ///   the action claims focus without leaving the widget permanently
3002    ///   capturing every later pointer event. A widget that does not claim focus
3003    ///   on `Down` is unaffected, and `Cancel` never fires an on-press callback.
3004    /// * any other action → ignored (a no-op [`EventOutcome`]); richer actions are
3005    ///   deferred.
3006    ///
3007    /// An unknown `node_id` (not in the current tree) is a benign no-op. Requires
3008    /// a prior [`RenderRoot::layout`] so the bounds are valid. Synthetic-pointer
3009    /// activation cannot drive widgets that require a real drag (e.g. a slider) —
3010    /// an accepted v1 limitation.
3011    pub fn perform_accessibility_action(
3012        &mut self,
3013        state: &mut State,
3014        node_id: accesskit::NodeId,
3015        action: accesskit::Action,
3016    ) -> EventOutcome {
3017        // Recover the node's absolute bounds from a fresh semantics pass (the
3018        // id → absolute-bounds map this action routing needs; recomputing keeps
3019        // it in step with the live tree without a stored cache).
3020        let update = self.semantics();
3021        let Some(bounds) = update
3022            .nodes
3023            .iter()
3024            .find(|(id, _)| *id == node_id)
3025            .and_then(|(_, node)| node.bounds())
3026        else {
3027            return EventOutcome::default();
3028        };
3029        let center = Point::new((bounds.x0 + bounds.x1) / 2.0, (bounds.y0 + bounds.y1) / 2.0);
3030        let synth = |phase| {
3031            InputEvent::Pointer(PointerEvent {
3032                phase,
3033                position: center,
3034                button: PointerButton::Primary,
3035            })
3036        };
3037        match action {
3038            accesskit::Action::Click => {
3039                let down = self.event(state, &synth(PointerPhase::Down));
3040                let up = self.event(state, &synth(PointerPhase::Up));
3041                EventOutcome {
3042                    handled: down.handled || up.handled,
3043                    needs_redraw: down.needs_redraw || up.needs_redraw,
3044                }
3045            }
3046            accesskit::Action::Focus => {
3047                // A `Down` claims focus for a widget that opts in on `Down`; a
3048                // synthetic `Cancel` then releases the capture that `Down` also
3049                // opened (mirroring a real gesture steal), leaving the recorded
3050                // focus path intact — `Cancel` clears both `active` and
3051                // `capture_claimant` while never touching `focused`/`focus_active`.
3052                // Without the `Cancel`, the `Down` alone would leave the widget
3053                // permanently capturing every subsequent pointer event.
3054                let down = self.event(state, &synth(PointerPhase::Down));
3055                let cancel = self.event(state, &synth(PointerPhase::Cancel));
3056                EventOutcome {
3057                    handled: down.handled || cancel.handled,
3058                    needs_redraw: down.needs_redraw || cancel.needs_redraw,
3059                }
3060            }
3061            // Other actions are not modelled in v1: ignore rather than guess.
3062            _ => EventOutcome::default(),
3063        }
3064    }
3065}
3066
3067impl<State: 'static, V: View<State>> Default for RenderRoot<State, V> {
3068    fn default() -> Self {
3069        Self::new()
3070    }
3071}
3072
3073#[cfg(test)]
3074mod tests {
3075    use super::*;
3076    use crate::event::{ImeContentType, ScrollDelta};
3077    use crate::overlay::OverlayKey;
3078
3079    /// Application state for the tests.
3080    #[derive(Default)]
3081    struct AppState {
3082        label: String,
3083    }
3084
3085    /// The retained widget produced by `MockTextView`: stores the current text
3086    /// and records what it painted.
3087    struct TextWidget {
3088        text: String,
3089    }
3090
3091    impl crate::widget::Widget for TextWidget {
3092        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3093            // A crude intrinsic size: width proportional to text length.
3094            let intrinsic = Size::new(self.text.len() as f64 * 8.0, 16.0);
3095            bc.constrain(intrinsic)
3096        }
3097        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3098            scene.draw_text(ctx.origin(), &self.text);
3099        }
3100    }
3101
3102    /// The task's `MockTextView`: a real `View` impl living in tests.
3103    struct MockTextView {
3104        text: String,
3105    }
3106
3107    impl View<AppState> for MockTextView {
3108        type Element = TextWidget;
3109
3110        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
3111            TextWidget {
3112                text: self.text.clone(),
3113            }
3114        }
3115
3116        fn rebuild(
3117            &self,
3118            prev: &Self,
3119            element: &mut Self::Element,
3120            _ctx: &mut BuildCtx<'_>,
3121        ) -> ChangeFlags {
3122            if prev.text != self.text {
3123                element.text = self.text.clone();
3124                // Text change: same size model would relayout, but the intrinsic
3125                // width can change, so signal PAINT here and let callers decide.
3126                ChangeFlags::PAINT
3127            } else {
3128                ChangeFlags::NONE
3129            }
3130        }
3131    }
3132
3133    /// A scene recorder for asserting paint output.
3134    #[derive(Default)]
3135    struct RecordingScene {
3136        texts: Vec<(Point, String)>,
3137    }
3138    impl PaintScene for RecordingScene {
3139        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: peniko::Color) {}
3140        fn draw_text(&mut self, origin: Point, text: &str) {
3141            self.texts.push((origin, text.to_string()));
3142        }
3143        fn draw_scene_texture(&mut self, _id: u64, _dest: Rect) {}
3144    }
3145
3146    fn build(state: &mut AppState) -> MockTextView {
3147        MockTextView {
3148            text: state.label.clone(),
3149        }
3150    }
3151
3152    #[test]
3153    fn build_inserts_widget_into_arena() {
3154        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3155        let mut state = AppState {
3156            label: "hello".to_string(),
3157        };
3158        let flags = root.rebuild(&mut build, &mut state);
3159        // First build dirties both passes.
3160        assert!(flags.needs_layout());
3161        assert!(flags.needs_paint());
3162        let id = root.root_id().expect("root built");
3163        let pod = root.tree().pod(id).expect("pod in arena");
3164        assert!(pod.widget().downcast_ref_is::<TextWidget>());
3165    }
3166
3167    #[test]
3168    fn rebuild_changed_data_yields_paint() {
3169        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3170        let mut state = AppState {
3171            label: "a".to_string(),
3172        };
3173        root.rebuild(&mut build, &mut state);
3174        state.label = "b".to_string();
3175        let flags = root.rebuild(&mut build, &mut state);
3176        assert_eq!(flags, ChangeFlags::PAINT);
3177    }
3178
3179    #[test]
3180    fn rebuild_unchanged_data_yields_none() {
3181        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3182        let mut state = AppState {
3183            label: "same".to_string(),
3184        };
3185        root.rebuild(&mut build, &mut state);
3186        let flags = root.rebuild(&mut build, &mut state);
3187        assert_eq!(flags, ChangeFlags::NONE);
3188    }
3189
3190    #[test]
3191    fn layout_stores_size_in_pod() {
3192        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3193        let mut state = AppState {
3194            label: "hi".to_string(),
3195        };
3196        root.rebuild(&mut build, &mut state);
3197        let size = root.layout(Size::new(800.0, 600.0));
3198        // "hi" -> 2 * 8 = 16 wide, 16 tall, within the window.
3199        assert_eq!(size, Size::new(16.0, 16.0));
3200        let id = root.root_id().unwrap();
3201        let pod = root.tree().pod(id).unwrap();
3202        assert_eq!(pod.origin(), Point::ZERO);
3203        assert_eq!(pod.size(), Size::new(16.0, 16.0));
3204    }
3205
3206    #[test]
3207    fn inspect_reports_the_laid_out_root() {
3208        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3209        let mut state = AppState {
3210            label: "hi".to_string(),
3211        };
3212        // Before the first build there is nothing to inspect.
3213        assert!(root.inspect().is_empty());
3214
3215        root.rebuild(&mut build, &mut state);
3216        root.layout(Size::new(800.0, 600.0));
3217
3218        let nodes = root.inspect();
3219        assert_eq!(nodes.len(), 1);
3220        let node = &nodes[0];
3221        assert_eq!(node.id, root.root_id().unwrap());
3222        assert_eq!(node.parent, None);
3223        assert_eq!(node.depth, 0);
3224        assert!(node.children.is_empty());
3225        // The concrete element type is captured, not the erased box.
3226        assert!(node.type_name.ends_with("TextWidget"), "{}", node.type_name);
3227        assert_eq!(node.debug_label, None);
3228        // Bounds match what the layout pass recorded on the pod.
3229        let pod = root.tree().pod(node.id).unwrap();
3230        assert_eq!(
3231            node.bounds,
3232            Rect::from_origin_size(pod.origin(), pod.size())
3233        );
3234        assert_eq!(node.bounds, Rect::new(0.0, 0.0, 16.0, 16.0));
3235    }
3236
3237    #[test]
3238    fn layout_clamps_to_window() {
3239        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3240        let mut state = AppState {
3241            label: "wwwwwwwwwww".to_string(), // 10 chars -> 80 wide intrinsic
3242        };
3243        root.rebuild(&mut build, &mut state);
3244        let size = root.layout(Size::new(40.0, 40.0));
3245        // Intrinsic width 80 is clamped to the 40-wide window.
3246        assert_eq!(size.width, 40.0);
3247    }
3248
3249    #[test]
3250    fn paint_emits_current_text() {
3251        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3252        let mut state = AppState {
3253            label: "one".to_string(),
3254        };
3255        root.rebuild(&mut build, &mut state);
3256        root.layout(Size::new(200.0, 200.0));
3257
3258        let mut scene = RecordingScene::default();
3259        root.paint(&mut scene, FrameTime::ZERO);
3260        assert_eq!(scene.texts, vec![(Point::ZERO, "one".to_string())]);
3261
3262        // Change data, rebuild, repaint -> new text.
3263        state.label = "two".to_string();
3264        root.rebuild(&mut build, &mut state);
3265        let mut scene2 = RecordingScene::default();
3266        root.paint(&mut scene2, FrameTime::ZERO);
3267        assert_eq!(scene2.texts, vec![(Point::ZERO, "two".to_string())]);
3268    }
3269
3270    /// A leaf widget that publishes a fixed [`PlatformViewFrame`] — and reports
3271    /// a z-shield rect over its own bounds — on every paint, unless
3272    /// `should_publish` is false (the widget-level toggle that simulates a slot
3273    /// no longer publishing between two rebuilds). Both channels ride the same
3274    /// toggle so one fixture covers both replace-per-pass contracts.
3275    struct PlatformViewProbeWidget {
3276        slot_id: u64,
3277        should_publish: bool,
3278    }
3279
3280    impl crate::widget::Widget for PlatformViewProbeWidget {
3281        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3282            bc.constrain(Size::new(10.0, 10.0))
3283        }
3284        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3285            if self.should_publish {
3286                ctx.publish_platform_view(PlatformViewFrame {
3287                    slot_id: self.slot_id,
3288                    view_type: "dev.frust.Probe".to_string(),
3289                    params_json: String::new(),
3290                    params_generation: 0,
3291                    rect: kurbo::Rect::from_origin_size(ctx.origin(), ctx.size()),
3292                    clip: None,
3293                    visible: true,
3294                    interactive: false,
3295                    shields: Vec::new(),
3296                });
3297                ctx.report_input_shield(kurbo::Rect::from_origin_size(ctx.origin(), ctx.size()));
3298            }
3299        }
3300    }
3301
3302    /// A root widget owning two independently toggleable [`ChildPod`]s (a
3303    /// minimal two-slot container) so a rebuild can flip either slot's
3304    /// `should_publish` — the fixture the "two slots in one pass" and
3305    /// "empty-pass clears stale frames" tests below need.
3306    struct PlatformViewRootWidget {
3307        a: crate::widget::ChildPod,
3308        b: crate::widget::ChildPod,
3309    }
3310
3311    impl crate::widget::Widget for PlatformViewRootWidget {
3312        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3313            self.a.layout_child(ctx, bc);
3314            self.b.layout_child(ctx, bc);
3315            bc.constrain(Size::new(10.0, 10.0))
3316        }
3317        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3318            self.a.paint_child(ctx, scene);
3319            self.b.paint_child(ctx, scene);
3320        }
3321    }
3322
3323    /// The `View` producing [`PlatformViewRootWidget`], reconciling each
3324    /// slot's `should_publish` flag on rebuild like any controlled widget.
3325    struct PlatformViewRootView {
3326        publish_a: bool,
3327        publish_b: bool,
3328    }
3329
3330    impl View<PvState> for PlatformViewRootView {
3331        type Element = PlatformViewRootWidget;
3332
3333        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
3334            PlatformViewRootWidget {
3335                a: crate::widget::ChildPod::new(Box::new(PlatformViewProbeWidget {
3336                    slot_id: 1,
3337                    should_publish: self.publish_a,
3338                })),
3339                b: crate::widget::ChildPod::new(Box::new(PlatformViewProbeWidget {
3340                    slot_id: 2,
3341                    should_publish: self.publish_b,
3342                })),
3343            }
3344        }
3345
3346        fn rebuild(
3347            &self,
3348            prev: &Self,
3349            element: &mut Self::Element,
3350            _ctx: &mut BuildCtx<'_>,
3351        ) -> ChangeFlags {
3352            if prev.publish_a != self.publish_a || prev.publish_b != self.publish_b {
3353                element
3354                    .a
3355                    .widget_mut()
3356                    .downcast_mut::<PlatformViewProbeWidget>()
3357                    .expect("slot a stays a PlatformViewProbeWidget")
3358                    .should_publish = self.publish_a;
3359                element
3360                    .b
3361                    .widget_mut()
3362                    .downcast_mut::<PlatformViewProbeWidget>()
3363                    .expect("slot b stays a PlatformViewProbeWidget")
3364                    .should_publish = self.publish_b;
3365                ChangeFlags::PAINT
3366            } else {
3367                ChangeFlags::NONE
3368            }
3369        }
3370    }
3371
3372    /// App state for the platform-view frame-channel tests.
3373    #[derive(Default)]
3374    struct PvState {
3375        publish_a: bool,
3376        publish_b: bool,
3377    }
3378
3379    fn platform_view_logic(state: &mut PvState) -> PlatformViewRootView {
3380        PlatformViewRootView {
3381            publish_a: state.publish_a,
3382            publish_b: state.publish_b,
3383        }
3384    }
3385
3386    #[test]
3387    fn platform_view_frames_arrive_in_order_and_clear_on_empty_pass() {
3388        let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3389        let mut state = PvState {
3390            publish_a: true,
3391            publish_b: true,
3392        };
3393        root.rebuild(&mut platform_view_logic, &mut state);
3394        root.layout(Size::new(200.0, 200.0));
3395
3396        let mut scene = RecordingScene::default();
3397        root.paint(&mut scene, FrameTime::ZERO);
3398
3399        // Two slots publishing in one pass both arrive, in paint order — the
3400        // regression test for the overwrite hazard (an Option-based `ime_state`
3401        // shape here would leave only the second slot's frame).
3402        let frames = root.platform_view_frames();
3403        assert_eq!(frames.len(), 2);
3404        assert_eq!(frames[0].slot_id, 1);
3405        assert_eq!(frames[1].slot_id, 2);
3406
3407        // Next pass: neither slot publishes (simulates both going away/culled).
3408        // The collection is REPLACED, so the previous pass's frames must not
3409        // survive as stale entries.
3410        state.publish_a = false;
3411        state.publish_b = false;
3412        root.rebuild(&mut platform_view_logic, &mut state);
3413        root.layout(Size::new(200.0, 200.0));
3414        root.paint(&mut scene, FrameTime::ZERO);
3415        assert!(
3416            root.platform_view_frames().is_empty(),
3417            "a paint pass with no publishers must yield an empty slice"
3418        );
3419    }
3420
3421    #[test]
3422    fn input_shields_arrive_in_order_and_clear_on_empty_pass() {
3423        // The shield channel's half of the contract above:
3424        // two shields reported in one pass both survive (the `Vec`
3425        // extend, not an `Option` overwrite), and a pass that reports none
3426        // replaces the collection rather than merging — a stale shield must
3427        // never keep stealing input from an interactive slot.
3428        let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3429        let mut state = PvState {
3430            publish_a: true,
3431            publish_b: true,
3432        };
3433        root.rebuild(&mut platform_view_logic, &mut state);
3434        root.layout(Size::new(200.0, 200.0));
3435
3436        let mut scene = RecordingScene::default();
3437        root.paint(&mut scene, FrameTime::ZERO);
3438        assert_eq!(root.input_shields().len(), 2);
3439        assert_eq!(
3440            root.input_shields()[0],
3441            Rect::from_origin_size(Point::ZERO, Size::new(10.0, 10.0)),
3442            "a shield is reported in absolute paint coordinates"
3443        );
3444
3445        state.publish_a = false;
3446        state.publish_b = false;
3447        root.rebuild(&mut platform_view_logic, &mut state);
3448        root.layout(Size::new(200.0, 200.0));
3449        root.paint(&mut scene, FrameTime::ZERO);
3450        assert!(
3451            root.input_shields().is_empty(),
3452            "a paint pass reporting no shields must yield an empty slice"
3453        );
3454    }
3455
3456    #[test]
3457    fn retired_platform_views_drain_exactly_once() {
3458        // The prompt-teardown channel: a reported slot
3459        // id is handed to the shell once and then gone, mirroring
3460        // `take_change_flags`. Serialized against the other test touching the
3461        // process-wide list (see `RETIRE_TEST_LOCK`).
3462        let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3463        let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3464        let _ = root.take_retired_platform_views(); // clear anything a sibling left
3465
3466        crate::widget::report_retired_slot(7);
3467        crate::widget::report_retired_slot(9);
3468        assert_eq!(root.take_retired_platform_views(), vec![7, 9]);
3469        assert!(
3470            root.take_retired_platform_views().is_empty(),
3471            "draining is destructive — a second drain reports nothing"
3472        );
3473    }
3474
3475    #[test]
3476    fn retired_platform_views_are_capped_dropping_the_oldest() {
3477        // A shell that never drains (desktop: no native compositor) must not
3478        // grow this list forever; past the cap the OLDEST id is dropped and the
3479        // differ's missing-streak backstop covers it.
3480        let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3481        let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3482        let _ = root.take_retired_platform_views();
3483
3484        for slot_id in 0..1_000u64 {
3485            crate::widget::report_retired_slot(slot_id);
3486        }
3487        let drained = root.take_retired_platform_views();
3488        assert!(drained.len() <= 256, "the pending list stays bounded");
3489        assert_eq!(
3490            *drained.last().expect("non-empty"),
3491            999,
3492            "the newest report always survives"
3493        );
3494        assert!(
3495            !drained.contains(&0),
3496            "the oldest reports are the ones dropped"
3497        );
3498    }
3499
3500    /// Serializes the two tests that drive the process-wide retire list
3501    /// (`crate::widget::report_retired_slot`), which `cargo test`'s parallel
3502    /// threads would otherwise interleave — the same shape
3503    /// `frust-shell-common::theme_override`'s tests use for its global slot.
3504    static RETIRE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3505
3506    /// A root widget that advances no state but requests a continuation frame on
3507    /// every paint — stands in for an animating widget (e.g. a scroll fling).
3508    struct FrameWidget;
3509    impl crate::widget::Widget for FrameWidget {
3510        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3511            bc.constrain(Size::new(10.0, 10.0))
3512        }
3513        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3514            ctx.request_frame();
3515        }
3516    }
3517
3518    struct FrameView;
3519    impl View<AppState> for FrameView {
3520        type Element = FrameWidget;
3521        fn build(&self, _ctx: &mut BuildCtx<'_>) -> FrameWidget {
3522            FrameWidget
3523        }
3524        fn rebuild(
3525            &self,
3526            _prev: &Self,
3527            _element: &mut FrameWidget,
3528            _ctx: &mut BuildCtx<'_>,
3529        ) -> ChangeFlags {
3530            ChangeFlags::NONE
3531        }
3532    }
3533
3534    #[test]
3535    fn paint_reports_needs_frame_from_animating_root() {
3536        // A still root reports no continuation frame.
3537        let mut still: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3538        let mut state = AppState {
3539            label: "x".to_string(),
3540        };
3541        still.rebuild(&mut build, &mut state);
3542        still.layout(Size::new(100.0, 100.0));
3543        let mut scene = RecordingScene::default();
3544        assert!(!still.paint(&mut scene, FrameTime::ZERO).needs_frame);
3545
3546        // An animating root bubbles request_frame out as PaintOutcome::needs_frame.
3547        let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
3548        anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
3549        anim.layout(Size::new(100.0, 100.0));
3550        let mut scene2 = RecordingScene::default();
3551        assert!(anim.paint(&mut scene2, FrameTime::ZERO).needs_frame);
3552    }
3553
3554    /// A root widget whose animation changes its layout: it requests a layout
3555    /// re-run on every paint — stands in for an expanding accordion.
3556    struct LayoutFrameWidget;
3557    impl crate::widget::Widget for LayoutFrameWidget {
3558        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3559            bc.constrain(Size::new(10.0, 10.0))
3560        }
3561        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3562            ctx.request_layout();
3563        }
3564    }
3565
3566    struct LayoutFrameView;
3567    impl View<AppState> for LayoutFrameView {
3568        type Element = LayoutFrameWidget;
3569        fn build(&self, _ctx: &mut BuildCtx<'_>) -> LayoutFrameWidget {
3570            LayoutFrameWidget
3571        }
3572        fn rebuild(
3573            &self,
3574            _prev: &Self,
3575            _element: &mut LayoutFrameWidget,
3576            _ctx: &mut BuildCtx<'_>,
3577        ) -> ChangeFlags {
3578            ChangeFlags::NONE
3579        }
3580    }
3581
3582    #[test]
3583    fn paint_folds_request_layout_into_pending_change_flags() {
3584        let mut state = AppState {
3585            label: "x".to_string(),
3586        };
3587
3588        // A root calling `request_layout` in paint surfaces it on the outcome AND
3589        // folds LAYOUT into `pending`, so the NEXT frame's `take_change_flags`
3590        // reports `needs_layout()`.
3591        let mut anim: RenderRoot<AppState, LayoutFrameView> = RenderRoot::new();
3592        anim.rebuild(&mut |_s: &mut AppState| LayoutFrameView, &mut state);
3593        anim.layout(Size::new(100.0, 100.0));
3594        // Drain any rebuild/layout dirtiness so we observe only paint's fold.
3595        let _ = anim.take_change_flags();
3596        let mut scene = RecordingScene::default();
3597        let outcome = anim.paint(&mut scene, FrameTime::ZERO);
3598        assert!(outcome.needs_layout, "outcome reports needs_layout");
3599        // `request_layout` implies `request_frame`, so the animation still runs.
3600        assert!(outcome.needs_frame, "request_layout implies needs_frame");
3601        assert!(
3602            anim.has_pending_change_flags(),
3603            "the fold survives to the next frame"
3604        );
3605        assert!(
3606            anim.take_change_flags().needs_layout(),
3607            "next frame's take_change_flags reports needs_layout"
3608        );
3609    }
3610
3611    #[test]
3612    fn paint_request_frame_only_does_not_fold_layout() {
3613        let mut state = AppState {
3614            label: "x".to_string(),
3615        };
3616
3617        // A paint-only animation (request_frame, no request_layout) must NOT fold
3618        // LAYOUT — the mobile layout-skip win depends on this staying opt-in.
3619        let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
3620        anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
3621        anim.layout(Size::new(100.0, 100.0));
3622        let _ = anim.take_change_flags();
3623        let mut scene = RecordingScene::default();
3624        let outcome = anim.paint(&mut scene, FrameTime::ZERO);
3625        assert!(outcome.needs_frame);
3626        assert!(
3627            !outcome.needs_layout,
3628            "request_frame alone: no needs_layout"
3629        );
3630        assert!(
3631            !anim.has_pending_change_flags(),
3632            "request_frame alone must not fold LAYOUT into pending"
3633        );
3634    }
3635
3636    /// A root whose paint requests a *pacable* cosmetic-loop frame — stands in
3637    /// for a skeleton shimmer whose cadence the mobile frame gate may throttle.
3638    struct PacedFrameWidget;
3639    impl crate::widget::Widget for PacedFrameWidget {
3640        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3641            bc.constrain(Size::new(10.0, 10.0))
3642        }
3643        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3644            ctx.request_frame_paced();
3645        }
3646    }
3647
3648    struct PacedFrameView;
3649    impl View<AppState> for PacedFrameView {
3650        type Element = PacedFrameWidget;
3651        fn build(&self, _ctx: &mut BuildCtx<'_>) -> PacedFrameWidget {
3652            PacedFrameWidget
3653        }
3654        fn rebuild(
3655            &self,
3656            _prev: &Self,
3657            _element: &mut PacedFrameWidget,
3658            _ctx: &mut BuildCtx<'_>,
3659        ) -> ChangeFlags {
3660            ChangeFlags::NONE
3661        }
3662    }
3663
3664    #[test]
3665    fn paint_surfaces_paced_only_tick_class_on_outcome() {
3666        let mut state = AppState {
3667            label: "x".to_string(),
3668        };
3669
3670        // A paced-only root surfaces `needs_frame_paced_only` on the outcome so
3671        // the mobile frame gate may throttle its cadence.
3672        let mut paced: RenderRoot<AppState, PacedFrameView> = RenderRoot::new();
3673        paced.rebuild(&mut |_s: &mut AppState| PacedFrameView, &mut state);
3674        paced.layout(Size::new(100.0, 100.0));
3675        let mut scene = RecordingScene::default();
3676        let outcome = paced.paint(&mut scene, FrameTime::ZERO);
3677        assert!(outcome.needs_frame);
3678        assert!(
3679            outcome.needs_frame_paced_only,
3680            "a purely-cosmetic frame surfaces as paced-only"
3681        );
3682        assert_eq!(
3683            outcome.paced_interval,
3684            Some(std::time::Duration::ZERO),
3685            "a bare `request_frame_paced` names no interval (the theme's own rate)"
3686        );
3687
3688        // A Transition-class (`request_frame`) root is never paced-only, keeping
3689        // today's every-vsync behavior for existing callers.
3690        let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
3691        anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
3692        anim.layout(Size::new(100.0, 100.0));
3693        let mut scene2 = RecordingScene::default();
3694        let outcome2 = anim.paint(&mut scene2, FrameTime::ZERO);
3695        assert!(outcome2.needs_frame);
3696        assert!(
3697            !outcome2.needs_frame_paced_only,
3698            "request_frame stays unpaced (Transition)"
3699        );
3700        assert_eq!(
3701            outcome2.paced_interval, None,
3702            "an unpaced frame names no paced interval"
3703        );
3704    }
3705
3706    /// A root widget whose decorative loop names its own slow cadence — the
3707    /// `request_frame_paced_at` counterpart of [`PacedFrameWidget`].
3708    struct SlowPacedFrameWidget;
3709    impl crate::widget::Widget for SlowPacedFrameWidget {
3710        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3711            bc.constrain(Size::new(10.0, 10.0))
3712        }
3713        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3714            ctx.request_frame_paced_at(std::time::Duration::from_millis(500));
3715        }
3716    }
3717    struct SlowPacedFrameView;
3718    impl View<AppState> for SlowPacedFrameView {
3719        type Element = SlowPacedFrameWidget;
3720        fn build(&self, _ctx: &mut BuildCtx<'_>) -> SlowPacedFrameWidget {
3721            SlowPacedFrameWidget
3722        }
3723        fn rebuild(
3724            &self,
3725            _prev: &Self,
3726            _element: &mut SlowPacedFrameWidget,
3727            _ctx: &mut BuildCtx<'_>,
3728        ) -> ChangeFlags {
3729            ChangeFlags::NONE
3730        }
3731    }
3732
3733    #[test]
3734    fn paint_surfaces_the_requested_paced_interval_on_outcome() {
3735        // The end-to-end core half of the per-request pacing seam: a widget's
3736        // `request_frame_paced_at` reaches the shell on `PaintOutcome`, which is
3737        // what the mobile gate latches into `FramePacing`.
3738        let mut state = AppState {
3739            label: "x".to_string(),
3740        };
3741        let mut root: RenderRoot<AppState, SlowPacedFrameView> = RenderRoot::new();
3742        root.rebuild(&mut |_s: &mut AppState| SlowPacedFrameView, &mut state);
3743        root.layout(Size::new(100.0, 100.0));
3744        let mut scene = RecordingScene::default();
3745        let outcome = root.paint(&mut scene, FrameTime::ZERO);
3746        assert!(outcome.needs_frame_paced_only);
3747        assert_eq!(
3748            outcome.paced_interval,
3749            Some(std::time::Duration::from_millis(500))
3750        );
3751    }
3752
3753    /// A root widget that records the `frame_time` its paint observed, so a test
3754    /// can prove the shell-injected clock reaches `PaintCtx::frame_time()`.
3755    struct ClockWidget {
3756        seen: std::rc::Rc<std::cell::Cell<Option<FrameTime>>>,
3757    }
3758    impl crate::widget::Widget for ClockWidget {
3759        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3760            bc.constrain(Size::new(10.0, 10.0))
3761        }
3762        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3763            self.seen.set(Some(ctx.frame_time()));
3764        }
3765    }
3766
3767    struct ClockView {
3768        seen: std::rc::Rc<std::cell::Cell<Option<FrameTime>>>,
3769    }
3770    impl View<AppState> for ClockView {
3771        type Element = ClockWidget;
3772        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ClockWidget {
3773            ClockWidget {
3774                seen: self.seen.clone(),
3775            }
3776        }
3777        fn rebuild(
3778            &self,
3779            _prev: &Self,
3780            _element: &mut ClockWidget,
3781            _ctx: &mut BuildCtx<'_>,
3782        ) -> ChangeFlags {
3783            ChangeFlags::NONE
3784        }
3785    }
3786
3787    #[test]
3788    fn paint_threads_injected_frame_time_to_widget() {
3789        let seen = std::rc::Rc::new(std::cell::Cell::new(None));
3790        let mut root: RenderRoot<AppState, ClockView> = RenderRoot::new();
3791        let mut state = AppState::default();
3792        let seen_for_view = seen.clone();
3793        root.rebuild(
3794            &mut move |_s: &mut AppState| ClockView {
3795                seen: seen_for_view.clone(),
3796            },
3797            &mut state,
3798        );
3799        root.layout(Size::new(100.0, 100.0));
3800
3801        // Two paints with distinct injected times: the widget observes each one,
3802        // proving the clock is shell-fed (not read from an ambient `Instant`).
3803        let mut scene = RecordingScene::default();
3804        root.paint(&mut scene, FrameTime::from_nanos(1_000));
3805        assert_eq!(seen.get(), Some(FrameTime::from_nanos(1_000)));
3806        root.paint(&mut scene, FrameTime::from_nanos(17_000));
3807        assert_eq!(seen.get(), Some(FrameTime::from_nanos(17_000)));
3808    }
3809
3810    // --- Theme threading: a dummy theme recovered during paint/layout. ---
3811
3812    /// A dummy theme type standing in for `frust_theme::Theme` — `frust-core`
3813    /// never names the real one, so this proves the type-erased slot works for
3814    /// any `'static` type.
3815    #[derive(Debug, Clone, PartialEq)]
3816    struct TestTheme {
3817        accent: u32,
3818    }
3819
3820    /// A root widget recording the theme accent it recovered during paint (and
3821    /// during layout), or `None` when no theme was threaded in.
3822    struct ThemeWidget {
3823        seen_paint: std::rc::Rc<std::cell::Cell<Option<u32>>>,
3824        seen_layout: std::rc::Rc<std::cell::Cell<Option<u32>>>,
3825    }
3826    impl crate::widget::Widget for ThemeWidget {
3827        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3828            self.seen_layout
3829                .set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
3830            bc.constrain(Size::new(10.0, 10.0))
3831        }
3832        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3833            self.seen_paint
3834                .set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
3835        }
3836    }
3837
3838    struct ThemeView {
3839        seen_paint: std::rc::Rc<std::cell::Cell<Option<u32>>>,
3840        seen_layout: std::rc::Rc<std::cell::Cell<Option<u32>>>,
3841    }
3842    impl View<AppState> for ThemeView {
3843        type Element = ThemeWidget;
3844        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ThemeWidget {
3845            ThemeWidget {
3846                seen_paint: self.seen_paint.clone(),
3847                seen_layout: self.seen_layout.clone(),
3848            }
3849        }
3850        fn rebuild(
3851            &self,
3852            _prev: &Self,
3853            _element: &mut ThemeWidget,
3854            _ctx: &mut BuildCtx<'_>,
3855        ) -> ChangeFlags {
3856            ChangeFlags::NONE
3857        }
3858    }
3859
3860    fn drive_theme_root(theme: Option<TestTheme>) -> (Option<u32>, Option<u32>) {
3861        let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
3862        let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
3863        let mut root: RenderRoot<AppState, ThemeView> = RenderRoot::new();
3864        if let Some(theme) = theme {
3865            root.set_theme(Box::new(theme));
3866        }
3867        let mut state = AppState::default();
3868        let sp = seen_paint.clone();
3869        let sl = seen_layout.clone();
3870        root.rebuild(
3871            &mut move |_s: &mut AppState| ThemeView {
3872                seen_paint: sp.clone(),
3873                seen_layout: sl.clone(),
3874            },
3875            &mut state,
3876        );
3877        root.layout(Size::new(100.0, 100.0));
3878        let mut scene = RecordingScene::default();
3879        root.paint(&mut scene, FrameTime::ZERO);
3880        (seen_layout.get(), seen_paint.get())
3881    }
3882
3883    #[test]
3884    fn set_theme_threads_into_layout_and_paint() {
3885        let (layout, paint) = drive_theme_root(Some(TestTheme { accent: 5 }));
3886        assert_eq!(layout, Some(5));
3887        assert_eq!(paint, Some(5));
3888    }
3889
3890    #[test]
3891    fn no_theme_yields_none_in_layout_and_paint() {
3892        let (layout, paint) = drive_theme_root(None);
3893        assert_eq!(layout, None);
3894        assert_eq!(paint, None);
3895    }
3896
3897    #[test]
3898    fn set_theme_replaces_the_previous_theme() {
3899        // A second `set_theme` (a live dark-mode flip on desktop) wins on the
3900        // next paint.
3901        let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
3902        let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
3903        let mut root: RenderRoot<AppState, ThemeView> = RenderRoot::new();
3904        root.set_theme(Box::new(TestTheme { accent: 1 }));
3905        let mut state = AppState::default();
3906        let sp = seen_paint.clone();
3907        let sl = seen_layout.clone();
3908        root.rebuild(
3909            &mut move |_s: &mut AppState| ThemeView {
3910                seen_paint: sp.clone(),
3911                seen_layout: sl.clone(),
3912            },
3913            &mut state,
3914        );
3915        root.layout(Size::new(100.0, 100.0));
3916        let mut scene = RecordingScene::default();
3917        root.paint(&mut scene, FrameTime::ZERO);
3918        assert_eq!(seen_paint.get(), Some(1));
3919
3920        // Flip the theme, repaint — the new accent is observed.
3921        root.set_theme(Box::new(TestTheme { accent: 2 }));
3922        root.layout(Size::new(100.0, 100.0));
3923        root.paint(&mut scene, FrameTime::ZERO);
3924        assert_eq!(seen_paint.get(), Some(2));
3925    }
3926
3927    // --- Event-pass fixtures: a widget that mutates state on pointer-down. ---
3928
3929    #[derive(Default)]
3930    struct ClickState {
3931        clicks: u32,
3932    }
3933
3934    struct ButtonWidget;
3935    impl crate::widget::Widget for ButtonWidget {
3936        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3937            bc.constrain(Size::new(40.0, 20.0))
3938        }
3939        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3940        fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
3941            if let InputEvent::Pointer(p) = event {
3942                match p.phase {
3943                    PointerPhase::Down => {
3944                        ctx.state_mut::<ClickState>().clicks += 1;
3945                        ctx.request_redraw();
3946                        ctx.capture_pointer();
3947                        return EventResult::Handled;
3948                    }
3949                    PointerPhase::Up | PointerPhase::Cancel => return EventResult::Handled,
3950                    PointerPhase::Move => {}
3951                }
3952            }
3953            EventResult::Ignored
3954        }
3955    }
3956
3957    struct ButtonView;
3958    impl View<ClickState> for ButtonView {
3959        type Element = ButtonWidget;
3960        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ButtonWidget {
3961            ButtonWidget
3962        }
3963        fn rebuild(
3964            &self,
3965            _prev: &Self,
3966            _element: &mut ButtonWidget,
3967            _ctx: &mut BuildCtx<'_>,
3968        ) -> ChangeFlags {
3969            ChangeFlags::NONE
3970        }
3971    }
3972
3973    fn button_logic(_state: &mut ClickState) -> ButtonView {
3974        ButtonView
3975    }
3976
3977    fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
3978        InputEvent::Pointer(crate::event::PointerEvent {
3979            phase,
3980            position: Point::new(x, y),
3981            button: crate::event::PointerButton::Primary,
3982        })
3983    }
3984
3985    #[test]
3986    fn event_reaches_root_widget_and_mutates_state() {
3987        let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
3988        let mut state = ClickState::default();
3989        root.rebuild(&mut button_logic, &mut state);
3990        root.layout(Size::new(200.0, 200.0));
3991
3992        let outcome = root.event(&mut state, &pointer(PointerPhase::Down, 5.0, 5.0));
3993        assert!(outcome.handled);
3994        assert!(outcome.needs_redraw);
3995        assert_eq!(state.clicks, 1);
3996        // A captured Down opens the root gesture.
3997        assert!(root.is_pointer_captured());
3998    }
3999
4000    #[test]
4001    fn event_before_build_is_a_benign_no_op() {
4002        let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
4003        let mut state = ClickState::default();
4004        let outcome = root.event(&mut state, &pointer(PointerPhase::Down, 1.0, 1.0));
4005        assert_eq!(outcome, EventOutcome::default());
4006        assert_eq!(state.clicks, 0);
4007    }
4008
4009    #[test]
4010    fn capture_releases_on_pointer_up() {
4011        let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
4012        let mut state = ClickState::default();
4013        root.rebuild(&mut button_logic, &mut state);
4014        root.layout(Size::new(200.0, 200.0));
4015
4016        root.event(&mut state, &pointer(PointerPhase::Down, 5.0, 5.0));
4017        assert!(root.is_pointer_captured());
4018        root.event(&mut state, &pointer(PointerPhase::Up, 5.0, 5.0));
4019        assert!(!root.is_pointer_captured());
4020    }
4021
4022    #[test]
4023    fn take_change_flags_drains_accumulated_dirtiness() {
4024        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4025        let mut state = AppState {
4026            label: "x".to_string(),
4027        };
4028        root.rebuild(&mut build, &mut state);
4029        // First build accumulated LAYOUT|PAINT.
4030        let flags = root.take_change_flags();
4031        assert!(flags.needs_layout());
4032        // Draining leaves it empty until the next rebuild.
4033        assert!(root.take_change_flags().is_empty());
4034    }
4035
4036    #[test]
4037    fn has_pending_change_flags_peeks_without_draining() {
4038        // The frame-gate peek: observe pending dirtiness without
4039        // clearing it, so a skipped frame preserves the flags for the next
4040        // frame that actually runs.
4041        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4042        assert!(
4043            !root.has_pending_change_flags(),
4044            "a fresh root has nothing pending"
4045        );
4046        let mut state = AppState {
4047            label: "x".to_string(),
4048        };
4049        root.rebuild(&mut build, &mut state);
4050        // First build accumulated LAYOUT|PAINT — the peek sees it...
4051        assert!(root.has_pending_change_flags());
4052        // ...and repeated peeks do NOT drain it.
4053        assert!(root.has_pending_change_flags());
4054        // Only `take_change_flags` drains.
4055        assert!(!root.take_change_flags().is_empty());
4056        assert!(!root.has_pending_change_flags());
4057    }
4058
4059    #[test]
4060    fn set_theme_marks_layout_and_paint_pending() {
4061        // `set_theme` alone (no rebuild) must dirty layout/paint so a shell
4062        // gating on `take_change_flags` doesn't skip re-resolving theme-baked
4063        // widget state (e.g. Text's themed glyph color) on a bare theme swap.
4064        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4065        root.set_theme(Box::new(TestTheme { accent: 1 }));
4066        let flags = root.take_change_flags();
4067        assert!(flags.needs_layout());
4068        assert!(flags.needs_paint());
4069
4070        // Draining clears it until the next `set_theme`/rebuild.
4071        assert!(root.take_change_flags().is_empty());
4072        root.set_theme(Box::new(TestTheme { accent: 2 }));
4073        let flags = root.take_change_flags();
4074        assert!(flags.needs_layout());
4075        assert!(flags.needs_paint());
4076    }
4077
4078    // --- Window insets: pushed value reaches layout/paint contexts. ---
4079
4080    use crate::insets::{EdgeInsets, WindowInsets};
4081
4082    /// A root widget recording the `WindowInsets` it observed during layout and
4083    /// paint, proving the shell-pushed value threads through both contexts.
4084    struct InsetsWidget {
4085        seen_layout: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4086        seen_paint: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4087    }
4088    impl crate::widget::Widget for InsetsWidget {
4089        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4090            self.seen_layout.set(Some(ctx.window_insets()));
4091            bc.constrain(Size::new(10.0, 10.0))
4092        }
4093        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4094            self.seen_paint.set(Some(ctx.window_insets()));
4095        }
4096    }
4097
4098    struct InsetsView {
4099        seen_layout: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4100        seen_paint: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4101    }
4102    impl View<AppState> for InsetsView {
4103        type Element = InsetsWidget;
4104        fn build(&self, _ctx: &mut BuildCtx<'_>) -> InsetsWidget {
4105            InsetsWidget {
4106                seen_layout: self.seen_layout.clone(),
4107                seen_paint: self.seen_paint.clone(),
4108            }
4109        }
4110        fn rebuild(
4111            &self,
4112            _prev: &Self,
4113            _element: &mut InsetsWidget,
4114            _ctx: &mut BuildCtx<'_>,
4115        ) -> ChangeFlags {
4116            ChangeFlags::NONE
4117        }
4118    }
4119
4120    fn drive_insets_root(
4121        insets: Option<WindowInsets>,
4122    ) -> (Option<WindowInsets>, Option<WindowInsets>) {
4123        let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
4124        let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
4125        let mut root: RenderRoot<AppState, InsetsView> = RenderRoot::new();
4126        if let Some(insets) = insets {
4127            root.set_insets(insets);
4128        }
4129        let mut state = AppState::default();
4130        let sl = seen_layout.clone();
4131        let sp = seen_paint.clone();
4132        root.rebuild(
4133            &mut move |_s: &mut AppState| InsetsView {
4134                seen_layout: sl.clone(),
4135                seen_paint: sp.clone(),
4136            },
4137            &mut state,
4138        );
4139        root.layout(Size::new(100.0, 100.0));
4140        let mut scene = RecordingScene::default();
4141        root.paint(&mut scene, FrameTime::ZERO);
4142        (seen_layout.get(), seen_paint.get())
4143    }
4144
4145    #[test]
4146    fn set_insets_threads_into_layout_and_paint() {
4147        let insets = WindowInsets::new(
4148            EdgeInsets::new(0.0, 24.0, 0.0, 34.0),
4149            EdgeInsets::new(0.0, 0.0, 0.0, 0.0),
4150        );
4151        let (layout, paint) = drive_insets_root(Some(insets));
4152        assert_eq!(layout, Some(insets));
4153        assert_eq!(paint, Some(insets));
4154    }
4155
4156    #[test]
4157    fn no_insets_yields_zero_in_layout_and_paint() {
4158        let (layout, paint) = drive_insets_root(None);
4159        assert_eq!(layout, Some(WindowInsets::default()));
4160        assert_eq!(paint, Some(WindowInsets::default()));
4161    }
4162
4163    #[test]
4164    fn set_insets_marks_layout_and_paint_pending() {
4165        // Mirrors `set_theme_marks_layout_and_paint_pending`: a bare inset push
4166        // (no rebuild) must dirty layout/paint so a shell gating on
4167        // `take_change_flags` relayouts a `SafeArea` when the insets move.
4168        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4169        root.set_insets(WindowInsets::new(
4170            EdgeInsets::new(0.0, 24.0, 0.0, 0.0),
4171            EdgeInsets::ZERO,
4172        ));
4173        let flags = root.take_change_flags();
4174        assert!(flags.needs_layout());
4175        assert!(flags.needs_paint());
4176        // Drained until the next change.
4177        assert!(root.take_change_flags().is_empty());
4178    }
4179
4180    #[test]
4181    fn set_insets_no_op_when_unchanged_marks_nothing() {
4182        // The `PartialEq` no-op guard: re-pushing the current insets dirties
4183        // nothing, so a shell that forwards the platform insets every frame
4184        // never forces a needless relayout.
4185        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4186        let insets = WindowInsets::new(EdgeInsets::new(0.0, 24.0, 0.0, 34.0), EdgeInsets::ZERO);
4187        root.set_insets(insets);
4188        assert!(!root.take_change_flags().is_empty());
4189        // Same value again: no dirtiness.
4190        root.set_insets(insets);
4191        assert!(root.take_change_flags().is_empty());
4192        // A different value dirties again.
4193        root.set_insets(WindowInsets::default());
4194        assert!(!root.take_change_flags().is_empty());
4195    }
4196
4197    #[test]
4198    fn set_insets_round_trips_corner_insets() {
4199        use crate::insets::{CornerInset, CornerInsets};
4200        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4201        let corners = CornerInsets::new(
4202            CornerInset::ZERO,
4203            CornerInset::new(72.0, 24.0),
4204            CornerInset::ZERO,
4205            CornerInset::ZERO,
4206        );
4207        let insets = WindowInsets::default().with_corner_insets(corners);
4208        root.set_insets(insets);
4209        let flags = root.take_change_flags();
4210        assert!(flags.needs_layout());
4211        assert!(flags.needs_paint());
4212        assert_eq!(root.insets().corner_insets, corners);
4213        // Identical re-push marks nothing.
4214        root.set_insets(insets);
4215        assert!(root.take_change_flags().is_empty());
4216        // A push differing only in corners dirties again.
4217        let moved = insets.with_corner_insets(CornerInsets::new(
4218            CornerInset::new(72.0, 24.0),
4219            CornerInset::ZERO,
4220            CornerInset::ZERO,
4221            CornerInset::ZERO,
4222        ));
4223        root.set_insets(moved);
4224        let flags = root.take_change_flags();
4225        assert!(flags.needs_layout());
4226        assert!(flags.needs_paint());
4227    }
4228
4229    // --- Presented-frame count: pushed value reaches the paint context, unset
4230    //     yields `None`, and — unlike theme/insets — the setter dirties nothing. ---
4231
4232    /// A root widget recording the `presented_frames` count it observed during
4233    /// paint, proving the shell-pushed value threads through `PaintCtx`.
4234    struct PresentedWidget {
4235        seen_paint: std::rc::Rc<std::cell::Cell<Option<Option<u64>>>>,
4236    }
4237    impl crate::widget::Widget for PresentedWidget {
4238        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4239            bc.constrain(Size::new(10.0, 10.0))
4240        }
4241        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4242            self.seen_paint.set(Some(ctx.presented_frames()));
4243        }
4244    }
4245
4246    struct PresentedView {
4247        seen_paint: std::rc::Rc<std::cell::Cell<Option<Option<u64>>>>,
4248    }
4249    impl View<AppState> for PresentedView {
4250        type Element = PresentedWidget;
4251        fn build(&self, _ctx: &mut BuildCtx<'_>) -> PresentedWidget {
4252            PresentedWidget {
4253                seen_paint: self.seen_paint.clone(),
4254            }
4255        }
4256        fn rebuild(
4257            &self,
4258            _prev: &Self,
4259            _element: &mut PresentedWidget,
4260            _ctx: &mut BuildCtx<'_>,
4261        ) -> ChangeFlags {
4262            ChangeFlags::NONE
4263        }
4264    }
4265
4266    fn drive_presented_root(presented: Option<u64>) -> Option<u64> {
4267        let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
4268        let mut root: RenderRoot<AppState, PresentedView> = RenderRoot::new();
4269        if let Some(presented) = presented {
4270            root.set_presented_frames(presented);
4271        }
4272        let mut state = AppState::default();
4273        let sp = seen_paint.clone();
4274        root.rebuild(
4275            &mut move |_s: &mut AppState| PresentedView {
4276                seen_paint: sp.clone(),
4277            },
4278            &mut state,
4279        );
4280        root.layout(Size::new(100.0, 100.0));
4281        let mut scene = RecordingScene::default();
4282        root.paint(&mut scene, FrameTime::ZERO);
4283        // Unwrap the "did paint run" outer Option; the inner is what the widget saw.
4284        seen_paint.get().expect("paint ran")
4285    }
4286
4287    #[test]
4288    fn set_presented_frames_threads_into_paint() {
4289        assert_eq!(drive_presented_root(Some(12)), Some(12));
4290    }
4291
4292    #[test]
4293    fn unset_presented_frames_yields_none_in_paint() {
4294        assert_eq!(drive_presented_root(None), None);
4295    }
4296
4297    #[test]
4298    fn set_presented_frames_marks_no_change_flags() {
4299        // Unlike `set_theme`/`set_insets`, a presented-count push is a paint-only
4300        // observation — it must dirty NOTHING, so a monotonically ticking counter
4301        // never forces a relayout or (on mobile) keeps the frame gate perpetually
4302        // "Run" (the menu-idle behavior depends on this).
4303        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4304        let gen_before = root.semantics_generation();
4305        root.set_presented_frames(1);
4306        assert!(root.take_change_flags().is_empty());
4307        assert!(!root.has_pending_change_flags());
4308        // A second, changed push still dirties nothing.
4309        root.set_presented_frames(2);
4310        assert!(root.take_change_flags().is_empty());
4311        // And bumps no semantics generation (mirrors the no-dirty contract).
4312        assert_eq!(root.semantics_generation(), gen_before);
4313    }
4314
4315    // Small test helper: does the boxed widget downcast to `W`?
4316    trait DowncastRefIs {
4317        fn downcast_ref_is<W: crate::widget::Widget>(&self) -> bool;
4318    }
4319    impl DowncastRefIs for dyn crate::widget::Widget {
4320        fn downcast_ref_is<W: crate::widget::Widget>(&self) -> bool {
4321            (self as &dyn std::any::Any).is::<W>()
4322        }
4323    }
4324
4325    // --- Focus / IME surface fixtures: a root editable that focuses + publishes
4326    //     an IME surface on a `Down` in its left half, and blurs (no focus) on a
4327    //     `Down` in its right half. ---
4328
4329    use std::cell::RefCell;
4330    use std::rc::Rc;
4331
4332    use crate::event::{EditingState, ImeState};
4333
4334    struct ImeWidget;
4335    impl crate::widget::Widget for ImeWidget {
4336        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4337            bc.constrain(Size::new(100.0, 100.0))
4338        }
4339        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4340        fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4341            if let InputEvent::Pointer(p) = event {
4342                if p.phase == PointerPhase::Down && p.position.x < 50.0 {
4343                    ctx.request_focus();
4344                    ctx.publish_ime_state(ImeState {
4345                        active: true,
4346                        editing: EditingState {
4347                            text: "abc".to_string(),
4348                            selection_base: 3,
4349                            selection_extent: 3,
4350                            composing_base: -1,
4351                            composing_extent: -1,
4352                        },
4353                        caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
4354                        content_type: Default::default(),
4355                        suppress_soft_keyboard: false,
4356                    });
4357                    return EventResult::Handled;
4358                }
4359                if p.phase == PointerPhase::Down {
4360                    // Right-half tap: a blur (no focus request).
4361                    return EventResult::Handled;
4362                }
4363            }
4364            EventResult::Ignored
4365        }
4366    }
4367
4368    struct ImeView;
4369    impl View<ClickState> for ImeView {
4370        type Element = ImeWidget;
4371        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeWidget {
4372            ImeWidget
4373        }
4374        fn rebuild(
4375            &self,
4376            _prev: &Self,
4377            _element: &mut ImeWidget,
4378            _ctx: &mut BuildCtx<'_>,
4379        ) -> ChangeFlags {
4380            ChangeFlags::NONE
4381        }
4382    }
4383
4384    fn ime_logic(_state: &mut ClickState) -> ImeView {
4385        ImeView
4386    }
4387
4388    #[test]
4389    fn focus_and_ime_state_surface_and_clear_on_blur() {
4390        let mut root: RenderRoot<ClickState, ImeView> = RenderRoot::new();
4391        let mut state = ClickState::default();
4392        root.rebuild(&mut ime_logic, &mut state);
4393        root.layout(Size::new(100.0, 100.0));
4394
4395        // No focus / no IME surface initially.
4396        assert!(!root.is_focus_active());
4397        assert!(root.ime_state().is_none());
4398
4399        // A left-half Down focuses the widget and publishes an IME surface.
4400        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4401        assert!(root.is_focus_active());
4402        let ime = root
4403            .ime_state()
4404            .expect("focused widget published an IME surface");
4405        assert!(ime.active);
4406        assert_eq!(ime.editing.text, "abc");
4407
4408        // The published surface survives a rebuild (shell can query it between
4409        // frames).
4410        root.rebuild(&mut ime_logic, &mut state);
4411        assert!(root.ime_state().is_some());
4412
4413        // A right-half Down is a blur: focus and the IME surface both clear.
4414        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
4415        assert!(!root.is_focus_active());
4416        assert!(root.ime_state().is_none());
4417    }
4418
4419    // --- Session release: the focus session must die with its owner -----------
4420    //
4421    // Two routes end a session without any user input reaching the root:
4422    //
4423    //  (a) a widget publishes an INACTIVE IME surface (the navigator's post-pop
4424    //      `cleared_ime_state`, `PatternSwitcher`'s equivalent, a `TextInput`
4425    //      turned disabled under a live focus), and
4426    //  (b) a reconciler tears the focused pod out of the tree (any generic
4427    //      unmount — `frust-widgets`' `cancel_active_children`/`teardown_child`),
4428    //      which raises `mark_focus_orphaned` because it has no `RenderRoot` to
4429    //      reach from a `BuildCtx` pass.
4430    //
4431    // Both must perform the SAME full release a blur does. Leaving either half
4432    // standing — `focus_active` true, or `ime_state` parked at `Some(inactive)` —
4433    // is what stranded a popped screen: `is_focus_active()` kept lying, the
4434    // shell's IME poll kept seeing a surface, and the next real focus
4435    // interaction started from a corrupt baseline.
4436
4437    /// The navigator's cleared surface, spelled out here so the fixture below
4438    /// publishes exactly the shape `nav::navigator::cleared_ime_state` does
4439    /// (`frust-core` cannot name it — `frust-widgets` sits above this crate).
4440    fn cleared_surface() -> ImeState {
4441        ImeState {
4442            active: false,
4443            editing: EditingState {
4444                text: String::new(),
4445                selection_base: -1,
4446                selection_extent: -1,
4447                composing_base: -1,
4448                composing_extent: -1,
4449            },
4450            caret: None,
4451            content_type: Default::default(),
4452            suppress_soft_keyboard: false,
4453        }
4454    }
4455
4456    /// A focused editable that publishes its active surface from paint (like a
4457    /// real field), and — once `clear` is raised — publishes the *inactive*
4458    /// surface from paint instead: the container-after-a-pop shape.
4459    struct PopImeWidget {
4460        clear: Rc<Cell<bool>>,
4461    }
4462    impl crate::widget::Widget for PopImeWidget {
4463        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4464            bc.constrain(Size::new(100.0, 100.0))
4465        }
4466        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4467            if self.clear.get() {
4468                ctx.publish_ime_state(cleared_surface());
4469            } else if ctx.has_focus() {
4470                ctx.publish_ime_state(PaintImeWidget::surface("abc"));
4471            }
4472        }
4473        fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4474            match event {
4475                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
4476                    ctx.request_focus();
4477                    ctx.publish_ime_state(PaintImeWidget::surface("abc"));
4478                    EventResult::Handled
4479                }
4480                _ => EventResult::Ignored,
4481            }
4482        }
4483    }
4484
4485    struct PopImeView {
4486        clear: Rc<Cell<bool>>,
4487    }
4488    impl View<ClickState> for PopImeView {
4489        type Element = PopImeWidget;
4490        fn build(&self, _ctx: &mut BuildCtx<'_>) -> PopImeWidget {
4491            PopImeWidget {
4492                clear: self.clear.clone(),
4493            }
4494        }
4495        fn rebuild(
4496            &self,
4497            _prev: &Self,
4498            _element: &mut PopImeWidget,
4499            _ctx: &mut BuildCtx<'_>,
4500        ) -> ChangeFlags {
4501            ChangeFlags::NONE
4502        }
4503    }
4504
4505    #[test]
4506    fn inactive_paint_publish_releases_the_whole_session() {
4507        // (a) The pop shape. Before this, the paint take stored `Some(inactive)`
4508        // and never touched `focus_active`, so the session outlived the page.
4509        let clear = Rc::new(Cell::new(false));
4510        let mut build = {
4511            let clear = clear.clone();
4512            move |_state: &mut ClickState| PopImeView {
4513                clear: clear.clone(),
4514            }
4515        };
4516        let mut root: RenderRoot<ClickState, PopImeView> = RenderRoot::new();
4517        let mut state = ClickState::default();
4518        root.rebuild(&mut build, &mut state);
4519        root.layout(Size::new(100.0, 100.0));
4520
4521        let mut scene = RecordingScene::default();
4522        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4523        root.paint(&mut scene, FrameTime::ZERO);
4524        assert!(root.is_focus_active());
4525        assert!(root.ime_state().is_some_and(|s| s.active));
4526        let focused = root.focus_ime_generation();
4527
4528        // The "pop": the next paint publishes the cleared surface.
4529        clear.set(true);
4530        root.paint(&mut scene, FrameTime::ZERO);
4531        assert!(
4532            !root.is_focus_active(),
4533            "an inactive publish ends the session, not just the surface"
4534        );
4535        assert_eq!(
4536            root.ime_state(),
4537            None,
4538            "the surface is dropped, never parked at Some(inactive)"
4539        );
4540        assert_eq!(
4541            root.focus_ime_generation(),
4542            focused.wrapping_add(1),
4543            "one release is exactly one edge"
4544        );
4545
4546        // The widget keeps publishing the cleared surface every frame (a real
4547        // one-shot flag would not, but an idle screen must survive the worst
4548        // case): the paint take's `focus_active` guard makes each a no-op, so
4549        // the released session neither resurrects nor spins the edge.
4550        let released = root.focus_ime_generation();
4551        for _ in 0..30 {
4552            root.paint(&mut scene, FrameTime::ZERO);
4553        }
4554        assert!(!root.is_focus_active());
4555        assert!(root.ime_state().is_none());
4556        assert_eq!(
4557            root.focus_ime_generation(),
4558            released,
4559            "an inactive publish against an already-released root is inert"
4560        );
4561    }
4562
4563    /// A view whose rebuild raises the generic-unmount orphan mark on demand —
4564    /// standing in for `frust-widgets`' reconcilers, which clear a focused
4565    /// `ChildPod` mid-diff and raise exactly this flag (this crate has no
4566    /// multi-child container of its own to diff).
4567    struct UnmountView {
4568        orphan: Rc<Cell<bool>>,
4569    }
4570    impl View<ClickState> for UnmountView {
4571        type Element = ImeWidget;
4572        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeWidget {
4573            ImeWidget
4574        }
4575        fn rebuild(
4576            &self,
4577            _prev: &Self,
4578            _element: &mut ImeWidget,
4579            _ctx: &mut BuildCtx<'_>,
4580        ) -> ChangeFlags {
4581            if self.orphan.get() {
4582                crate::event::mark_focus_orphaned();
4583                return ChangeFlags::LAYOUT | ChangeFlags::PAINT;
4584            }
4585            ChangeFlags::NONE
4586        }
4587    }
4588
4589    #[test]
4590    fn generic_unmount_orphan_releases_the_whole_session() {
4591        // (b) The child-list-diff shape: no publish, no event — the focused
4592        // widget simply stops existing. Nothing self-corrects this on an idle
4593        // screen, which is why the reconciler's mark is drained here.
4594        let _ = crate::event::take_focus_orphaned();
4595        let orphan = Rc::new(Cell::new(false));
4596        let mut build = {
4597            let orphan = orphan.clone();
4598            move |_state: &mut ClickState| UnmountView {
4599                orphan: orphan.clone(),
4600            }
4601        };
4602        let mut root: RenderRoot<ClickState, UnmountView> = RenderRoot::new();
4603        let mut state = ClickState::default();
4604        root.rebuild(&mut build, &mut state);
4605        root.layout(Size::new(100.0, 100.0));
4606
4607        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4608        assert!(root.is_focus_active());
4609        assert!(root.ime_state().is_some());
4610        let focused = root.focus_ime_generation();
4611
4612        // The unmount rebuild.
4613        orphan.set(true);
4614        root.rebuild(&mut build, &mut state);
4615        assert!(
4616            !root.is_focus_active(),
4617            "the root's focus mirror does not outlive the widget it mirrors"
4618        );
4619        assert!(root.ime_state().is_none());
4620        assert_eq!(
4621            root.focus_ime_generation(),
4622            focused.wrapping_add(1),
4623            "one orphaned focus path is exactly one edge"
4624        );
4625
4626        // The mark was drained, so an ordinary rebuild afterwards is inert...
4627        let released = root.focus_ime_generation();
4628        orphan.set(false);
4629        root.rebuild(&mut build, &mut state);
4630        assert_eq!(root.focus_ime_generation(), released);
4631
4632        // ...and re-marking against an already-released root fires no edge
4633        // either (a stale `focused` flag torn down later must not spin it).
4634        orphan.set(true);
4635        root.rebuild(&mut build, &mut state);
4636        assert_eq!(root.focus_ime_generation(), released);
4637        assert!(!root.is_focus_active());
4638    }
4639
4640    // --- The focus/IME EDGE generation ---------------------------------------
4641    //
4642    // `focus_ime_generation` is the shell-facing edge behind the mobile frame
4643    // gate's `FrameInputs::focus_or_ime_changed`: a shell caches the value and
4644    // runs a frame when it moves. Two properties make that safe, and both are
4645    // pinned below: EVERY real transition moves it (or a focus change strands
4646    // unpainted), and NO same-value write moves it (or a focused screen forces
4647    // a frame every vsync — the level-input behavior this replaced, measured at
4648    // 62–120 fps on a static focused screen).
4649
4650    /// A widget that focuses on `Down`, releases focus on any `Key`, and — the
4651    /// point of the fixture — re-publishes an IME surface from its **paint**
4652    /// pass on every frame, reading the text from a shared cell so a test can
4653    /// make a republish genuinely change (or genuinely not).
4654    struct PaintImeWidget {
4655        published: Rc<RefCell<String>>,
4656    }
4657    impl PaintImeWidget {
4658        fn surface(text: &str) -> ImeState {
4659            ImeState {
4660                active: true,
4661                editing: EditingState {
4662                    text: text.to_string(),
4663                    selection_base: 0,
4664                    selection_extent: 0,
4665                    composing_base: -1,
4666                    composing_extent: -1,
4667                },
4668                caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
4669                content_type: Default::default(),
4670                suppress_soft_keyboard: false,
4671            }
4672        }
4673    }
4674    impl crate::widget::Widget for PaintImeWidget {
4675        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4676            bc.constrain(Size::new(100.0, 100.0))
4677        }
4678        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4679            ctx.publish_ime_state(Self::surface(&self.published.borrow()));
4680        }
4681        fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4682            match event {
4683                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
4684                    ctx.request_focus();
4685                    EventResult::Handled
4686                }
4687                InputEvent::Key(_) => {
4688                    ctx.release_focus();
4689                    EventResult::Handled
4690                }
4691                _ => EventResult::Ignored,
4692            }
4693        }
4694    }
4695
4696    struct PaintImeView {
4697        published: Rc<RefCell<String>>,
4698    }
4699    impl View<ClickState> for PaintImeView {
4700        type Element = PaintImeWidget;
4701        fn build(&self, _ctx: &mut BuildCtx<'_>) -> PaintImeWidget {
4702            PaintImeWidget {
4703                published: self.published.clone(),
4704            }
4705        }
4706        fn rebuild(
4707            &self,
4708            _prev: &Self,
4709            _element: &mut PaintImeWidget,
4710            _ctx: &mut BuildCtx<'_>,
4711        ) -> ChangeFlags {
4712            ChangeFlags::NONE
4713        }
4714    }
4715
4716    fn key_event() -> InputEvent {
4717        InputEvent::Key(crate::event::KeyEvent {
4718            key: crate::event::Key::Named(crate::event::NamedKey::Enter),
4719            modifiers: crate::event::Modifiers::default(),
4720            repeat: false,
4721        })
4722    }
4723
4724    #[test]
4725    fn focus_ime_generation_moves_on_every_pointer_transition_only() {
4726        let mut root: RenderRoot<ClickState, ImeView> = RenderRoot::new();
4727        let mut state = ClickState::default();
4728        root.rebuild(&mut ime_logic, &mut state);
4729        root.layout(Size::new(100.0, 100.0));
4730
4731        // Idle: a rebuild/layout touches neither focus nor the IME surface.
4732        let idle = root.focus_ime_generation();
4733        root.rebuild(&mut ime_logic, &mut state);
4734        assert_eq!(
4735            root.focus_ime_generation(),
4736            idle,
4737            "a rebuild is not an edge"
4738        );
4739
4740        // Focus gained + IME surface published (one transition for a shell,
4741        // however many field writes it took).
4742        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4743        let focused = root.focus_ime_generation();
4744        assert_ne!(
4745            focused, idle,
4746            "focus + IME publish must move the generation"
4747        );
4748
4749        // The SAME tap again, on the already-focused widget publishing the
4750        // identical surface: no state moved, so no edge. This is the case that
4751        // decides whether a live text field forces a frame per vsync.
4752        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4753        assert_eq!(
4754            root.focus_ime_generation(),
4755            focused,
4756            "a same-value focus/IME write must not spin the edge"
4757        );
4758
4759        // Blur: focus cleared and the surface dropped — a real transition.
4760        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
4761        let blurred = root.focus_ime_generation();
4762        assert_ne!(blurred, focused, "a blur must move the generation");
4763
4764        // Blur while already blurred (a tap on inert chrome — the commonest
4765        // event there is) writes `false`/`None` back over `false`/`None`.
4766        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 20.0));
4767        assert_eq!(
4768            root.focus_ime_generation(),
4769            blurred,
4770            "blurring an already-blurred root must not move the generation"
4771        );
4772    }
4773
4774    #[test]
4775    fn focus_ime_generation_ignores_an_unchanged_paint_republish() {
4776        // The paint pass re-publishes the focused widget's IME surface on EVERY
4777        // frame (that is how a rebuild-applied controlled change refreshes the
4778        // shell-facing state). If that unconditional write moved the
4779        // generation, the frame gate's edge would fire every single frame for
4780        // the whole life of a focus session — exactly the per-vsync forcing the
4781        // edge exists to remove.
4782        let published = Rc::new(RefCell::new("abc".to_string()));
4783        let mut build = {
4784            let published = published.clone();
4785            move |_state: &mut ClickState| PaintImeView {
4786                published: published.clone(),
4787            }
4788        };
4789        let mut root: RenderRoot<ClickState, PaintImeView> = RenderRoot::new();
4790        let mut state = ClickState::default();
4791        root.rebuild(&mut build, &mut state);
4792        root.layout(Size::new(100.0, 100.0));
4793
4794        // Focus the field, then let it paint: the first paint publishes.
4795        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4796        let mut scene = RecordingScene::default();
4797        root.paint(&mut scene, FrameTime::ZERO);
4798        let steady = root.focus_ime_generation();
4799        assert_eq!(
4800            root.ime_state(),
4801            Some(PaintImeWidget::surface("abc")),
4802            "the paint pass published the focused widget's surface"
4803        );
4804
4805        // 120 further frames of the same focused, unchanged field: the caret
4806        // blinks, nothing else moves. Not one edge.
4807        for _ in 0..120 {
4808            root.paint(&mut scene, FrameTime::ZERO);
4809        }
4810        assert_eq!(
4811            root.focus_ime_generation(),
4812            steady,
4813            "an unchanged paint republish must never move the generation"
4814        );
4815
4816        // A real change (the app applied a controlled edit) publishes a
4817        // different surface: exactly one edge, then quiet again.
4818        *published.borrow_mut() = "abcd".to_string();
4819        root.paint(&mut scene, FrameTime::ZERO);
4820        let edited = root.focus_ime_generation();
4821        assert_ne!(edited, steady, "a changed republish IS an edge");
4822        for _ in 0..10 {
4823            root.paint(&mut scene, FrameTime::ZERO);
4824        }
4825        assert_eq!(
4826            root.focus_ime_generation(),
4827            edited,
4828            "the session goes quiet again at the new value"
4829        );
4830    }
4831
4832    #[test]
4833    fn focus_ime_generation_moves_on_a_focus_release_only_once() {
4834        // The Key/Ime/Scroll arm of the root focus path: a dispatch that
4835        // RELEASES focus clears both the flag and the published surface.
4836        let published = Rc::new(RefCell::new("abc".to_string()));
4837        let mut build = {
4838            let published = published.clone();
4839            move |_state: &mut ClickState| PaintImeView {
4840                published: published.clone(),
4841            }
4842        };
4843        let mut root: RenderRoot<ClickState, PaintImeView> = RenderRoot::new();
4844        let mut state = ClickState::default();
4845        root.rebuild(&mut build, &mut state);
4846        root.layout(Size::new(100.0, 100.0));
4847        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4848        let mut scene = RecordingScene::default();
4849        root.paint(&mut scene, FrameTime::ZERO);
4850        let focused = root.focus_ime_generation();
4851        assert!(root.is_focus_active());
4852
4853        // A key that releases focus: one edge.
4854        root.event(&mut state, &key_event());
4855        let released = root.focus_ime_generation();
4856        assert!(!root.is_focus_active());
4857        assert!(root.ime_state().is_none());
4858        assert_ne!(
4859            released, focused,
4860            "a focus release must move the generation"
4861        );
4862
4863        // A second release against an already-released root: no edge. (The
4864        // paint pass republishes nothing now — the paint-take arm only accepts
4865        // a publish while focus is active.)
4866        root.event(&mut state, &key_event());
4867        root.paint(&mut scene, FrameTime::ZERO);
4868        assert_eq!(
4869            root.focus_ime_generation(),
4870            released,
4871            "releasing an already-released focus must not move the generation"
4872        );
4873    }
4874
4875    // --- The focus session's IDENTITY, beside the edge generation ------------
4876    //
4877    // `focus_epoch` answers a question `focus_ime_generation` cannot: "is this
4878    // still the session that asked?". A caller that binds a slow, asynchronous
4879    // answer to the field that asked for it needs an identity, and a counter
4880    // over the published surface's *value* is not one — two fields publish
4881    // equal surfaces, and a field is free to take focus and publish nothing at
4882    // all.
4883    //
4884    // Each test below asserts what BOTH counters did at the same moment. The
4885    // `focus_ime_generation` assertions are the point rather than decoration:
4886    // they are what states, in a form the compiler checks, that the edge
4887    // generation stands still exactly where the identity moves.
4888
4889    /// The text both fields publish from a press, so neither can be told from
4890    /// the other by the published value alone.
4891    const SHARED_FIELD_TEXT: &str = "shared";
4892
4893    /// One field of the two-field fixture.
4894    ///
4895    /// Takes the focus session on any press inside itself; publishes an IME
4896    /// surface on that press only when built to; and treats an `Ime` event as an
4897    /// edit — the text changes and the surface is republished, but nothing
4898    /// re-claims a session the field already holds.
4899    ///
4900    /// `publishes: false` is not a contrivance: the baseline text input claims
4901    /// focus and republishes nothing for a press that lands inside text it
4902    /// already had selected (such a press moves no caret and collapses no
4903    /// selection), and any app-authored focusable that publishes no IME surface
4904    /// of its own behaves the same way.
4905    ///
4906    /// A press publishes the *shared* surface, so the two fields are
4907    /// indistinguishable to anything reading the published value — that is the
4908    /// case under test. An edit publishes the field's own `name` instead, which
4909    /// is how a test proves which field a focus-routed event actually reached.
4910    struct SessionField {
4911        name: &'static str,
4912        publishes: bool,
4913    }
4914
4915    impl SessionField {
4916        /// The surface a field publishes. Deliberately carries nothing that
4917        /// tells one field from another: `ImeState` is
4918        /// `{active, editing, caret, content_type}` and names no widget, so two
4919        /// fields holding the same text and caret publish equal values — which
4920        /// is the ordinary shape of two empty fields, or two overlapping ones
4921        /// mid-transition.
4922        fn surface(text: &str) -> ImeState {
4923            ImeState {
4924                active: true,
4925                editing: EditingState {
4926                    text: text.to_string(),
4927                    selection_base: 0,
4928                    selection_extent: 0,
4929                    composing_base: -1,
4930                    composing_extent: -1,
4931                },
4932                caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
4933                content_type: Default::default(),
4934                suppress_soft_keyboard: false,
4935            }
4936        }
4937    }
4938
4939    impl crate::widget::Widget for SessionField {
4940        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4941            bc.constrain(Size::new(100.0, 20.0))
4942        }
4943        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4944        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4945            match event {
4946                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
4947                    ctx.request_focus();
4948                    if self.publishes {
4949                        ctx.publish_ime_state(Self::surface(SHARED_FIELD_TEXT));
4950                    }
4951                    EventResult::Handled
4952                }
4953                InputEvent::Ime(_) => {
4954                    ctx.publish_ime_state(Self::surface(self.name));
4955                    EventResult::Handled
4956                }
4957                _ => EventResult::Ignored,
4958            }
4959        }
4960    }
4961
4962    /// The fixture's root: two stacked fields, with a pointer event hit-tested
4963    /// to the one under it and a focus-routed event forwarded down the recorded
4964    /// focus path without a hit test — the routing every real container does.
4965    struct TwoFields {
4966        top: crate::widget::ChildPod,
4967        bottom: crate::widget::ChildPod,
4968    }
4969
4970    impl crate::widget::Widget for TwoFields {
4971        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4972            self.top.layout_child(ctx, bc);
4973            self.top.set_origin(Point::new(0.0, 0.0));
4974            self.bottom.layout_child(ctx, bc);
4975            self.bottom.set_origin(Point::new(0.0, 50.0));
4976            bc.max()
4977        }
4978        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4979            self.top.paint_child(ctx, scene);
4980            self.bottom.paint_child(ctx, scene);
4981        }
4982        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4983            if matches!(event, InputEvent::Pointer(_)) {
4984                let pos = event.position();
4985                if self.top.contains(pos) {
4986                    return self.top.event_child(ctx, event);
4987                }
4988                if self.bottom.contains(pos) {
4989                    return self.bottom.event_child(ctx, event);
4990                }
4991                return EventResult::Ignored;
4992            }
4993            if self.top.holds_live_focus() {
4994                return self.top.event_child(ctx, event);
4995            }
4996            if self.bottom.holds_live_focus() {
4997                return self.bottom.event_child(ctx, event);
4998            }
4999            EventResult::Ignored
5000        }
5001        fn semantics(&self, ctx: &mut SemanticsCtx) {
5002            self.top.semantics_child(ctx);
5003            self.bottom.semantics_child(ctx);
5004        }
5005    }
5006
5007    struct TwoFieldsView {
5008        bottom_publishes: bool,
5009    }
5010
5011    impl View<ClickState> for TwoFieldsView {
5012        type Element = TwoFields;
5013        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TwoFields {
5014            TwoFields {
5015                top: crate::widget::ChildPod::new(Box::new(SessionField {
5016                    name: "top",
5017                    publishes: true,
5018                })),
5019                bottom: crate::widget::ChildPod::new(Box::new(SessionField {
5020                    name: "bottom",
5021                    publishes: self.bottom_publishes,
5022                })),
5023            }
5024        }
5025        fn rebuild(
5026            &self,
5027            _prev: &Self,
5028            _element: &mut TwoFields,
5029            _ctx: &mut BuildCtx<'_>,
5030        ) -> ChangeFlags {
5031            ChangeFlags::NONE
5032        }
5033    }
5034
5035    /// An edit pushed down the focus path by the platform IME: it claims no
5036    /// focus, so only the field already holding the session sees it — which is
5037    /// what makes the surface it republishes name that field.
5038    fn edit_event() -> InputEvent {
5039        InputEvent::Ime(crate::event::ImeEvent::ApplyEditingState(
5040            SessionField::surface(SHARED_FIELD_TEXT).editing,
5041        ))
5042    }
5043
5044    /// Mount the two-field fixture and press the top field, returning the root
5045    /// with a live session on it.
5046    fn two_fields_focused(
5047        bottom_publishes: bool,
5048    ) -> (RenderRoot<ClickState, TwoFieldsView>, ClickState) {
5049        let mut root: RenderRoot<ClickState, TwoFieldsView> = RenderRoot::new();
5050        let mut state = ClickState::default();
5051        root.rebuild(&mut |_| TwoFieldsView { bottom_publishes }, &mut state);
5052        root.layout(Size::new(200.0, 200.0));
5053        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
5054        assert!(root.is_focus_active(), "the top field opened a session");
5055        (root, state)
5056    }
5057
5058    #[test]
5059    fn focus_epoch_moves_when_focus_crosses_two_fields_publishing_alike() {
5060        let (mut root, mut state) = two_fields_focused(true);
5061        let first_session = root.focus_epoch();
5062        let steady_edge = root.focus_ime_generation();
5063        assert_eq!(
5064            root.ime_state(),
5065            Some(SessionField::surface(SHARED_FIELD_TEXT))
5066        );
5067
5068        // Press the bottom field. Focus really does move — and nothing
5069        // observable about the published surface moves with it: claiming while
5070        // some field is already focused writes `true` over `true`, and the
5071        // surface the second field publishes compares equal to the first's.
5072        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
5073        assert!(root.is_focus_active());
5074        assert_eq!(
5075            root.focus_ime_generation(),
5076            steady_edge,
5077            "the edge generation is blind to this move, which is why a caller \
5078             asking 'is this the same session?' must not be built on it"
5079        );
5080        assert_ne!(
5081            root.focus_epoch(),
5082            first_session,
5083            "the session identity must move when focus crosses to another field"
5084        );
5085
5086        // Not merely "some counter moved": the focus PATH is the bottom
5087        // field's now, which a focus-routed event proves by reaching it.
5088        root.event(&mut state, &edit_event());
5089        assert_eq!(
5090            root.ime_state(),
5091            Some(SessionField::surface("bottom")),
5092            "the second field is the one holding the session"
5093        );
5094    }
5095
5096    #[test]
5097    fn focus_epoch_moves_when_the_field_taking_focus_publishes_nothing() {
5098        let (mut root, mut state) = two_fields_focused(false);
5099        let first_session = root.focus_epoch();
5100        let steady_edge = root.focus_ime_generation();
5101
5102        // Press the bottom field, which takes the session and publishes no
5103        // surface of its own. A publish-nothing dispatch leaves the standing
5104        // surface standing, so what the shell still sees describes the field
5105        // the user just left — for as long as this session lasts, not merely
5106        // until the next frame.
5107        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
5108        assert!(root.is_focus_active());
5109        assert_eq!(
5110            root.ime_state(),
5111            Some(SessionField::surface(SHARED_FIELD_TEXT)),
5112            "the field that lost focus is still the one the surface describes"
5113        );
5114        assert_eq!(
5115            root.focus_ime_generation(),
5116            steady_edge,
5117            "no published value moved, so the edge generation cannot have"
5118        );
5119        assert_ne!(
5120            root.focus_epoch(),
5121            first_session,
5122            "an honoured claim moves the session identity whether or not the \
5123             claimant publishes anything"
5124        );
5125
5126        // And again, the move is a real one: the focus path now ends at the
5127        // field that published nothing.
5128        root.event(&mut state, &edit_event());
5129        assert_eq!(
5130            root.ime_state(),
5131            Some(SessionField::surface("bottom")),
5132            "the second field is the one holding the session"
5133        );
5134    }
5135
5136    #[test]
5137    fn focus_epoch_ignores_an_edit_inside_one_session() {
5138        // The converse direction, and the reason the two counters are kept
5139        // apart rather than merged: a caller holding an identity can let a
5140        // harmless edit ride, where a caller comparing the published value has
5141        // to treat every keystroke as a reason to give up.
5142        let (mut root, mut state) = two_fields_focused(true);
5143        let session = root.focus_epoch();
5144        let before_edit = root.focus_ime_generation();
5145
5146        root.event(&mut state, &edit_event());
5147        assert_eq!(
5148            root.ime_state(),
5149            Some(SessionField::surface("top")),
5150            "the focused field applied the edit and republished"
5151        );
5152        assert_ne!(
5153            root.focus_ime_generation(),
5154            before_edit,
5155            "a changed surface IS an edge"
5156        );
5157        assert_eq!(
5158            root.focus_epoch(),
5159            session,
5160            "an edit does not end or restart the session it lands in"
5161        );
5162
5163        // A release ends the identity too, so a stale one can never come back
5164        // round to matching by standing still.
5165        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 90.0));
5166        assert!(!root.is_focus_active());
5167        assert_ne!(
5168            root.focus_epoch(),
5169            session,
5170            "a release retires the session's identity"
5171        );
5172    }
5173
5174    // --- Semantics: stable ids + accessibility action routing ---
5175    //
5176    // Fixtures: an accessibility-visible button (fire-on-up-inside, contributes a
5177    // `Role::Button` node) and a checkbox variant (`Role::CheckBox`), plus a
5178    // labelled leaf used to prove id stability survives a pod relocation.
5179
5180    use accesskit::{Action, NodeId, Role};
5181
5182    /// A fire-on-up-inside button that also contributes a semantics node — the
5183    /// end-to-end target for `perform_accessibility_action(Click)`.
5184    struct A11yButtonWidget;
5185    impl crate::widget::Widget for A11yButtonWidget {
5186        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5187            bc.constrain(Size::new(40.0, 20.0))
5188        }
5189        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5190        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5191            if let InputEvent::Pointer(p) = event {
5192                match p.phase {
5193                    PointerPhase::Down => {
5194                        ctx.capture_pointer();
5195                        return EventResult::Handled;
5196                    }
5197                    PointerPhase::Up => {
5198                        let size = ctx.size();
5199                        let inside = p.position.x >= 0.0
5200                            && p.position.y >= 0.0
5201                            && p.position.x <= size.width
5202                            && p.position.y <= size.height;
5203                        if inside {
5204                            ctx.state_mut::<ClickState>().clicks += 1;
5205                            ctx.request_redraw();
5206                        }
5207                        return EventResult::Handled;
5208                    }
5209                    _ => {}
5210                }
5211            }
5212            EventResult::Ignored
5213        }
5214        fn semantics(&self, ctx: &mut SemanticsCtx) {
5215            ctx.push_node(Role::Button, |n| n.set_label("Go"));
5216        }
5217    }
5218
5219    struct A11yButtonView;
5220    impl View<ClickState> for A11yButtonView {
5221        type Element = A11yButtonWidget;
5222        fn build(&self, _ctx: &mut BuildCtx<'_>) -> A11yButtonWidget {
5223            A11yButtonWidget
5224        }
5225        fn rebuild(
5226            &self,
5227            _prev: &Self,
5228            _el: &mut A11yButtonWidget,
5229            _ctx: &mut BuildCtx<'_>,
5230        ) -> ChangeFlags {
5231            ChangeFlags::NONE
5232        }
5233    }
5234
5235    fn a11y_button_logic(_state: &mut ClickState) -> A11yButtonView {
5236        A11yButtonView
5237    }
5238
5239    fn button_node_id(update: &SemanticsUpdate, role: Role) -> NodeId {
5240        update
5241            .nodes
5242            .iter()
5243            .find(|(_, n)| n.role() == role)
5244            .map(|(id, _)| *id)
5245            .unwrap_or_else(|| panic!("a {role:?} node is present"))
5246    }
5247
5248    #[test]
5249    fn semantics_ids_are_stable_across_frames() {
5250        let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5251        let mut state = ClickState::default();
5252        root.rebuild(&mut a11y_button_logic, &mut state);
5253        root.layout(Size::new(200.0, 200.0));
5254
5255        let first = button_node_id(&root.semantics(), Role::Button);
5256        // Re-run rebuild+layout+semantics several times: the button keeps its id.
5257        for _ in 0..3 {
5258            root.rebuild(&mut a11y_button_logic, &mut state);
5259            root.layout(Size::new(200.0, 200.0));
5260            assert_eq!(
5261                button_node_id(&root.semantics(), Role::Button),
5262                first,
5263                "the same widget must keep its NodeId across frames"
5264            );
5265        }
5266        // The window root is the reserved constant id.
5267        assert_eq!(root.semantics().root, ROOT_NODE_ID);
5268    }
5269
5270    #[test]
5271    fn semantics_ids_survive_a_pod_relocation() {
5272        // A keyed reorder relocates the whole `ChildPod` (preserving its cached
5273        // semantics id); simulate that here by swapping two pods in place and
5274        // asserting each labelled node keeps its id despite changing position.
5275        struct LabeledLeaf {
5276            label: &'static str,
5277        }
5278        impl crate::widget::Widget for LabeledLeaf {
5279            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5280                bc.constrain(Size::new(10.0, 10.0))
5281            }
5282            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5283            fn semantics(&self, ctx: &mut SemanticsCtx) {
5284                let label = self.label;
5285                ctx.push_node(Role::Label, |n| n.set_label(label));
5286            }
5287        }
5288
5289        let mut pods = vec![
5290            crate::widget::ChildPod::new(Box::new(LabeledLeaf { label: "A" })),
5291            crate::widget::ChildPod::new(Box::new(LabeledLeaf { label: "B" })),
5292        ];
5293
5294        // Collect (label -> id) for a given pod order. `SemanticsCtx` is
5295        // crate-private, so this drives the pods directly — the same allocation
5296        // path `RenderRoot::semantics` uses.
5297        let collect = |pods: &[crate::widget::ChildPod]| {
5298            let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), 2);
5299            for pod in pods {
5300                pod.semantics_child(&mut ctx);
5301            }
5302            let update = ctx.finish(ROOT_NODE_ID);
5303            update
5304                .nodes
5305                .iter()
5306                .filter(|(id, _)| *id != ROOT_NODE_ID)
5307                .map(|(id, n)| (n.label().unwrap().to_string(), *id))
5308                .collect::<Vec<_>>()
5309        };
5310
5311        let before = collect(&pods);
5312        // Relocate: swap the pods (the pods themselves, with their cached ids,
5313        // move — mirroring the keyed reconciler's `take`-and-reorder).
5314        pods.swap(0, 1);
5315        let after = collect(&pods);
5316
5317        for (label, id) in &before {
5318            let relocated = after.iter().find(|(l, _)| l == label).unwrap().1;
5319            assert_eq!(
5320                *id, relocated,
5321                "widget {label:?} must keep its NodeId across the reorder"
5322            );
5323        }
5324        // And the reorder actually changed positions (A now second).
5325        assert_eq!(after[0].0, "B");
5326        assert_eq!(after[1].0, "A");
5327    }
5328
5329    #[test]
5330    fn semantics_full_update_assembles_window_and_child() {
5331        let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5332        let mut state = ClickState::default();
5333        root.rebuild(&mut a11y_button_logic, &mut state);
5334        root.layout(Size::new(200.0, 200.0));
5335
5336        let update = root.semantics();
5337        // Window root + the button.
5338        assert_eq!(update.nodes.len(), 2);
5339        assert_eq!(update.root, ROOT_NODE_ID);
5340        let root_node = update
5341            .nodes
5342            .iter()
5343            .find(|(id, _)| *id == update.root)
5344            .unwrap();
5345        assert_eq!(root_node.1.role(), Role::Window);
5346        let button = button_node_id(&update, Role::Button);
5347        assert_eq!(
5348            root_node.1.children(),
5349            &[button],
5350            "the button attaches under the window root"
5351        );
5352        // Nothing focused → the adapter-facing focus id defaults to the root.
5353        assert!(update.focus.is_none());
5354        assert_eq!(update.focus_id(), ROOT_NODE_ID);
5355    }
5356
5357    #[test]
5358    fn perform_click_action_activates_a_button() {
5359        let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5360        let mut state = ClickState::default();
5361        root.rebuild(&mut a11y_button_logic, &mut state);
5362        root.layout(Size::new(200.0, 200.0));
5363
5364        let button = button_node_id(&root.semantics(), Role::Button);
5365        let outcome = root.perform_accessibility_action(&mut state, button, Action::Click);
5366        assert!(outcome.handled, "the synthesized Down+Up was handled");
5367        assert!(outcome.needs_redraw);
5368        assert_eq!(state.clicks, 1, "Click synthesized a real up-inside tap");
5369
5370        // An unknown node id is a benign no-op.
5371        let outcome = root.perform_accessibility_action(&mut state, NodeId(999_999), Action::Click);
5372        assert_eq!(outcome, EventOutcome::default());
5373        assert_eq!(state.clicks, 1);
5374
5375        // An unmodelled action is ignored.
5376        let outcome = root.perform_accessibility_action(&mut state, button, Action::ScrollDown);
5377        assert_eq!(outcome, EventOutcome::default());
5378        assert_eq!(state.clicks, 1);
5379    }
5380
5381    #[test]
5382    fn perform_click_action_toggles_a_checkbox() {
5383        // A checkbox-shaped widget (`Role::CheckBox`) reached through the same
5384        // synthetic-pointer path — proving Click drives any fire-on-up-inside
5385        // control, not just buttons.
5386        struct CheckboxWidget;
5387        impl crate::widget::Widget for CheckboxWidget {
5388            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5389                bc.constrain(Size::new(24.0, 24.0))
5390            }
5391            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5392            fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5393                if let InputEvent::Pointer(p) = event {
5394                    match p.phase {
5395                        PointerPhase::Down => {
5396                            ctx.capture_pointer();
5397                            return EventResult::Handled;
5398                        }
5399                        PointerPhase::Up => {
5400                            let size = ctx.size();
5401                            if p.position.x >= 0.0
5402                                && p.position.y >= 0.0
5403                                && p.position.x <= size.width
5404                                && p.position.y <= size.height
5405                            {
5406                                ctx.state_mut::<ClickState>().clicks += 1;
5407                            }
5408                            return EventResult::Handled;
5409                        }
5410                        _ => {}
5411                    }
5412                }
5413                EventResult::Ignored
5414            }
5415            fn semantics(&self, ctx: &mut SemanticsCtx) {
5416                ctx.push_node(Role::CheckBox, |n| n.set_label("Agree"));
5417            }
5418        }
5419        struct CheckboxView;
5420        impl View<ClickState> for CheckboxView {
5421            type Element = CheckboxWidget;
5422            fn build(&self, _ctx: &mut BuildCtx<'_>) -> CheckboxWidget {
5423                CheckboxWidget
5424            }
5425            fn rebuild(
5426                &self,
5427                _p: &Self,
5428                _e: &mut CheckboxWidget,
5429                _c: &mut BuildCtx<'_>,
5430            ) -> ChangeFlags {
5431                ChangeFlags::NONE
5432            }
5433        }
5434
5435        let mut root: RenderRoot<ClickState, CheckboxView> = RenderRoot::new();
5436        let mut state = ClickState::default();
5437        root.rebuild(&mut |_| CheckboxView, &mut state);
5438        root.layout(Size::new(200.0, 200.0));
5439
5440        let cb = button_node_id(&root.semantics(), Role::CheckBox);
5441        root.perform_accessibility_action(&mut state, cb, Action::Click);
5442        assert_eq!(state.clicks, 1, "Click toggled the checkbox once");
5443    }
5444
5445    #[test]
5446    fn perform_focus_action_claims_focus_and_clears_capture() {
5447        // Two focus-claiming, fire-on-up-inside buttons in a container. A11y
5448        // `Focus` on B must claim focus for B *and* release the capture the
5449        // synthesized `Down` opened — the CRITICAL leak this regresses: without
5450        // the trailing `Cancel`, B stayed captured and swallowed every later
5451        // pointer event, so a tap on A never reached A.
5452
5453        #[derive(Default)]
5454        struct FocusState {
5455            a_press: u32,
5456            b_press: u32,
5457            b_move: u32,
5458        }
5459
5460        #[derive(Clone, Copy)]
5461        enum Btn {
5462            A,
5463            B,
5464        }
5465
5466        /// A button that opts into both recorded paths (capture + focus) on
5467        /// `Down` and fires its press only on `Up`-inside — never on `Cancel`.
5468        struct FocusButton {
5469            id: Btn,
5470        }
5471        impl crate::widget::Widget for FocusButton {
5472            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5473                bc.constrain(Size::new(40.0, 20.0))
5474            }
5475            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5476            fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5477                let InputEvent::Pointer(p) = event else {
5478                    return EventResult::Ignored;
5479                };
5480                match p.phase {
5481                    PointerPhase::Down => {
5482                        ctx.capture_pointer();
5483                        ctx.request_focus();
5484                        EventResult::Handled
5485                    }
5486                    PointerPhase::Move => {
5487                        if let Btn::B = self.id {
5488                            ctx.state_mut::<FocusState>().b_move += 1;
5489                        }
5490                        EventResult::Handled
5491                    }
5492                    PointerPhase::Up => {
5493                        let size = ctx.size();
5494                        let inside = p.position.x >= 0.0
5495                            && p.position.y >= 0.0
5496                            && p.position.x <= size.width
5497                            && p.position.y <= size.height;
5498                        if inside {
5499                            match self.id {
5500                                Btn::A => ctx.state_mut::<FocusState>().a_press += 1,
5501                                Btn::B => ctx.state_mut::<FocusState>().b_press += 1,
5502                            }
5503                        }
5504                        EventResult::Handled
5505                    }
5506                    // A `Cancel` clears without firing on_press and never touches
5507                    // state — the contract the Focus action's trailing Cancel rides.
5508                    PointerPhase::Cancel => EventResult::Handled,
5509                }
5510            }
5511            fn semantics(&self, ctx: &mut SemanticsCtx) {
5512                let label = match self.id {
5513                    Btn::A => "A",
5514                    Btn::B => "B",
5515                };
5516                ctx.push_node(Role::Button, |n| n.set_label(label));
5517            }
5518        }
5519
5520        /// A minimal two-child container mirroring `frust-widgets`'
5521        /// `route_event`: a captured gesture goes straight to the active child
5522        /// (auto-released on `Up`/`Cancel`), otherwise the event is hit-tested to
5523        /// the child under it.
5524        struct TwoButtons {
5525            a: crate::widget::ChildPod,
5526            b: crate::widget::ChildPod,
5527        }
5528        impl crate::widget::Widget for TwoButtons {
5529            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5530                self.a.layout_child(ctx, bc);
5531                self.a.set_origin(Point::new(0.0, 0.0));
5532                self.b.layout_child(ctx, bc);
5533                self.b.set_origin(Point::new(0.0, 30.0));
5534                bc.max()
5535            }
5536            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
5537                self.a.paint_child(ctx, scene);
5538                self.b.paint_child(ctx, scene);
5539            }
5540            fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5541                let releases = matches!(
5542                    event,
5543                    InputEvent::Pointer(p)
5544                        if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
5545                );
5546                // Capture fast-path: a recorded active child receives every event
5547                // until it releases on Up/Cancel, bypassing the hit test entirely.
5548                if self.a.is_active() {
5549                    let r = self.a.event_child(ctx, event);
5550                    if releases {
5551                        self.a.set_active(false);
5552                    }
5553                    return r;
5554                }
5555                if self.b.is_active() {
5556                    let r = self.b.event_child(ctx, event);
5557                    if releases {
5558                        self.b.set_active(false);
5559                    }
5560                    return r;
5561                }
5562                // Fresh event: route to the child under the point.
5563                let pos = event.position();
5564                if self.a.contains(pos) {
5565                    return self.a.event_child(ctx, event);
5566                }
5567                if self.b.contains(pos) {
5568                    return self.b.event_child(ctx, event);
5569                }
5570                EventResult::Ignored
5571            }
5572            fn semantics(&self, ctx: &mut SemanticsCtx) {
5573                self.a.semantics_child(ctx);
5574                self.b.semantics_child(ctx);
5575            }
5576        }
5577
5578        struct TwoButtonsView;
5579        impl View<FocusState> for TwoButtonsView {
5580            type Element = TwoButtons;
5581            fn build(&self, _ctx: &mut BuildCtx<'_>) -> TwoButtons {
5582                TwoButtons {
5583                    a: crate::widget::ChildPod::new(Box::new(FocusButton { id: Btn::A })),
5584                    b: crate::widget::ChildPod::new(Box::new(FocusButton { id: Btn::B })),
5585                }
5586            }
5587            fn rebuild(
5588                &self,
5589                _p: &Self,
5590                _e: &mut TwoButtons,
5591                _c: &mut BuildCtx<'_>,
5592            ) -> ChangeFlags {
5593                ChangeFlags::NONE
5594            }
5595        }
5596
5597        let mut root: RenderRoot<FocusState, TwoButtonsView> = RenderRoot::new();
5598        let mut state = FocusState::default();
5599        root.rebuild(&mut |_| TwoButtonsView, &mut state);
5600        root.layout(Size::new(200.0, 200.0));
5601
5602        // B's semantics node (label "B") is the a11y Focus target.
5603        let b_id = root
5604            .semantics()
5605            .nodes
5606            .iter()
5607            .find(|(_, n)| n.label().is_some_and(|l| l == "B"))
5608            .map(|(id, _)| *id)
5609            .expect("button B contributes a semantics node");
5610
5611        // A11y `Focus` on B: claims the focus session, fires no on_press, and —
5612        // crucially — leaves nothing captured (the Down+Cancel shape).
5613        root.perform_accessibility_action(&mut state, b_id, Action::Focus);
5614        assert!(root.is_focus_active(), "Focus opened the focus session");
5615        assert!(
5616            !root.is_pointer_captured(),
5617            "the trailing Cancel released the capture the Focus Down opened"
5618        );
5619        assert_eq!(
5620            state.b_press, 0,
5621            "Focus (Down+Cancel) must not fire B's on_press"
5622        );
5623
5624        // B is not stuck-captured: a `Move` outside both buttons is ignored. Were
5625        // B still captured, the capture fast-path would route this to B regardless
5626        // of position (b_move would tick).
5627        let outside = InputEvent::Pointer(PointerEvent {
5628            phase: PointerPhase::Move,
5629            position: Point::new(100.0, 100.0),
5630            button: PointerButton::Primary,
5631        });
5632        root.event(&mut state, &outside);
5633        assert_eq!(state.b_move, 0, "no leaked capture: B saw no stray Move");
5634
5635        // A real Down+Up on A activates A exactly once and never reaches B.
5636        let at_a = |phase| {
5637            InputEvent::Pointer(PointerEvent {
5638                phase,
5639                position: Point::new(20.0, 10.0),
5640                button: PointerButton::Primary,
5641            })
5642        };
5643        root.event(&mut state, &at_a(PointerPhase::Down));
5644        root.event(&mut state, &at_a(PointerPhase::Up));
5645        assert_eq!(state.a_press, 1, "A fired once from its own tap");
5646        assert_eq!(
5647            state.b_press, 0,
5648            "B never fired — its capture never leaked onto A's tap"
5649        );
5650    }
5651
5652    #[test]
5653    fn semantics_if_changed_gates_on_generation() {
5654        let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5655        let mut state = ClickState::default();
5656        // First build bumps the generation from 0.
5657        root.rebuild(&mut a11y_button_logic, &mut state);
5658        root.layout(Size::new(200.0, 200.0));
5659
5660        let generation = root.semantics_generation();
5661        assert!(generation > 0);
5662        // A shell that already pushed `generation` sees no change.
5663        assert!(root.semantics_if_changed(generation).is_none());
5664        // A stale generation triggers a fresh pull.
5665        assert!(root.semantics_if_changed(generation - 1).is_some());
5666
5667        // A theme swap marks the tree semantics-dirty.
5668        root.set_theme(Box::new(0u32));
5669        assert!(root.semantics_generation() > generation);
5670        assert!(root.semantics_if_changed(generation).is_some());
5671    }
5672
5673    #[test]
5674    fn orientation_from_size_is_portrait_when_taller_than_wide() {
5675        assert_eq!(
5676            Orientation::from_size(Size::new(400.0, 800.0)),
5677            Orientation::Portrait
5678        );
5679    }
5680
5681    #[test]
5682    fn orientation_from_size_is_landscape_when_wider_than_tall() {
5683        assert_eq!(
5684            Orientation::from_size(Size::new(800.0, 400.0)),
5685            Orientation::Landscape
5686        );
5687    }
5688
5689    #[test]
5690    fn orientation_from_size_square_reads_as_portrait() {
5691        // Height >= width is the derivation rule (see `Orientation::from_size`'s
5692        // doc); an exact square satisfies `>=` and must not panic/ambiguously
5693        // resolve, so this is pinned explicitly rather than left implicit.
5694        assert_eq!(
5695            Orientation::from_size(Size::new(500.0, 500.0)),
5696            Orientation::Portrait
5697        );
5698    }
5699
5700    #[test]
5701    fn window_metrics_new_derives_orientation_from_size() {
5702        let insets = WindowInsets::default();
5703        let portrait = WindowMetrics::new(Size::new(390.0, 844.0), 3.0, insets);
5704        assert_eq!(portrait.orientation, Orientation::Portrait);
5705        assert_eq!(portrait.size, Size::new(390.0, 844.0));
5706        assert_eq!(portrait.scale, 3.0);
5707        assert_eq!(portrait.insets, insets);
5708
5709        let landscape = WindowMetrics::new(Size::new(844.0, 390.0), 3.0, insets);
5710        assert_eq!(landscape.orientation, Orientation::Landscape);
5711
5712        let square = WindowMetrics::new(Size::new(500.0, 500.0), 2.0, insets);
5713        assert_eq!(square.orientation, Orientation::Portrait);
5714    }
5715
5716    // --- Deferred state-bearing callbacks: the `InputEvent::Housekeeping` flush
5717    //     `RenderRoot::rebuild` dispatches. ---
5718
5719    /// App state for the flush tests.
5720    #[derive(Default)]
5721    struct FlushState {
5722        /// How many deferred callbacks have run.
5723        flushes: u32,
5724        /// How many more times a running callback re-queues itself — the knob the
5725        /// chained/capped tests turn.
5726        chain_left: u32,
5727        /// The `flushes` value each build-closure run observed, in order. This is
5728        /// what proves the rebuild re-runs the build closure *after* a flush rather than
5729        /// shipping the now-stale pre-flush view.
5730        observed: Vec<u32>,
5731    }
5732
5733    /// The navigator's deferred-callback shape reduced to one leaf: a shared
5734    /// `Rc<Cell<u32>>` op queue (the `NavigatorController` analog) is drained
5735    /// during the state-free [`View::rebuild`], which can therefore only *queue*
5736    /// the callback and raise the flush mark; [`crate::widget::Widget::event`]
5737    /// runs it when the broadcast arrives, where `&mut State` finally exists.
5738    struct FlushView {
5739        ops: std::rc::Rc<Cell<u32>>,
5740    }
5741
5742    struct FlushWidget {
5743        ops: std::rc::Rc<Cell<u32>>,
5744        queued: u32,
5745    }
5746
5747    impl FlushWidget {
5748        fn drain_ops(&mut self) {
5749            let ops = self.ops.replace(0);
5750            if ops > 0 {
5751                self.queued += ops;
5752                crate::event::mark_pending_result_flush();
5753            }
5754        }
5755    }
5756
5757    impl View<FlushState> for FlushView {
5758        type Element = FlushWidget;
5759        fn build(&self, _ctx: &mut BuildCtx<'_>) -> FlushWidget {
5760            let mut widget = FlushWidget {
5761                ops: self.ops.clone(),
5762                queued: 0,
5763            };
5764            widget.drain_ops();
5765            widget
5766        }
5767        fn rebuild(
5768            &self,
5769            _prev: &Self,
5770            element: &mut FlushWidget,
5771            _ctx: &mut BuildCtx<'_>,
5772        ) -> ChangeFlags {
5773            element.ops = self.ops.clone();
5774            element.drain_ops();
5775            ChangeFlags::NONE
5776        }
5777    }
5778
5779    impl crate::widget::Widget for FlushWidget {
5780        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5781            bc.constrain(Size::new(10.0, 10.0))
5782        }
5783        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5784        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5785            if !event.is_broadcast() || self.queued == 0 {
5786                return EventResult::Ignored;
5787            }
5788            let queued = std::mem::take(&mut self.queued);
5789            let state = ctx.state_mut::<FlushState>();
5790            for _ in 0..queued {
5791                state.flushes += 1;
5792                if state.chain_left > 0 {
5793                    state.chain_left -= 1;
5794                    self.ops.set(self.ops.get() + 1);
5795                }
5796            }
5797            EventResult::Ignored
5798        }
5799    }
5800
5801    fn flush_logic(ops: std::rc::Rc<Cell<u32>>) -> impl FnMut(&mut FlushState) -> FlushView {
5802        move |state: &mut FlushState| {
5803            state.observed.push(state.flushes);
5804            FlushView { ops: ops.clone() }
5805        }
5806    }
5807
5808    #[test]
5809    fn a_queued_callback_flushes_and_re_diffs_inside_one_rebuild() {
5810        let ops = std::rc::Rc::new(Cell::new(0u32));
5811        let mut root: RenderRoot<FlushState, FlushView> = RenderRoot::new();
5812        let mut app = flush_logic(ops.clone());
5813        let mut state = FlushState::default();
5814
5815        root.rebuild(&mut app, &mut state);
5816        assert_eq!(state.flushes, 0);
5817        assert_eq!(
5818            state.observed,
5819            vec![0],
5820            "nothing queued ⇒ exactly one build run, no broadcast"
5821        );
5822
5823        // Queue one op — the `NavigatorController::pop_with_result` analog.
5824        state.observed.clear();
5825        ops.set(1);
5826        root.rebuild(&mut app, &mut state);
5827        assert_eq!(
5828            state.flushes, 1,
5829            "the queued callback ran inside this rebuild — no event was dispatched \
5830             by anyone but the rebuild itself"
5831        );
5832        assert_eq!(
5833            state.observed,
5834            vec![0, 1],
5835            "the build closure re-ran after the flush and saw the post-callback state, so \
5836             the view this frame ships is not the stale pre-flush one"
5837        );
5838        assert!(
5839            !crate::event::take_pending_result_flush(),
5840            "the mark was consumed; nothing is owed to a later frame"
5841        );
5842    }
5843
5844    #[test]
5845    fn a_runaway_callback_chain_is_capped_and_deferred_to_the_next_frame() {
5846        let ops = std::rc::Rc::new(Cell::new(0u32));
5847        let mut root: RenderRoot<FlushState, FlushView> = RenderRoot::new();
5848        let mut app = flush_logic(ops.clone());
5849        // Far more chaining than the cap allows: unbounded, this rebuild would
5850        // never return. Reaching the assertions below at all is the no-spin proof.
5851        let mut state = FlushState {
5852            chain_left: 100,
5853            ..Default::default()
5854        };
5855        root.rebuild(&mut app, &mut state);
5856
5857        ops.set(1);
5858        root.rebuild(&mut app, &mut state);
5859        assert_eq!(
5860            state.flushes, MAX_PENDING_RESULT_FLUSH_PASSES as u32,
5861            "exactly the cap's worth of flush passes, then stop"
5862        );
5863
5864        // The remainder is owed, not lost: the mark still stands and the next
5865        // paint asks for the follow-up frame that will finish it.
5866        root.layout(Size::new(50.0, 50.0));
5867        let mut scene = RecordingScene::default();
5868        let outcome = root.paint(&mut scene, FrameTime::ZERO);
5869        assert!(
5870            outcome.needs_frame,
5871            "hitting the cap requests one more frame, so a dirty-driven shell \
5872             wakes instead of waiting for input"
5873        );
5874        assert!(
5875            !outcome.needs_frame_paced_only,
5876            "a deferred flush is not a cosmetic loop — the mobile frame gate must \
5877             not throttle it"
5878        );
5879
5880        // That next frame picks up exactly where the capped one left off.
5881        root.rebuild(&mut app, &mut state);
5882        assert_eq!(
5883            state.flushes,
5884            2 * MAX_PENDING_RESULT_FLUSH_PASSES as u32,
5885            "the deferred remainder resumed on the following frame"
5886        );
5887
5888        // Leave this thread's flag clean for anything else in the binary.
5889        let _ = crate::event::take_pending_result_flush();
5890    }
5891
5892    /// The redraw-only flush shape: a widget that queues a callback exactly like
5893    /// [`FlushView`] above, but whose broadcast handler touches **no** state at
5894    /// all — it only calls [`EventCtx::request_redraw`]. `frust-widgets`' gesture
5895    /// long-press latch is the shipped instance (its `on_long_press` consumer may
5896    /// mutate nothing the view diff can see), and the widget's own
5897    /// `ctx.request_redraw()` after firing is then the whole wake signal.
5898    struct RedrawOnlyView {
5899        ops: std::rc::Rc<Cell<u32>>,
5900    }
5901
5902    struct RedrawOnlyWidget {
5903        ops: std::rc::Rc<Cell<u32>>,
5904        queued: bool,
5905    }
5906
5907    impl RedrawOnlyWidget {
5908        fn drain_ops(&mut self) {
5909            if self.ops.replace(0) > 0 {
5910                self.queued = true;
5911                crate::event::mark_pending_result_flush();
5912            }
5913        }
5914    }
5915
5916    impl View<()> for RedrawOnlyView {
5917        type Element = RedrawOnlyWidget;
5918        fn build(&self, _ctx: &mut BuildCtx<'_>) -> RedrawOnlyWidget {
5919            let mut widget = RedrawOnlyWidget {
5920                ops: self.ops.clone(),
5921                queued: false,
5922            };
5923            widget.drain_ops();
5924            widget
5925        }
5926        fn rebuild(
5927            &self,
5928            _prev: &Self,
5929            element: &mut RedrawOnlyWidget,
5930            _ctx: &mut BuildCtx<'_>,
5931        ) -> ChangeFlags {
5932            element.ops = self.ops.clone();
5933            element.drain_ops();
5934            // The whole point: the re-diff after the flush reports nothing, so
5935            // the dispatch's own outcome is the only wake signal there is.
5936            ChangeFlags::NONE
5937        }
5938    }
5939
5940    impl crate::widget::Widget for RedrawOnlyWidget {
5941        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5942            bc.constrain(Size::new(10.0, 10.0))
5943        }
5944        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5945        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5946            if event.is_broadcast() && std::mem::take(&mut self.queued) {
5947                // No `state_mut`, no signal, no view-visible change — a repaint
5948                // request and nothing else.
5949                ctx.request_redraw();
5950            }
5951            EventResult::Ignored
5952        }
5953    }
5954
5955    #[test]
5956    fn a_redraw_only_flushed_callback_wakes_both_loop_styles() {
5957        let ops = std::rc::Rc::new(Cell::new(0u32));
5958        let mut root: RenderRoot<(), RedrawOnlyView> = RenderRoot::new();
5959        let mut app = |_state: &mut ()| RedrawOnlyView { ops: ops.clone() };
5960        let mut state = ();
5961
5962        // Settle the first build so the assertions below observe only the flush.
5963        root.rebuild(&mut app, &mut state);
5964        root.layout(Size::new(50.0, 50.0));
5965        let mut scene = RecordingScene::default();
5966        let settled = root.paint(&mut scene, FrameTime::ZERO);
5967        assert!(!settled.needs_frame, "nothing queued ⇒ the tree is at rest");
5968        let _ = root.take_change_flags();
5969
5970        // Queue the redraw-only callback (the gesture long-press latch analog: a
5971        // prior pass marks, this rebuild flushes).
5972        ops.set(1);
5973        let flags = root.rebuild(&mut app, &mut state);
5974        assert!(
5975            flags.needs_paint(),
5976            "the broadcast's `needs_redraw` folds into the rebuild's flags even \
5977             though the re-diff saw no view change"
5978        );
5979        assert!(
5980            root.has_pending_change_flags(),
5981            "PAINT reached `pending`, which is the input the mobile frame gate \
5982             reads to decide the next tick runs at all"
5983        );
5984
5985        let outcome = root.paint(&mut scene, FrameTime::ZERO);
5986        assert!(
5987            outcome.needs_frame,
5988            "the same wake surfaces as `needs_frame`, which is how the desktop \
5989             `ControlFlow::Wait` loop schedules a frame with no input pending"
5990        );
5991        assert!(
5992            !outcome.needs_frame_paced_only,
5993            "a flushed callback's repaint is not a cosmetic loop — the mobile \
5994             frame gate must not throttle it"
5995        );
5996        assert!(
5997            !crate::event::take_pending_result_flush(),
5998            "the mark was consumed; nothing is owed to a later frame"
5999        );
6000
6001        // And it settles: the next frame asks for nothing, so neither loop spins.
6002        let _ = root.take_change_flags();
6003        root.rebuild(&mut app, &mut state);
6004        let settled = root.paint(&mut scene, FrameTime::ZERO);
6005        assert!(
6006            !settled.needs_frame,
6007            "one wake, not a perpetual one — the flush is over"
6008        );
6009        assert!(!root.has_pending_change_flags());
6010    }
6011
6012    // --- Hover: the claim pipeline -------------------------------------------
6013    //
6014    // Hover has no Enter/Leave phase to lean on (adding one to `PointerPhase`
6015    // would break every out-of-tree exhaustive match). It is instead an opt-in
6016    // claim a widget makes from its uncaptured `Move` arm, recorded as an epoch
6017    // stamp down the pod chain — so the fixture below is deliberately shaped like
6018    // a real container: two hit-tested children, a capture fast-path, and paint
6019    // recording what `PaintCtx::is_hovered` reported.
6020
6021    /// Which of the fixture's two leaves an assertion is about.
6022    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
6023    enum Leaf {
6024        Top,
6025        Bottom,
6026    }
6027
6028    /// What one hover leaf observed, shared out of the widget tree.
6029    #[derive(Default)]
6030    struct HoverProbe {
6031        /// `PaintCtx::is_hovered()` as of the last paint.
6032        painted_hovered: Cell<bool>,
6033        /// `EventCtx::is_hovered()` as of the last event dispatch that reached it.
6034        event_hovered: Cell<bool>,
6035    }
6036
6037    /// A leaf that claims hover on any `Move` landing inside its own bounds — the
6038    /// canonical opt-in shape — and optionally captures the pointer on `Down` (the
6039    /// drag fixture: a captured pointer must never create hover).
6040    ///
6041    /// With `latches` set it follows the whole consumer contract: the same hit test
6042    /// updates an internal flag, `request_redraw` is gated on that flag changing,
6043    /// and `paint` self-corrects the flag from the authoritative
6044    /// `PaintCtx::is_hovered`. Clearing `latches` is a deliberate negative control —
6045    /// a claimant that keeps no flag — used to pin which frames the pipeline itself
6046    /// does and does not manufacture.
6047    struct HoverLeaf {
6048        captures: bool,
6049        latches: bool,
6050        hovered: bool,
6051        probe: Rc<HoverProbe>,
6052    }
6053
6054    impl crate::widget::Widget for HoverLeaf {
6055        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6056            bc.constrain(Size::new(100.0, 30.0))
6057        }
6058        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
6059            self.probe.painted_hovered.set(ctx.is_hovered());
6060            if self.latches {
6061                // The self-correction half of the contract: authoritative here,
6062                // whatever the event arm last recorded.
6063                self.hovered = ctx.is_hovered();
6064            }
6065        }
6066        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6067            self.probe.event_hovered.set(ctx.is_hovered());
6068            let InputEvent::Pointer(p) = event else {
6069                return EventResult::Ignored;
6070            };
6071            match p.phase {
6072                PointerPhase::Move => {
6073                    let size = ctx.size();
6074                    let inside = p.position.x >= 0.0
6075                        && p.position.y >= 0.0
6076                        && p.position.x < size.width
6077                        && p.position.y < size.height;
6078                    if inside {
6079                        ctx.claim_hover();
6080                    }
6081                    if self.latches && self.hovered != inside {
6082                        self.hovered = inside;
6083                        ctx.request_redraw();
6084                    }
6085                    // Deliberately `Ignored`: a hovering widget does not consume a
6086                    // move it merely watched (the shipped `ListItem` shape).
6087                    EventResult::Ignored
6088                }
6089                PointerPhase::Down => {
6090                    if self.captures {
6091                        ctx.capture_pointer();
6092                    }
6093                    EventResult::Handled
6094                }
6095                _ => EventResult::Ignored,
6096            }
6097        }
6098    }
6099
6100    /// Whether the container claims hover for itself, and when relative to routing
6101    /// the move into its child — the ordering the claim contract binds a container
6102    /// to.
6103    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
6104    enum GroupClaim {
6105        /// Never claims — the transparent container, hovered only via the path.
6106        Never,
6107        /// Claims *after* routing: the contract-following container, whose claim is
6108        /// a fallback the child's claim beats.
6109        AfterRouting,
6110        /// Claims *before* routing: the documented anti-pattern, kept as a
6111        /// negative control.
6112        BeforeRouting,
6113    }
6114
6115    /// A container wrapping one hover leaf, recording what its **own**
6116    /// `PaintCtx::is_hovered`/`EventCtx::is_hovered` reported — the
6117    /// ancestor-on-the-claim-path case, which the two sibling leaves alone cannot
6118    /// show. With `claims` set it also wants hover chrome of its own, claiming
6119    /// either side of the route to exercise the ordering rule.
6120    ///
6121    /// The child is an `Option` so a rebuild can *remove* it — the unmount case,
6122    /// where the claimant stops existing between hover passes.
6123    struct HoverGroup {
6124        probe: Rc<HoverProbe>,
6125        claims: GroupClaim,
6126        child: Option<crate::widget::ChildPod>,
6127    }
6128
6129    impl HoverGroup {
6130        /// Whether this event is an uncaptured-move-shaped pass landing inside the
6131        /// container's own bounds — the same local hit test a leaf claims on.
6132        fn claims_on(&self, ctx: &EventCtx, event: &InputEvent) -> bool {
6133            let InputEvent::Pointer(p) = event else {
6134                return false;
6135            };
6136            let size = ctx.size();
6137            matches!(p.phase, PointerPhase::Move)
6138                && p.position.x >= 0.0
6139                && p.position.y >= 0.0
6140                && p.position.x < size.width
6141                && p.position.y < size.height
6142        }
6143    }
6144
6145    impl crate::widget::Widget for HoverGroup {
6146        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6147            match &mut self.child {
6148                Some(child) => {
6149                    let size = child.layout_child(ctx, bc);
6150                    child.set_origin(Point::ZERO);
6151                    size
6152                }
6153                // The same box with nothing in it, so removing the claimant
6154                // changes what is under the pointer without moving the container.
6155                None => bc.constrain(Size::new(100.0, 30.0)),
6156            }
6157        }
6158        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
6159            self.probe.painted_hovered.set(ctx.is_hovered());
6160            if let Some(child) = &mut self.child {
6161                child.paint_child(ctx, scene);
6162            }
6163        }
6164        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6165            self.probe.event_hovered.set(ctx.is_hovered());
6166            let claims = self.claims != GroupClaim::Never && self.claims_on(ctx, event);
6167            if claims && self.claims == GroupClaim::BeforeRouting {
6168                ctx.claim_hover();
6169            }
6170            let result = match &mut self.child {
6171                Some(child) => child.event_child(ctx, event),
6172                None => EventResult::Ignored,
6173            };
6174            if claims && self.claims == GroupClaim::AfterRouting {
6175                ctx.claim_hover();
6176            }
6177            result
6178        }
6179    }
6180
6181    /// Two stacked hover leaves with a hit-tested route and a capture fast-path —
6182    /// the minimum container that can show a claim moving between siblings.
6183    struct HoverPair {
6184        top: crate::widget::ChildPod,
6185        bottom: crate::widget::ChildPod,
6186    }
6187
6188    impl crate::widget::Widget for HoverPair {
6189        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6190            self.top.layout_child(ctx, bc);
6191            self.top.set_origin(Point::ZERO);
6192            self.bottom.layout_child(ctx, bc);
6193            self.bottom.set_origin(Point::new(0.0, 30.0));
6194            bc.max()
6195        }
6196        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
6197            self.top.paint_child(ctx, scene);
6198            self.bottom.paint_child(ctx, scene);
6199        }
6200        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6201            let releases = matches!(
6202                event,
6203                InputEvent::Pointer(p)
6204                    if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
6205            );
6206            for pod in [&mut self.top, &mut self.bottom] {
6207                if pod.is_active() {
6208                    let r = pod.event_child(ctx, event);
6209                    if releases {
6210                        pod.set_active(false);
6211                    }
6212                    return r;
6213                }
6214            }
6215            let pos = event.position();
6216            for pod in [&mut self.top, &mut self.bottom] {
6217                if pod.contains(pos) {
6218                    return pod.event_child(ctx, event);
6219                }
6220            }
6221            EventResult::Ignored
6222        }
6223    }
6224
6225    /// How one `HoverHarness` is shaped.
6226    #[derive(Clone, Copy)]
6227    struct HoverFixture {
6228        /// Leaves capture the pointer on `Down` (the drag case).
6229        captures: bool,
6230        /// Leaves follow the consumer contract (latched flag + change-gated redraw
6231        /// + paint-time self-correction).
6232        latches: bool,
6233        /// Wrap the top leaf in a [`HoverGroup`], so the claim path has an
6234        /// ancestor pod between the claimant and the root.
6235        nested: bool,
6236        /// Whether (and when) that container claims hover for itself.
6237        group_claims: GroupClaim,
6238        /// Rebuild the container without its child: the unmount case, where the
6239        /// pod holding the hover link is dropped by the view diff.
6240        drop_claimant: bool,
6241        /// Report [`ChangeFlags::NONE`] from that removal — the hand-rolled
6242        /// container outside this workspace, which drops a pod while reporting
6243        /// whatever it likes. The in-tree reconcilers report `LAYOUT | PAINT`,
6244        /// which is what the release used to lean on instead of flagging its own.
6245        silent_reconciler: bool,
6246    }
6247
6248    struct HoverPairView {
6249        top: Rc<HoverProbe>,
6250        bottom: Rc<HoverProbe>,
6251        group: Rc<HoverProbe>,
6252        fixture: HoverFixture,
6253    }
6254
6255    impl HoverPairView {
6256        fn leaf(&self, probe: &Rc<HoverProbe>) -> Box<dyn crate::widget::Widget> {
6257            Box::new(HoverLeaf {
6258                captures: self.fixture.captures,
6259                latches: self.fixture.latches,
6260                hovered: false,
6261                probe: probe.clone(),
6262            })
6263        }
6264    }
6265
6266    impl View<()> for HoverPairView {
6267        type Element = HoverPair;
6268        fn build(&self, _ctx: &mut BuildCtx<'_>) -> HoverPair {
6269            let top: Box<dyn crate::widget::Widget> = if self.fixture.nested {
6270                Box::new(HoverGroup {
6271                    probe: self.group.clone(),
6272                    claims: self.fixture.group_claims,
6273                    child: Some(crate::widget::ChildPod::new(self.leaf(&self.top))),
6274                })
6275            } else {
6276                self.leaf(&self.top)
6277            };
6278            HoverPair {
6279                top: crate::widget::ChildPod::new(top),
6280                bottom: crate::widget::ChildPod::new(self.leaf(&self.bottom)),
6281            }
6282        }
6283        fn rebuild(&self, p: &Self, e: &mut HoverPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
6284            // The only structural op this fixture performs: drop the nested
6285            // container's child pod, the way a real reconciler drops a truncated
6286            // or conditionally-removed child.
6287            let removes_child = self.fixture.drop_claimant && !p.fixture.drop_claimant;
6288            if !removes_child {
6289                return ChangeFlags::NONE;
6290            }
6291            let group = e
6292                .top
6293                .widget_mut()
6294                .downcast_mut::<HoverGroup>()
6295                .expect("the drop-claimant fixture is the nested one");
6296            group.child = None;
6297            if self.fixture.silent_reconciler {
6298                ChangeFlags::NONE
6299            } else {
6300                ChangeFlags::LAYOUT | ChangeFlags::PAINT
6301            }
6302        }
6303    }
6304
6305    /// A `RenderRoot` over the hover fixture, plus its probes.
6306    struct HoverHarness {
6307        root: RenderRoot<(), HoverPairView>,
6308        top: Rc<HoverProbe>,
6309        bottom: Rc<HoverProbe>,
6310        group: Rc<HoverProbe>,
6311        state: (),
6312        /// The shape the next rebuild re-states, so
6313        /// [`HoverHarness::rebuild_without_claimant`] can flip one flag without
6314        /// restating the rest.
6315        fixture: HoverFixture,
6316    }
6317
6318    impl HoverHarness {
6319        /// The contract-following fixture: two flat leaves that latch their own
6320        /// hover flag, optionally capturing on `Down`.
6321        fn new(captures: bool) -> Self {
6322            Self::build(HoverFixture {
6323                captures,
6324                latches: true,
6325                nested: false,
6326                group_claims: GroupClaim::Never,
6327                drop_claimant: false,
6328                silent_reconciler: false,
6329            })
6330        }
6331
6332        /// The negative control: leaves that claim hover but keep no flag of their
6333        /// own, so only frames the pipeline manufactures show up.
6334        fn without_consumer_flag() -> Self {
6335            Self::build(HoverFixture {
6336                captures: false,
6337                latches: false,
6338                nested: false,
6339                group_claims: GroupClaim::Never,
6340                drop_claimant: false,
6341                silent_reconciler: false,
6342            })
6343        }
6344
6345        /// The top leaf wrapped in a container pod, for the path-semantics case.
6346        fn nested() -> Self {
6347            Self::build(HoverFixture {
6348                captures: false,
6349                latches: true,
6350                nested: true,
6351                group_claims: GroupClaim::Never,
6352                drop_claimant: false,
6353                silent_reconciler: false,
6354            })
6355        }
6356
6357        /// The same nesting, with the container claiming hover for itself the way
6358        /// the contract requires: after routing the move into its child.
6359        fn nested_group_claiming(claims: GroupClaim) -> Self {
6360            Self::build(HoverFixture {
6361                captures: false,
6362                latches: true,
6363                nested: true,
6364                group_claims: claims,
6365                drop_claimant: false,
6366                silent_reconciler: false,
6367            })
6368        }
6369
6370        /// The same nesting, with a container that drops its child while
6371        /// reporting no flags — the hand-rolled container outside this workspace
6372        /// the destructor route exists to cover.
6373        fn nested_group_with_silent_reconciler() -> Self {
6374            Self::build(HoverFixture {
6375                captures: false,
6376                latches: true,
6377                nested: true,
6378                group_claims: GroupClaim::AfterRouting,
6379                drop_claimant: false,
6380                silent_reconciler: true,
6381            })
6382        }
6383
6384        fn build(fixture: HoverFixture) -> Self {
6385            let top = Rc::new(HoverProbe::default());
6386            let bottom = Rc::new(HoverProbe::default());
6387            let group = Rc::new(HoverProbe::default());
6388            let mut root: RenderRoot<(), HoverPairView> = RenderRoot::new();
6389            let mut state = ();
6390            let (t, b, g) = (top.clone(), bottom.clone(), group.clone());
6391            root.rebuild(
6392                &mut move |_: &mut ()| HoverPairView {
6393                    top: t.clone(),
6394                    bottom: b.clone(),
6395                    group: g.clone(),
6396                    fixture,
6397                },
6398                &mut state,
6399            );
6400            root.layout(Size::new(100.0, 60.0));
6401            HoverHarness {
6402                root,
6403                top,
6404                bottom,
6405                group,
6406                state,
6407                fixture,
6408            }
6409        }
6410
6411        /// Rebuild with the nested container's child removed — the claimant
6412        /// unmounting between hover passes — and re-lay out, returning what the
6413        /// diff reported.
6414        fn rebuild_without_claimant(&mut self) -> ChangeFlags {
6415            self.fixture.drop_claimant = true;
6416            self.rebuild_current()
6417        }
6418
6419        /// Rebuild restating the shape already on screen: nothing of this root's
6420        /// own is severed, so any hover end it performs came from elsewhere.
6421        fn rebuild_unchanged(&mut self) -> ChangeFlags {
6422            self.rebuild_current()
6423        }
6424
6425        /// Re-run the diff against the fixture as it currently stands, then
6426        /// re-lay out, returning what the diff reported.
6427        fn rebuild_current(&mut self) -> ChangeFlags {
6428            let fixture = self.fixture;
6429            let (t, b, g) = (self.top.clone(), self.bottom.clone(), self.group.clone());
6430            let flags = self.root.rebuild(
6431                &mut move |_: &mut ()| HoverPairView {
6432                    top: t.clone(),
6433                    bottom: b.clone(),
6434                    group: g.clone(),
6435                    fixture,
6436                },
6437                &mut self.state,
6438            );
6439            self.root.layout(Size::new(100.0, 60.0));
6440            flags
6441        }
6442
6443        /// Dispatch a pointer event at `(x, y)` in window space.
6444        fn dispatch(&mut self, phase: PointerPhase, x: f64, y: f64) -> EventOutcome {
6445            let event = InputEvent::Pointer(PointerEvent {
6446                phase,
6447                position: Point::new(x, y),
6448                button: PointerButton::Primary,
6449            });
6450            self.root.event(&mut self.state, &event)
6451        }
6452
6453        /// Move the pointer over the given leaf's middle.
6454        fn move_over(&mut self, leaf: Leaf) -> EventOutcome {
6455            match leaf {
6456                Leaf::Top => self.dispatch(PointerPhase::Move, 50.0, 15.0),
6457                Leaf::Bottom => self.dispatch(PointerPhase::Move, 50.0, 45.0),
6458            }
6459        }
6460
6461        /// Paint the tree, refreshing both probes' recorded hover state.
6462        fn paint(&mut self) {
6463            let mut scene = RecordingScene::default();
6464            self.root.paint(&mut scene, FrameTime::ZERO);
6465        }
6466
6467        /// `(top, bottom)` hover as the last paint reported it.
6468        fn painted(&mut self) -> (bool, bool) {
6469            self.paint();
6470            (
6471                self.top.painted_hovered.get(),
6472                self.bottom.painted_hovered.get(),
6473            )
6474        }
6475    }
6476
6477    #[test]
6478    fn an_uncaptured_move_claims_hover_and_paint_reports_it() {
6479        let mut h = HoverHarness::new(false);
6480        assert!(!h.root.is_hover_active(), "nothing is hovered at rest");
6481        assert_eq!(h.painted(), (false, false));
6482
6483        let outcome = h.move_over(Leaf::Top);
6484        assert!(h.root.is_hover_active(), "the claim reached the root");
6485        assert!(
6486            !outcome.handled,
6487            "a hovering widget need not consume the move"
6488        );
6489        assert_eq!(
6490            h.painted(),
6491            (true, false),
6492            "the claimant reads as hovered, its sibling does not"
6493        );
6494
6495        // A second move within the same leaf keeps the link (the claim is
6496        // re-recorded every pass) without re-reporting a change.
6497        h.dispatch(PointerPhase::Move, 60.0, 20.0);
6498        assert_eq!(h.painted(), (true, false));
6499    }
6500
6501    #[test]
6502    fn a_second_widgets_claim_clears_the_first_and_asks_for_a_repaint() {
6503        let mut h = HoverHarness::new(false);
6504        h.move_over(Leaf::Top);
6505        assert_eq!(h.painted(), (true, false));
6506
6507        // The pointer moves onto the sibling. The container never has to clear
6508        // anything: the epoch advance strands the top pod's stamp.
6509        h.move_over(Leaf::Bottom);
6510        assert!(h.root.is_hover_active());
6511        assert_eq!(
6512            h.painted(),
6513            (false, true),
6514            "the previous claimant lost its link when the new one recorded"
6515        );
6516
6517        // Both widgets need a repaint, and a repaint is global — one request
6518        // covers them. The *losing* side is what the root itself must guarantee:
6519        // moving onto a leaf that claims nothing still repaints.
6520        let outcome = h.dispatch(PointerPhase::Move, 50.0, 200.0);
6521        assert!(
6522            !h.root.is_hover_active(),
6523            "a move claiming nothing ends the hover"
6524        );
6525        assert!(
6526            outcome.needs_redraw,
6527            "the widget that lost hover cannot ask for the repaint itself"
6528        );
6529        assert_eq!(h.painted(), (false, false));
6530
6531        // ...and the same move repeated is not a change any more.
6532        let settled = h.dispatch(PointerPhase::Move, 50.0, 200.0);
6533        assert!(
6534            !settled.needs_redraw,
6535            "an already-hoverless move requests nothing"
6536        );
6537    }
6538
6539    #[test]
6540    fn a_captured_move_cannot_claim_hover() {
6541        let mut h = HoverHarness::new(true);
6542        // Press the top leaf: it captures, and the `Down` itself ends any hover.
6543        h.dispatch(PointerPhase::Down, 50.0, 15.0);
6544        assert!(h.root.is_pointer_captured());
6545        assert!(!h.root.is_hover_active());
6546
6547        // Drag: every one of these moves routes to the captured leaf, whose `Move`
6548        // arm hit-tests inside and calls `claim_hover()` — and must record nothing.
6549        h.dispatch(PointerPhase::Move, 50.0, 16.0);
6550        assert!(
6551            !h.root.is_hover_active(),
6552            "a captured pointer never creates hover"
6553        );
6554        assert_eq!(h.painted(), (false, false));
6555
6556        // Dragging outside the leaf keeps routing to it (capture), still no hover.
6557        h.dispatch(PointerPhase::Move, 50.0, 45.0);
6558        assert!(!h.root.is_hover_active());
6559        assert_eq!(h.painted(), (false, false));
6560
6561        // Release, then a fresh uncaptured move: hover is claimable again.
6562        h.dispatch(PointerPhase::Up, 50.0, 15.0);
6563        h.move_over(Leaf::Top);
6564        assert!(h.root.is_hover_active());
6565        assert_eq!(h.painted(), (true, false));
6566    }
6567
6568    #[test]
6569    fn a_down_up_or_cancel_ends_the_hover() {
6570        // Every pointer phase other than an uncaptured `Move` ends the link. `Up`
6571        // is in here for touch: a lifted finger sends no further move, so a tint
6572        // claimed during an uncaptured touch drag would otherwise stand for good.
6573        for ending in [PointerPhase::Down, PointerPhase::Up, PointerPhase::Cancel] {
6574            let mut h = HoverHarness::new(false);
6575            h.move_over(Leaf::Top);
6576            assert!(h.root.is_hover_active());
6577
6578            let outcome = h.dispatch(ending, 50.0, 15.0);
6579            assert!(
6580                !h.root.is_hover_active(),
6581                "{ending:?} ends the hover link outright"
6582            );
6583            assert!(
6584                outcome.needs_redraw,
6585                "{ending:?} that dropped a hover asks for the repaint"
6586            );
6587            assert_eq!(h.painted(), (false, false));
6588        }
6589    }
6590
6591    #[test]
6592    fn a_non_pointer_pass_leaves_a_live_hover_standing() {
6593        let mut h = HoverHarness::new(false);
6594        h.move_over(Leaf::Top);
6595        assert_eq!(h.painted(), (true, false));
6596
6597        // Neither a scroll, a key, nor the housekeeping broadcast is a hover pass:
6598        // the pointer has not moved, so the link must survive them untouched.
6599        h.root.event(
6600            &mut h.state,
6601            &InputEvent::Scroll {
6602                position: Point::new(50.0, 15.0),
6603                delta: crate::event::ScrollDelta::Lines(0.0, 1.0),
6604            },
6605        );
6606        assert!(h.root.is_hover_active());
6607        h.root.event(&mut h.state, &InputEvent::Housekeeping);
6608        assert!(h.root.is_hover_active());
6609        assert_eq!(h.painted(), (true, false));
6610    }
6611
6612    #[test]
6613    fn a_container_on_the_claim_path_reads_hovered_and_a_sibling_does_not() {
6614        // The recorded thing is a path, so hover is `:hover`-shaped: the claimant
6615        // and every ancestor enclosing it read hovered, nothing off the path does.
6616        let mut h = HoverHarness::nested();
6617        h.move_over(Leaf::Top);
6618        h.paint();
6619        assert!(h.top.painted_hovered.get(), "the claimant itself");
6620        assert!(
6621            h.group.painted_hovered.get(),
6622            "the container enclosing the claimant is on the path too"
6623        );
6624        assert!(
6625            !h.bottom.painted_hovered.get(),
6626            "a sibling leaf is off the path"
6627        );
6628
6629        // Move onto the sibling: the container goes unhovered with its child, and
6630        // both reads agree about it on the next pass.
6631        h.move_over(Leaf::Bottom);
6632        h.paint();
6633        assert!(!h.group.painted_hovered.get());
6634        assert!(!h.top.painted_hovered.get());
6635        assert!(h.bottom.painted_hovered.get());
6636        h.move_over(Leaf::Top);
6637        assert!(
6638            !h.group.event_hovered.get(),
6639            "the event read reports the previous pass, like the leaf's"
6640        );
6641        h.move_over(Leaf::Top);
6642        assert!(
6643            h.group.event_hovered.get(),
6644            "the container observes the link its child holds"
6645        );
6646    }
6647
6648    #[test]
6649    fn an_ancestor_claiming_after_routing_loses_to_its_child_and_still_reads_hovered() {
6650        // A container that wants hover chrome of its own claims after routing the
6651        // move into its child. Only one claim per pass is recorded and the first one
6652        // recorded wins, so the child's claim is the one that lands; the container's
6653        // own late call is a silent no-op, and it reads hovered through the stamped
6654        // path anyway — which is what makes this ordering correct in every case.
6655        let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6656        let gain = h.move_over(Leaf::Top);
6657        h.paint();
6658        assert!(
6659            h.top.painted_hovered.get(),
6660            "the child under the pointer holds the link"
6661        );
6662        assert!(
6663            h.group.painted_hovered.get(),
6664            "the container is on that path, so it reads hovered too"
6665        );
6666        assert!(
6667            !h.bottom.painted_hovered.get(),
6668            "a sibling leaf is off the path"
6669        );
6670        assert!(gain.needs_redraw, "hover gain repaints");
6671
6672        // The child's latched flag now agrees with the authoritative paint read, so
6673        // wandering on within the same widget settles instead of repainting.
6674        let settled = h.dispatch(PointerPhase::Move, 60.0, 20.0);
6675        assert!(!settled.needs_redraw, "an unchanged flag asks for nothing");
6676        h.paint();
6677        assert!(h.top.painted_hovered.get());
6678        assert!(h.group.painted_hovered.get());
6679    }
6680
6681    #[test]
6682    fn an_ancestor_claiming_before_routing_starves_its_subtree() {
6683        // The negative control for the ordering rule above, pinning the trap it
6684        // exists to prevent: a container that claims *before* forwarding is recorded
6685        // first, which closes the pass to every descendant. The child under the
6686        // pointer can never read hovered, so its hover chrome never appears — and
6687        // because its latched flag is corrected back to `false` at paint time, it
6688        // flips and asks for a frame again on every single move.
6689        let mut h = HoverHarness::nested_group_claiming(GroupClaim::BeforeRouting);
6690        h.move_over(Leaf::Top);
6691        h.paint();
6692        assert!(
6693            !h.top.painted_hovered.get(),
6694            "the ancestor's earlier claim made its child ineligible"
6695        );
6696        assert!(
6697            h.group.painted_hovered.get(),
6698            "the outermost claimant is the one holding the link here"
6699        );
6700        assert!(!h.bottom.painted_hovered.get());
6701
6702        // Repaint-per-move: the flag never converges, because the event arm and the
6703        // authoritative paint read permanently disagree.
6704        let again = h.dispatch(PointerPhase::Move, 60.0, 20.0);
6705        assert!(
6706            again.needs_redraw,
6707            "the starved child re-flips its flag on every move"
6708        );
6709        h.paint();
6710        assert!(!h.top.painted_hovered.get(), "and still paints no chrome");
6711    }
6712
6713    #[test]
6714    fn hover_gain_is_repainted_by_the_consumers_own_flag() {
6715        let mut h = HoverHarness::new(false);
6716        // Entering a widget: the root manufactures nothing here (its mirror went
6717        // `false` → `true`, and it cannot know which widget cares), so the frame
6718        // comes from the claimant's own change-gated request.
6719        let gain = h.move_over(Leaf::Top);
6720        assert!(gain.needs_redraw, "hover gain repaints");
6721        assert_eq!(h.painted(), (true, false));
6722
6723        // Wandering within the same widget claims again but changes nothing, so it
6724        // must not repaint per event.
6725        let settled = h.dispatch(PointerPhase::Move, 60.0, 20.0);
6726        assert!(!settled.needs_redraw, "an unchanged flag asks for nothing");
6727
6728        // A handoff repaints both sides at once: the arriving leaf's flag changed
6729        // (it asks), and because a repaint is global that same frame is what lets
6730        // the departing leaf drop its chrome from the authoritative paint read.
6731        let handoff = h.move_over(Leaf::Bottom);
6732        assert!(handoff.needs_redraw, "a claimant handoff repaints");
6733        assert_eq!(
6734            h.painted(),
6735            (false, true),
6736            "one frame settles both the loss and the gain"
6737        );
6738    }
6739
6740    #[test]
6741    fn a_claimant_without_its_own_flag_gets_only_the_loss_frame() {
6742        // The negative control for the contract above: leaves that claim hover but
6743        // keep no flag of their own. Gain and handoff are invisible to the root
6744        // (its hover mirror is identity-free — `false` → `true` and `true` →
6745        // `true`), so nothing repaints for them, which is exactly why the
6746        // consumer's latched flag is normative rather than an optimization.
6747        let mut h = HoverHarness::without_consumer_flag();
6748        let gain = h.move_over(Leaf::Top);
6749        assert!(h.root.is_hover_active());
6750        assert!(!gain.needs_redraw, "no widget asked, and the root cannot");
6751
6752        let handoff = h.move_over(Leaf::Bottom);
6753        assert!(h.root.is_hover_active());
6754        assert!(!handoff.needs_redraw, "a handoff is `true` → `true` here");
6755
6756        // Loss is the one edge the root does cover, since no widget can see it.
6757        let loss = h.dispatch(PointerPhase::Move, 50.0, 200.0);
6758        assert!(!h.root.is_hover_active());
6759        assert!(loss.needs_redraw, "the root manufactures the loss frame");
6760    }
6761
6762    #[test]
6763    fn a_rebuild_that_removes_the_claimant_ends_the_hover() {
6764        // The one severance the epoch cannot strand: the claimant is dropped by a
6765        // view diff, so it will never see the `Move` that would have re-derived
6766        // the link. Its pod reports the drop and `rebuild` ends the hover.
6767        let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6768        h.move_over(Leaf::Top);
6769        h.paint();
6770        assert!(h.root.is_hover_active());
6771        assert!(h.top.painted_hovered.get(), "the claimant holds the link");
6772        assert!(
6773            h.group.painted_hovered.get(),
6774            "its container is on the path"
6775        );
6776
6777        let flags = h.rebuild_without_claimant();
6778        assert!(
6779            !h.root.is_hover_active(),
6780            "the mirror cannot outlive the widget it described"
6781        );
6782        assert!(
6783            flags.contains(ChangeFlags::PAINT),
6784            "the correction states its own need for a frame, whatever the \
6785             reconciler that dropped the claimant reported"
6786        );
6787
6788        // The survivor is the ancestor that was on the claim path: its own stamp
6789        // still names the epoch the claim recorded, so nothing but the epoch
6790        // advance keeps it from painting hover chrome for a child that is gone.
6791        h.paint();
6792        assert!(
6793            !h.group.painted_hovered.get(),
6794            "the surviving ancestor lost the link with its child"
6795        );
6796        assert!(
6797            !h.bottom.painted_hovered.get(),
6798            "and the sibling never had it"
6799        );
6800
6801        // And the pipeline is not wedged: the next move over the same spot claims
6802        // cleanly, now for the container itself. (No frame is manufactured for
6803        // that gain — this container keeps no latched flag of its own, which is
6804        // the documented consumer-side half of the contract, not a pipeline job.)
6805        h.move_over(Leaf::Top);
6806        assert!(h.root.is_hover_active(), "the next move re-claims");
6807        h.paint();
6808        assert!(
6809            h.group.painted_hovered.get(),
6810            "the container is the claimant now"
6811        );
6812    }
6813
6814    #[test]
6815    fn a_silent_reconcilers_removal_still_carries_its_own_repaint() {
6816        // The reason the release flags `PAINT` itself rather than trusting the
6817        // diff to have reported one. The destructor route deliberately reaches
6818        // containers this workspace never sees, and such a container can drop the
6819        // claimant while reporting nothing — leaving the hover correctly ended but
6820        // the frame that shows it unrequested, on a pointer the user has stopped
6821        // moving.
6822        let mut h = HoverHarness::nested_group_with_silent_reconciler();
6823        h.move_over(Leaf::Top);
6824        assert!(h.root.is_hover_active());
6825
6826        let flags = h.rebuild_without_claimant();
6827        assert!(!h.root.is_hover_active(), "the link ends either way");
6828        assert_eq!(
6829            flags,
6830            ChangeFlags::PAINT,
6831            "and the frame it needs comes from the release, not from the diff"
6832        );
6833    }
6834
6835    #[test]
6836    fn a_rebuild_that_keeps_the_claimant_leaves_the_hover_standing() {
6837        // The negative control for the release above, and the reason the mark is
6838        // gated on the *live* epoch rather than on "some pod with a stamp died":
6839        // an ordinary rebuild — including one that drops pods carrying stale
6840        // stamps — must not touch a link the pointer still rests on.
6841        let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6842        // Hover the top leaf, then hand the link to the sibling. The top pod chain
6843        // keeps its (now stale) stamp, which is what the next rebuild drops.
6844        h.move_over(Leaf::Top);
6845        h.move_over(Leaf::Bottom);
6846        assert!(h.root.is_hover_active());
6847
6848        h.rebuild_without_claimant();
6849        assert!(
6850            h.root.is_hover_active(),
6851            "dropping a stale stamp is not a severance"
6852        );
6853        h.paint();
6854        assert!(
6855            h.bottom.painted_hovered.get(),
6856            "the widget actually under the pointer keeps its chrome"
6857        );
6858    }
6859
6860    #[test]
6861    fn another_roots_dying_claimant_cannot_end_this_roots_hover() {
6862        // Two roots on one thread. Each has run exactly one hover pass, so their
6863        // epoch counters hold the identical integer — the collision the root
6864        // identity exists to break. Without it, the first root's pods dying (its
6865        // window closing, a page tearing down) raise a mark the second root's
6866        // next rebuild drains, ending a hover the pointer is still resting on.
6867        let mut first = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6868        let mut second = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6869        first.move_over(Leaf::Top);
6870        second.move_over(Leaf::Top);
6871        assert_eq!(
6872            first.root.hover_epoch, second.root.hover_epoch,
6873            "the two roots' epochs collide, which is the whole premise"
6874        );
6875        assert!(first.root.is_hover_active());
6876        assert!(second.root.is_hover_active());
6877
6878        // Drop the first root outright: every pod it owns runs the destructor
6879        // that reports a severed hover link, including the claimant's.
6880        drop(first);
6881
6882        second.rebuild_unchanged();
6883        assert!(
6884            second.root.is_hover_active(),
6885            "a mark another root raised is none of this root's business"
6886        );
6887        second.paint();
6888        assert!(
6889            second.top.painted_hovered.get(),
6890            "the widget under the pointer keeps its chrome"
6891        );
6892    }
6893
6894    #[test]
6895    fn event_ctx_hover_reports_the_previous_pass_not_this_ones_claim() {
6896        let mut h = HoverHarness::new(false);
6897        // First move over the top leaf: it was not hovered when its handler ran.
6898        h.move_over(Leaf::Top);
6899        assert!(
6900            !h.top.event_hovered.get(),
6901            "a fresh claim does not retroactively flip `is_hovered`"
6902        );
6903        // Second move over the same leaf: now it observes the link it holds.
6904        h.move_over(Leaf::Top);
6905        assert!(
6906            h.top.event_hovered.get(),
6907            "the link recorded last pass is visible to this pass's handler"
6908        );
6909    }
6910
6911    // --- Cursor: the per-pass request channel ---------------------------------
6912    //
6913    // The cursor is hover's sibling and is deliberately not derived from it (the
6914    // root's hover mirror is identity-free), so it gets its own fixture: two
6915    // leaves that ask for different shapes, a container that can speak before or
6916    // after routing (the last-writer case), and a capture fast-path (the drag
6917    // case, where the shape must survive the pointer leaving the widget).
6918
6919    /// How the cursor fixture's container speaks relative to its children.
6920    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
6921    enum ContainerCursor {
6922        /// Says nothing at all — the ordinary container.
6923        Silent,
6924        /// Asks *before* routing, so whatever the child asks for comes later.
6925        BeforeRouting(CursorIcon),
6926        /// Asks *after* routing, deliberately overriding its child.
6927        AfterRouting(CursorIcon),
6928    }
6929
6930    /// A leaf that asks for one cursor while the pointer is over it and another
6931    /// while it holds the capture — the two shapes a real draggable control wants.
6932    struct CursorLeaf {
6933        hover_icon: Option<CursorIcon>,
6934        drag_icon: Option<CursorIcon>,
6935        captures: bool,
6936        captured: bool,
6937    }
6938
6939    impl crate::widget::Widget for CursorLeaf {
6940        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6941            bc.constrain(Size::new(100.0, 30.0))
6942        }
6943        // A cursor is resolved entirely in the event pass — this fixture never
6944        // paints, unlike the hover one (whose authoritative read is at paint time).
6945        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
6946        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6947            let InputEvent::Pointer(p) = event else {
6948                return EventResult::Ignored;
6949            };
6950            match p.phase {
6951                PointerPhase::Move => {
6952                    if self.captured {
6953                        // The captured pass belongs to this widget wherever the
6954                        // pointer has gone — re-asking here is what keeps the drag
6955                        // shape alive outside its own bounds.
6956                        if let Some(icon) = self.drag_icon {
6957                            ctx.set_cursor(icon);
6958                        }
6959                        return EventResult::Handled;
6960                    }
6961                    let size = ctx.size();
6962                    let inside = p.position.x >= 0.0
6963                        && p.position.y >= 0.0
6964                        && p.position.x < size.width
6965                        && p.position.y < size.height;
6966                    if inside && let Some(icon) = self.hover_icon {
6967                        ctx.set_cursor(icon);
6968                    }
6969                    EventResult::Ignored
6970                }
6971                PointerPhase::Down => {
6972                    if self.captures {
6973                        ctx.capture_pointer();
6974                        self.captured = true;
6975                    }
6976                    EventResult::Handled
6977                }
6978                PointerPhase::Up | PointerPhase::Cancel => {
6979                    self.captured = false;
6980                    EventResult::Ignored
6981                }
6982            }
6983        }
6984    }
6985
6986    /// Two stacked cursor leaves routed exactly like [`HoverPair`], plus the
6987    /// container's own optional request.
6988    struct CursorPair {
6989        top: crate::widget::ChildPod,
6990        bottom: crate::widget::ChildPod,
6991        own: ContainerCursor,
6992    }
6993
6994    impl crate::widget::Widget for CursorPair {
6995        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6996            self.top.layout_child(ctx, bc);
6997            self.top.set_origin(Point::ZERO);
6998            self.bottom.layout_child(ctx, bc);
6999            self.bottom.set_origin(Point::new(0.0, 30.0));
7000            bc.max()
7001        }
7002        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7003        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7004            if let ContainerCursor::BeforeRouting(icon) = self.own {
7005                ctx.set_cursor(icon);
7006            }
7007            let result = self.route(ctx, event);
7008            if let ContainerCursor::AfterRouting(icon) = self.own {
7009                ctx.set_cursor(icon);
7010            }
7011            result
7012        }
7013    }
7014
7015    impl CursorPair {
7016        /// The capture-first, then hit-test routing every real container does.
7017        fn route(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7018            let releases = matches!(
7019                event,
7020                InputEvent::Pointer(p)
7021                    if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
7022            );
7023            for pod in [&mut self.top, &mut self.bottom] {
7024                if pod.is_active() {
7025                    let r = pod.event_child(ctx, event);
7026                    if releases {
7027                        pod.set_active(false);
7028                    }
7029                    return r;
7030                }
7031            }
7032            let pos = event.position();
7033            for pod in [&mut self.top, &mut self.bottom] {
7034                if pod.contains(pos) {
7035                    return pod.event_child(ctx, event);
7036                }
7037            }
7038            EventResult::Ignored
7039        }
7040    }
7041
7042    #[derive(Clone, Copy)]
7043    struct CursorPairView {
7044        own: ContainerCursor,
7045        captures: bool,
7046    }
7047
7048    impl View<()> for CursorPairView {
7049        type Element = CursorPair;
7050        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CursorPair {
7051            CursorPair {
7052                top: crate::widget::ChildPod::new(Box::new(CursorLeaf {
7053                    hover_icon: Some(CursorIcon::Pointer),
7054                    drag_icon: Some(CursorIcon::Grabbing),
7055                    captures: self.captures,
7056                    captured: false,
7057                })),
7058                bottom: crate::widget::ChildPod::new(Box::new(CursorLeaf {
7059                    hover_icon: Some(CursorIcon::Text),
7060                    drag_icon: None,
7061                    captures: false,
7062                    captured: false,
7063                })),
7064                own: self.own,
7065            }
7066        }
7067        fn rebuild(&self, _p: &Self, _e: &mut CursorPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
7068            ChangeFlags::NONE
7069        }
7070    }
7071
7072    /// A `RenderRoot` over the cursor fixture.
7073    struct CursorHarness {
7074        root: RenderRoot<(), CursorPairView>,
7075        state: (),
7076    }
7077
7078    impl CursorHarness {
7079        fn new(own: ContainerCursor, captures: bool) -> Self {
7080            let mut root: RenderRoot<(), CursorPairView> = RenderRoot::new();
7081            let mut state = ();
7082            root.rebuild(
7083                &mut move |_: &mut ()| CursorPairView { own, captures },
7084                &mut state,
7085            );
7086            root.layout(Size::new(100.0, 60.0));
7087            CursorHarness { root, state }
7088        }
7089
7090        fn dispatch(&mut self, phase: PointerPhase, x: f64, y: f64) -> EventOutcome {
7091            let event = InputEvent::Pointer(PointerEvent {
7092                phase,
7093                position: Point::new(x, y),
7094                button: PointerButton::Primary,
7095            });
7096            self.root.event(&mut self.state, &event)
7097        }
7098
7099        fn move_over(&mut self, leaf: Leaf) {
7100            match leaf {
7101                Leaf::Top => self.dispatch(PointerPhase::Move, 50.0, 15.0),
7102                Leaf::Bottom => self.dispatch(PointerPhase::Move, 50.0, 45.0),
7103            };
7104        }
7105
7106        fn cursor(&self) -> CursorIcon {
7107            self.root.cursor()
7108        }
7109    }
7110
7111    #[test]
7112    fn a_move_resolves_the_requested_cursor_and_absence_resolves_default() {
7113        let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7114        assert_eq!(
7115            h.cursor(),
7116            CursorIcon::Default,
7117            "nothing has asked for anything yet"
7118        );
7119
7120        h.move_over(Leaf::Top);
7121        assert_eq!(
7122            h.cursor(),
7123            CursorIcon::Pointer,
7124            "the request reached the root"
7125        );
7126
7127        // Moving onto the sibling re-resolves to *its* shape with nothing cleared:
7128        // the pass simply has a different last writer.
7129        h.move_over(Leaf::Bottom);
7130        assert_eq!(h.cursor(), CursorIcon::Text);
7131
7132        // Moving off both: the next pass has no writer at all, and absence is the
7133        // default rather than a stale value — the whole point of a stateless
7134        // request.
7135        h.dispatch(PointerPhase::Move, 50.0, 200.0);
7136        assert_eq!(
7137            h.cursor(),
7138            CursorIcon::Default,
7139            "a widget that stops asking falls back with nothing to clear"
7140        );
7141    }
7142
7143    #[test]
7144    fn a_cursor_change_does_not_ask_for_a_repaint() {
7145        // Applying a cursor is a platform call the shell makes after the pass, with
7146        // no frame involved; folding it into `needs_redraw` would repaint the whole
7147        // tree on every hover move.
7148        let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7149        let outcome = h.dispatch(PointerPhase::Move, 50.0, 15.0);
7150        assert_eq!(h.cursor(), CursorIcon::Pointer);
7151        assert!(
7152            !outcome.needs_redraw,
7153            "a cursor request alone never schedules a frame"
7154        );
7155    }
7156
7157    #[test]
7158    fn the_last_writer_on_the_routed_path_wins() {
7159        // A container that asks before routing loses to its child: the child's
7160        // handler runs later in the same pass, which is what makes a specific
7161        // control override the generic surface behind it.
7162        let mut h =
7163            CursorHarness::new(ContainerCursor::BeforeRouting(CursorIcon::ColResize), false);
7164        h.move_over(Leaf::Top);
7165        assert_eq!(
7166            h.cursor(),
7167            CursorIcon::Pointer,
7168            "the innermost widget the route reached spoke last"
7169        );
7170
7171        // ...and the container's own request still resolves where no child asks
7172        // (moving off both leaves leaves the container as the only writer).
7173        h.dispatch(PointerPhase::Move, 50.0, 200.0);
7174        assert_eq!(h.cursor(), CursorIcon::ColResize);
7175
7176        // The deliberate override is the mirror case: asking *after* routing beats
7177        // the child.
7178        let mut h =
7179            CursorHarness::new(ContainerCursor::AfterRouting(CursorIcon::NotAllowed), false);
7180        h.move_over(Leaf::Top);
7181        assert_eq!(
7182            h.cursor(),
7183            CursorIcon::NotAllowed,
7184            "a container overriding its children asks after routing"
7185        );
7186    }
7187
7188    #[test]
7189    fn only_a_pointer_move_re_resolves_the_cursor() {
7190        let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7191        h.move_over(Leaf::Top);
7192        assert_eq!(h.cursor(), CursorIcon::Pointer);
7193
7194        // A press/release says nothing about the cursor, and must not blink it back
7195        // to `Default` for the duration of a click.
7196        h.dispatch(PointerPhase::Down, 50.0, 15.0);
7197        assert_eq!(h.cursor(), CursorIcon::Pointer, "a Down leaves it standing");
7198        h.dispatch(PointerPhase::Up, 50.0, 15.0);
7199        assert_eq!(h.cursor(), CursorIcon::Pointer, "an Up leaves it standing");
7200
7201        // Neither does a scroll, a key, or the housekeeping broadcast.
7202        h.root.event(
7203            &mut h.state,
7204            &InputEvent::Scroll {
7205                position: Point::new(50.0, 15.0),
7206                delta: crate::event::ScrollDelta::Lines(0.0, 1.0),
7207            },
7208        );
7209        assert_eq!(h.cursor(), CursorIcon::Pointer);
7210        h.root.event(&mut h.state, &InputEvent::Housekeeping);
7211        assert_eq!(h.cursor(), CursorIcon::Pointer);
7212    }
7213
7214    #[test]
7215    fn a_captured_drag_keeps_the_capturing_widgets_cursor() {
7216        let mut h = CursorHarness::new(ContainerCursor::Silent, true);
7217        h.move_over(Leaf::Top);
7218        assert_eq!(h.cursor(), CursorIcon::Pointer);
7219
7220        // Press the top leaf: it captures. The `Down` itself resolves nothing.
7221        h.dispatch(PointerPhase::Down, 50.0, 15.0);
7222        assert!(h.root.is_pointer_captured());
7223        assert_eq!(h.cursor(), CursorIcon::Pointer);
7224
7225        // Drag inside, then well outside its own bounds and over the sibling: every
7226        // one of these moves routes to the captured leaf alone, so its drag shape is
7227        // the only request in the pass — the sibling's `Text` never gets a say.
7228        h.dispatch(PointerPhase::Move, 50.0, 16.0);
7229        assert_eq!(h.cursor(), CursorIcon::Grabbing);
7230        h.dispatch(PointerPhase::Move, 50.0, 45.0);
7231        assert_eq!(
7232            h.cursor(),
7233            CursorIcon::Grabbing,
7234            "a captured drag keeps its own cursor outside its bounds"
7235        );
7236        h.dispatch(PointerPhase::Move, 50.0, 500.0);
7237        assert_eq!(h.cursor(), CursorIcon::Grabbing);
7238
7239        // Release, then a fresh uncaptured move: the ordinary hit-tested resolution
7240        // is back, and the drag shape is gone with nothing cleared.
7241        h.dispatch(PointerPhase::Up, 50.0, 45.0);
7242        h.move_over(Leaf::Bottom);
7243        assert_eq!(h.cursor(), CursorIcon::Text);
7244    }
7245
7246    use crate::event::{EditCommand, Key, KeyEvent, Modifiers, NamedKey};
7247
7248    // --- Clipboard: the per-pass write / paste-request channels ---------------
7249    //
7250    // The cursor's two siblings, resolved by the same bracket but committed on
7251    // every pass rather than on a pointer `Move` alone, and drained one-shot
7252    // rather than read as a standing level. The fixture is the cursor pair's
7253    // shape with focus in place of hit testing, because a clipboard verb is
7254    // focus-routed: two stacked leaves, a container that can speak before or
7255    // after routing (the last-writer case) or ask for a paste of its own (the
7256    // two-channels-in-one-pass case).
7257
7258    /// How the clipboard fixture's container speaks relative to its children.
7259    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7260    enum ContainerClipboard {
7261        /// Says nothing at all — the ordinary container.
7262        Silent,
7263        /// Writes *before* routing, so whatever the child writes comes later.
7264        WritesBeforeRouting(&'static str),
7265        /// Writes *after* routing, deliberately overriding its child.
7266        WritesAfterRouting(&'static str),
7267        /// Asks for a paste before routing — a toolbar refreshing whether its
7268        /// paste button should be enabled, which is how a write and a request
7269        /// legitimately ride one pass.
7270        AsksForPaste,
7271    }
7272
7273    /// A focusable editable stand-in: claims focus on a `Down`, answers a
7274    /// copy/cut by writing its "selection", and asks for the clipboard when it
7275    /// sees the hardware [`NamedKey::Paste`] key it decoded itself.
7276    struct ClipboardLeaf {
7277        /// What this leaf would copy.
7278        text: &'static str,
7279        /// Every [`EditCommand`] this leaf was handed, in order — the proof of
7280        /// who the focus routing actually reached.
7281        seen: Rc<RefCell<Vec<EditCommand>>>,
7282    }
7283
7284    impl crate::widget::Widget for ClipboardLeaf {
7285        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7286            bc.constrain(Size::new(100.0, 30.0))
7287        }
7288        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7289        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7290            match event {
7291                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
7292                    ctx.request_focus();
7293                    EventResult::Handled
7294                }
7295                InputEvent::EditCommand(cmd) => {
7296                    self.seen.borrow_mut().push(cmd.clone());
7297                    match cmd {
7298                        // The widget owns the selection, so it is the only thing
7299                        // that can say what "copy" means.
7300                        EditCommand::Copy | EditCommand::Cut => {
7301                            ctx.write_clipboard(self.text.to_string());
7302                        }
7303                        // A paste arrives with its text already read by the shell,
7304                        // and select-all touches no clipboard at all.
7305                        EditCommand::Paste(_) | EditCommand::SelectAll => {}
7306                    }
7307                    EventResult::Handled
7308                }
7309                // The hardware clipboard key, decoded by the widget rather than by
7310                // the shell: it carries no text, so the widget asks for some.
7311                InputEvent::Key(k) if k.key == Key::Named(NamedKey::Paste) => {
7312                    ctx.request_paste();
7313                    EventResult::Handled
7314                }
7315                _ => EventResult::Ignored,
7316            }
7317        }
7318    }
7319
7320    /// Two stacked clipboard leaves, routed by focus for a focus-routed event and
7321    /// by hit test for a pointer one, plus the container's own optional request.
7322    struct ClipboardPair {
7323        top: crate::widget::ChildPod,
7324        bottom: crate::widget::ChildPod,
7325        own: ContainerClipboard,
7326    }
7327
7328    impl crate::widget::Widget for ClipboardPair {
7329        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7330            self.top.layout_child(ctx, bc);
7331            self.top.set_origin(Point::ZERO);
7332            self.bottom.layout_child(ctx, bc);
7333            self.bottom.set_origin(Point::new(0.0, 30.0));
7334            bc.max()
7335        }
7336        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7337        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7338            match self.own {
7339                ContainerClipboard::WritesBeforeRouting(text) => {
7340                    ctx.write_clipboard(text.to_string())
7341                }
7342                ContainerClipboard::AsksForPaste => ctx.request_paste(),
7343                _ => {}
7344            }
7345            let result = self.route(ctx, event);
7346            if let ContainerClipboard::WritesAfterRouting(text) = self.own {
7347                ctx.write_clipboard(text.to_string());
7348            }
7349            result
7350        }
7351    }
7352
7353    impl ClipboardPair {
7354        /// Focus routing for a focus-routed event, hit testing for the rest —
7355        /// the two branches every real container has.
7356        fn route(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7357            if event.is_focus_routed() {
7358                for pod in [&mut self.top, &mut self.bottom] {
7359                    if pod.is_focused() {
7360                        return pod.event_child(ctx, event);
7361                    }
7362                }
7363                return EventResult::Ignored;
7364            }
7365            let pos = event.position();
7366            let blurs = matches!(
7367                event,
7368                InputEvent::Pointer(p) if p.phase == PointerPhase::Down
7369            );
7370            let mut result = EventResult::Ignored;
7371            let mut hit = false;
7372            for pod in [&mut self.top, &mut self.bottom] {
7373                if !hit && pod.contains(pos) {
7374                    hit = true;
7375                    result = pod.event_child(ctx, event);
7376                } else if blurs {
7377                    // A `Down` that lands elsewhere blurs the chain, exactly as
7378                    // `frust-widgets`' routers do.
7379                    pod.set_focused(false);
7380                }
7381            }
7382            result
7383        }
7384    }
7385
7386    /// The fixture's view. It carries the two `seen` logs rather than letting the
7387    /// harness reach into the built tree for them: `Widget` has no downcast, and
7388    /// a shared handle is the same way the hover fixture's probe is watched.
7389    #[derive(Clone)]
7390    struct ClipboardPairView {
7391        own: ContainerClipboard,
7392        top_seen: Rc<RefCell<Vec<EditCommand>>>,
7393        bottom_seen: Rc<RefCell<Vec<EditCommand>>>,
7394    }
7395
7396    impl View<()> for ClipboardPairView {
7397        type Element = ClipboardPair;
7398        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ClipboardPair {
7399            ClipboardPair {
7400                top: crate::widget::ChildPod::new(Box::new(ClipboardLeaf {
7401                    text: "top selection",
7402                    seen: self.top_seen.clone(),
7403                })),
7404                bottom: crate::widget::ChildPod::new(Box::new(ClipboardLeaf {
7405                    text: "bottom selection",
7406                    seen: self.bottom_seen.clone(),
7407                })),
7408                own: self.own,
7409            }
7410        }
7411        fn rebuild(&self, _p: &Self, _e: &mut ClipboardPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
7412            ChangeFlags::NONE
7413        }
7414    }
7415
7416    /// A `RenderRoot` over the clipboard fixture, plus the two leaves' `seen`
7417    /// logs (cloned out of the built widgets, which the root owns).
7418    struct ClipboardHarness {
7419        root: RenderRoot<(), ClipboardPairView>,
7420        state: (),
7421        top_seen: Rc<RefCell<Vec<EditCommand>>>,
7422        bottom_seen: Rc<RefCell<Vec<EditCommand>>>,
7423    }
7424
7425    impl ClipboardHarness {
7426        fn new(own: ContainerClipboard) -> Self {
7427            let mut root: RenderRoot<(), ClipboardPairView> = RenderRoot::new();
7428            let mut state = ();
7429            let view = ClipboardPairView {
7430                own,
7431                top_seen: Rc::new(RefCell::new(Vec::new())),
7432                bottom_seen: Rc::new(RefCell::new(Vec::new())),
7433            };
7434            let (top_seen, bottom_seen) = (view.top_seen.clone(), view.bottom_seen.clone());
7435            root.rebuild(&mut move |_: &mut ()| view.clone(), &mut state);
7436            root.layout(Size::new(100.0, 60.0));
7437            ClipboardHarness {
7438                root,
7439                state,
7440                top_seen,
7441                bottom_seen,
7442            }
7443        }
7444
7445        /// Focus a leaf the way a user does: a `Down` inside its bounds.
7446        fn focus(&mut self, leaf: Leaf) {
7447            let y = match leaf {
7448                Leaf::Top => 15.0,
7449                Leaf::Bottom => 45.0,
7450            };
7451            self.root
7452                .event(&mut self.state, &pointer(PointerPhase::Down, 50.0, y));
7453        }
7454
7455        fn dispatch(&mut self, event: &InputEvent) -> EventOutcome {
7456            self.root.event(&mut self.state, event)
7457        }
7458    }
7459
7460    #[test]
7461    fn a_copy_reaches_the_focused_leaf_alone_and_its_text_drains_once() {
7462        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7463        h.focus(Leaf::Top);
7464        assert!(
7465            h.root.take_clipboard_write().is_none(),
7466            "a focusing tap writes nothing"
7467        );
7468
7469        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7470        assert_eq!(
7471            h.top_seen.borrow().as_slice(),
7472            &[EditCommand::Copy],
7473            "the focused leaf received the command"
7474        );
7475        assert!(
7476            h.bottom_seen.borrow().is_empty(),
7477            "and the unfocused sibling never saw it — focus routing, not hit testing"
7478        );
7479        assert_eq!(
7480            h.root.take_clipboard_write().as_deref(),
7481            Some("top selection")
7482        );
7483        assert_eq!(
7484            h.root.take_clipboard_write(),
7485            None,
7486            "the drain is one-shot: a clipboard write is an edge, not a level"
7487        );
7488    }
7489
7490    #[test]
7491    fn a_copy_follows_the_focus_when_it_moves() {
7492        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7493        h.focus(Leaf::Top);
7494        h.focus(Leaf::Bottom);
7495        h.dispatch(&InputEvent::EditCommand(EditCommand::Cut));
7496        assert!(
7497            h.top_seen.borrow().is_empty(),
7498            "the blurred leaf is out of the routed path"
7499        );
7500        assert_eq!(h.bottom_seen.borrow().as_slice(), &[EditCommand::Cut]);
7501        assert_eq!(
7502            h.root.take_clipboard_write().as_deref(),
7503            Some("bottom selection")
7504        );
7505    }
7506
7507    #[test]
7508    fn a_paste_request_drains_once_and_its_answer_is_an_ordinary_dispatch() {
7509        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7510        h.focus(Leaf::Top);
7511        assert!(
7512            !h.root.take_paste_request(),
7513            "a focusing tap asks for nothing"
7514        );
7515
7516        // The widget decoded the hardware Paste key itself and asked the shell.
7517        h.dispatch(&InputEvent::Key(KeyEvent {
7518            key: Key::Named(NamedKey::Paste),
7519            modifiers: Modifiers::default(),
7520            repeat: false,
7521        }));
7522        assert!(h.root.take_paste_request());
7523        assert!(
7524            !h.root.take_paste_request(),
7525            "the flag is one-shot, so a shell answers a request once"
7526        );
7527
7528        // The shell's answer is a new, focus-routed dispatch carrying the text.
7529        h.dispatch(&InputEvent::EditCommand(EditCommand::Paste(
7530            "from the host".to_string(),
7531        )));
7532        assert_eq!(
7533            h.top_seen.borrow().as_slice(),
7534            &[EditCommand::Paste("from the host".to_string())]
7535        );
7536        assert!(
7537            !h.root.take_paste_request(),
7538            "answering a request does not raise a new one"
7539        );
7540        assert!(
7541            h.root.take_clipboard_write().is_none(),
7542            "and a paste writes nothing back to the host clipboard"
7543        );
7544    }
7545
7546    #[test]
7547    fn a_paste_answered_after_a_blur_reaches_nobody() {
7548        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7549        h.focus(Leaf::Top);
7550        h.dispatch(&InputEvent::Key(KeyEvent {
7551            key: Key::Named(NamedKey::Paste),
7552            modifiers: Modifiers::default(),
7553            repeat: false,
7554        }));
7555        assert!(h.root.take_paste_request());
7556
7557        // Focus is released while the shell's clipboard read is in flight: a
7558        // `Down` on empty chrome past both leaves blurs the chain.
7559        h.dispatch(&pointer(PointerPhase::Down, 50.0, 100.0));
7560        assert!(!h.root.is_focus_active(), "the tap blurred the field");
7561
7562        // The shell answers anyway — it never has to track who asked, because the
7563        // answer is focus-routed and simply reaches no widget.
7564        h.dispatch(&InputEvent::EditCommand(EditCommand::Paste(
7565            "from the host".to_string(),
7566        )));
7567        assert!(h.top_seen.borrow().is_empty());
7568        assert!(h.bottom_seen.borrow().is_empty());
7569    }
7570
7571    #[test]
7572    fn the_last_clipboard_write_of_a_pass_wins_at_the_root() {
7573        // A container that writes BEFORE routing yields to its child, exactly as
7574        // `set_cursor` does: the innermost widget the route reaches speaks last.
7575        let mut h = ClipboardHarness::new(ContainerClipboard::WritesBeforeRouting("container"));
7576        h.focus(Leaf::Top);
7577        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7578        assert_eq!(
7579            h.root.take_clipboard_write().as_deref(),
7580            Some("top selection")
7581        );
7582
7583        // A container that writes AFTER routing deliberately overrides it.
7584        let mut h = ClipboardHarness::new(ContainerClipboard::WritesAfterRouting("container"));
7585        h.focus(Leaf::Top);
7586        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7587        assert_eq!(h.root.take_clipboard_write().as_deref(), Some("container"));
7588    }
7589
7590    #[test]
7591    fn a_write_and_a_paste_request_ride_one_pass_independently() {
7592        let mut h = ClipboardHarness::new(ContainerClipboard::AsksForPaste);
7593        h.focus(Leaf::Top);
7594        // The focusing tap already carried the container's ask; drain it so the
7595        // assertion below is about the copy pass alone.
7596        assert!(h.root.take_paste_request());
7597
7598        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7599        assert_eq!(
7600            h.root.take_clipboard_write().as_deref(),
7601            Some("top selection"),
7602            "the leaf's write landed"
7603        );
7604        assert!(
7605            h.root.take_paste_request(),
7606            "and the container's request landed in the same pass"
7607        );
7608    }
7609
7610    #[test]
7611    fn a_pass_that_asks_for_neither_leaves_both_empty() {
7612        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7613        h.focus(Leaf::Top);
7614        // A select-all is a real clipboard verb that touches no clipboard, and a
7615        // pointer move touches nothing at all.
7616        h.dispatch(&InputEvent::EditCommand(EditCommand::SelectAll));
7617        h.dispatch(&pointer(PointerPhase::Move, 50.0, 15.0));
7618        assert_eq!(h.top_seen.borrow().as_slice(), &[EditCommand::SelectAll]);
7619        assert_eq!(h.root.take_clipboard_write(), None);
7620        assert!(!h.root.take_paste_request());
7621    }
7622
7623    #[test]
7624    fn an_edit_command_moves_focus_only_when_the_dispatch_asks() {
7625        // The bookkeeping arm `EditCommand` shares with `Key`/`Ime`: unlike a
7626        // `Down`, it neither claims nor blurs by itself.
7627        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7628        h.focus(Leaf::Top);
7629        assert!(h.root.is_focus_active());
7630        let gen_before = h.root.focus_ime_generation();
7631        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7632        assert!(
7633            h.root.is_focus_active(),
7634            "a clipboard verb leaves the focus session exactly as it found it"
7635        );
7636        assert_eq!(h.root.focus_ime_generation(), gen_before);
7637    }
7638
7639    // ---------------------------------------------------------------------
7640    // Overlay portal: an owner that floats a pod, a sibling painted after it,
7641    // and a root that routes like any hand-written container.
7642    // ---------------------------------------------------------------------
7643
7644    /// What the owner does when a `Down` arrives inside one of its surfaces.
7645    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7646    enum OwnerReaction {
7647        /// Nothing — the commonest shape (a menu item acts on `Up`).
7648        Nothing,
7649        /// Capture the pointer, the drag-from-inside-a-surface shape.
7650        Capture,
7651        /// Claim focus, the text-field-inside-a-popover shape.
7652        Focus,
7653    }
7654
7655    /// One floated surface the fixture's owner registers.
7656    #[derive(Clone, Debug, PartialEq)]
7657    struct SurfaceSpec {
7658        key: OverlayKey,
7659        label: &'static str,
7660        rect: Rect,
7661        band: crate::overlay::OverlayBand,
7662        input: OverlayInput,
7663        outside_tap: OutsideTap,
7664        /// Whether the owner registers it at all this frame — the fixture for
7665        /// "stop registering and the surface stops existing".
7666        register: bool,
7667        on_down: OwnerReaction,
7668        /// The same, for a `Move` inside the surface — a drag threshold latching
7669        /// a capture mid-gesture, the shape that claims one on no `Down` at all.
7670        on_move: OwnerReaction,
7671        /// The same again, for an `Up` — the phase on which a capture claim has
7672        /// nothing left to own, and the one that reaches the surface as an
7673        /// overlay event precisely because no capture was standing to divert it.
7674        on_up: OwnerReaction,
7675        /// Whether the leaf INSIDE the pod claims focus on a `Down` of its own —
7676        /// the editable-in-a-popover shape, distinct from `on_down`'s claim,
7677        /// which the owner makes on the surface's behalf.
7678        pod_claims_focus: bool,
7679        /// Whether that leaf republishes an active IME surface on every paint.
7680        /// Stated unconditionally, so the fixture can publish from a pod holding
7681        /// no focus link at all — the provenance case.
7682        pod_publishes_ime: bool,
7683        /// The same publish, from the leaf's `event` handler instead of its
7684        /// paint — the event-route half of the provenance case, and equally
7685        /// ungated.
7686        pod_publishes_ime_on_event: bool,
7687    }
7688
7689    impl SurfaceSpec {
7690        fn floating(label: &'static str, rect: Rect) -> Self {
7691            Self {
7692                key: OverlayKey::next(),
7693                label,
7694                rect,
7695                band: crate::overlay::OverlayBand::Floating,
7696                input: OverlayInput::Interactive,
7697                outside_tap: OutsideTap::Ignore,
7698                register: true,
7699                on_down: OwnerReaction::Nothing,
7700                on_move: OwnerReaction::Nothing,
7701                on_up: OwnerReaction::Nothing,
7702                pod_claims_focus: false,
7703                pod_publishes_ime: false,
7704                pod_publishes_ime_on_event: false,
7705            }
7706        }
7707    }
7708
7709    /// Everything the overlay fixture recorded, shared between the widgets and
7710    /// the test.
7711    #[derive(Default)]
7712    struct OverlayLog {
7713        /// Events the owner received, in the order they arrived.
7714        owner: Vec<InputEvent>,
7715        /// `(surface label, event)` for everything a floated pod's content saw —
7716        /// positions here are the pod's own local space.
7717        pod: Vec<(&'static str, InputEvent)>,
7718        /// Events the main-tree sibling received.
7719        sibling: Vec<InputEvent>,
7720        /// What the sibling read from `EventCtx::has_focus` on each event.
7721        sibling_focus: Vec<bool>,
7722        /// What the pod's leaf read from `PaintCtx::has_focus` on each paint.
7723        pod_paint_focus: Vec<bool>,
7724        /// The same read for the main-tree sibling. The two together answer
7725        /// "which branches believe they are focused".
7726        sibling_paint_focus: Vec<bool>,
7727        /// The window size the owner observed in its own (child) layout.
7728        child_window_size: Option<Size>,
7729    }
7730
7731    type Log = std::rc::Rc<std::cell::RefCell<OverlayLog>>;
7732
7733    /// The leaf inside a floated pod: paints its label at the pod's absolute
7734    /// origin and records what reaches it.
7735    ///
7736    /// Optionally an editable — it claims focus on a `Down` of its own and
7737    /// republishes an IME surface on every paint. The publish is deliberately
7738    /// ungated: a pod describing a session it holds no focus link for is
7739    /// precisely what the root has to answer for.
7740    struct OverlayContentWidget {
7741        label: &'static str,
7742        log: Log,
7743        claims_focus: bool,
7744        publishes_ime: bool,
7745        publishes_ime_on_event: bool,
7746    }
7747
7748    impl OverlayContentWidget {
7749        /// What such a pod publishes: an ordinary field's surface, carrying no
7750        /// secure-text configuration of its own.
7751        fn surface() -> ImeState {
7752            ImeState {
7753                active: true,
7754                content_type: ImeContentType::Normal,
7755                ..Default::default()
7756            }
7757        }
7758    }
7759
7760    impl crate::widget::Widget for OverlayContentWidget {
7761        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7762            bc.constrain(Size::new(80.0, 30.0))
7763        }
7764        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
7765            scene.draw_text(ctx.origin(), self.label);
7766            self.log.borrow_mut().pod_paint_focus.push(ctx.has_focus());
7767            if self.publishes_ime {
7768                ctx.publish_ime_state(Self::surface());
7769            }
7770        }
7771        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7772            self.log.borrow_mut().pod.push((self.label, event.clone()));
7773            if self.publishes_ime_on_event {
7774                ctx.publish_ime_state(Self::surface());
7775            }
7776            if self.claims_focus
7777                && let InputEvent::Pointer(pointer) = event
7778                && pointer.phase == PointerPhase::Down
7779            {
7780                ctx.request_focus();
7781            }
7782            EventResult::Handled
7783        }
7784    }
7785
7786    /// One live surface: its spec plus the pod the owner keeps.
7787    struct OwnedSurface {
7788        spec: SurfaceSpec,
7789        pod: crate::overlay::OverlayPod,
7790    }
7791
7792    /// The overlay owner: hosts the pods, registers them from `paint`, and
7793    /// forwards the broadcasts the root routes back to it into the right pod.
7794    struct OverlayOwnerWidget {
7795        surfaces: Vec<OwnedSurface>,
7796        log: Log,
7797    }
7798
7799    impl OverlayOwnerWidget {
7800        fn new(specs: &[SurfaceSpec], log: Log) -> Self {
7801            let mut owner = Self {
7802                surfaces: Vec::new(),
7803                log,
7804            };
7805            owner.sync(specs);
7806            owner
7807        }
7808
7809        /// Reconcile the live surfaces against `specs`, keeping each pod alive
7810        /// across a rebuild (a real owner keeps its popover's widget state).
7811        fn sync(&mut self, specs: &[SurfaceSpec]) {
7812            self.surfaces
7813                .retain(|s| specs.iter().any(|n| n.key == s.spec.key));
7814            for spec in specs {
7815                match self.surfaces.iter_mut().find(|s| s.spec.key == spec.key) {
7816                    Some(live) => live.spec = spec.clone(),
7817                    None => {
7818                        let log = std::rc::Rc::clone(&self.log);
7819                        self.surfaces.push(OwnedSurface {
7820                            spec: spec.clone(),
7821                            pod: std::rc::Rc::new(std::cell::RefCell::new(
7822                                crate::widget::ChildPod::new(Box::new(OverlayContentWidget {
7823                                    label: spec.label,
7824                                    log,
7825                                    claims_focus: spec.pod_claims_focus,
7826                                    publishes_ime: spec.pod_publishes_ime,
7827                                    publishes_ime_on_event: spec.pod_publishes_ime_on_event,
7828                                })),
7829                            )),
7830                        });
7831                    }
7832                }
7833            }
7834        }
7835    }
7836
7837    impl crate::widget::Widget for OverlayOwnerWidget {
7838        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7839            // The window size reaches a CHILD's layout unchanged — this widget is
7840            // a `ChildPod` of the fixture's root.
7841            self.log.borrow_mut().child_window_size = Some(ctx.window_size());
7842            // A floated pod is laid out against the WINDOW, never against the
7843            // owner's own constraints: it escapes the owner's box entirely.
7844            let window = BoxConstraints::loose(ctx.window_size());
7845            for surface in &mut self.surfaces {
7846                surface.pod.borrow_mut().layout_child(ctx, &window);
7847            }
7848            bc.constrain(Size::new(40.0, 20.0))
7849        }
7850
7851        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
7852            scene.draw_text(ctx.origin(), "owner");
7853            for surface in &self.surfaces {
7854                if !surface.spec.register {
7855                    continue;
7856                }
7857                // Registered, never painted here: the root paints it last.
7858                ctx.register_overlay(OverlayEntry {
7859                    key: surface.spec.key,
7860                    band: surface.spec.band,
7861                    input: surface.spec.input,
7862                    outside_tap: surface.spec.outside_tap,
7863                    window_rect: surface.spec.rect,
7864                    pod: std::rc::Rc::clone(&surface.pod),
7865                    insets: ctx.window_insets(),
7866                });
7867            }
7868        }
7869
7870        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7871            let InputEvent::Overlay(overlay) = event else {
7872                self.log.borrow_mut().owner.push(event.clone());
7873                return EventResult::Ignored;
7874            };
7875            let Some(surface) = self.surfaces.iter_mut().find(|s| s.spec.key == overlay.key) else {
7876                // Another owner's surface: the broadcast reached us, and we
7877                // ignore it. This fall-through is the whole addressing rule.
7878                return EventResult::Ignored;
7879            };
7880            self.log.borrow_mut().owner.push(event.clone());
7881            match &overlay.kind {
7882                OverlayEventKind::Pointer(pointer) => {
7883                    let reaction = match pointer.phase {
7884                        PointerPhase::Down => Some(surface.spec.on_down),
7885                        PointerPhase::Move => Some(surface.spec.on_move),
7886                        PointerPhase::Up => Some(surface.spec.on_up),
7887                        _ => None,
7888                    };
7889                    match reaction {
7890                        Some(OwnerReaction::Capture) => ctx.capture_pointer(),
7891                        Some(OwnerReaction::Focus) => ctx.request_focus(),
7892                        Some(OwnerReaction::Nothing) | None => {}
7893                    }
7894                    // Window space → the pod's own space is one subtraction: the
7895                    // registered rect's origin.
7896                    let local = InputEvent::Pointer(PointerEvent {
7897                        position: pointer.position - surface.spec.rect.origin().to_vec2(),
7898                        ..*pointer
7899                    });
7900                    surface.pod.borrow_mut().event_child(ctx, &local);
7901                }
7902                OverlayEventKind::Scroll { position, delta } => {
7903                    let local = InputEvent::Scroll {
7904                        position: *position - surface.spec.rect.origin().to_vec2(),
7905                        delta: *delta,
7906                    };
7907                    surface.pod.borrow_mut().event_child(ctx, &local);
7908                }
7909                OverlayEventKind::Scale {
7910                    focal,
7911                    phase,
7912                    scale_delta,
7913                    velocity,
7914                } => {
7915                    let local = InputEvent::Scale(crate::event::ScaleEvent {
7916                        phase: *phase,
7917                        scale_delta: *scale_delta,
7918                        focal: *focal - surface.spec.rect.origin().to_vec2(),
7919                        velocity: *velocity,
7920                    });
7921                    surface.pod.borrow_mut().event_child(ctx, &local);
7922                }
7923                OverlayEventKind::OutsideDown => {}
7924            }
7925            // A broadcast is never consumed, whatever the pod returned.
7926            EventResult::Ignored
7927        }
7928    }
7929
7930    /// The main-tree sibling: painted AFTER the owner, claims focus on a `Down`
7931    /// inside it, and records what it sees.
7932    ///
7933    /// It stands in for a secure text field: the surface it publishes carries
7934    /// [`ImeContentType::Password`], and it republishes that surface on every
7935    /// paint for as long as the pass seeds it focused — the shipped field's
7936    /// republish-and-self-correct shape, in miniature.
7937    struct SiblingLeafWidget {
7938        log: Log,
7939        /// The selection-toolbar request to publish each paint, if any.
7940        publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
7941    }
7942
7943    impl SiblingLeafWidget {
7944        /// The secure surface this field describes its session with.
7945        fn surface() -> ImeState {
7946            ImeState {
7947                active: true,
7948                content_type: ImeContentType::Password,
7949                ..Default::default()
7950            }
7951        }
7952    }
7953
7954    impl crate::widget::Widget for SiblingLeafWidget {
7955        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7956            bc.constrain(Size::new(60.0, 40.0))
7957        }
7958        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
7959            scene.draw_text(ctx.origin(), "sibling");
7960            self.log
7961                .borrow_mut()
7962                .sibling_paint_focus
7963                .push(ctx.has_focus());
7964            if ctx.has_focus() {
7965                ctx.publish_ime_state(Self::surface());
7966            }
7967            if let Some(request) = self.publish {
7968                ctx.publish_selection_toolbar(request);
7969            }
7970        }
7971        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7972            {
7973                let mut log = self.log.borrow_mut();
7974                log.sibling.push(event.clone());
7975                log.sibling_focus.push(ctx.has_focus());
7976            }
7977            match event {
7978                InputEvent::Pointer(pointer) if pointer.phase == PointerPhase::Down => {
7979                    ctx.request_focus();
7980                    ctx.publish_ime_state(Self::surface());
7981                    EventResult::Handled
7982                }
7983                // A scroll is hit-tested like a press but travels the root's
7984                // keyboard-class focus arm (`Scroll | Key | Ime | EditCommand`),
7985                // so claiming focus here is how the fixture expresses a focus
7986                // move that is not a pointer `Down`.
7987                InputEvent::Scroll { .. } => {
7988                    ctx.request_focus();
7989                    ctx.publish_ime_state(Self::surface());
7990                    EventResult::Handled
7991                }
7992                _ => EventResult::Ignored,
7993            }
7994        }
7995    }
7996
7997    /// The fixture's root: a hand-written two-child container routing exactly
7998    /// like `frust-widgets`' helpers — broadcast first, then capture, then focus,
7999    /// then a topmost-first hit test.
8000    struct OverlayRootWidget {
8001        owner: crate::widget::ChildPod,
8002        sibling: crate::widget::ChildPod,
8003    }
8004
8005    impl crate::widget::Widget for OverlayRootWidget {
8006        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
8007            self.owner.layout_child(ctx, bc);
8008            self.owner.set_origin(Point::new(0.0, 0.0));
8009            self.sibling.layout_child(ctx, bc);
8010            self.sibling.set_origin(Point::new(0.0, 100.0));
8011            bc.constrain(Size::new(200.0, 200.0))
8012        }
8013
8014        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
8015            // The owner paints FIRST, the sibling after it — so an overlay pod
8016            // landing after both proves the root's post-pass really is last.
8017            self.owner.paint_child(ctx, scene);
8018            self.sibling.paint_child(ctx, scene);
8019        }
8020
8021        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
8022            if event.is_broadcast() {
8023                self.owner.event_child(ctx, event);
8024                self.sibling.event_child(ctx, event);
8025                return EventResult::Ignored;
8026            }
8027            let ends = matches!(
8028                event,
8029                InputEvent::Pointer(p)
8030                    if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
8031            );
8032            for (pod, _) in [(&mut self.owner, 0), (&mut self.sibling, 1)] {
8033                if pod.is_active() {
8034                    let result = pod.event_child(ctx, event);
8035                    if ends {
8036                        pod.set_active(false);
8037                    }
8038                    return result;
8039                }
8040            }
8041            if event.is_focus_routed() {
8042                // `holds_live_focus`, not `is_focused` — the same read
8043                // `frust-widgets`' `route_event` makes, and the reason this
8044                // fixture can stand in for it: both children can carry a
8045                // recorded link at once, and only one of them can carry the live
8046                // session's.
8047                if self.sibling.holds_live_focus() {
8048                    return self.sibling.event_child(ctx, event);
8049                }
8050                if self.owner.holds_live_focus() {
8051                    return self.owner.event_child(ctx, event);
8052                }
8053                return EventResult::Ignored;
8054            }
8055            // Topmost-first: the sibling paints last, so it hit-tests first.
8056            let position = event.position();
8057            if self.sibling.contains(position) {
8058                return self.sibling.event_child(ctx, event);
8059            }
8060            if self.owner.contains(position) {
8061                return self.owner.event_child(ctx, event);
8062            }
8063            EventResult::Ignored
8064        }
8065    }
8066
8067    /// The view producing the fixture, reconciling the surface specs in place.
8068    struct OverlayRootView {
8069        specs: Vec<SurfaceSpec>,
8070        publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
8071        log: Log,
8072    }
8073
8074    impl View<OverlayState> for OverlayRootView {
8075        type Element = OverlayRootWidget;
8076
8077        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
8078            OverlayRootWidget {
8079                owner: crate::widget::ChildPod::new(Box::new(OverlayOwnerWidget::new(
8080                    &self.specs,
8081                    std::rc::Rc::clone(&self.log),
8082                ))),
8083                sibling: crate::widget::ChildPod::new(Box::new(SiblingLeafWidget {
8084                    log: std::rc::Rc::clone(&self.log),
8085                    publish: self.publish,
8086                })),
8087            }
8088        }
8089
8090        fn rebuild(
8091            &self,
8092            prev: &Self,
8093            element: &mut Self::Element,
8094            _ctx: &mut BuildCtx<'_>,
8095        ) -> ChangeFlags {
8096            element
8097                .owner
8098                .widget_mut()
8099                .downcast_mut::<OverlayOwnerWidget>()
8100                .expect("the owner keeps its type")
8101                .sync(&self.specs);
8102            element
8103                .sibling
8104                .widget_mut()
8105                .downcast_mut::<SiblingLeafWidget>()
8106                .expect("the sibling keeps its type")
8107                .publish = self.publish;
8108            if prev.specs != self.specs || prev.publish != self.publish {
8109                ChangeFlags::PAINT
8110            } else {
8111                ChangeFlags::NONE
8112            }
8113        }
8114    }
8115
8116    /// App state for the overlay fixture: the surface specs the next rebuild
8117    /// applies, plus the shared log.
8118    struct OverlayState {
8119        specs: Vec<SurfaceSpec>,
8120        publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
8121        log: Log,
8122    }
8123
8124    fn overlay_logic(state: &mut OverlayState) -> OverlayRootView {
8125        OverlayRootView {
8126            specs: state.specs.clone(),
8127            publish: state.publish,
8128            log: std::rc::Rc::clone(&state.log),
8129        }
8130    }
8131
8132    /// Drives the overlay fixture the way a shell does: rebuild, layout, paint,
8133    /// then dispatch.
8134    struct OverlayHarness {
8135        root: RenderRoot<OverlayState, OverlayRootView>,
8136        state: OverlayState,
8137        log: Log,
8138    }
8139
8140    impl OverlayHarness {
8141        fn new(specs: Vec<SurfaceSpec>) -> Self {
8142            let log: Log = std::rc::Rc::new(std::cell::RefCell::new(OverlayLog::default()));
8143            let mut harness = Self {
8144                root: RenderRoot::new(),
8145                state: OverlayState {
8146                    specs,
8147                    publish: None,
8148                    log: std::rc::Rc::clone(&log),
8149                },
8150                log,
8151            };
8152            harness.frame();
8153            harness
8154        }
8155
8156        /// One full frame: rebuild, layout at a 200x200 window, paint.
8157        fn frame(&mut self) -> RecordingScene {
8158            self.root.rebuild(&mut overlay_logic, &mut self.state);
8159            self.root.layout(Size::new(200.0, 200.0));
8160            let mut scene = RecordingScene::default();
8161            self.root.paint(&mut scene, FrameTime::ZERO);
8162            scene
8163        }
8164
8165        fn dispatch(&mut self, event: &InputEvent) -> EventOutcome {
8166            self.root.event(&mut self.state, event)
8167        }
8168
8169        fn down(&mut self, x: f64, y: f64) -> EventOutcome {
8170            self.dispatch(&pointer(PointerPhase::Down, x, y))
8171        }
8172
8173        fn clear_log(&mut self) {
8174            let mut log = self.log.borrow_mut();
8175            log.owner.clear();
8176            log.pod.clear();
8177            log.sibling.clear();
8178            log.sibling_focus.clear();
8179            log.pod_paint_focus.clear();
8180            log.sibling_paint_focus.clear();
8181        }
8182
8183        /// What the pod's leaf and the main-tree sibling each read from
8184        /// `PaintCtx::has_focus` on the most recent frame — "which branches
8185        /// believe they are focused", read from the branches themselves.
8186        fn branch_focus(&self) -> (bool, bool) {
8187            let log = self.log.borrow();
8188            (
8189                *log.pod_paint_focus
8190                    .last()
8191                    .expect("the pod painted at least once"),
8192                *log.sibling_paint_focus
8193                    .last()
8194                    .expect("the sibling painted at least once"),
8195            )
8196        }
8197
8198        /// The overlay events the owner received, as `(key, kind)`.
8199        fn owner_overlays(&self) -> Vec<(OverlayKey, OverlayEventKind)> {
8200            self.log
8201                .borrow()
8202                .owner
8203                .iter()
8204                .filter_map(|event| match event {
8205                    InputEvent::Overlay(o) => Some((o.key, o.kind.clone())),
8206                    _ => None,
8207                })
8208                .collect()
8209        }
8210
8211        /// Pointer events the sibling saw — the "did the main tree get it?" read.
8212        fn sibling_pointers(&self) -> Vec<PointerEvent> {
8213            self.log
8214                .borrow()
8215                .sibling
8216                .iter()
8217                .filter_map(|event| match event {
8218                    InputEvent::Pointer(p) => Some(*p),
8219                    _ => None,
8220                })
8221                .collect()
8222        }
8223    }
8224
8225    /// A rect well clear of the sibling (which sits at y >= 100).
8226    fn floating_rect() -> Rect {
8227        Rect::new(120.0, 10.0, 200.0, 60.0)
8228    }
8229
8230    #[test]
8231    fn a_registered_pod_paints_after_a_later_sibling_at_its_window_rect() {
8232        let spec = SurfaceSpec::floating("popover", floating_rect());
8233        let mut h = OverlayHarness::new(vec![spec.clone()]);
8234        let scene = h.frame();
8235
8236        assert_eq!(
8237            scene
8238                .texts
8239                .iter()
8240                .map(|(_, text)| text.as_str())
8241                .collect::<Vec<_>>(),
8242            vec!["owner", "sibling", "popover"],
8243            "the floated pod paints after the owner AND after the sibling painted \
8244             later than the owner — escaping paint order is the whole point"
8245        );
8246        assert_eq!(
8247            scene.texts[2].0,
8248            floating_rect().origin(),
8249            "and it paints at its registered window rect, not at its owner's origin"
8250        );
8251        assert_eq!(h.root.overlay_hits.len(), 1);
8252        assert_eq!(h.root.overlay_hits[0].key, spec.key);
8253        assert_eq!(h.root.overlay_hits[0].window_rect, floating_rect());
8254    }
8255
8256    #[test]
8257    fn two_bands_paint_floating_then_tooltip_whatever_the_registration_order() {
8258        // Registered tooltip-first, so registration order and band order
8259        // disagree: the band must win.
8260        let mut tooltip = SurfaceSpec::floating("tooltip", Rect::new(0.0, 0.0, 40.0, 20.0));
8261        tooltip.band = crate::overlay::OverlayBand::Tooltip;
8262        tooltip.input = OverlayInput::Transparent;
8263        let floating = SurfaceSpec::floating("floating", floating_rect());
8264        let mut h = OverlayHarness::new(vec![tooltip, floating]);
8265        let scene = h.frame();
8266
8267        assert_eq!(
8268            scene
8269                .texts
8270                .iter()
8271                .map(|(_, text)| text.as_str())
8272                .collect::<Vec<_>>(),
8273            vec!["owner", "sibling", "floating", "tooltip"],
8274            "Floating paints below Tooltip regardless of who registered first"
8275        );
8276    }
8277
8278    #[test]
8279    fn the_registry_is_empty_at_the_start_of_every_paint() {
8280        let spec = SurfaceSpec::floating("popover", floating_rect());
8281        let mut h = OverlayHarness::new(vec![spec.clone()]);
8282        h.frame();
8283        assert_eq!(h.root.overlay_hits.len(), 1);
8284
8285        // Paint again with nothing changed: the entry is re-registered, not
8286        // accumulated — an owner registering every frame must not grow the table.
8287        let scene = h.frame();
8288        assert_eq!(h.root.overlay_hits.len(), 1);
8289        assert_eq!(scene.texts.len(), 3);
8290
8291        // The owner stops registering (its popover closed). There is nothing to
8292        // unregister: the next paint simply does not see it.
8293        h.state.specs[0].register = false;
8294        let scene = h.frame();
8295        assert!(
8296            h.root.overlay_hits.is_empty(),
8297            "a surface nobody registers stops existing after the next paint"
8298        );
8299        assert_eq!(
8300            scene
8301                .texts
8302                .iter()
8303                .map(|(_, text)| text.as_str())
8304                .collect::<Vec<_>>(),
8305            vec!["owner", "sibling"],
8306            "and stops painting"
8307        );
8308
8309        // A `Down` inside where it used to be now reaches the main tree.
8310        h.clear_log();
8311        h.down(150.0, 30.0);
8312        assert!(h.owner_overlays().is_empty());
8313    }
8314
8315    #[test]
8316    fn the_window_size_reaches_a_childs_layout() {
8317        let h = OverlayHarness::new(vec![SurfaceSpec::floating("popover", floating_rect())]);
8318        assert_eq!(
8319            h.log.borrow().child_window_size,
8320            Some(Size::new(200.0, 200.0)),
8321            "a child lays out knowing the window, which is what an overlay pod is \
8322             sized against"
8323        );
8324    }
8325
8326    #[test]
8327    fn a_down_inside_a_floating_surface_reaches_only_its_owner_and_never_blurs() {
8328        let spec = SurfaceSpec::floating("popover", floating_rect());
8329        let mut h = OverlayHarness::new(vec![spec.clone()]);
8330
8331        // A field elsewhere in the main tree takes focus first.
8332        h.down(30.0, 120.0);
8333        assert!(
8334            h.root.is_focus_active(),
8335            "the sibling holds a focus session"
8336        );
8337        let ime_before = h.root.ime_state();
8338        assert!(ime_before.is_some());
8339        let focus_gen_before = h.root.focus_ime_generation();
8340        h.clear_log();
8341
8342        // Now press inside the floated surface.
8343        h.down(150.0, 30.0);
8344
8345        assert_eq!(
8346            h.owner_overlays(),
8347            vec![(
8348                spec.key,
8349                OverlayEventKind::Pointer(PointerEvent {
8350                    phase: PointerPhase::Down,
8351                    position: Point::new(150.0, 30.0),
8352                    button: PointerButton::Primary,
8353                })
8354            )],
8355            "the owner is reached by key, with a WINDOW-space payload"
8356        );
8357        assert_eq!(
8358            h.log.borrow().pod,
8359            vec![(
8360                "popover",
8361                InputEvent::Pointer(PointerEvent {
8362                    phase: PointerPhase::Down,
8363                    // 150-120, 30-10: the owner's one subtraction, the rect origin.
8364                    position: Point::new(30.0, 20.0),
8365                    button: PointerButton::Primary,
8366                })
8367            )],
8368            "and the owner forwards it into the pod in the pod's own space"
8369        );
8370        assert!(
8371            h.sibling_pointers().is_empty(),
8372            "the main tree never saw the press"
8373        );
8374        assert!(
8375            h.root.is_focus_active(),
8376            "and the press did NOT blur the field the surface belongs to"
8377        );
8378        assert_eq!(
8379            h.root.ime_state(),
8380            ime_before,
8381            "nor disturb its IME surface"
8382        );
8383        assert_eq!(
8384            h.root.focus_ime_generation(),
8385            focus_gen_before,
8386            "so no focus/IME edge fires at the shell either"
8387        );
8388        assert_eq!(
8389            h.log.borrow().sibling_focus.last(),
8390            Some(&true),
8391            "the focused leaf still reads as focused when the broadcast reaches it"
8392        );
8393    }
8394
8395    #[test]
8396    fn a_down_inside_a_transparent_surface_reaches_the_main_tree_normally() {
8397        // The transparent surface covers the sibling exactly.
8398        let mut spec = SurfaceSpec::floating("tooltip", Rect::new(0.0, 100.0, 60.0, 140.0));
8399        spec.input = OverlayInput::Transparent;
8400        spec.outside_tap = OutsideTap::Notify { consume: true };
8401        let mut h = OverlayHarness::new(vec![spec]);
8402        h.frame();
8403        h.clear_log();
8404
8405        h.down(30.0, 120.0);
8406
8407        assert!(
8408            h.owner_overlays().is_empty(),
8409            "a transparent surface is never hit-tested — not even for OutsideDown"
8410        );
8411        assert_eq!(
8412            h.sibling_pointers().len(),
8413            1,
8414            "the pointer passed straight through to the widget underneath"
8415        );
8416        assert!(h.root.is_focus_active(), "which claimed focus as usual");
8417    }
8418
8419    #[test]
8420    fn a_scroll_inside_a_surface_routes_to_its_owner_in_window_space() {
8421        let spec = SurfaceSpec::floating("popover", floating_rect());
8422        let mut h = OverlayHarness::new(vec![spec.clone()]);
8423        h.clear_log();
8424
8425        h.dispatch(&InputEvent::Scroll {
8426            position: Point::new(150.0, 30.0),
8427            delta: ScrollDelta::Lines(0.0, 3.0),
8428        });
8429
8430        assert_eq!(
8431            h.owner_overlays(),
8432            vec![(
8433                spec.key,
8434                OverlayEventKind::Scroll {
8435                    position: Point::new(150.0, 30.0),
8436                    delta: ScrollDelta::Lines(0.0, 3.0),
8437                }
8438            )]
8439        );
8440        assert_eq!(
8441            h.log.borrow().pod,
8442            vec![(
8443                "popover",
8444                InputEvent::Scroll {
8445                    position: Point::new(30.0, 20.0),
8446                    delta: ScrollDelta::Lines(0.0, 3.0),
8447                }
8448            )]
8449        );
8450    }
8451
8452    #[test]
8453    fn a_capture_from_inside_a_surface_routes_the_next_move_by_the_capture_path() {
8454        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8455        spec.on_down = OwnerReaction::Capture;
8456        let mut h = OverlayHarness::new(vec![spec.clone()]);
8457        h.clear_log();
8458
8459        h.down(150.0, 30.0);
8460        assert!(
8461            h.root.is_pointer_captured(),
8462            "a capture bubbled from an overlay Down opens a gesture exactly like a \
8463             hit-tested one"
8464        );
8465        h.clear_log();
8466
8467        // Still INSIDE the surface's own rect — which is the case that
8468        // discriminates: the pre-pass would happily hit-test this one and route it
8469        // as another broadcast, and it must not, because the gesture is captured.
8470        h.dispatch(&pointer(PointerPhase::Move, 160.0, 45.0));
8471        assert!(
8472            h.owner_overlays().is_empty(),
8473            "a live capture short-circuits the overlay pre-pass even inside the \
8474             surface's own rect"
8475        );
8476        assert_eq!(
8477            h.log.borrow().owner,
8478            vec![InputEvent::Pointer(PointerEvent {
8479                phase: PointerPhase::Move,
8480                position: Point::new(160.0, 45.0),
8481                button: PointerButton::Primary,
8482            })],
8483            "the move reaches the owner by the ordinary captured path instead"
8484        );
8485        h.clear_log();
8486
8487        // And the drag may wander far outside the surface — over the sibling, in
8488        // fact — without the sibling ever hearing about it.
8489        h.dispatch(&pointer(PointerPhase::Move, 30.0, 120.0));
8490
8491        assert!(h.owner_overlays().is_empty());
8492        assert_eq!(
8493            h.log.borrow().owner,
8494            vec![InputEvent::Pointer(PointerEvent {
8495                phase: PointerPhase::Move,
8496                position: Point::new(30.0, 120.0),
8497                button: PointerButton::Primary,
8498            })],
8499            "which is what lets a drag begun inside a floated surface continue \
8500             outside it"
8501        );
8502        assert!(
8503            h.sibling_pointers().is_empty(),
8504            "and never reaches the widget it passed over"
8505        );
8506
8507        // The `Up` closes the gesture through the ordinary pointer arm.
8508        h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
8509        assert!(!h.root.is_pointer_captured());
8510    }
8511
8512    /// A capture claimed on a phase other than `Down` is a capture all the
8513    /// same. The pods record their own active path on any phase, so a root that
8514    /// mirrored the `Down` alone left the surface latched with no `Up` able to
8515    /// reach it — and a latched surface diverts every later pointer event its
8516    /// owner is reached by.
8517    #[test]
8518    fn a_capture_claimed_on_a_move_inside_a_surface_reaches_the_root() {
8519        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8520        spec.on_move = OwnerReaction::Capture;
8521        let mut h = OverlayHarness::new(vec![spec]);
8522
8523        h.dispatch(&pointer(PointerPhase::Move, 150.0, 30.0));
8524        assert!(
8525            h.root.is_pointer_captured(),
8526            "the root mirrors a claim the surface made mid-gesture"
8527        );
8528
8529        // Which is what ends it: the mirrored capture short-circuits the overlay
8530        // pre-pass, so the `Up` routes down the capture path — outside the
8531        // surface's own rect, where a hit test would never have delivered it.
8532        h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
8533        assert!(
8534            !h.root.is_pointer_captured(),
8535            "and the gesture closes on the ordinary pointer arm"
8536        );
8537    }
8538
8539    /// The release side of the root's overlay capture mirror. The claim is
8540    /// mirrored on any phase, so the release has to be bounded on any phase too:
8541    /// an `Up` reaches a surface as an overlay event exactly when no capture was
8542    /// standing to divert it, and a claim made there has no gesture left to own.
8543    /// A latch left standing short-circuits the overlay pre-pass, which stops
8544    /// every floated surface taking input until some unrelated pointer release
8545    /// happens along.
8546    #[test]
8547    fn a_capture_claimed_on_an_overlay_up_does_not_outlive_the_gesture() {
8548        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8549        spec.on_up = OwnerReaction::Capture;
8550        let mut h = OverlayHarness::new(vec![spec]);
8551
8552        h.down(150.0, 30.0);
8553        h.dispatch(&pointer(PointerPhase::Up, 150.0, 30.0));
8554
8555        assert!(
8556            !h.root.is_pointer_captured(),
8557            "the phase that ends a gesture releases the mirror it just set"
8558        );
8559
8560        // Which is what keeps the surfaces routable: a press inside one still
8561        // reaches its owner rather than being diverted down a capture path.
8562        h.clear_log();
8563        h.down(150.0, 30.0);
8564        assert_eq!(
8565            h.owner_overlays().len(),
8566            1,
8567            "the overlay pre-pass is still hit-testing"
8568        );
8569    }
8570
8571    #[test]
8572    fn a_focus_request_from_inside_a_surface_opens_a_session() {
8573        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8574        spec.on_down = OwnerReaction::Focus;
8575        let mut h = OverlayHarness::new(vec![spec]);
8576        assert!(!h.root.is_focus_active());
8577
8578        h.down(150.0, 30.0);
8579
8580        assert!(
8581            h.root.is_focus_active(),
8582            "a text field inside a popover may claim focus — the overlay arm \
8583             honours the request even though it refuses the blur"
8584        );
8585    }
8586
8587    /// The case the test above cannot express: the claim arrives while ANOTHER
8588    /// branch already holds the session. Honouring it without retiring what it
8589    /// supersedes leaves two branches believing they are focused — and leaves
8590    /// the root describing, to the shell, a field that no longer owns anything.
8591    #[test]
8592    fn a_pod_focus_claim_retires_the_branch_it_supersedes() {
8593        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8594        spec.pod_claims_focus = true;
8595        let mut h = OverlayHarness::new(vec![spec]);
8596
8597        // A secure field in the main tree takes the session first.
8598        h.down(30.0, 120.0);
8599        h.frame();
8600        assert_eq!(
8601            h.branch_focus(),
8602            (false, true),
8603            "only the field's own branch reads focused while it holds the session"
8604        );
8605        assert_eq!(
8606            h.root.ime_state().map(|ime| ime.content_type),
8607            Some(ImeContentType::Password),
8608            "and the surface the shell configures its keyboard from is the \
8609             secure one that field published"
8610        );
8611
8612        // Now the editable inside the floated surface claims focus.
8613        h.down(150.0, 30.0);
8614        assert!(h.root.is_focus_active(), "the claim is honoured");
8615
8616        // The claim stamped its own chain with a session identity nothing older
8617        // carries, so the field's link stops counting on the very next pass —
8618        // no convergence frame, and nothing had to visit the branch the session
8619        // left in order to clear it.
8620        h.frame();
8621        assert_eq!(
8622            h.branch_focus(),
8623            (true, false),
8624            "exactly one branch believes it is focused: the one that claimed it"
8625        );
8626        assert_eq!(
8627            h.root.ime_state(),
8628            None,
8629            "and the surface that field published for the session it just lost \
8630             does NOT stand: the branch that took the session described none of \
8631             its own, so the shell is left configuring nothing rather than a \
8632             secure field nobody is in"
8633        );
8634
8635        h.frame();
8636        assert_eq!(
8637            h.branch_focus(),
8638            (true, false),
8639            "which is a settled state, not a frame of transition"
8640        );
8641        assert!(
8642            h.root.is_focus_active(),
8643            "and the session the pod opened is still the live one"
8644        );
8645    }
8646
8647    /// The return direction, which the test above never exercises: the session
8648    /// goes to a floated surface and the user then presses the field in the main
8649    /// tree again.
8650    ///
8651    /// A press outside every floated rect is hit-tested through the containers
8652    /// like any other, so the claim it produces is the main tree's by
8653    /// construction — and the surface's own recorded link has to stop counting
8654    /// the moment that claim lands, or the tree is de-seeded for the rest of the
8655    /// pod's life and the secure surface the field publishes is thrown away
8656    /// every frame.
8657    #[test]
8658    fn a_hit_tested_claim_takes_the_session_back_from_a_floated_surface() {
8659        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8660        spec.pod_claims_focus = true;
8661        let mut h = OverlayHarness::new(vec![spec]);
8662
8663        // The field takes the session, then the editable inside the surface
8664        // takes it away.
8665        h.down(30.0, 120.0);
8666        h.frame();
8667        h.down(150.0, 30.0);
8668        h.frame();
8669        assert_eq!(
8670            h.branch_focus(),
8671            (true, false),
8672            "the surface holds the session and the field's link no longer counts"
8673        );
8674
8675        // The user presses the field again, outside `floating_rect()`, so the
8676        // press routes through the containers exactly as the first one did.
8677        h.down(30.0, 120.0);
8678        h.frame();
8679
8680        assert_eq!(
8681            h.branch_focus(),
8682            (false, true),
8683            "the session is the field's again and the surface's link is retired"
8684        );
8685        assert_eq!(
8686            h.root.ime_state().map(|ime| ime.content_type),
8687            Some(ImeContentType::Password),
8688            "and the secure surface that field publishes reaches the shell"
8689        );
8690    }
8691
8692    /// The same move, driven from the root's keyboard-class arm
8693    /// (`Scroll | Key | Ime | EditCommand`) rather than a pointer `Down`. That
8694    /// arm honours the claim and nothing else, so a mechanism that only retires
8695    /// a surface's link on a press leaves the session split here.
8696    #[test]
8697    fn a_keyboard_class_claim_in_the_tree_retires_a_surfaces_link() {
8698        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8699        spec.pod_claims_focus = true;
8700        let mut h = OverlayHarness::new(vec![spec]);
8701
8702        h.down(150.0, 30.0);
8703        h.frame();
8704        assert!(h.branch_focus().0, "the surface holds the session");
8705
8706        // A scroll over the field, outside every floated rect: hit-tested into
8707        // the main tree, but routed through the root's non-pointer focus arm.
8708        h.dispatch(&InputEvent::Scroll {
8709            position: Point::new(30.0, 120.0),
8710            delta: ScrollDelta::Lines(0.0, 3.0),
8711        });
8712        h.frame();
8713
8714        assert_eq!(
8715            h.branch_focus(),
8716            (false, true),
8717            "a claim that arrives without a pointer `Down` retires the surface's \
8718             link just the same"
8719        );
8720        assert_eq!(
8721            h.root.ime_state().map(|ime| ime.content_type),
8722            Some(ImeContentType::Password),
8723        );
8724    }
8725
8726    /// A pod dropped while it holds the link takes the session with it. Nothing
8727    /// else can end that session: the widget that owned it no longer exists, so
8728    /// no later pass can reach it to release it, and the root would otherwise go
8729    /// on reporting a live focus session to the shell for a surface nobody can
8730    /// see.
8731    #[test]
8732    fn a_pod_dropped_while_it_holds_the_link_ends_the_session_it_owned() {
8733        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8734        spec.pod_claims_focus = true;
8735        let mut h = OverlayHarness::new(vec![spec]);
8736
8737        h.down(150.0, 30.0);
8738        h.frame();
8739        assert!(h.root.is_focus_active(), "the surface holds the session");
8740
8741        // The owner stops owning the surface: the spec goes, the pod is dropped
8742        // by the next rebuild.
8743        h.state.specs.clear();
8744        h.frame();
8745
8746        assert!(
8747            !h.root.is_focus_active(),
8748            "the session dies with the widget that held it"
8749        );
8750        assert_eq!(h.root.ime_state(), None);
8751
8752        // And the tree is not left de-seeded: the next press focuses normally.
8753        h.down(30.0, 120.0);
8754        h.frame();
8755        assert!(
8756            h.branch_focus().1,
8757            "a field in the tree takes the session as if no surface had existed"
8758        );
8759        assert_eq!(
8760            h.root.ime_state().map(|ime| ime.content_type),
8761            Some(ImeContentType::Password),
8762        );
8763    }
8764
8765    /// The event-route half of the provenance rule. `paint_overlays` refuses a
8766    /// paint-time publish from a pod holding no link; the dispatch that carries
8767    /// an overlay broadcast has to refuse the same publish, or a surface the
8768    /// user merely touched reconfigures the platform keyboard for a secure field
8769    /// it has nothing to do with.
8770    #[test]
8771    fn a_pod_publish_from_an_event_cannot_overwrite_a_field_it_does_not_own() {
8772        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8773        // Publishes from its `event`, and never claims focus.
8774        spec.pod_publishes_ime_on_event = true;
8775        let mut h = OverlayHarness::new(vec![spec]);
8776
8777        h.down(30.0, 120.0);
8778        assert_eq!(
8779            h.root.ime_state().map(|ime| ime.content_type),
8780            Some(ImeContentType::Password),
8781            "the focused field's own surface"
8782        );
8783
8784        // A press inside the surface. The pod publishes, holds no focus link,
8785        // and claims none.
8786        h.down(150.0, 30.0);
8787
8788        assert_eq!(
8789            h.root.ime_state().map(|ime| ime.content_type),
8790            Some(ImeContentType::Password),
8791            "a publish from a branch that owns no session describes nobody's"
8792        );
8793        assert!(
8794            h.root.is_focus_active(),
8795            "and the field it did not own still holds the session"
8796        );
8797    }
8798
8799    /// A pod publishing an IME surface it holds no focus link for describes
8800    /// nobody's session. Accepting it would let a surface overwrite a focused
8801    /// field's — including the content type that configures the platform
8802    /// keyboard as a secure one.
8803    #[test]
8804    fn a_pod_publish_cannot_overwrite_the_surface_of_a_field_it_does_not_own() {
8805        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8806        // Publishes on every paint, and never claims focus.
8807        spec.pod_publishes_ime = true;
8808        let mut h = OverlayHarness::new(vec![spec]);
8809
8810        h.down(30.0, 120.0);
8811        assert_eq!(
8812            h.root.ime_state().map(|ime| ime.content_type),
8813            Some(ImeContentType::Password),
8814            "the focused field's own surface"
8815        );
8816
8817        h.frame();
8818
8819        assert!(
8820            h.root.is_focus_active(),
8821            "the field still holds the session"
8822        );
8823        assert_eq!(
8824            h.root.ime_state().map(|ime| ime.content_type),
8825            Some(ImeContentType::Password),
8826            "and it still describes it: the publishing pod holds no focus link, \
8827             so its surface is not the session's"
8828        );
8829    }
8830
8831    /// The provenance rule is a check, not a refusal: a pod that DOES hold the
8832    /// recorded focus path publishes exactly like a field in the tree.
8833    #[test]
8834    fn a_pod_that_holds_the_focus_path_publishes_its_own_surface() {
8835        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8836        spec.pod_claims_focus = true;
8837        spec.pod_publishes_ime = true;
8838        let mut h = OverlayHarness::new(vec![spec]);
8839
8840        h.down(30.0, 120.0);
8841        h.down(150.0, 30.0);
8842        h.frame();
8843
8844        assert!(h.root.is_focus_active());
8845        assert_eq!(
8846            h.root.ime_state().map(|ime| ime.content_type),
8847            Some(ImeContentType::Normal),
8848            "the surface published by the branch that owns the session"
8849        );
8850    }
8851
8852    #[test]
8853    fn an_outside_press_notifies_a_consuming_surface_and_stops_there() {
8854        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8855        spec.outside_tap = OutsideTap::Notify { consume: true };
8856        let mut h = OverlayHarness::new(vec![spec.clone()]);
8857        h.clear_log();
8858
8859        // Inside the sibling, outside every floated rect.
8860        h.down(30.0, 120.0);
8861
8862        assert_eq!(
8863            h.owner_overlays(),
8864            vec![(spec.key, OverlayEventKind::OutsideDown)],
8865            "the light-dismiss notification carries no position"
8866        );
8867        assert!(
8868            h.sibling_pointers().is_empty(),
8869            "and the press that dismissed the menu did not also activate what was \
8870             underneath it"
8871        );
8872        assert!(
8873            !h.root.is_focus_active(),
8874            "the main tree saw no Down at all, so nothing claimed focus"
8875        );
8876    }
8877
8878    #[test]
8879    fn a_pass_through_outside_press_notifies_and_still_reaches_the_main_tree() {
8880        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8881        spec.outside_tap = OutsideTap::Notify { consume: false };
8882        let mut h = OverlayHarness::new(vec![spec.clone()]);
8883        h.clear_log();
8884
8885        let outcome = h.down(30.0, 120.0);
8886
8887        assert_eq!(
8888            h.owner_overlays(),
8889            vec![(spec.key, OverlayEventKind::OutsideDown)]
8890        );
8891        assert_eq!(
8892            h.sibling_pointers().len(),
8893            1,
8894            "consume: false means both — the owner hears, and the press continues"
8895        );
8896        assert!(h.root.is_focus_active(), "so the tapped field took focus");
8897        assert!(
8898            outcome.handled,
8899            "and the main dispatch's own outcome survives"
8900        );
8901    }
8902
8903    #[test]
8904    fn an_ignoring_surface_hears_nothing_about_an_outside_press() {
8905        // `Ignore` is the default; state it explicitly.
8906        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8907        spec.outside_tap = OutsideTap::Ignore;
8908        let mut h = OverlayHarness::new(vec![spec]);
8909        h.clear_log();
8910
8911        h.down(30.0, 120.0);
8912
8913        assert!(
8914            h.owner_overlays().is_empty(),
8915            "a surface that dismisses some other way is never told"
8916        );
8917        assert_eq!(h.sibling_pointers().len(), 1);
8918    }
8919
8920    #[test]
8921    fn only_a_primary_press_dismisses() {
8922        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8923        spec.outside_tap = OutsideTap::Notify { consume: true };
8924        let mut h = OverlayHarness::new(vec![spec]);
8925        h.clear_log();
8926
8927        // A secondary press is a context gesture, not a dismissal.
8928        h.dispatch(&InputEvent::Pointer(PointerEvent {
8929            phase: PointerPhase::Down,
8930            position: Point::new(30.0, 120.0),
8931            button: PointerButton::Secondary,
8932        }));
8933        assert!(h.owner_overlays().is_empty());
8934        assert_eq!(h.sibling_pointers().len(), 1, "and it reaches the tree");
8935
8936        // Nor does a move or a lift outside the surface.
8937        h.clear_log();
8938        h.dispatch(&pointer(PointerPhase::Move, 30.0, 120.0));
8939        h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
8940        assert!(h.owner_overlays().is_empty());
8941    }
8942
8943    #[test]
8944    fn an_overlay_press_leaves_the_main_trees_hover_standing() {
8945        let spec = SurfaceSpec::floating("popover", floating_rect());
8946        let mut h = OverlayHarness::new(vec![spec]);
8947        let epoch_before = h.root.hover_epoch;
8948
8949        h.down(150.0, 30.0);
8950
8951        assert_eq!(
8952            h.root.hover_epoch, epoch_before,
8953            "an overlay pass advances no hover epoch, so a live hover in the main \
8954             tree is not stranded by a press on a floated surface"
8955        );
8956    }
8957
8958    // ---------------------------------------------------------------------
8959    // Selection toolbar: publish, generation, clear.
8960    // ---------------------------------------------------------------------
8961
8962    /// A request with the menu up, anchored at `x` — the shape a field
8963    /// publishes while its bar stands.
8964    fn toolbar_request(x: f64) -> crate::selection_toolbar::SelectionToolbarRequest {
8965        crate::selection_toolbar::SelectionToolbarRequest {
8966            anchor: Rect::new(x, 100.0, x + 50.0, 120.0),
8967            actions: crate::selection_toolbar::SelectionToolbarActions {
8968                copy: true,
8969                cut: true,
8970                paste: false,
8971                select_all: true,
8972            },
8973            present_menu: true,
8974        }
8975    }
8976
8977    /// The same request with no menu wanted — what a focused field publishes
8978    /// with nothing on screen, so the platform can still answer "may I offer
8979    /// Copy?" for a hardware shortcut.
8980    fn toolbar_level(x: f64) -> crate::selection_toolbar::SelectionToolbarRequest {
8981        crate::selection_toolbar::SelectionToolbarRequest {
8982            present_menu: false,
8983            ..toolbar_request(x)
8984        }
8985    }
8986
8987    #[test]
8988    fn a_selection_toolbar_publish_resolves_and_only_a_menu_edge_moves_the_generation() {
8989        let mut h = OverlayHarness::new(vec![]);
8990        // A toolbar describes the FOCUSED field's selection, so open a session
8991        // first — a publish with nothing focused describes nothing.
8992        h.down(30.0, 120.0);
8993        assert!(h.root.is_focus_active());
8994
8995        h.state.publish = Some(toolbar_request(10.0));
8996        h.frame();
8997        assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(10.0)));
8998        let first_gen = h.root.selection_toolbar_generation();
8999        assert!(first_gen > 0, "appearing is an edge");
9000
9001        // The field republishes the same request every frame its selection
9002        // stands: that must not ask the shell to re-present the menu per vsync.
9003        h.frame();
9004        h.frame();
9005        assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(10.0)));
9006        assert_eq!(h.root.selection_toolbar_generation(), first_gen);
9007
9008        // A moved anchor is stored — a shell re-reads it to place a menu it
9009        // already has on screen — but it is NOT a menu edge. This assertion used
9010        // to read `first_gen + 1`: the anchor is recomputed every painted frame
9011        // and a drag that widens a selection moves it on every touch sample, so
9012        // bumping here asked the platform to re-present its menu per sample.
9013        h.state.publish = Some(toolbar_request(60.0));
9014        h.frame();
9015        assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(60.0)));
9016        assert_eq!(
9017            h.root.selection_toolbar_generation(),
9018            first_gen,
9019            "an anchor following the selection is not a reason to re-present"
9020        );
9021
9022        // A verb changing IS: the menu's own contents just changed.
9023        let mut fewer_verbs = toolbar_request(60.0);
9024        fewer_verbs.actions.select_all = false;
9025        h.state.publish = Some(fewer_verbs);
9026        h.frame();
9027        assert_eq!(h.root.selection_toolbar_generation(), first_gen + 1);
9028
9029        // The field blurs: it stops publishing, and the menu goes away with
9030        // nothing retracted.
9031        h.state.publish = None;
9032        h.frame();
9033        assert_eq!(h.root.selection_toolbar(), None);
9034        assert_eq!(
9035            h.root.selection_toolbar_generation(),
9036            first_gen + 2,
9037            "going away is an edge too, or a shell never learns to dismiss"
9038        );
9039
9040        // ...and staying away is not.
9041        h.frame();
9042        assert_eq!(h.root.selection_toolbar_generation(), first_gen + 2);
9043    }
9044
9045    #[test]
9046    fn only_the_menu_flag_going_up_asks_a_shell_to_present() {
9047        let mut h = OverlayHarness::new(vec![]);
9048        h.down(30.0, 120.0);
9049        assert!(h.root.is_focus_active());
9050
9051        // A focused field with no bar up publishes all the same: the verbs are
9052        // the answer a platform responder chain needs for a hardware shortcut
9053        // that arrives with nothing on screen.
9054        h.state.publish = Some(toolbar_level(10.0));
9055        h.frame();
9056        assert_eq!(h.root.selection_toolbar(), Some(toolbar_level(10.0)));
9057        let level_gen = h.root.selection_toolbar_generation();
9058
9059        // Republishing the same level is not an edge, however many frames it
9060        // stands for.
9061        h.frame();
9062        h.frame();
9063        assert_eq!(h.root.selection_toolbar_generation(), level_gen);
9064
9065        // The gesture fires and the field asks for a menu: THAT is the edge.
9066        h.state.publish = Some(toolbar_request(10.0));
9067        h.frame();
9068        assert_eq!(
9069            h.root.selection_toolbar_generation(),
9070            level_gen + 1,
9071            "the flag going false to true is what presents the menu"
9072        );
9073
9074        // And dropping it again is the dismiss edge, with the field still
9075        // focused and still publishing its verbs.
9076        h.state.publish = Some(toolbar_level(10.0));
9077        h.frame();
9078        assert!(h.root.selection_toolbar().is_some());
9079        assert_eq!(h.root.selection_toolbar_generation(), level_gen + 2);
9080    }
9081
9082    #[test]
9083    fn a_pass_that_publishes_nothing_still_moves_the_generation() {
9084        // `RenderRoot::paint` resolves a pass nobody published in to `None`,
9085        // which reads as "no menu, no verbs" and must differ from whatever
9086        // stood — otherwise a shell holding a presented menu never learns to
9087        // put it away.
9088        let mut h = OverlayHarness::new(vec![]);
9089        h.down(30.0, 120.0);
9090        h.state.publish = Some(toolbar_request(10.0));
9091        h.frame();
9092        let standing = h.root.selection_toolbar_generation();
9093
9094        h.state.publish = None;
9095        h.frame();
9096        assert_eq!(h.root.selection_toolbar(), None);
9097        assert_eq!(h.root.selection_toolbar_generation(), standing + 1);
9098    }
9099
9100    #[test]
9101    fn a_blur_clears_the_selection_toolbar_on_the_event_pass() {
9102        let mut h = OverlayHarness::new(vec![]);
9103        h.down(30.0, 120.0);
9104        h.state.publish = Some(toolbar_request(10.0));
9105        h.frame();
9106        assert!(h.root.selection_toolbar().is_some());
9107        let gen_before = h.root.selection_toolbar_generation();
9108
9109        // A press on chrome that claims no focus ends the session — and the menu
9110        // must go with it immediately, not a frame later.
9111        h.down(150.0, 30.0);
9112        assert!(!h.root.is_focus_active());
9113        assert_eq!(
9114            h.root.selection_toolbar(),
9115            None,
9116            "a toolbar cannot outlive the focus session its selection belonged to"
9117        );
9118        assert_eq!(h.root.selection_toolbar_generation(), gen_before + 1);
9119
9120        // And a field still publishing into the blurred session cannot resurrect
9121        // it — the same refusal the IME republish makes.
9122        h.frame();
9123        assert_eq!(h.root.selection_toolbar(), None);
9124    }
9125}
9126
9127/// The root half of the multi-contact contract (`InputEvent::PointerContact`):
9128/// how a contact's id, the claimant latch and the `capture_contacts` opt-in
9129/// decide where an event goes.
9130#[cfg(test)]
9131mod contact_tests {
9132    use super::*;
9133
9134    /// What every probe saw: which probe, which contact, which phase.
9135    #[derive(Default)]
9136    struct Log {
9137        seen: Vec<(char, PointerId, PointerPhase)>,
9138    }
9139
9140    /// A leaf that logs every pointer event it receives with the contact id its
9141    /// context reports, and on a `Down` captures (optionally opting into the
9142    /// gesture's other contacts) and claims focus.
9143    struct Probe {
9144        tag: char,
9145        opt_in: bool,
9146    }
9147    impl crate::widget::Widget for Probe {
9148        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9149            bc.constrain(Size::new(40.0, 20.0))
9150        }
9151        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
9152        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9153            let InputEvent::Pointer(p) = event else {
9154                return EventResult::Ignored;
9155            };
9156            let id = ctx.pointer_id();
9157            ctx.state_mut::<Log>().seen.push((self.tag, id, p.phase));
9158            if p.phase == PointerPhase::Down {
9159                ctx.capture_pointer();
9160                if self.opt_in {
9161                    ctx.capture_contacts();
9162                }
9163                ctx.request_focus();
9164            }
9165            EventResult::Handled
9166        }
9167    }
9168
9169    /// Two probes stacked vertically (A at y 0..20, B at y 30..50) behind the
9170    /// standard recorded-path routing: a captured gesture goes straight to the
9171    /// active child and releases it on `Up`/`Cancel`; anything else is
9172    /// hit-tested, and a `Down` that hits nothing blurs both.
9173    struct Pair {
9174        a: crate::widget::ChildPod,
9175        b: crate::widget::ChildPod,
9176    }
9177    impl crate::widget::Widget for Pair {
9178        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9179            self.a.layout_child(ctx, bc);
9180            self.a.set_origin(Point::new(0.0, 0.0));
9181            self.b.layout_child(ctx, bc);
9182            self.b.set_origin(Point::new(0.0, 30.0));
9183            bc.max()
9184        }
9185        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9186            self.a.paint_child(ctx, scene);
9187            self.b.paint_child(ctx, scene);
9188        }
9189        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9190            let releases = matches!(
9191                event,
9192                InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
9193            );
9194            for pod in [&mut self.a, &mut self.b] {
9195                if pod.is_active() {
9196                    let result = pod.event_child(ctx, event);
9197                    if releases {
9198                        pod.set_active(false);
9199                    }
9200                    return result;
9201                }
9202            }
9203            let pos = event.position();
9204            let down = matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Down);
9205            for pod in [&mut self.a, &mut self.b] {
9206                if pod.contains(pos) {
9207                    return pod.event_child(ctx, event);
9208                }
9209            }
9210            if down {
9211                self.a.set_focused(false);
9212                self.b.set_focused(false);
9213            }
9214            EventResult::Ignored
9215        }
9216    }
9217
9218    struct PairView {
9219        opt_in: bool,
9220    }
9221    impl View<Log> for PairView {
9222        type Element = Pair;
9223        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Pair {
9224            Pair {
9225                a: crate::widget::ChildPod::new(Box::new(Probe {
9226                    tag: 'A',
9227                    opt_in: self.opt_in,
9228                })),
9229                b: crate::widget::ChildPod::new(Box::new(Probe {
9230                    tag: 'B',
9231                    opt_in: self.opt_in,
9232                })),
9233            }
9234        }
9235        fn rebuild(&self, _p: &Self, _e: &mut Pair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
9236            ChangeFlags::NONE
9237        }
9238    }
9239
9240    fn root_with(opt_in: bool) -> (RenderRoot<Log, PairView>, Log) {
9241        let mut root: RenderRoot<Log, PairView> = RenderRoot::new();
9242        let mut log = Log::default();
9243        root.rebuild(&mut |_| PairView { opt_in }, &mut log);
9244        root.layout(Size::new(200.0, 200.0));
9245        (root, log)
9246    }
9247
9248    fn ev(phase: PointerPhase, x: f64, y: f64) -> PointerEvent {
9249        PointerEvent {
9250            phase,
9251            position: Point::new(x, y),
9252            button: PointerButton::Primary,
9253        }
9254    }
9255
9256    fn mouse(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
9257        InputEvent::Pointer(ev(phase, x, y))
9258    }
9259
9260    fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
9261        InputEvent::PointerContact {
9262            pointer_id: PointerId::touch(slot),
9263            event: ev(phase, x, y),
9264        }
9265    }
9266
9267    /// Positions: over A, over B, and over neither.
9268    const A: (f64, f64) = (10.0, 10.0);
9269    const B: (f64, f64) = (10.0, 40.0);
9270    const NOWHERE: (f64, f64) = (150.0, 150.0);
9271
9272    #[test]
9273    fn slot_zero_touch_takes_the_same_path_as_a_plain_pointer() {
9274        use PointerPhase::{Down, Move, Up};
9275        let script = [
9276            (Down, A),
9277            (Move, NOWHERE),
9278            (Up, NOWHERE),
9279            (Down, NOWHERE),
9280            (Move, B),
9281            (Down, B),
9282            (Up, B),
9283        ];
9284        let (mut via_pointer, mut pointer_log) = root_with(false);
9285        let (mut via_contact, mut contact_log) = root_with(false);
9286        for (phase, (x, y)) in script {
9287            let a = via_pointer.event(&mut pointer_log, &mouse(phase, x, y));
9288            let b = via_contact.event(&mut contact_log, &touch(0, phase, x, y));
9289            assert_eq!(a, b, "{phase:?} at ({x}, {y}): same outcome");
9290            assert_eq!(
9291                via_pointer.is_pointer_captured(),
9292                via_contact.is_pointer_captured()
9293            );
9294            assert_eq!(via_pointer.is_focus_active(), via_contact.is_focus_active());
9295            assert_eq!(via_pointer.is_hover_active(), via_contact.is_hover_active());
9296            assert_eq!(
9297                via_pointer.focus_ime_generation(),
9298                via_contact.focus_ime_generation()
9299            );
9300        }
9301        // Same widgets, same phases; only the reported id differs.
9302        let strip = |log: &Log| -> Vec<(char, PointerPhase)> {
9303            log.seen.iter().map(|(t, _, p)| (*t, *p)).collect()
9304        };
9305        assert_eq!(strip(&pointer_log), strip(&contact_log));
9306        assert!(
9307            pointer_log
9308                .seen
9309                .iter()
9310                .all(|(_, id, _)| *id == PointerId::MOUSE)
9311        );
9312        assert!(
9313            contact_log
9314                .seen
9315                .iter()
9316                .all(|(_, id, _)| *id == PointerId::touch(0))
9317        );
9318        // The capture the contact's Down took named it as the claimant.
9319        via_contact.event(&mut contact_log, &touch(0, Down, A.0, A.1));
9320        assert_eq!(
9321            via_contact.pointer_capture_claimant(),
9322            Some(PointerId::touch(0))
9323        );
9324    }
9325
9326    #[test]
9327    fn an_additional_contact_with_nothing_captured_is_dropped() {
9328        let (mut root, mut log) = root_with(true);
9329        let outcome = root.event(&mut log, &touch(1, PointerPhase::Down, A.0, A.1));
9330        assert_eq!(outcome, EventOutcome::default());
9331        assert!(log.seen.is_empty(), "no widget saw the slot-1 contact");
9332        assert!(!root.is_pointer_captured());
9333        assert!(!root.is_focus_active(), "a dropped contact claims nothing");
9334    }
9335
9336    #[test]
9337    fn an_opted_in_captor_receives_the_other_contacts_on_the_captured_path() {
9338        use PointerPhase::{Down, Move, Up};
9339        let (mut root, mut log) = root_with(true);
9340        root.event(&mut log, &touch(0, Down, A.0, A.1));
9341        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9342
9343        // The second finger lands over B, yet travels the captured path to A.
9344        let t1 = PointerId::touch(1);
9345        assert!(root.event(&mut log, &touch(1, Down, B.0, B.1)).handled);
9346        root.event(&mut log, &touch(1, Move, NOWHERE.0, NOWHERE.1));
9347        root.event(&mut log, &touch(1, Up, NOWHERE.0, NOWHERE.1));
9348        assert_eq!(
9349            log.seen[1..],
9350            [('A', t1, Down), ('A', t1, Move), ('A', t1, Up)],
9351            "A saw touch(1)'s whole contact, B saw nothing"
9352        );
9353        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9354    }
9355
9356    #[test]
9357    fn a_captor_that_did_not_opt_in_never_sees_the_other_contacts() {
9358        use PointerPhase::{Down, Move, Up};
9359        let (mut root, mut log) = root_with(false);
9360        root.event(&mut log, &touch(0, Down, A.0, A.1));
9361        for phase in [Down, Move, Up] {
9362            let outcome = root.event(&mut log, &touch(1, phase, B.0, B.1));
9363            assert_eq!(
9364                outcome,
9365                EventOutcome::default(),
9366                "touch(1) {phase:?} dropped"
9367            );
9368        }
9369        assert_eq!(log.seen, [('A', PointerId::touch(0), Down)]);
9370        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9371    }
9372
9373    #[test]
9374    fn a_touch_release_never_ends_a_mouse_capture_and_vice_versa() {
9375        use PointerPhase::{Cancel, Down, Move, Up};
9376        // A mouse drag holds A; a finger tapping elsewhere must not break it.
9377        let (mut root, mut log) = root_with(false);
9378        root.event(&mut log, &mouse(Down, A.0, A.1));
9379        root.event(&mut log, &touch(0, Down, B.0, B.1));
9380        root.event(&mut log, &touch(0, Up, B.0, B.1));
9381        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::MOUSE));
9382        root.event(&mut log, &mouse(Move, NOWHERE.0, NOWHERE.1));
9383        assert_eq!(
9384            log.seen.last(),
9385            Some(&('A', PointerId::MOUSE, Move)),
9386            "the drag still reaches its captor"
9387        );
9388        assert!(log.seen.iter().all(|(tag, _, _)| *tag == 'A'));
9389        root.event(&mut log, &mouse(Up, NOWHERE.0, NOWHERE.1));
9390        assert!(!root.is_pointer_captured());
9391
9392        // The mirror image: a touch drag holds B; the mouse cannot end it.
9393        let (mut root, mut log) = root_with(false);
9394        root.event(&mut log, &touch(0, Down, B.0, B.1));
9395        root.event(&mut log, &mouse(Up, A.0, A.1));
9396        root.event(&mut log, &mouse(Cancel, A.0, A.1));
9397        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9398        root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9399        assert_eq!(log.seen.last(), Some(&('B', PointerId::touch(0), Move)));
9400        root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9401        assert!(!root.is_pointer_captured());
9402    }
9403
9404    #[test]
9405    fn only_the_claimants_release_clears_the_latch() {
9406        use PointerPhase::{Cancel, Down, Move, Up};
9407        let (mut root, mut log) = root_with(true);
9408        root.event(&mut log, &touch(0, Down, A.0, A.1));
9409        // Another contact ends twice over — delivered, yet nothing releases:
9410        // not the root latch, and not the container's active link either.
9411        root.event(&mut log, &touch(1, Down, A.0, A.1));
9412        root.event(&mut log, &touch(1, Up, A.0, A.1));
9413        root.event(&mut log, &touch(2, Down, A.0, A.1));
9414        root.event(&mut log, &touch(2, Cancel, A.0, A.1));
9415        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9416        // Still on the captured path: a claimant move far outside A reaches A.
9417        root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9418        assert_eq!(log.seen.last(), Some(&('A', PointerId::touch(0), Move)));
9419
9420        // The claimant's own release ends it; later contacts fall to rule (b).
9421        root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9422        assert!(!root.is_pointer_captured());
9423        let before = log.seen.len();
9424        root.event(&mut log, &touch(1, Move, A.0, A.1));
9425        assert_eq!(
9426            log.seen.len(),
9427            before,
9428            "touch(1) dropped once the gesture ended"
9429        );
9430        // And the container's link went with it: a fresh contact is hit-tested.
9431        root.event(&mut log, &touch(0, Down, B.0, B.1));
9432        assert_eq!(log.seen.last(), Some(&('B', PointerId::touch(0), Down)));
9433    }
9434
9435    #[test]
9436    fn a_non_claimant_contact_never_blurs_or_moves_hover() {
9437        use PointerPhase::{Down, Up};
9438        let (mut root, mut log) = root_with(true);
9439        root.event(&mut log, &touch(0, Down, A.0, A.1));
9440        assert!(root.is_focus_active());
9441        let generation = root.focus_ime_generation();
9442        root.event(&mut log, &touch(1, Down, NOWHERE.0, NOWHERE.1));
9443        root.event(&mut log, &touch(1, Up, NOWHERE.0, NOWHERE.1));
9444        assert!(
9445            root.is_focus_active(),
9446            "a second finger is not a tap outside"
9447        );
9448        assert_eq!(root.focus_ime_generation(), generation);
9449    }
9450
9451    /// A container in front of [`Pair`] with pointer handling of its own that
9452    /// another contact must never reach: it logs every pointer event it routes
9453    /// (tag `'G'`) and treats a `Down` outside its child as a tap outside — an
9454    /// explicit focus release. With `forwards_broadcasts` unset it drops every
9455    /// broadcast instead of forwarding it (a container the forward-only walk
9456    /// cannot pass).
9457    struct Guard {
9458        inner: crate::widget::ChildPod,
9459        forwards_broadcasts: bool,
9460    }
9461    impl crate::widget::Widget for Guard {
9462        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9463            self.inner
9464                .layout_child(ctx, &BoxConstraints::tight(Size::new(100.0, 60.0)));
9465            self.inner.set_origin(Point::ZERO);
9466            bc.max()
9467        }
9468        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9469            self.inner.paint_child(ctx, scene);
9470        }
9471        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9472            if event.is_broadcast() {
9473                if self.forwards_broadcasts {
9474                    self.inner.event_child(ctx, event);
9475                }
9476                return EventResult::Ignored;
9477            }
9478            let InputEvent::Pointer(p) = event else {
9479                return EventResult::Ignored;
9480            };
9481            let id = ctx.pointer_id();
9482            ctx.state_mut::<Log>().seen.push(('G', id, p.phase));
9483            let inside = self.inner.contains(p.position);
9484            let result = if self.inner.is_active() || inside {
9485                self.inner.event_child(ctx, event)
9486            } else {
9487                EventResult::Ignored
9488            };
9489            if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel) {
9490                self.inner.set_active(false);
9491            }
9492            if p.phase == PointerPhase::Down && !inside {
9493                ctx.release_focus();
9494            }
9495            result
9496        }
9497    }
9498
9499    struct GuardView {
9500        forwards_broadcasts: bool,
9501    }
9502    impl View<Log> for GuardView {
9503        type Element = Guard;
9504        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Guard {
9505            Guard {
9506                inner: crate::widget::ChildPod::new(Box::new(Pair {
9507                    a: crate::widget::ChildPod::new(Box::new(Probe {
9508                        tag: 'A',
9509                        opt_in: true,
9510                    })),
9511                    b: crate::widget::ChildPod::new(Box::new(Probe {
9512                        tag: 'B',
9513                        opt_in: true,
9514                    })),
9515                })),
9516                forwards_broadcasts: self.forwards_broadcasts,
9517            }
9518        }
9519        fn rebuild(&self, _p: &Self, _e: &mut Guard, _c: &mut BuildCtx<'_>) -> ChangeFlags {
9520            ChangeFlags::NONE
9521        }
9522    }
9523
9524    fn guarded_root(forwards_broadcasts: bool) -> (RenderRoot<Log, GuardView>, Log) {
9525        let mut root: RenderRoot<Log, GuardView> = RenderRoot::new();
9526        let mut log = Log::default();
9527        root.rebuild(
9528            &mut |_| GuardView {
9529                forwards_broadcasts,
9530            },
9531            &mut log,
9532        );
9533        root.layout(Size::new(200.0, 200.0));
9534        (root, log)
9535    }
9536
9537    #[test]
9538    fn another_contact_reaches_only_the_captor_and_moves_no_hover_or_focus() {
9539        use PointerPhase::{Down, Move, Up};
9540        let (mut root, mut log) = guarded_root(true);
9541        let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
9542        root.event(&mut log, &touch(0, Down, A.0, A.1));
9543        assert!(root.is_focus_active());
9544        assert_eq!(root.pointer_capture_claimant(), Some(t0));
9545        assert!(root.pointer_capture_contacts());
9546        let (focus_generation, hover) = (root.focus_ime_generation(), root.is_hover_active());
9547
9548        // A second finger landing outside the guard's child: delivered to A down
9549        // the captured path, while the guard — whose own `Down` handling would
9550        // release focus on a tap outside — never runs on it.
9551        assert!(
9552            root.event(&mut log, &touch(1, Down, NOWHERE.0, NOWHERE.1))
9553                .handled
9554        );
9555        root.event(&mut log, &touch(1, Move, B.0, B.1));
9556        root.event(&mut log, &touch(1, Up, B.0, B.1));
9557        assert_eq!(
9558            log.seen,
9559            [
9560                ('G', t0, Down),
9561                ('A', t0, Down),
9562                ('A', t1, Down),
9563                ('A', t1, Move),
9564                ('A', t1, Up),
9565            ],
9566            "only the captor saw touch(1)"
9567        );
9568        assert!(root.is_focus_active(), "no tap outside was seen");
9569        assert_eq!(root.focus_ime_generation(), focus_generation);
9570        assert_eq!(root.is_hover_active(), hover);
9571        assert_eq!(root.pointer_capture_claimant(), Some(t0));
9572        assert!(root.pointer_capture_contacts());
9573
9574        // The claimant itself still takes the ordinary path through the guard.
9575        root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9576        assert_eq!(
9577            log.seen[5..],
9578            [('G', t0, Move), ('A', t0, Move)],
9579            "the claimant's own move runs every handler on the path"
9580        );
9581        root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9582        assert!(!root.is_pointer_captured());
9583        assert!(!root.pointer_capture_contacts());
9584    }
9585
9586    #[test]
9587    fn a_walk_a_container_cannot_pass_falls_back_to_the_ordinary_delivery() {
9588        use PointerPhase::Down;
9589        let (mut root, mut log) = guarded_root(false);
9590        let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
9591        root.event(&mut log, &touch(0, Down, A.0, A.1));
9592        // The guard drops the carrier, so it is handed the real event instead —
9593        // and the captor still receives it, down the captured path.
9594        assert!(root.event(&mut log, &touch(1, Down, B.0, B.1)).handled);
9595        assert_eq!(
9596            log.seen,
9597            [
9598                ('G', t0, Down),
9599                ('A', t0, Down),
9600                ('G', t1, Down),
9601                ('A', t1, Down),
9602            ]
9603        );
9604        assert_eq!(root.pointer_capture_claimant(), Some(t0));
9605    }
9606
9607    /// A root container that takes a gesture over from its child once the
9608    /// claimant moves more than 10 px: it cancels the child and releases it
9609    /// through [`EventCtx::release_captured_child`]. Logs every pointer event it
9610    /// sees (tag `'T'`); with `opt_in` it also opts into the gesture's other
9611    /// contacts itself, ahead of its child, on the `Down`.
9612    struct TakeOver {
9613        inner: crate::widget::ChildPod,
9614        opt_in: bool,
9615        down_at: Option<Point>,
9616        released_seen: bool,
9617    }
9618    impl crate::widget::Widget for TakeOver {
9619        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9620            self.inner.layout_child(ctx, bc);
9621            self.inner.set_origin(Point::ZERO);
9622            bc.max()
9623        }
9624        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9625            self.inner.paint_child(ctx, scene);
9626        }
9627        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9628            if event.is_broadcast() {
9629                self.inner.event_child(ctx, event);
9630                return EventResult::Ignored;
9631            }
9632            let InputEvent::Pointer(p) = event else {
9633                return EventResult::Ignored;
9634            };
9635            let id = ctx.pointer_id();
9636            ctx.state_mut::<Log>().seen.push(('T', id, p.phase));
9637            match p.phase {
9638                PointerPhase::Down if self.down_at.is_none() => {
9639                    self.down_at = Some(p.position);
9640                    ctx.capture_pointer();
9641                    if self.opt_in {
9642                        ctx.capture_contacts();
9643                    }
9644                    if self.inner.contains(p.position) {
9645                        self.inner.event_child(ctx, event);
9646                    }
9647                }
9648                PointerPhase::Move => {
9649                    let moved = self.down_at.map(|at| (p.position - at).hypot());
9650                    if self.inner.is_active() && moved.is_some_and(|d| d > 10.0) {
9651                        let cancel = InputEvent::Pointer(PointerEvent {
9652                            phase: PointerPhase::Cancel,
9653                            ..*p
9654                        });
9655                        self.inner.event_child(ctx, &cancel);
9656                        ctx.release_captured_child(&mut self.inner);
9657                        self.released_seen = ctx.is_capture_released();
9658                    } else if self.inner.is_active() {
9659                        self.inner.event_child(ctx, event);
9660                    }
9661                }
9662                PointerPhase::Up | PointerPhase::Cancel => {
9663                    if self.inner.is_active() {
9664                        self.inner.event_child(ctx, event);
9665                        self.inner.set_active(false);
9666                    }
9667                    self.down_at = None;
9668                }
9669                PointerPhase::Down => {}
9670            }
9671            EventResult::Handled
9672        }
9673    }
9674
9675    struct TakeOverView {
9676        opt_in: bool,
9677    }
9678    impl View<Log> for TakeOverView {
9679        type Element = TakeOver;
9680        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TakeOver {
9681            TakeOver {
9682                inner: crate::widget::ChildPod::new(Box::new(Probe {
9683                    tag: 'A',
9684                    opt_in: true,
9685                })),
9686                opt_in: self.opt_in,
9687                down_at: None,
9688                released_seen: false,
9689            }
9690        }
9691        fn rebuild(&self, _p: &Self, _e: &mut TakeOver, _c: &mut BuildCtx<'_>) -> ChangeFlags {
9692            ChangeFlags::NONE
9693        }
9694    }
9695
9696    fn take_over_root(opt_in: bool) -> (RenderRoot<Log, TakeOverView>, Log) {
9697        let mut root: RenderRoot<Log, TakeOverView> = RenderRoot::new();
9698        let mut log = Log::default();
9699        root.rebuild(&mut |_| TakeOverView { opt_in }, &mut log);
9700        root.layout(Size::new(200.0, 200.0));
9701        (root, log)
9702    }
9703
9704    fn take_over_widget(root: &mut RenderRoot<Log, TakeOverView>) -> &mut TakeOver {
9705        let id = root.root_id().expect("root built");
9706        root.tree
9707            .pod_mut(id)
9708            .expect("root pod")
9709            .widget_mut()
9710            .downcast_mut::<TakeOver>()
9711            .expect("root is a TakeOver")
9712    }
9713
9714    #[test]
9715    fn a_takeover_ends_the_contact_opt_in_but_keeps_the_claimant() {
9716        use PointerPhase::{Cancel, Down, Move, Up};
9717        let (mut root, mut log) = take_over_root(false);
9718        let t0 = PointerId::touch(0);
9719        root.event(&mut log, &touch(0, Down, A.0, A.1));
9720        assert_eq!(root.pointer_capture_claimant(), Some(t0));
9721        assert!(root.pointer_capture_contacts(), "A opted in");
9722
9723        // The claimant moves past the container's threshold: it cancels A and
9724        // releases it, and the release reaches the root.
9725        root.event(&mut log, &touch(0, Move, A.0, A.1 + 30.0));
9726        assert!(take_over_widget(&mut root).released_seen);
9727        assert_eq!(log.seen[3], ('A', t0, Cancel));
9728        assert_eq!(
9729            root.pointer_capture_claimant(),
9730            Some(t0),
9731            "the gesture is still the claimant's, now held by the container"
9732        );
9733        assert!(
9734            !root.pointer_capture_contacts(),
9735            "the widget that asked for the other contacts is gone"
9736        );
9737
9738        // Another finger is dropped from here on — neither the container nor
9739        // the cancelled captor hears it.
9740        let before = log.seen.len();
9741        let outcome = root.event(&mut log, &touch(1, Down, A.0, A.1));
9742        assert_eq!(outcome, EventOutcome::default());
9743        assert_eq!(log.seen.len(), before);
9744
9745        // The claimant keeps driving the container, and its release ends it.
9746        root.event(&mut log, &touch(0, Move, A.0, A.1 + 60.0));
9747        assert_eq!(log.seen.last(), Some(&('T', t0, Move)));
9748        root.event(&mut log, &touch(0, Up, A.0, A.1 + 60.0));
9749        assert!(!root.is_pointer_captured());
9750    }
9751
9752    #[test]
9753    fn a_takeover_by_the_widget_holding_the_opt_in_keeps_it() {
9754        use PointerPhase::{Down, Move};
9755        let (mut root, mut log) = take_over_root(true);
9756        let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
9757        root.event(&mut log, &touch(0, Down, A.0, A.1));
9758        root.event(&mut log, &touch(0, Move, A.0, A.1 + 30.0));
9759        assert!(
9760            !take_over_widget(&mut root).released_seen,
9761            "the container itself holds the opt-in, so releasing its child ends nothing"
9762        );
9763        assert!(root.pointer_capture_contacts());
9764        // The container is the captor: another finger reaches it directly.
9765        assert!(root.event(&mut log, &touch(1, Down, A.0, A.1)).handled);
9766        assert_eq!(log.seen.last(), Some(&('T', t1, Down)));
9767        assert_eq!(root.pointer_capture_claimant(), Some(t0));
9768    }
9769}
9770
9771/// [`InputEvent::Scale`]: hit-tested by [`ScaleEvent::focal`] exactly like
9772/// [`InputEvent::Scroll`], translated down the tree the same way, and bubbles
9773/// topmost-first until a widget reports [`EventResult::Handled`].
9774#[cfg(test)]
9775mod scale_tests {
9776    use super::*;
9777    use crate::event::{ScaleEvent, ScalePhase};
9778
9779    /// What every leaf saw: which leaf, which event.
9780    #[derive(Default)]
9781    struct Log {
9782        seen: Vec<(char, ScaleEvent)>,
9783    }
9784
9785    /// Logs every [`InputEvent::Scale`] it receives, in its own local space,
9786    /// and reports `Handled` only when `handles` is set — everything else
9787    /// (including a non-`Scale` event) is ignored.
9788    struct ScaleLeaf {
9789        tag: char,
9790        handles: bool,
9791    }
9792    impl crate::widget::Widget for ScaleLeaf {
9793        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9794            bc.constrain(Size::new(40.0, 40.0))
9795        }
9796        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
9797        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9798            let InputEvent::Scale(scale) = event else {
9799                return EventResult::Ignored;
9800            };
9801            ctx.state_mut::<Log>().seen.push((self.tag, *scale));
9802            if self.handles {
9803                EventResult::Handled
9804            } else {
9805                EventResult::Ignored
9806            }
9807        }
9808    }
9809
9810    /// Two leaves at identical bounds `(0, 0)..(40, 40)` — `top` painted (and
9811    /// hit-tested) before `bottom`, mirroring `frust-widgets::route_event`'s
9812    /// topmost-first, fall-through-on-`Ignored` hit test: a `Scale` landing in
9813    /// the shared rect reaches `top` first, and only reaches `bottom` if `top`
9814    /// ignores it.
9815    struct Overlapping {
9816        top: crate::widget::ChildPod,
9817        bottom: crate::widget::ChildPod,
9818    }
9819    impl crate::widget::Widget for Overlapping {
9820        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9821            self.top.layout_child(ctx, bc);
9822            self.top.set_origin(Point::ZERO);
9823            self.bottom.layout_child(ctx, bc);
9824            self.bottom.set_origin(Point::ZERO);
9825            bc.max()
9826        }
9827        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9828            self.bottom.paint_child(ctx, scene);
9829            self.top.paint_child(ctx, scene);
9830        }
9831        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9832            let pos = event.position();
9833            for pod in [&mut self.top, &mut self.bottom] {
9834                if pod.contains(pos) && pod.event_child(ctx, event) == EventResult::Handled {
9835                    return EventResult::Handled;
9836                }
9837            }
9838            EventResult::Ignored
9839        }
9840    }
9841
9842    /// Wraps [`Overlapping`] one container deeper, offset by `(10, 20)` — so
9843    /// the fixture also proves [`InputEvent::translated`] shifts a `Scale`
9844    /// event's focal point correctly across a container boundary.
9845    struct Offset {
9846        inner: crate::widget::ChildPod,
9847    }
9848    impl crate::widget::Widget for Offset {
9849        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9850            self.inner.layout_child(ctx, bc);
9851            self.inner.set_origin(Point::new(10.0, 20.0));
9852            bc.max()
9853        }
9854        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9855            self.inner.paint_child(ctx, scene);
9856        }
9857        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9858            let pos = event.position();
9859            if self.inner.contains(pos) {
9860                return self.inner.event_child(ctx, event);
9861            }
9862            EventResult::Ignored
9863        }
9864    }
9865
9866    struct OffsetView {
9867        top_handles: bool,
9868    }
9869    impl View<Log> for OffsetView {
9870        type Element = Offset;
9871        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Offset {
9872            Offset {
9873                inner: crate::widget::ChildPod::new(Box::new(Overlapping {
9874                    top: crate::widget::ChildPod::new(Box::new(ScaleLeaf {
9875                        tag: 'T',
9876                        handles: self.top_handles,
9877                    })),
9878                    bottom: crate::widget::ChildPod::new(Box::new(ScaleLeaf {
9879                        tag: 'B',
9880                        handles: true,
9881                    })),
9882                })),
9883            }
9884        }
9885        fn rebuild(
9886            &self,
9887            _prev: &Self,
9888            _element: &mut Offset,
9889            _ctx: &mut BuildCtx<'_>,
9890        ) -> ChangeFlags {
9891            ChangeFlags::NONE
9892        }
9893    }
9894
9895    fn root_with(top_handles: bool) -> (RenderRoot<Log, OffsetView>, Log) {
9896        let mut root: RenderRoot<Log, OffsetView> = RenderRoot::new();
9897        let mut log = Log::default();
9898        root.rebuild(&mut |_| OffsetView { top_handles }, &mut log);
9899        root.layout(Size::new(200.0, 200.0));
9900        (root, log)
9901    }
9902
9903    fn scale(phase: ScalePhase, scale_delta: f64, x: f64, y: f64) -> InputEvent {
9904        InputEvent::Scale(ScaleEvent {
9905            phase,
9906            scale_delta,
9907            focal: Point::new(x, y),
9908            velocity: 0.0,
9909        })
9910    }
9911
9912    #[test]
9913    fn scale_hit_tests_by_focal_point_and_translates_through_a_container() {
9914        let (mut root, mut log) = root_with(true);
9915
9916        // Window (5, 5): outside the offset container entirely (it starts at
9917        // (10, 20)) — nothing is hit, nothing logged.
9918        let outside = root.event(&mut log, &scale(ScalePhase::Begin, 1.1, 5.0, 5.0));
9919        assert!(
9920            log.seen.is_empty(),
9921            "a focal point outside every pod hits nothing"
9922        );
9923        assert!(!outside.handled);
9924
9925        // Window (30, 40): inside the container, local (20, 20) once the
9926        // Offset container's (10, 20) origin is subtracted by
9927        // `InputEvent::translated` — squarely inside both overlapping 40x40
9928        // leaves, so the topmost one (`top`) is the one that sees it.
9929        let inside = root.event(&mut log, &scale(ScalePhase::Update, 1.2, 30.0, 40.0));
9930        assert!(inside.handled);
9931        assert_eq!(log.seen.len(), 1, "the topmost leaf alone handled it");
9932        let (tag, seen) = log.seen[0];
9933        assert_eq!(tag, 'T');
9934        assert_eq!(
9935            seen.focal,
9936            Point::new(20.0, 20.0),
9937            "translated() shifted the focal point into the container's local space"
9938        );
9939        assert_eq!(seen.scale_delta, 1.2);
9940        assert_eq!(seen.phase, ScalePhase::Update);
9941    }
9942
9943    #[test]
9944    fn an_ignored_scale_bubbles_to_the_next_hit_widget() {
9945        // `top` ignores every `Scale` it sees; `bottom`, at the identical
9946        // bounds, still handles it — proving the hit test falls through to
9947        // the next topmost-first candidate instead of stopping (and
9948        // swallowing the event) at the first hit.
9949        let (mut root, mut log) = root_with(false);
9950        let outcome = root.event(&mut log, &scale(ScalePhase::Begin, 0.9, 30.0, 40.0));
9951        assert!(outcome.handled, "the bottom leaf still handled it");
9952        assert_eq!(
9953            log.seen.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
9954            vec!['T', 'B'],
9955            "the ignoring top leaf saw it first, and bottom is what it bubbled to"
9956        );
9957    }
9958}
9959
9960/// The [`RenderRoot::event`] file-drag follow-up: every `Drop`/`Cancel`,
9961/// handled or not, is followed by an `Ended` broadcast.
9962#[cfg(test)]
9963mod file_drop_tests {
9964    use super::*;
9965
9966    /// Every `FileDrop` phase a leaf saw, in order, and whether the leaf
9967    /// handles the hit-tested phases.
9968    #[derive(Default)]
9969    struct DropLog {
9970        handles: bool,
9971        seen: Vec<(char, FileDropPhase)>,
9972    }
9973
9974    struct DropLeaf {
9975        tag: char,
9976    }
9977    impl crate::widget::Widget for DropLeaf {
9978        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9979            bc.constrain(Size::new(40.0, 40.0))
9980        }
9981        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
9982        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9983            let InputEvent::FileDrop(drop) = event else {
9984                return EventResult::Ignored;
9985            };
9986            let log = ctx.state_mut::<DropLog>();
9987            log.seen.push((self.tag, drop.phase));
9988            if log.handles && !event.is_broadcast() {
9989                EventResult::Handled
9990            } else {
9991                EventResult::Ignored
9992            }
9993        }
9994    }
9995
9996    /// One 40x40 leaf at the origin of a 200x200 root that routes like
9997    /// `frust-widgets`' helpers: a broadcast to the child unconditionally,
9998    /// anything else only on a hit.
9999    struct DropRoot {
10000        leaf: crate::widget::ChildPod,
10001    }
10002    impl crate::widget::Widget for DropRoot {
10003        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
10004            self.leaf.layout_child(ctx, bc);
10005            self.leaf.set_origin(Point::ZERO);
10006            bc.max()
10007        }
10008        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
10009            self.leaf.paint_child(ctx, scene);
10010        }
10011        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
10012            if event.is_broadcast() {
10013                self.leaf.event_child(ctx, event);
10014                return EventResult::Ignored;
10015            }
10016            if self.leaf.contains(event.position()) {
10017                return self.leaf.event_child(ctx, event);
10018            }
10019            EventResult::Ignored
10020        }
10021    }
10022
10023    struct DropRootView;
10024    impl View<DropLog> for DropRootView {
10025        type Element = DropRoot;
10026        fn build(&self, _ctx: &mut BuildCtx<'_>) -> DropRoot {
10027            DropRoot {
10028                leaf: crate::widget::ChildPod::new(Box::new(DropLeaf { tag: 'L' })),
10029            }
10030        }
10031        fn rebuild(
10032            &self,
10033            _prev: &Self,
10034            _element: &mut DropRoot,
10035            _ctx: &mut BuildCtx<'_>,
10036        ) -> ChangeFlags {
10037            ChangeFlags::NONE
10038        }
10039    }
10040
10041    fn drop_root(handles: bool) -> (RenderRoot<DropLog, DropRootView>, DropLog) {
10042        let mut root: RenderRoot<DropLog, DropRootView> = RenderRoot::new();
10043        let mut log = DropLog {
10044            handles,
10045            seen: Vec::new(),
10046        };
10047        root.rebuild(&mut |_| DropRootView, &mut log);
10048        root.layout(Size::new(200.0, 200.0));
10049        (root, log)
10050    }
10051
10052    fn file_drop(phase: FileDropPhase, x: f64, y: f64) -> InputEvent {
10053        InputEvent::FileDrop(FileDropEvent {
10054            phase,
10055            position: Point::new(x, y),
10056            paths: Vec::new(),
10057        })
10058    }
10059
10060    #[test]
10061    fn an_unhandled_file_drop_end_is_re_broadcast_to_the_whole_tree() {
10062        for phase in [FileDropPhase::Drop, FileDropPhase::Cancel] {
10063            let (mut root, mut log) = drop_root(false);
10064            // Far outside the leaf: the hit-tested dispatch reaches nothing.
10065            let outcome = root.event(&mut log, &file_drop(phase, 150.0, 150.0));
10066            assert!(!outcome.handled, "a broadcast is never consumed");
10067            assert_eq!(
10068                log.seen,
10069                vec![('L', FileDropPhase::Ended)],
10070                "the {phase:?} that missed the leaf is followed by an Ended broadcast it hears"
10071            );
10072
10073            // Over the leaf but ignored by it: the leaf hears the hit-tested
10074            // phase, then Ended.
10075            log.seen.clear();
10076            root.event(&mut log, &file_drop(phase, 10.0, 10.0));
10077            assert_eq!(log.seen, vec![('L', phase), ('L', FileDropPhase::Ended)]);
10078        }
10079    }
10080
10081    #[test]
10082    fn a_handled_file_drop_end_is_still_re_broadcast_to_the_whole_tree() {
10083        let (mut root, mut log) = drop_root(true);
10084        for phase in [FileDropPhase::Drop, FileDropPhase::Cancel] {
10085            log.seen.clear();
10086            let outcome = root.event(&mut log, &file_drop(phase, 10.0, 10.0));
10087            assert!(outcome.handled, "the hit-tested {phase:?} was handled");
10088            assert_eq!(
10089                log.seen,
10090                vec![('L', phase), ('L', FileDropPhase::Ended)],
10091                "and is still followed by the Ended broadcast"
10092            );
10093        }
10094    }
10095
10096    #[test]
10097    fn a_hover_and_a_direct_ended_get_no_follow_up() {
10098        let (mut root, mut log) = drop_root(false);
10099        root.event(&mut log, &file_drop(FileDropPhase::Hover, 150.0, 150.0));
10100        assert!(log.seen.is_empty(), "a Hover is never re-broadcast");
10101
10102        root.event(&mut log, &file_drop(FileDropPhase::Ended, 0.0, 0.0));
10103        assert_eq!(
10104            log.seen,
10105            vec![('L', FileDropPhase::Ended)],
10106            "an Ended dispatched directly is delivered once, not followed again"
10107        );
10108    }
10109}