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, RefCell};
18use std::collections::HashMap;
19use std::num::NonZeroU64;
20use std::rc::{Rc, Weak};
21use std::sync::atomic::{AtomicU64, Ordering};
22
23use kurbo::{Point, Rect, Size};
24
25use crate::anim::FrameTime;
26use crate::event::{
27    ContactFrame, ContactPass, CursorIcon, EventCtx, EventOutcome, EventResult, FileDropEvent,
28    FileDropPhase, ImeState, InputEvent, OverlayEvent, OverlayEventKind, PointerButton,
29    PointerEvent, PointerId, PointerPhase, RequestPass,
30};
31use crate::insets::WindowInsets;
32use crate::layout::BoxConstraints;
33use crate::overlay::{
34    OutsideTap, OverlayEntry, OverlayHit, OverlayInput, OverlayKey, OverlayPaintPass,
35    sort_into_paint_order,
36};
37use crate::selection_toolbar::{
38    SelectionToolbarActions, SelectionToolbarPass, SelectionToolbarRequest,
39};
40use crate::semantics::{ROOT_NODE_ID, SemanticsCtx, SemanticsUpdate};
41use crate::tree::{InspectNode, WidgetPod, WidgetTree};
42use crate::view::{BuildCtx, ChangeFlags, View, WidgetId};
43use crate::widget::{ChildPod, LayoutCtx, PaintCtx, PaintOutcome, PaintScene, PlatformViewFrame};
44
45/// The window's shape and platform-occlusion state, delivered to app code as a
46/// plain [`provide_context`](reactive_graph::owner::provide_context)-carried
47/// value — logical size, device-pixel scale, a
48/// [derived](Orientation::from_size) orientation, and the current
49/// [`WindowInsets`].
50///
51/// # Plain value, not a signal
52///
53/// `WindowMetrics` is delivered exactly like `Theme` and [`WindowInsets`]
54/// already are: a shell calls `provide_context` with a freshly-built value on
55/// change, and app code recovers it with `use_context::<WindowMetrics>()`
56/// inside `Component::build`. It is **not** an `RwSignal` — only `deep_link`
57/// and `back` are true signals in `frust-reactive`; every other host-signal
58/// carrier (theme, insets, and now this) is a re-provided plain value.
59///
60/// # Alongside `WindowInsets`, not superseding it
61///
62/// `WindowInsets` already reaches `Component::build` on Android and iOS today
63/// (each shell's `push_insets` calls `provide_context(insets)` independently
64/// of anything here — desktop has no such arm for either value yet).
65/// `WindowMetrics` is additive: a shell that starts providing it keeps
66/// providing the standalone `WindowInsets` context too, so an existing
67/// `use_context::<WindowInsets>()` call site never breaks. `insets` on this
68/// type is a **copy** of that same value for convenience (a widget laying
69/// itself out around window shape wants size/scale/orientation/insets
70/// together), not a replacement for the independent context.
71///
72/// # Orientation is derived, not platform-sourced
73///
74/// No platform callback in either mobile shell carries an orientation enum —
75/// Android's `nativeOnSurfaceChanged` and iOS's `frust_resize` each hand the
76/// shell only a `(width, height, scale)` triple. [`Orientation`] is therefore
77/// always computed from `size` via [`Orientation::from_size`]
78/// (portrait when `height >= width`, so an exact square reads as portrait);
79/// it never tracks a device orientation-lock setting or a platform rotation
80/// event directly.
81///
82/// # Context is not reactive
83///
84/// `provide_context` is a plain insert into the owner's context map — it
85/// notifies nothing — and `use_context` inside `Component::build` (or the
86/// root build closure) creates no subscription, so re-providing a changed
87/// `WindowMetrics` does not itself mark anything dirty or wake a frame. A new
88/// value becomes visible only on the next rebuild, which the resize or inset
89/// change that produced it already drives; do not write a shell that assumes
90/// a `provide_context` write triggers one. A shell wiring this up (see
91/// `docs/SHELLS_ARCHITECTURE.md`) must still re-provide `WindowMetrics` only
92/// on an actual change (mirroring `RenderRoot::set_insets`'s
93/// `PartialEq`-guarded no-op) — the reason is cost at the FFI boundary (a
94/// lock write plus an allocation every frame), not a rebuild storm.
95/// Separately, there is no per-component rebuild skipping in this framework
96/// (a component always re-runs `build` on any rebuild it does take part in),
97/// which is affordable only because builds are cheap by construction.
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct WindowMetrics {
100    /// The window's logical (density-independent) size.
101    pub size: Size,
102    /// The device-pixel scale factor (logical → physical px multiplier).
103    pub scale: f64,
104    /// The orientation derived from `size` — see the type's doc for why this
105    /// is computed, never platform-sourced.
106    pub orientation: Orientation,
107    /// A copy of the window's current insets — see the type's doc for why
108    /// this does not replace the standalone `WindowInsets` context.
109    pub insets: WindowInsets,
110}
111
112impl WindowMetrics {
113    /// Construct a [`WindowMetrics`] from its transported fields, deriving
114    /// [`orientation`](Self::orientation) from `size` rather than accepting it
115    /// as an input — see the type's doc for why orientation is never
116    /// platform-sourced.
117    pub fn new(size: Size, scale: f64, insets: WindowInsets) -> Self {
118        Self {
119            size,
120            scale,
121            orientation: Orientation::from_size(size),
122            insets,
123        }
124    }
125}
126
127/// A window's derived portrait/landscape orientation.
128///
129/// Always computed from a [`WindowMetrics::size`] via [`Orientation::from_size`]
130/// — see [`WindowMetrics`]'s doc for why no platform callback carries this as
131/// an enum directly.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum Orientation {
134    /// `size.height >= size.width`, including the exact-square case.
135    Portrait,
136    /// `size.height < size.width`.
137    Landscape,
138}
139
140impl Orientation {
141    /// Derives orientation from a logical window size: portrait when
142    /// `height >= width` (an exact square reads as portrait), landscape
143    /// otherwise.
144    pub fn from_size(size: Size) -> Self {
145        if size.height >= size.width {
146            Orientation::Portrait
147        } else {
148            Orientation::Landscape
149        }
150    }
151}
152
153/// How many [`InputEvent::Housekeeping`] flush passes one
154/// [`RenderRoot::rebuild`] will run before deferring the rest to the next frame.
155///
156/// A flushed pop-result callback may itself push or pop, queueing another
157/// callback — so the flush/re-diff cycle has to be allowed to iterate, but it
158/// must never be allowed to spin: a pair of callbacks that push each other would
159/// otherwise hang the frame. Three passes covers every shape observed in
160/// practice (a result that navigates once, and that page's own result), while
161/// keeping the worst case at four build-closure runs per frame — the closure is
162/// cheap by construction (see [`RenderRoot::rebuild`]).
163///
164/// Past the cap the mark stays raised and one more frame is requested, so the
165/// remaining work lands next frame instead of being lost.
166const MAX_PENDING_RESULT_FLUSH_PASSES: usize = 3;
167
168/// Change-guarded write of the shell-facing IME surface: replaces `slot` and
169/// bumps `generation` **only** when the value actually moves ([`ImeState`] is
170/// `PartialEq`).
171///
172/// A free function over the two fields rather than a `&mut self` method, so the
173/// change guard has exactly one implementation whatever borrows its caller
174/// happens to hold; [`RenderRoot::store_ime_state`] is the `&mut self` form, and
175/// is what every current caller goes through.
176///
177/// The change guard is load-bearing, not an optimisation: the paint pass
178/// re-publishes the focused widget's IME surface every frame, so an
179/// unconditional bump would make the shell's `focus_or_ime_changed` edge fire on
180/// every vsync for the whole life of a focus session — the level-input behavior
181/// the generation exists to replace.
182fn store_ime_state_in(slot: &mut Option<ImeState>, generation: &mut u64, next: Option<ImeState>) {
183    if *slot != next {
184        *slot = next;
185        *generation = generation.wrapping_add(1);
186    }
187}
188
189/// Release the whole focus/IME session: drop `focus_active` **and** the
190/// shell-facing surface together, bumping `generation` **exactly once** if
191/// either actually moved.
192///
193/// The paired form of [`RenderRoot::set_focus_active`]`(false)` +
194/// [`RenderRoot::store_ime_state`]`(None)`, and the single primitive every
195/// release site goes through — the blur-on-outside-tap `Down`, an explicit
196/// [`EventCtx::release_focus`](crate::event::EventCtx::release_focus), a widget
197/// publishing an *inactive* surface (see [`RenderRoot::paint`]), and the
198/// generic-unmount orphan drain in [`RenderRoot::rebuild`]. Keeping them on one
199/// primitive is what makes "a session ends" mean the same thing everywhere,
200/// rather than four hand-assembled pairs that can drift apart.
201///
202/// **One release is one edge.** The two field writers bump on each field's own
203/// change, so calling them in sequence would move
204/// [`focus_ime_generation`](RenderRoot::focus_ime_generation) *twice* for the
205/// ordinary release (focus `true`→`false` and surface `Some`→`None`). A shell
206/// only ever compares the value, so two bumps and one bump raise the same single
207/// `focus_or_ime_changed` edge — but a counter that moves once per observable
208/// transition is the contract the field doc states, and is what the release
209/// tests pin. The change guard itself is unchanged: an already-released root
210/// writes the same values back and moves nothing.
211///
212/// **A release ends the session's identity too.** On an actual move it advances
213/// `focus_epoch` and republishes it, which strands every recorded focus link at
214/// once — the chain the session ran through, and a floated surface's link that
215/// no container's blur sweep can reach. That is why the release has to own the
216/// epoch rather than leave it to the caller: a session cleared without moving
217/// its identity leaves links behind that still name it.
218///
219/// A free function over the fields (not a `&mut self` method) for the same
220/// reason [`store_ime_state_in`] is — one implementation of the contract,
221/// whatever borrows the caller holds. [`RenderRoot::release_focus_session`] is
222/// the method form, and is what every current caller goes through.
223fn release_focus_session_in(
224    focus_active: &mut bool,
225    ime_slot: &mut Option<ImeState>,
226    generation: &mut u64,
227    focus_epoch: &mut u64,
228    root_identity: u64,
229) {
230    let moved = *focus_active || ime_slot.is_some();
231    *focus_active = false;
232    *ime_slot = None;
233    if moved {
234        *generation = generation.wrapping_add(1);
235        // A session that ends strands every link recorded against it, wherever
236        // in (or off) the tree it sits — the one clearing sweep no container can
237        // be asked to run. Only on an actual release: a `Down` on already-blurred
238        // chrome is the commonest event there is and must move nothing.
239        *focus_epoch = advance_focus_epoch(*focus_epoch);
240        crate::widget::set_live_focus_session(root_identity, *focus_epoch, *focus_epoch);
241    }
242}
243
244/// The allocator behind [`RenderRoot::root_identity`], handing every root a
245/// value no other root shares.
246///
247/// Starts at `1` so `0` stays available as "no root" (a pod that has never held a
248/// claim, and the at-rest published hover link — see
249/// `crate::event::set_live_hover_link`).
250///
251/// A module-level static rather than an associated const/`static` inside the
252/// generic `impl`: the latter is monomorphized per `<State, V>` pair, which would
253/// hand two roots of different concrete types the same identity — exactly the
254/// collision this counter exists to remove. `Relaxed` is enough because the value
255/// is only ever compared for equality, never used to order anything.
256static NEXT_ROOT_IDENTITY: AtomicU64 = AtomicU64::new(1);
257
258/// The next focus epoch after `epoch`, skipping `0`.
259///
260/// `0` is reserved twice over — it is a never-claimed
261/// [`ChildPod`](crate::widget::ChildPod)'s stamp, and it is the epoch half of
262/// the `(0, 0)` pair a dispatch driven with no root at all compares against — so
263/// a root that wrapped onto it would hand every unclaimed pod in the tree a live
264/// link at once.
265fn advance_focus_epoch(epoch: u64) -> u64 {
266    match epoch.wrapping_add(1) {
267        0 => 1,
268        next => next,
269    }
270}
271
272/// What [`RenderRoot`]'s overlay pre-pass decided about one incoming event.
273///
274/// The pre-pass runs before anything else [`RenderRoot::event`] does, and has
275/// exactly two answers: the event belonged to a floated surface (or was swallowed
276/// by a modal light-dismiss) and the main tree must not see it, or it did not and
277/// today's dispatch continues. Both arms carry an [`EventOutcome`], because even
278/// the "continue" answer may already have produced one — an
279/// [`OutsideTap::Notify`]`{ consume: false }` surface is told about the press
280/// *and* lets it through, and the redraw that notification asked for must not be
281/// dropped on the floor when the main dispatch's own outcome replaces it.
282enum OverlayRoute {
283    /// The overlay layer consumed the event; return this outcome unchanged.
284    Consumed(EventOutcome),
285    /// The event continues into today's dispatch; merge this outcome into
286    /// whatever that produces.
287    Continue(EventOutcome),
288}
289
290/// Layout-audit bookkeeping ([`RenderRoot::set_layout_audit`]).
291#[derive(Default)]
292struct LayoutAudit {
293    enabled: bool,
294    mismatches: u64,
295    /// Audited frames so far.
296    frame: u64,
297    /// Audited frame each `(type, label)` was last logged on.
298    last_logged: HashMap<(&'static str, Option<String>), u64>,
299}
300
301/// One node's geometry in a layout-audit snapshot.
302struct AuditGeom {
303    id: WidgetId,
304    type_name: &'static str,
305    label: Option<String>,
306    origin: Point,
307    size: Size,
308}
309
310/// Owns the retained tree and drives the rebuild/layout/paint passes for a
311/// single-root application.
312///
313/// Generic over the application `State` and the concrete root view type `V`
314/// returned by the build closure.
315pub struct RenderRoot<State: 'static, V: View<State>> {
316    tree: WidgetTree,
317    root_id: Option<WidgetId>,
318    /// The previous view, retained to diff against on the next rebuild.
319    prev_view: Option<V>,
320    /// Monotonic widget-id counter, borrowed by each `BuildCtx`.
321    next_id: u64,
322    window_size: Size,
323    /// The **claimant** of the pointer capture in flight, if any: the contact
324    /// whose `Down` requested capture. Set on that `Down`, cleared only by the
325    /// claimant's own `Up`/`Cancel` — another contact's release never touches
326    /// it (rule (c) of [`InputEvent::PointerContact`]'s multi-contact contract).
327    /// Root-level mirror of the per-container `active` path bookkeeping, which
328    /// is keyed on the same claimant (see [`crate::widget::ChildPod::set_active`]).
329    capture_claimant: Option<PointerId>,
330    /// Whether the live capture's captor opted into the gesture's other
331    /// contacts ([`EventCtx::capture_contacts`]) on the `Down` it captured
332    /// with. Meaningless — and kept `false` — while nothing is captured.
333    /// Cleared early when a container takes the gesture over from that captor
334    /// ([`EventCtx::release_captured_child`]).
335    capture_contacts: bool,
336    /// Whether the live opt-in was made by the root widget itself rather than
337    /// by a pod below it — in which case a non-claimant contact is handed to the
338    /// root widget directly instead of walking the active path (see
339    /// [`crate::widget::ChildPod::event_child`]). Same lifetime as
340    /// `capture_contacts`.
341    contacts_captor_is_root: bool,
342    /// Whether some widget in the tree currently holds focus. Root-level mirror of
343    /// the per-container `focused` path bookkeeping (the focus analog of
344    /// `capture_claimant`): set when a dispatch requested focus, cleared by a
345    /// session release — a blur-on-outside-tap `Down`, an explicit focus release,
346    /// a widget publishing an inactive IME surface, or the generic-unmount orphan
347    /// drain in [`RenderRoot::rebuild`] (see [`release_focus_session_in`]).
348    focus_active: bool,
349    /// Which branch the live focus/IME session belongs to: `None` for the main
350    /// tree, `Some(key)` for the floated surface whose pod holds the recorded
351    /// focus path. The identity `focus_active` deliberately does not carry — one
352    /// bool cannot say *whose* session it is, and a surface's chain and the main
353    /// tree's are not siblings any container's blur sweep can reach across.
354    ///
355    /// **Resolved from links that can be shown to be live.** The only
356    /// authoritative view of a pod's recorded link the root ever gets is the pod
357    /// itself, which it holds for exactly the length of
358    /// [`RenderRoot::paint_overlays`] — so that pass writes this, from
359    /// [`crate::widget::ChildPod::holds_live_focus`], and the event pass only ever
360    /// *clears* it (a hit-tested claim is the main tree's by construction, and a
361    /// release ends the session outright). An overlay-pass focus request cannot
362    /// be attributed at the root: the bubble is a bare flag, and a field in the
363    /// main tree re-claiming its own session through a floated toolbar raises
364    /// exactly the same one as an editable inside the surface claiming it for the
365    /// first time.
366    ///
367    /// The liveness half is what makes the record worth keeping. A pod's raw
368    /// `focused` flag survives the session moving away from it — nothing visits
369    /// an abandoned branch to clear one — so a record resolved from the flag
370    /// alone latched on the first surface that ever took focus and never let go.
371    /// Resolved from the stamp instead, it answers a question the code can
372    /// falsify, and it names nobody the moment the session leaves every surface.
373    ///
374    /// **It no longer gates the tree's paint seed**, which is the other half of
375    /// the same correction: seeding is per-link, against `focus_epoch`, so a
376    /// branch proves its own claim rather than the root vouching for it from one
377    /// frame behind. What is left here is *provenance* — whether an overlay
378    /// dispatch's IME publish is the session owner's — plus the cross-surface
379    /// retirement in [`RenderRoot::event`], both of which genuinely need a name
380    /// rather than a per-link answer.
381    focus_surface: Option<OverlayKey>,
382    /// The identity of the live focus session — what a
383    /// [`ChildPod`](crate::widget::ChildPod) stamps beside its recorded focus
384    /// link, and the only thing that tells a link on the session the root has
385    /// now from one a moved session left behind.
386    ///
387    /// The focus analog of `hover_epoch`, with one difference that follows from
388    /// focus having no per-pass rhythm: this advances **around a dispatch**
389    /// rather than at the end of one. [`RenderRoot::event`] moves it forward
390    /// before it dispatches and puts it back afterwards unless the dispatch
391    /// actually recorded a claim — so a claim is stamped with a value nothing
392    /// older carries, and a pass that moved no focus leaves every standing link
393    /// exactly as it found it. Published to the pods through
394    /// [`crate::widget::set_live_focus_session`], which is where a container
395    /// deciding routing, and a pod's own destructor, read it.
396    ///
397    /// Starts at `1`, not `0`: a freshly built pod's stamp is `0`, and `(0, 0)`
398    /// is what a dispatch driven with no root at all sees, so a real root must
399    /// never publish that pair.
400    ///
401    /// Wrapping is deliberate and harmless — the value is only ever compared for
402    /// equality, never ordered — but it skips `0` on the way round (see
403    /// [`advance_focus_epoch`]).
404    focus_epoch: u64,
405    /// Whether the last completed hover pass left some widget in the tree holding
406    /// the hover link. Root-level mirror of the per-pod hover stamp (the hover
407    /// analog of `focus_active`), seeded into every event/paint pass so nothing
408    /// below can read as hovered while the root says nothing is.
409    hover_active: bool,
410    /// The live hover epoch: the identity of the most recent completed hover pass.
411    ///
412    /// Advanced by exactly one per hover pass — an **uncaptured**
413    /// [`PointerPhase::Move`] (which may record a claim), or the `Down`/`Up`/
414    /// `Cancel` that ends a hover outright (which may not) — and by nothing else,
415    /// so a scroll, key, IME, or housekeeping pass leaves a live hover standing.
416    /// A [`crate::widget::ChildPod`]'s recorded stamp counts as hovered only while
417    /// it equals this, which is what strands the previous claimant's path with no
418    /// container having to clear it (see the [`crate::event`] module docs).
419    ///
420    /// Starts at `1`, not `0`: a freshly built pod's stamp is `0`, and starting the
421    /// epoch past it means a never-claimed pod cannot match the live epoch by
422    /// accident before the first hover pass ever runs.
423    hover_epoch: u64,
424    /// This root's process-unique identity, assigned once at construction from
425    /// [`NEXT_ROOT_IDENTITY`] and never reused.
426    ///
427    /// It exists for exactly one comparison: the hover-orphan channel
428    /// (`crate::event`'s `mark_hover_orphaned`/`take_hover_orphaned`) is a
429    /// thread-local a *destructor* writes, so a second root driving passes on the
430    /// same thread can otherwise see a mark that is none of its business.
431    /// `hover_epoch` cannot tell them apart — every root's counter starts at `1`
432    /// and advances per hover pass, so two roots hold colliding integers as a rule
433    /// rather than as a fluke. Publishing and draining `(identity, epoch)` is what
434    /// keeps one root's unmounting claimant from ending another's live hover.
435    root_identity: u64,
436    /// The cursor the last cursor pass resolved — hover's sibling channel, and
437    /// the value a desktop shell reads through [`RenderRoot::cursor`].
438    ///
439    /// Deliberately **not** derived from `hover_active`: that mirror is
440    /// identity-free (it knows *that* something is hovered, not which widget or
441    /// what shape it wants), so a request travels its own pass-scoped slot
442    /// ([`EventCtx::set_cursor`]) and is resolved here.
443    ///
444    /// Re-resolved on every pointer [`PointerPhase::Move`], captured or not:
445    /// whatever the pass requested, or [`CursorIcon::Default`] when it requested
446    /// nothing. Every other pass leaves it standing — see [`RenderRoot::event`]
447    /// for why a `Down`/`Up` must not reset it. There is no generation counter
448    /// beside it: the shell compares the value it last applied (see
449    /// [`RenderRoot::cursor`]).
450    cursor: CursorIcon,
451    /// The text the tree last asked the shell to put on the host clipboard, or
452    /// `None` once drained — the cursor's write-only sibling, resolved from the
453    /// same kind of per-pass slot ([`EventCtx::write_clipboard`]) by the same
454    /// [`RequestPass`] bracket.
455    ///
456    /// **One-shot, unlike [`cursor`](RenderRoot::cursor).** A cursor is a *level*
457    /// (a standing shape a shell re-applies when it differs); a clipboard write
458    /// is an *edge* (a thing to do once), so the accessor
459    /// [`RenderRoot::take_clipboard_write`] drains it and a shell that forgets to
460    /// call it merely delays the write rather than repeating it.
461    ///
462    /// A pass that writes replaces whatever stood here undrained — the newest
463    /// copy is the one the user meant, and the shell is expected to drain after
464    /// every dispatch — while a pass that writes nothing leaves it alone rather
465    /// than silently discarding a write nobody has taken yet.
466    pending_clipboard_write: Option<String>,
467    /// Whether the tree has asked the shell to read the host clipboard back to it
468    /// ([`EventCtx::request_paste`]), until drained by
469    /// [`RenderRoot::take_paste_request`].
470    ///
471    /// The data-free twin of [`pending_clipboard_write`](RenderRoot::pending_clipboard_write),
472    /// and one-shot for the same reason. Raised by any pass in which a widget
473    /// asked and lowered only by the drain, so a shell that skips a drain answers
474    /// late rather than losing the paste.
475    pending_paste_request: bool,
476    /// The IME surface the focused widget last published (via
477    /// [`EventCtx::publish_ime_state`]), surfaced to the shell by
478    /// [`RenderRoot::ime_state`]. Persists across rebuilds/events until refreshed
479    /// by a new publish or dropped by a release (a blur, a focus release, an
480    /// inactive publish, or a generic-unmount orphan drain — see
481    /// [`release_focus_session_in`]).
482    ///
483    /// Only ever `None` or an **active** surface: an inactive publish is a
484    /// release, never a stored value (see [`RenderRoot::ime_state`]).
485    ime_state: Option<ImeState>,
486    /// A monotonically-increasing generation bumped on every **actual** change
487    /// of `focus_active` or `ime_state` — the focus/IME session's edge signal,
488    /// read by a shell through [`RenderRoot::focus_ime_generation`].
489    ///
490    /// The mobile frame gate turns this into an *edge* input
491    /// (`FrameInputs::focus_or_ime_changed`): a shell caches the last value it
492    /// saw and runs a frame when it moves. A *level* input ("something holds
493    /// focus") forced a frame every vsync for as long as a field stayed focused,
494    /// which made caret pacing unreachable — measured at 62–120 fps on a static
495    /// screen whose only live input was focus (Xiaomi 12).
496    ///
497    /// Same-value writes deliberately do **not** bump it (see
498    /// [`RenderRoot::set_focus_active`]/[`RenderRoot::store_ime_state`]): the
499    /// paint pass republishes the focused widget's IME surface on *every* frame,
500    /// so bumping on write rather than on change would re-create exactly the
501    /// per-vsync forcing this edge exists to remove.
502    focus_ime_gen: u64,
503    /// The [`PlatformViewFrame`]s the tree published during the most recent
504    /// [`RenderRoot::paint`], surfaced to the shell via
505    /// [`RenderRoot::platform_view_frames`]. Unlike `ime_state` above, this is
506    /// REPLACED wholesale every pass (never merged with the previous one), so
507    /// a pass that publishes none yields an empty `Vec` — a culled/removed
508    /// slot from the prior frame does not linger as a stale frame. Core stays
509    /// dumb here: the shell's differ owns absent-means-hide/dispose semantics.
510    platform_view_frames: Vec<PlatformViewFrame>,
511    /// The z-shield rects the tree reported during the most recent
512    /// [`RenderRoot::paint`] (via [`crate::widget::PaintCtx::report_input_shield`]),
513    /// surfaced to the shell via [`RenderRoot::input_shields`].
514    ///
515    /// Exactly the `platform_view_frames` discipline above — REPLACED wholesale
516    /// every pass, so a pass whose shields stopped painting reports none. Core
517    /// stays dumb: it never associates a shield with a slot, that is the
518    /// shell-side differ's job.
519    input_shields: Vec<Rect>,
520    /// Dirtiness accumulated since the last [`RenderRoot::take_change_flags`] —
521    /// merged from each rebuild so a shell can decide, in one place, whether a
522    /// frame needs layout/paint at all.
523    pending: ChangeFlags,
524    /// The app's active theme, stored type-erased so `frust-core` needs no
525    /// `frust-theme` dependency (the concrete `Theme` is boxed by the shell —
526    /// see [`RenderRoot::set_theme`]). Lent as `Option<&dyn Any>` into each
527    /// [`LayoutCtx`]/[`PaintCtx`]; `None` until a shell sets one (a supported
528    /// state — bare-core tests and pre-theme apps run without a theme).
529    theme: Option<Box<dyn Any>>,
530    /// The window's insets ([`WindowInsets`]), delivered by the shell via
531    /// [`RenderRoot::set_insets`] and threaded into every subsequent
532    /// layout/paint pass (recovered by widgets through
533    /// [`crate::widget::LayoutCtx::window_insets`]/
534    /// [`crate::widget::PaintCtx::window_insets`]). Unlike the theme this is a
535    /// concrete core-owned type (only `f64` scalars), stored by value — no
536    /// `Box<dyn Any>` erasure needed. Defaults to the zero inset until a shell
537    /// pushes one (a supported state — bare-core tests and pre-insets apps).
538    insets: WindowInsets,
539    /// The shell's running count of frames the render thread has actually
540    /// presented, threaded into every subsequent paint pass and recovered by
541    /// widgets through [`crate::widget::PaintCtx::presented_frames`]. A plain
542    /// `u64` core stores by value (like the insets). `None` until a shell pushes
543    /// one via [`RenderRoot::set_presented_frames`] — a supported state
544    /// (bare-core tests and pre-wiring shells run without it), so widgets can
545    /// fall back to a paint-cadence measure. Unlike the theme/insets this is a
546    /// pure observation: [`RenderRoot::set_presented_frames`] deliberately marks
547    /// NO [`ChangeFlags`] and bumps NO semantics generation (see its doc), so a
548    /// ticking presented count never forces a relayout or feeds the mobile frame
549    /// gate.
550    presented_frames: Option<u64>,
551    /// Whether the shell created a translucent (alpha-channel, "Mode B") GPU
552    /// surface, threaded into every subsequent paint pass and recovered by
553    /// widgets through [`crate::widget::PaintCtx::is_translucent`]. A plain
554    /// `bool` core stores by value (like the insets); `false` (opaque, "Mode A")
555    /// until a shell pushes one via [`RenderRoot::set_surface_translucent`] — the
556    /// supported default for every desktop app and bare-core test. The
557    /// platform-view hole-punch is the sole reader: a slot clears its rect only
558    /// on a translucent surface (see `frust-widgets`' `PlatformViewWidget`).
559    surface_translucent: bool,
560    /// The persistent, never-reused per-pod semantics base-id allocator's next
561    /// value. Seeded at `2` (ids `0`/`1` reserved: `0` keeps
562    /// `NonZeroU64` valid, `1` is the [`ROOT_NODE_ID`] window node), advanced as
563    /// [`ChildPod`](crate::widget::ChildPod)s are assigned bases on their first
564    /// semantics visit, and carried across passes so a pod that first appears on a
565    /// later frame never collides with an already-assigned one. A `Cell` because
566    /// [`RenderRoot::semantics`] runs behind `&self`.
567    semantics_alloc: Cell<u64>,
568    /// The root widget's stable semantics base id (the root pod is arena-backed,
569    /// not a [`ChildPod`](crate::widget::ChildPod), so it caches its base here
570    /// rather than in a pod). Lazily assigned on the first semantics pass.
571    root_semantics_id: Cell<Option<NonZeroU64>>,
572    /// A monotonically-increasing generation bumped whenever a rebuild or theme
573    /// swap could have changed the semantics tree, so a shell can cheaply skip
574    /// re-pulling + re-pushing an unchanged accessibility tree (the semantics
575    /// dirty gate — see [`RenderRoot::semantics_if_changed`]). v1 recompute is
576    /// acceptable; this is the seam a shell gates on.
577    semantics_gen: u64,
578    /// Set by [`RenderRoot::rebuild`] when the deferred-callback flush owes the
579    /// shell a frame, and folded into the next [`RenderRoot::paint`]'s
580    /// [`PaintOutcome::needs_frame`] (then cleared). Two raisers, both in the
581    /// flush loop: hitting [`MAX_PENDING_RESULT_FLUSH_PASSES`] with work still
582    /// owed, and a dispatched [`InputEvent::Housekeeping`] whose
583    /// [`EventOutcome::needs_redraw`] came back set.
584    ///
585    /// The frame-request half of the deferral: `pending |= PAINT` already tells
586    /// the mobile frame gate to run its next tick, but the desktop loop is
587    /// dirty-driven (`ControlFlow::Wait`) and schedules off `needs_frame`, so the
588    /// deferral has to surface there too — otherwise the remaining flush would
589    /// wait for whatever input happens to arrive next, which is the exact
590    /// failure this whole mechanism exists to remove.
591    deferred_frame: bool,
592    /// Whether the last main-tree paint asked for a frame or a layout — the
593    /// root widget's own copy of `ChildPod`'s trailing-paint bit. The root
594    /// widget sits in no pod, so nothing else remembers that the paint an
595    /// animation lands on (the first that asks for nothing) still owes one
596    /// layout pass; [`RenderRoot::paint`] folds `LAYOUT` for it once.
597    painted_animating: bool,
598    /// The routing half of the overlay entries the **last** [`RenderRoot::paint`]
599    /// registered, in paint order (`Floating` band then `Tooltip`, registration
600    /// order within each) — what [`RenderRoot::event`]'s overlay pre-pass
601    /// hit-tests before the main tree ever sees a pointer.
602    ///
603    /// Replaced wholesale every paint, exactly like `platform_view_frames`: an
604    /// owner keeps a surface routable by registering it again each frame, so a
605    /// surface whose owner stopped registering (or was unmounted) stops taking
606    /// input after the next paint with nothing to unregister.
607    ///
608    /// Carries **no pod handle** by construction (see
609    /// [`OverlayHit`](crate::overlay::OverlayHit)): the owner owns the pod, and a
610    /// root holding a clone of it between passes would both outlive the owner and
611    /// invite a borrow held across a pass boundary.
612    ///
613    /// One frame of lag is inherent and intended: input is routed against where
614    /// the surfaces were painted, which is the only place the user could have
615    /// seen them.
616    overlay_hits: Vec<OverlayHit>,
617    /// The selection-toolbar request the focused field published during the most
618    /// recent [`RenderRoot::paint`], surfaced to the shell through
619    /// [`RenderRoot::selection_toolbar`] for the platform edit-menu route.
620    ///
621    /// Resolved per paint pass: a pass in which nothing published clears it, which
622    /// is what puts the menu away when a selection collapses. A session release
623    /// clears it too (see [`RenderRoot::release_focus_session`]), so the menu can
624    /// never outlive the focus the selection belonged to — the event pass's blur
625    /// lands a whole frame before the paint that would otherwise notice.
626    selection_toolbar: Option<SelectionToolbarRequest>,
627    /// A monotonically-increasing generation bumped on every **actual** change of
628    /// `selection_toolbar` — the same change-guarded edge signal
629    /// `focus_ime_gen` is, and for the same reason: the publishing field
630    /// re-publishes an unchanged request every single frame its selection stands,
631    /// so bumping on write rather than on change would ask the shell to re-present
632    /// the platform menu on every vsync.
633    ///
634    /// Kept beside the value rather than inside it so the counter survives a
635    /// clear: a shell diffs the generation to notice the menu went *away* just as
636    /// much as to notice it appeared.
637    selection_toolbar_gen: u64,
638    /// The global layout epoch threaded into every layout pass
639    /// ([`LayoutCtx::layout_epoch`]). Bumped for each global layout input that
640    /// does not reach a widget through its constraints — a theme swap
641    /// ([`RenderRoot::set_theme`], which is also the path a shell's text-context
642    /// font drain takes), an actual inset change ([`RenderRoot::set_insets`]), a
643    /// window-size change seen by [`RenderRoot::layout`] — and when a floated
644    /// surface's pod is found dirty (see `overlay_pods`). A pod that last laid
645    /// out under another epoch lays out again, so a bump invalidates every
646    /// cached layout at once. Starts at `1`, past a fresh pod's `0`.
647    layout_epoch: u64,
648    /// The paint epoch threaded into every paint pass ([`PaintCtx::subtree_clean`]):
649    /// bumped with every layout-epoch bump, by a surface-translucency flip, and
650    /// at the start of a paint that follows a hover-link or focus-session
651    /// change (see `painted_links`), so a pod painted under another epoch is
652    /// never reported clean.
653    paint_epoch: u64,
654    /// The serial of the most recent [`RenderRoot::paint`] (`0` before the
655    /// first): each paint mints the next one, threads it to every pod
656    /// ([`PaintCtx::paint_serial`]) and reports it on
657    /// [`PaintOutcome::paint_serial`].
658    paint_serial: u64,
659    /// The hover link as the last completed hover pass left it: `None` while
660    /// nothing holds hover, otherwise the claimant pod's
661    /// [`ChildPod::inspect_id`](crate::widget::ChildPod::inspect_id) (`0` for a
662    /// claim the root widget made itself). Unlike `hover_epoch`, which advances
663    /// on every hover pass, this moves only when hover begins, moves to another
664    /// claimant, or ends.
665    hover_link: Option<u64>,
666    /// `(hover_link, focus_epoch)` as the last paint saw them. Both are read at
667    /// paint time ([`PaintCtx::is_hovered`], [`PaintCtx::has_focus`]) from links
668    /// the root moves without visiting the pod that loses one, so a paint that
669    /// finds either changed bumps `paint_epoch`: no retained page recorded
670    /// under the old link may be drawn under the new one.
671    painted_links: (Option<u64>, u64),
672    /// Whether retained layers are enabled ([`RenderRoot::set_retained_layers`]).
673    retained_layers: bool,
674    /// The renderer's resident-key ledger as of the previous frame
675    /// ([`RenderRoot::set_retained_ledger`]); empty until a shell sets one.
676    retained_ledger: crate::retained::RetainedLedger,
677    /// Whether layout passes may skip clean pods ([`RenderRoot::set_layout_skip`]);
678    /// `true` by default. `false` forces every pod to lay out every pass — the
679    /// kill switch a shell wires to an environment variable (this crate reads
680    /// none itself).
681    layout_skip: bool,
682    /// The layout audit's state ([`RenderRoot::set_layout_audit`]); off by default.
683    layout_audit: LayoutAudit,
684    /// Weak handles to the pods of the floated surfaces the **last** paint
685    /// drew, refreshed every paint beside `overlay_hits`.
686    ///
687    /// A floated pod is laid out by its *owner* (inside the owner's own
688    /// `layout`) but painted and fed paint-time requests by this root, outside
689    /// the owner's pod chain — so a paint-time frame request or a substituted
690    /// event dispatch can dirty the floated pod while every pod above its owner
691    /// stays clean, and the owner's layout (the only route to the floated pod)
692    /// would be skipped. [`RenderRoot::layout`] therefore checks these first and
693    /// bumps the epoch when any is dirty: conservative (the whole tree lays out
694    /// while a surface animates) but correct.
695    ///
696    /// Weak, never strong, so the root still co-owns no pod (see
697    /// `overlay_hits`): a surface whose owner dropped it simply fails to
698    /// upgrade. The check borrows each pod only for the length of one flag read,
699    /// at the start of a layout pass, when no pass holds a borrow.
700    overlay_pods: Vec<Weak<RefCell<ChildPod>>>,
701    _state: core::marker::PhantomData<fn(&mut State)>,
702}
703
704impl<State: 'static, V: View<State>> RenderRoot<State, V> {
705    /// Create an empty render root with no widget yet built.
706    pub fn new() -> Self {
707        Self {
708            tree: WidgetTree::new(),
709            root_id: None,
710            prev_view: None,
711            next_id: 0,
712            window_size: Size::ZERO,
713            capture_claimant: None,
714            capture_contacts: false,
715            contacts_captor_is_root: false,
716            focus_active: false,
717            focus_surface: None,
718            // Past a fresh pod's `0` stamp, and past the `(0, 0)` a rootless
719            // dispatch sees — see the field doc.
720            focus_epoch: 1,
721            hover_active: false,
722            // Past a fresh pod's `0` stamp — see the field doc.
723            hover_epoch: 1,
724            root_identity: NEXT_ROOT_IDENTITY.fetch_add(1, Ordering::Relaxed),
725            cursor: CursorIcon::Default,
726            pending_clipboard_write: None,
727            pending_paste_request: false,
728            ime_state: None,
729            focus_ime_gen: 0,
730            platform_view_frames: Vec::new(),
731            input_shields: Vec::new(),
732            pending: ChangeFlags::NONE,
733            theme: None,
734            insets: WindowInsets::default(),
735            presented_frames: None,
736            surface_translucent: false,
737            // Ids 0 and 1 are reserved (see the field doc); pods start at 2.
738            semantics_alloc: Cell::new(2),
739            root_semantics_id: Cell::new(None),
740            semantics_gen: 0,
741            deferred_frame: false,
742            painted_animating: false,
743            overlay_hits: Vec::new(),
744            selection_toolbar: None,
745            selection_toolbar_gen: 0,
746            layout_epoch: 1,
747            paint_epoch: 1,
748            paint_serial: 0,
749            hover_link: None,
750            // The links a fresh root starts with (no hover, focus epoch `1`).
751            painted_links: (None, 1),
752            retained_layers: true,
753            retained_ledger: crate::retained::RetainedLedger::empty(),
754            layout_skip: true,
755            layout_audit: LayoutAudit::default(),
756            overlay_pods: Vec::new(),
757            _state: core::marker::PhantomData,
758        }
759    }
760
761    /// Switch the layout skip on (the default) or off.
762    ///
763    /// With the skip on, a layout pass lays out only the pods that are dirty,
764    /// whose constraints changed, or that last laid out under an older layout
765    /// epoch — see [`ChildPod::layout_child`] for the rule and its dirty
766    /// sources. `false` forces every pod to lay out on every pass, exactly the
767    /// pre-skip behaviour: the kill switch a shell exposes (this crate reads no
768    /// environment variable itself). Takes effect from the next
769    /// [`RenderRoot::layout`]; dirties nothing by itself.
770    pub fn set_layout_skip(&mut self, skip: bool) {
771        self.layout_skip = skip;
772    }
773
774    /// Whether layout passes may skip clean pods ([`RenderRoot::set_layout_skip`]).
775    pub fn layout_skip(&self) -> bool {
776        self.layout_skip
777    }
778
779    /// Turn the layout audit on or off (off by default; a debug tool).
780    ///
781    /// When on, and only while the skip is enabled ([`RenderRoot::set_layout_skip`]),
782    /// every [`RenderRoot::layout`] pass proves the skip rule: after the normal
783    /// (skipping) pass it snapshots every pod's geometry, runs a second, **forced**
784    /// full pass over the same constraints, and compares. A pod whose origin
785    /// (relative to its parent) or size differs by more than `1e-3` logical px
786    /// changed its layout without signalling dirty, which is exactly the bug the
787    /// skip would turn into stale geometry. Each is counted on
788    /// [`RenderRoot::layout_audit_mismatches`] and logged at `warn` through the
789    /// `log` crate, prefixed `layout-audit:` and naming the widget type and debug
790    /// label, at most once per `(type, label)` per 60 audited frames.
791    ///
792    /// **Cost:** two layout passes per frame, the second laying out the whole
793    /// tree, so this erases the skip's savings; never leave it on in a shipped
794    /// build. The forced pass's geometry is the geometry that stays, so the audit
795    /// never ships wrong geometry. The forced pass does not bump the layout epoch;
796    /// like any forced pass it leaves every pod clean (`layout_dirty` cleared,
797    /// `last_constraints`/`last_epoch`/`last_insets` refreshed to the current
798    /// values), which is the state the next frame's skip decisions start from.
799    /// This crate reads no environment variable; shells wire `FRUST_LAYOUT_AUDIT`.
800    /// With the audit off the cost is one `bool` check per layout pass.
801    pub fn set_layout_audit(&mut self, audit: bool) {
802        self.layout_audit.enabled = audit;
803    }
804
805    /// Whether the layout audit is on ([`RenderRoot::set_layout_audit`]).
806    pub fn layout_audit(&self) -> bool {
807        self.layout_audit.enabled
808    }
809
810    /// Total pods the layout audit has found with geometry that differed between
811    /// the skipping and the forced pass, since the root was created. Counts every
812    /// occurrence, unlike the rate-limited log.
813    pub fn layout_audit_mismatches(&self) -> u64 {
814        self.layout_audit.mismatches
815    }
816
817    /// The global layout epoch the next layout pass runs under — read-only, for
818    /// tooling and tests. It moves on a theme swap, an inset change, a window
819    /// resize, and a dirty floated surface (see [`ChildPod::layout_child`]).
820    pub fn layout_epoch(&self) -> u64 {
821        self.layout_epoch
822    }
823
824    /// Invalidate every pod's cached layout: the next pass lays out the whole
825    /// tree.
826    fn bump_layout_epoch(&mut self) {
827        self.layout_epoch = self.layout_epoch.wrapping_add(1);
828        self.paint_epoch = self.paint_epoch.wrapping_add(1);
829    }
830
831    /// Enable or disable retained layers (default on). When off,
832    /// [`PaintCtx::subtree_clean`] answers `false` for every pod, so a boundary
833    /// widget repaints its child every frame. The core never reads the
834    /// environment: a shell wires `FRUST_NO_RETAINED_LAYERS` to this.
835    pub fn set_retained_layers(&mut self, enabled: bool) {
836        self.retained_layers = enabled;
837    }
838
839    /// Hand the root the renderer's resident-key ledger, as published after the
840    /// previous frame (one frame stale by design). Consulted by
841    /// [`PaintCtx::retained_available`]; defaults to empty (nothing resident).
842    /// Cloned only when it differs from the stored one.
843    pub fn set_retained_ledger(&mut self, ledger: &crate::retained::RetainedLedger) {
844        if self.retained_ledger != *ledger {
845            self.retained_ledger.clone_from(ledger);
846        }
847    }
848
849    /// Whether retained layers are enabled ([`RenderRoot::set_retained_layers`]).
850    pub fn retained_layers(&self) -> bool {
851        self.retained_layers
852    }
853
854    /// Whether any floated surface the last paint drew is layout-dirty (see
855    /// `overlay_pods`). A pod already borrowed elsewhere — impossible between
856    /// passes, but cheap to tolerate — counts as dirty.
857    fn overlay_pod_dirty(&self) -> bool {
858        self.overlay_pods.iter().any(|weak| {
859            weak.upgrade()
860                .is_some_and(|pod| pod.try_borrow().map_or(true, |pod| pod.is_layout_dirty()))
861        })
862    }
863
864    /// Store the app's active theme, threaded into every subsequent
865    /// layout/paint pass as `Option<&dyn Any>` and recovered by widgets via
866    /// [`crate::widget::PaintCtx::theme_as`]/[`crate::widget::LayoutCtx::theme_as`].
867    ///
868    /// The theme is boxed **type-erased** (`Box<dyn Any>`) so this crate stays
869    /// independent of `frust-theme`; the shell boxes the concrete `Theme`
870    /// (and re-boxes it on a live appearance change, e.g. dark-mode toggle).
871    /// Calling again replaces the stored theme.
872    ///
873    /// Marks `LAYOUT | PAINT` pending (drained by
874    /// [`RenderRoot::take_change_flags`]): a theme swap can change baked-in
875    /// paint state a widget resolves at layout time (e.g. `Text`'s themed
876    /// glyph color, cached into its `TextLayout` — see
877    /// `frust-widgets::text`), so a shell that later gates layout/paint on
878    /// this seam must still see a bare `set_theme` as dirty even though no
879    /// view changed.
880    pub fn set_theme(&mut self, theme: Box<dyn Any>) {
881        self.theme = Some(theme);
882        self.pending |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
883        // A theme is a global layout input no constraint carries: every cached
884        // layout is stale (the layout skip — see `layout_epoch`).
885        self.bump_layout_epoch();
886        // A theme swap can change semantics-visible state (e.g. a relabelled or
887        // re-bounded node once layout re-runs); treat it as semantics-dirty too.
888        self.semantics_gen = self.semantics_gen.wrapping_add(1);
889    }
890
891    /// Store the window's insets ([`WindowInsets`]), threaded into every
892    /// subsequent layout/paint pass and recovered by widgets via
893    /// [`crate::widget::LayoutCtx::window_insets`]/
894    /// [`crate::widget::PaintCtx::window_insets`].
895    ///
896    /// Mirrors [`RenderRoot::set_theme`]'s dirty-tracking contract: a change
897    /// marks `LAYOUT | PAINT` pending (drained by
898    /// [`RenderRoot::take_change_flags`]) so a shell gating layout/paint on that
899    /// seam still relayouts when the insets move — a `SafeArea` widget resolves
900    /// its inset at layout time, so the mobile layout-skip gate must see a bare
901    /// `set_insets` as dirty even though no view changed (the same reasoning as
902    /// the theme swap — see `docs/ARCHITECTURE.md`'s Theme delivery and Frame
903    /// gate). A change also bumps the semantics generation, since a moved inset
904    /// shifts laid-out node bounds.
905    ///
906    /// No-op guarded by [`WindowInsets`]'s `PartialEq`: pushing the current
907    /// value marks nothing dirty, so a shell that polls the platform insets
908    /// every frame and forwards unconditionally never forces a needless
909    /// relayout. (A shell may also skip the call itself by comparing first —
910    /// this is the same guard, held on the core side.)
911    pub fn set_insets(&mut self, insets: WindowInsets) {
912        if self.insets == insets {
913            return;
914        }
915        self.insets = insets;
916        self.pending |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
917        // Global and constraint-free, like the theme: invalidate every cached
918        // layout (see `layout_epoch`).
919        self.bump_layout_epoch();
920        // A moved inset shifts laid-out node bounds once layout re-runs; treat
921        // it as semantics-dirty too (mirrors `set_theme`).
922        self.semantics_gen = self.semantics_gen.wrapping_add(1);
923    }
924
925    /// The window's insets currently threaded into the layout/paint passes.
926    pub fn insets(&self) -> WindowInsets {
927        self.insets
928    }
929
930    /// Store the shell's running count of frames the render thread has actually
931    /// presented, threaded into every subsequent paint pass and recovered by
932    /// widgets via [`crate::widget::PaintCtx::presented_frames`]. A shell loads
933    /// the atomic its render side increments (once per presented frame) and
934    /// pushes it here once per UI frame, before `paint`.
935    ///
936    /// **Deliberately dirties nothing.** Unlike [`RenderRoot::set_theme`] and
937    /// [`RenderRoot::set_insets`] — which mark `LAYOUT | PAINT` pending because a
938    /// widget bakes their value in at layout time — this setter marks NO
939    /// [`ChangeFlags`] and bumps NO semantics generation. The presented count is
940    /// a paint-only *observation* a widget reads live every paint (never baked at
941    /// layout), so treating it as dirty would be wrong twice over: it would force
942    /// a needless relayout, and — critically — on the mobile shells a
943    /// monotonically ticking counter would keep the frame gate's pending-flags
944    /// input perpetually true, so the menu would never idle (the 32s-idle
945    /// behavior must survive). Keeping this setter dirt-free
946    /// is exactly what keeps the frame gate unaware of it (see
947    /// `docs/ARCHITECTURE.md`'s Frame gate).
948    pub fn set_presented_frames(&mut self, presented: u64) {
949        self.presented_frames = Some(presented);
950    }
951
952    /// The presented-frame count currently threaded into the paint pass, or
953    /// `None` if no shell has pushed one.
954    pub fn presented_frames(&self) -> Option<u64> {
955        self.presented_frames
956    }
957
958    /// Store whether the shell's GPU surface is translucent (alpha-channel,
959    /// "Mode B"), threaded into every subsequent paint pass and recovered by
960    /// widgets through [`crate::widget::PaintCtx::is_translucent`]. A shell
961    /// pushes the surface's **resolved** translucency here — what the GPU
962    /// backend reports after the surface is installed, not what the app
963    /// requested via `frust-shell-common::surface_mode`'s latch: a translucency
964    /// request the platform refuses must degrade to the opaque contract, or
965    /// every `platform_view` slot punches a hole in an opaque swapchain
966    /// (black rectangles). Every desktop app leaves the default `false`
967    /// (opaque, "Mode A").
968    ///
969    /// Marks `PAINT` pending on an actual change (`PartialEq`-guarded, mirroring
970    /// [`RenderRoot::set_insets`]'s no-op guard): translucency is read purely at
971    /// paint time (the hole-punch runs in `paint`, never baked at layout), so a
972    /// flip must repaint but need not relayout. A flip is rare but **real**: a
973    /// surface (re)install can resolve differently from the previous one, and
974    /// both mobile shells re-push this every frame (the no-op-if-unchanged
975    /// guard is what makes that free).
976    pub fn set_surface_translucent(&mut self, translucent: bool) {
977        if self.surface_translucent == translucent {
978            return;
979        }
980        self.surface_translucent = translucent;
981        // Translucency is read at paint time only: no relayout, but every
982        // retained subtree is stale.
983        self.paint_epoch = self.paint_epoch.wrapping_add(1);
984        self.pending |= ChangeFlags::PAINT;
985    }
986
987    /// Whether the shell's GPU surface is currently marked translucent.
988    pub fn is_surface_translucent(&self) -> bool {
989        self.surface_translucent
990    }
991
992    /// Whether a captured pointer gesture is currently in flight.
993    pub fn is_pointer_captured(&self) -> bool {
994        self.capture_claimant.is_some()
995    }
996
997    /// The contact that claimed the pointer capture in flight — the only one
998    /// whose `Up`/`Cancel` can end it — or `None` while nothing is captured.
999    pub fn pointer_capture_claimant(&self) -> Option<PointerId> {
1000        self.capture_claimant
1001    }
1002
1003    /// Whether the capture in flight routes the gesture's **other** contacts to
1004    /// its captor — the captor opted in with [`EventCtx::capture_contacts`] on
1005    /// the `Down` it captured with, and no container has since taken the
1006    /// gesture over from it ([`EventCtx::release_captured_child`]). `false`
1007    /// while nothing is captured.
1008    pub fn pointer_capture_contacts(&self) -> bool {
1009        self.capture_contacts
1010    }
1011
1012    /// Whether some widget in the tree currently holds keyboard/IME focus.
1013    pub fn is_focus_active(&self) -> bool {
1014        self.focus_active
1015    }
1016
1017    /// Whether some widget in the tree currently holds the hover link — i.e.
1018    /// whether the last hover pass (an uncaptured [`PointerPhase::Move`]) left the
1019    /// pointer over a widget that claimed it.
1020    ///
1021    /// The hover analog of [`RenderRoot::is_focus_active`], and a level accessor
1022    /// like it: hover is not a session (nothing has to be released), so there is no
1023    /// generation counterpart. `false` for any app whose widgets never call
1024    /// [`EventCtx::claim_hover`](crate::event::EventCtx::claim_hover). A touch app
1025    /// can still see it go `true` transiently: nothing distinguishes a touch
1026    /// contact from a mouse here, so an uncaptured touch drag over a
1027    /// non-capturing claimant is an ordinary hover pass — ended by the `Up` at
1028    /// lift (see `docs/LIMITATIONS.md`'s `hover-window-leave-standing`).
1029    ///
1030    /// A [`RenderRoot::rebuild`] that removes the claimant ends the link too, so
1031    /// this never reports a hover held by a widget that no longer exists — the
1032    /// hover counterpart of the unmount focus release (see that method).
1033    pub fn is_hover_active(&self) -> bool {
1034        self.hover_active
1035    }
1036
1037    /// The cursor the tree last asked the host to show — what a desktop shell
1038    /// pushes to its window (`frust-shell-desktop` maps it onto winit's own
1039    /// cursor icons).
1040    ///
1041    /// A **level** accessor like [`RenderRoot::is_hover_active`], not an edge one:
1042    /// the value re-resolves on every pointer [`PointerPhase::Move`] and stands
1043    /// unchanged through every other pass, so a shell caches what it last applied
1044    /// and calls the platform only when this differs. There is deliberately no
1045    /// generation counter — a cursor is a *value*, not a session, and an unmoved
1046    /// cursor is indistinguishable from one re-resolved to the same shape.
1047    ///
1048    /// [`CursorIcon::Default`] before the first `Move`, and after any `Move` in
1049    /// which no widget called
1050    /// [`EventCtx::set_cursor`](crate::event::EventCtx::set_cursor) — so any app
1051    /// whose widgets never request a cursor reads `Default` forever, and the mobile
1052    /// shells never read this at all regardless of what resolves here.
1053    ///
1054    /// **Residual:** a widget that is torn down (or moves out from under a
1055    /// stationary pointer) while its request stands leaves the last shape in
1056    /// place until the next `Move` re-resolves it — the same self-correction
1057    /// window hover has, and for the same reason: nothing re-resolves without
1058    /// pointer motion.
1059    pub fn cursor(&self) -> CursorIcon {
1060        self.cursor
1061    }
1062
1063    /// Take (and clear) the text the tree asked the shell to put on the host
1064    /// clipboard — the drain a shell performs immediately after every
1065    /// [`RenderRoot::event`], beside [`cursor()`](RenderRoot::cursor) and
1066    /// [`ime_state()`](RenderRoot::ime_state).
1067    ///
1068    /// `Some` exactly when some widget called
1069    /// [`EventCtx::write_clipboard`](crate::event::EventCtx::write_clipboard)
1070    /// during a pass since the last drain (answering a
1071    /// [`EditCommand::Copy`](crate::event::EditCommand::Copy)/[`Cut`](crate::event::EditCommand::Cut),
1072    /// or a chord the widget decoded itself). The shell hands the text to its host
1073    /// clipboard — winit's `arboard` on desktop, `ClipboardManager` on Android,
1074    /// `UIPasteboard` on iOS — and does nothing at all on `None`.
1075    ///
1076    /// **Destructive**, unlike [`cursor()`](RenderRoot::cursor): a clipboard write
1077    /// is an edge, not a standing level, so a caller that drains and drops the
1078    /// result loses that write. Draining twice after one pass yields `None` the
1079    /// second time.
1080    ///
1081    /// A widget that never copies leaves this `None` forever, so a shell with no
1082    /// clipboard (the mobile shells before their own clipboard work lands) may
1083    /// call it and discard the result, or not call it at all.
1084    pub fn take_clipboard_write(&mut self) -> Option<String> {
1085        self.pending_clipboard_write.take()
1086    }
1087
1088    /// Take (and clear) whether the tree asked the shell to read the host
1089    /// clipboard back to it — drained beside
1090    /// [`take_clipboard_write`](RenderRoot::take_clipboard_write) after every
1091    /// [`RenderRoot::event`].
1092    ///
1093    /// `true` exactly when some widget called
1094    /// [`EventCtx::request_paste`](crate::event::EventCtx::request_paste) during a
1095    /// pass since the last drain. The shell answers by reading its host clipboard
1096    /// and dispatching
1097    /// [`InputEvent::EditCommand`]`(`[`EditCommand::Paste`](crate::event::EditCommand::Paste)`(text))`
1098    /// — a *new* dispatch, because the read may be asynchronous and the pass that
1099    /// asked is over. That answer carries no identity of its own and is
1100    /// focus-routed to whoever holds focus when it lands: a release in between
1101    /// drops it harmlessly, but a focus *move* in between lands it in the new
1102    /// field rather than the one that asked. A synchronous read has no such
1103    /// window; an asynchronous one snapshots
1104    /// [`focus_epoch`](RenderRoot::focus_epoch) at this drain and discards an
1105    /// answer whose epoch no longer matches — not
1106    /// [`focus_ime_generation`](RenderRoot::focus_ime_generation), which also
1107    /// moves within a single session.
1108    ///
1109    /// **Destructive**, for [`take_clipboard_write`](RenderRoot::take_clipboard_write)'s
1110    /// reason. A pass may both write and request (a cut that immediately re-reads,
1111    /// or a widget answering two chords) — the two drains are independent.
1112    pub fn take_paste_request(&mut self) -> bool {
1113        std::mem::take(&mut self.pending_paste_request)
1114    }
1115
1116    /// The IME surface the focused widget published, for the shell to drive the
1117    /// platform input method (winit `set_ime_cursor_area`, Android
1118    /// `updateSelection`, iOS `inputDelegate`). `None` when nothing is focused or
1119    /// the focused widget publishes no IME surface.
1120    ///
1121    /// Written by the focused widget through [`EventCtx::publish_ime_state`] during
1122    /// the event pass and refreshed on every event; it survives a rebuild (so the
1123    /// shell can query it between frames) and is cleared when focus is lost.
1124    ///
1125    /// # `None` is the only "no session" form — an inactive surface is never stored
1126    ///
1127    /// A widget publishing `ImeState { active: false, .. }` is ending the session,
1128    /// not describing it, so both publish paths turn that into a full release
1129    /// (see [`RenderRoot::paint`]) and this returns `None` rather than
1130    /// `Some(inactive)`. A shell therefore never has to distinguish the two, and
1131    /// `is_some()` means "a live IME session" with no second check.
1132    ///
1133    /// **The platform still sees the keyboard-hide.** All three shells already
1134    /// map `None` onto the inactive form on the way out, so the observable wire
1135    /// behavior is unchanged: `frust-shell-android`'s `ime_state_to_json` returns
1136    /// `ImeJsonState::default()` (`active:false`, empty text, `-1` indices, null
1137    /// caret, `"normal"`) and `frust-shell-ios`' returns the byte-identical
1138    /// `ime_state_json(false, "", -1, -1, -1, -1, None, "normal")` — exactly what
1139    /// the navigator's own cleared surface serialised to before. Kotlin's
1140    /// `pollImeAfterDispatch` and Swift's `syncImeFocus` both branch on `active`
1141    /// alone (an inactive surface's text/caret/content-type are ignored), and the
1142    /// desktop shell's `sync_ime` reads `is_some_and(|s| s.active)`. Dropping the
1143    /// inactive surface's payload also stops a disabled *secret* field's text
1144    /// riding to the platform after its session ended.
1145    pub fn ime_state(&self) -> Option<ImeState> {
1146        self.ime_state.clone()
1147    }
1148
1149    /// The focus/IME session generation — bumped on every **actual** change of
1150    /// [`is_focus_active`](RenderRoot::is_focus_active) or
1151    /// [`ime_state`](RenderRoot::ime_state), and on nothing else.
1152    ///
1153    /// The *edge* counterpart of those two level accessors, for a shell that
1154    /// needs "did the focus/IME session move since I last looked?" rather than
1155    /// "is something focused?". A shell caches the value it last saw and
1156    /// compares (mirroring [`semantics_generation`](RenderRoot::semantics_generation)'s
1157    /// cheap dirty gate) — that comparison is the mobile frame gate's
1158    /// `FrameInputs::focus_or_ime_changed` input.
1159    ///
1160    /// A same-value write never moves it: re-publishing an identical IME
1161    /// surface (which the paint pass does on every frame a field stays focused)
1162    /// or re-blurring an already-blurred root is not an edge. Wrapping is
1163    /// deliberate and harmless — a comparison, never an ordering.
1164    ///
1165    /// # Not the session's identity
1166    ///
1167    /// This counts *changes to the published surface*, not *sessions*, and the
1168    /// two come apart in both directions — see
1169    /// [`focus_epoch`](RenderRoot::focus_epoch), which is what to reach for when
1170    /// the question is "is this still the same focus session?". Answering that
1171    /// one from this counter is wrong whenever focus moves between two fields
1172    /// without the published value changing.
1173    pub fn focus_ime_generation(&self) -> u64 {
1174        self.focus_ime_gen
1175    }
1176
1177    /// The live focus session's **identity** — advanced once per honoured focus
1178    /// claim and once per session release, and by nothing else.
1179    ///
1180    /// The neighbour of [`focus_ime_generation`](RenderRoot::focus_ime_generation)
1181    /// and easy to mistake for it, so: that one counts *changes to the published
1182    /// surface* (the focus flag, or the [`ImeState`] value), this one counts
1183    /// *sessions*. They come apart in both directions, which is why both exist:
1184    ///
1185    /// * Focus moving from one field to another moves this one and can leave
1186    ///   that one completely still. Claiming focus while some field already
1187    ///   holds it writes `true` over `true`, and the surface the new field
1188    ///   publishes may compare equal to the old field's ([`ImeState`] is
1189    ///   `{active, editing, caret, content_type}` and names no widget) — or may
1190    ///   not be published at all, since a widget is free to take focus and
1191    ///   publish nothing, which leaves the previous field's surface standing.
1192    /// * An edit landing, a caret moving, or the field being repositioned under
1193    ///   the user moves that one and leaves this one still: the session is the
1194    ///   same session throughout.
1195    ///
1196    /// So a caller binding an asynchronous answer to the session that asked for
1197    /// it wants this one; a caller asking "must I run a frame, or re-sync the
1198    /// platform IME?" wants that one.
1199    ///
1200    /// **Never `0`.** The counter is built at `1` and steps *past* `0` on wrap,
1201    /// because `0` is a never-claimed [`ChildPod`](crate::widget::ChildPod)'s
1202    /// stamp and a root publishing it would hand every unclaimed pod in the tree
1203    /// a live link. A caller is therefore free to use `0` as its own "no root /
1204    /// no answer" sentinel with no risk of colliding with a live value. Wrapping
1205    /// is otherwise deliberate and harmless: the value is compared for equality,
1206    /// never ordered.
1207    pub fn focus_epoch(&self) -> u64 {
1208        self.focus_epoch
1209    }
1210
1211    /// Set the root's focus flag, bumping [`RenderRoot::focus_ime_generation`]
1212    /// only when the value actually moves.
1213    ///
1214    /// One of the two writers of `focus_active` outside construction (the other
1215    /// is [`release_focus_session_in`], which clears it together with the IME
1216    /// surface as one edge): every focus/blur arm of [`RenderRoot::event`] goes
1217    /// through one of them, so the edge generation cannot drift from the state it
1218    /// describes. In practice this one only ever *sets* focus — a clear is always
1219    /// a session release.
1220    fn set_focus_active(&mut self, active: bool) {
1221        if self.focus_active != active {
1222            self.focus_active = active;
1223            self.focus_ime_gen = self.focus_ime_gen.wrapping_add(1);
1224        }
1225    }
1226
1227    /// Store (or clear) the shell-facing IME surface, bumping
1228    /// [`RenderRoot::focus_ime_generation`] only when the stored value actually
1229    /// moves — the `&mut self` form of [`store_ime_state_in`], for the event
1230    /// pass (the paint pass holds disjoint field borrows and calls that
1231    /// function directly).
1232    fn store_ime_state(&mut self, ime: Option<ImeState>) {
1233        store_ime_state_in(&mut self.ime_state, &mut self.focus_ime_gen, ime);
1234    }
1235
1236    /// End the focus/IME session: clear `focus_active` and drop the shell-facing
1237    /// surface together, moving [`RenderRoot::focus_ime_generation`] exactly once
1238    /// if either was set. The `&mut self` form of [`release_focus_session_in`]
1239    /// (whose doc carries the full contract), for the event and rebuild passes;
1240    /// the paint pass holds disjoint field borrows and calls that function
1241    /// directly.
1242    ///
1243    /// Idempotent: releasing an already-released root writes the same values
1244    /// back and fires no edge.
1245    fn release_focus_session(&mut self) {
1246        release_focus_session_in(
1247            &mut self.focus_active,
1248            &mut self.ime_state,
1249            &mut self.focus_ime_gen,
1250            &mut self.focus_epoch,
1251            self.root_identity,
1252        );
1253        // No session, no owner. A paint pass that releases re-resolves the owner
1254        // from the pods before it ends anyway (see
1255        // `RenderRoot::resolve_session_surface`), so this write is the event and
1256        // rebuild passes' own.
1257        self.focus_surface = None;
1258        // A selection toolbar describes the *focused* field's selection, so the
1259        // session ending is the toolbar ending — and it must end on the event
1260        // pass that blurred, not a frame later when the next paint happens to
1261        // publish nothing. Change-guarded like every other edge here: releasing
1262        // an already-toolbarless root moves no generation.
1263        if self.selection_toolbar.take().is_some() {
1264            self.selection_toolbar_gen = self.selection_toolbar_gen.wrapping_add(1);
1265        }
1266    }
1267
1268    /// Publish this root's live focus session so the pods can compare their own
1269    /// stamps against it — the focus counterpart of the `(root, epoch)` pair
1270    /// `crate::event::set_live_hover_link` publishes for hover.
1271    ///
1272    /// Called at the head of every pass, not only when the session moves: the
1273    /// channel mirrors one root, so a second root driving passes on the same
1274    /// thread would otherwise leave this one's pods comparing against a session
1275    /// that is none of their business. Re-publishing an unchanged triple costs a
1276    /// `Cell` write and notifies nothing.
1277    ///
1278    /// Publishes the session "at rest" — the live epoch and the epoch a claim
1279    /// would take are the same value. [`RenderRoot::event`] publishes the two
1280    /// apart for the length of its dispatch; see that method.
1281    fn publish_focus_session(&self) {
1282        crate::widget::set_live_focus_session(
1283            self.root_identity,
1284            self.focus_epoch,
1285            self.focus_epoch,
1286        );
1287    }
1288
1289    /// End the standing hover link outright, outside any hover pass: advance the
1290    /// epoch (which strands every stamp in the tree at once, so no container has
1291    /// to be told) and clear the mirror.
1292    ///
1293    /// Hover's analog of [`RenderRoot::release_focus_session`], and idempotent in
1294    /// the same way — ending a hover nothing holds writes the same mirror back and
1295    /// costs one epoch. There is no generation counter to move: hover is not a
1296    /// session a shell mirrors (see [`RenderRoot::is_hover_active`]).
1297    ///
1298    /// The one caller is [`RenderRoot::rebuild`]'s severed-claimant drain; a hover
1299    /// pass ends its own link inline, where it also decides the *new* one.
1300    fn end_hover_link(&mut self) {
1301        self.hover_epoch = self.hover_epoch.wrapping_add(1);
1302        self.hover_active = false;
1303        self.hover_link = None;
1304        crate::event::set_live_hover_link(self.root_identity, 0);
1305    }
1306
1307    /// The [`PlatformViewFrame`]s published during the most recent
1308    /// [`RenderRoot::paint`], in paint order.
1309    ///
1310    /// Replaced wholesale every pass (see the `platform_view_frames` field
1311    /// doc), so a pass with no publishers yields an empty slice — a shell
1312    /// never sees a stale frame for a slot that stopped painting.
1313    pub fn platform_view_frames(&self) -> &[PlatformViewFrame] {
1314        &self.platform_view_frames
1315    }
1316
1317    /// The z-shield rects reported during the most recent [`RenderRoot::paint`]
1318    /// (see [`crate::widget::PaintCtx::report_input_shield`]), in paint order.
1319    ///
1320    /// Replaced wholesale every pass, exactly like
1321    /// [`RenderRoot::platform_view_frames`] — a shell feeds both into the same
1322    /// differ ingest call, and the differ intersects these against each
1323    /// interactive slot's rect.
1324    pub fn input_shields(&self) -> &[Rect] {
1325        &self.input_shields
1326    }
1327
1328    /// Drain the slot ids whose `platform_view` widgets were torn down since the
1329    /// last call (`View::teardown` ran on them — see
1330    /// [`crate::widget::report_retired_slot`]).
1331    ///
1332    /// The prompt-teardown channel: a shell calls this once per frame, right
1333    /// after its rebuild, and retires each id in its platform-view differ
1334    /// (`PlatformViewState::retire`) so a disposed slot's native view goes away
1335    /// immediately instead of waiting out the differ's missing-streak
1336    /// heuristic. Draining is destructive, mirroring
1337    /// [`RenderRoot::take_change_flags`]: an id is reported exactly once, so a
1338    /// shell that drains and drops the result loses the prompt path (the
1339    /// missing-streak backstop still covers it).
1340    ///
1341    /// A merely *culled* slot (scrolled offscreen, a parent skipping paint)
1342    /// never appears here — culling doesn't run `teardown` — which is what
1343    /// keeps the camera keep-alive contract intact.
1344    pub fn take_retired_platform_views(&mut self) -> Vec<u64> {
1345        crate::widget::take_retired_slots()
1346    }
1347
1348    /// Take (and clear) the dirtiness accumulated since the last call.
1349    ///
1350    /// A shell can consult this to skip the layout/paint passes when nothing has
1351    /// changed and no redraw was requested (a desktop optimisation; the mobile
1352    /// continuous-loop shells may ignore it and repaint every tick). Each
1353    /// [`RenderRoot::rebuild`] merges its result here; this drains it.
1354    pub fn take_change_flags(&mut self) -> ChangeFlags {
1355        let flags = self.pending;
1356        self.pending = ChangeFlags::NONE;
1357        flags
1358    }
1359
1360    /// Non-draining peek at the dirtiness accumulated since the last
1361    /// [`RenderRoot::take_change_flags`] — `true` when any `LAYOUT`/`PAINT`
1362    /// bit is pending, without clearing it.
1363    ///
1364    /// Complements [`take_change_flags`](RenderRoot::take_change_flags) for a
1365    /// shell frame gate: the gate reads this as one of its
1366    /// "should this frame run" inputs *before* deciding, so a frame it chooses
1367    /// to skip leaves `pending` intact for the next non-skipped frame to drain
1368    /// and act on. Draining stays the job of `take_change_flags`, called only
1369    /// on a frame that actually runs its layout/paint passes. No behavioral
1370    /// change to rebuild/layout/paint.
1371    pub fn has_pending_change_flags(&self) -> bool {
1372        !self.pending.is_empty()
1373    }
1374
1375    /// The root widget id, once built.
1376    pub fn root_id(&self) -> Option<WidgetId> {
1377        self.root_id
1378    }
1379
1380    /// Shared access to the retained tree (for the shell / tests).
1381    pub fn tree(&self) -> &WidgetTree {
1382        &self.tree
1383    }
1384
1385    /// A read-only, pre-order snapshot of the retained tree for tooling: per
1386    /// node an id, its parent and children, the concrete widget's type name, an
1387    /// optional debug label, and its absolute border box in logical px.
1388    ///
1389    /// Computed on demand in O(nodes) and takes `&self` — no per-frame
1390    /// bookkeeping, no mutation, and nothing here participates in
1391    /// build/layout/paint. Bounds reflect the **last layout pass**, so call it
1392    /// after one (before the first, every rect is zero-sized).
1393    ///
1394    /// Scope: the walk covers the [`WidgetTree`] arena *and* the
1395    /// [`ChildPod`](crate::widget::ChildPod)s containers own, reached through
1396    /// [`Widget::visit_children`](crate::widget::Widget::visit_children) — so it
1397    /// is the real retained hierarchy, not just the arena (which holds little
1398    /// more than the root pod). A container that leaves that seam defaulted
1399    /// reads as a leaf.
1400    pub fn inspect(&self) -> Vec<InspectNode> {
1401        self.tree.inspect()
1402    }
1403
1404    /// Per-node layout-dirty state, in exactly [`RenderRoot::inspect`]'s
1405    /// pre-order and with its ids — read-only, for tooling and tests. See
1406    /// [`WidgetTree::inspect_layout_dirty`].
1407    pub fn inspect_layout_dirty(&self) -> Vec<(WidgetId, bool)> {
1408        self.tree.inspect_layout_dirty()
1409    }
1410
1411    /// Per-node paint-dirty state, read-only — see
1412    /// [`WidgetTree::inspect_paint_dirty`].
1413    pub fn inspect_paint_dirty(&self) -> Vec<(WidgetId, bool)> {
1414        self.tree.inspect_paint_dirty()
1415    }
1416
1417    /// Run the build closure, then build (first call) or rebuild (subsequent calls)
1418    /// the root widget, returning what changed.
1419    ///
1420    /// the build closure is expected to be cheap and re-entrant: it is
1421    /// re-run in full every rebuild.
1422    ///
1423    /// # Deferred-callback flush
1424    ///
1425    /// The view diff itself is state-free (`rebuild_view` below takes no
1426    /// `State`), so a widget applying a structural op there — the navigator
1427    /// draining its queued `push`/`pop` is the shipped case — cannot run an app
1428    /// callback that needs `&mut State`. It instead queues the callback and calls
1429    /// [`mark_pending_result_flush`](crate::event::mark_pending_result_flush);
1430    /// this method drains that flag and dispatches an
1431    /// [`InputEvent::Housekeeping`] broadcast through the ordinary
1432    /// [`event`](RenderRoot::event) plumbing, where `state` *is* in scope. This
1433    /// is the only unconditional per-frame pass that holds `&mut State`, which is
1434    /// why the dispatch lives here and not in a shell (flushing on the next
1435    /// real input meant waiting seconds for a touch, or forever when the next
1436    /// touch went to chrome outside the navigator).
1437    ///
1438    /// A flushed callback mutates `State`, so the view built before it ran is
1439    /// stale — the build closure + `rebuild_view` cycle therefore re-runs after
1440    /// each flush, and the same frame shows the result. Results can queue further
1441    /// nav ops, so the loop is **bounded**; past the cap the flag is left standing
1442    /// and one more frame is requested rather than spinning (see
1443    /// `MAX_PENDING_RESULT_FLUSH_PASSES`, this module's private cap constant).
1444    ///
1445    /// The broadcast's [`EventOutcome`] is propagated, not discarded: a
1446    /// `needs_redraw` coming back from the dispatch folds into this rebuild's
1447    /// [`ChangeFlags::PAINT`] and the deferred frame request, so a callback
1448    /// whose only effect is [`EventCtx::request_redraw`]
1449    /// — invisible to the re-diff, since no view-visible state changed — still
1450    /// wakes both the mobile frame gate and the desktop `Wait` loop.
1451    pub fn rebuild(
1452        &mut self,
1453        build: &mut impl FnMut(&mut State) -> V,
1454        state: &mut State,
1455    ) -> ChangeFlags {
1456        // Republish this root's focus session before the diff runs: a pod
1457        // severed by it compares its own stamp against the channel from its
1458        // destructor, and the channel mirrors one root at a time.
1459        self.publish_focus_session();
1460        let view = build(state);
1461        let mut flags = self.rebuild_view(view);
1462
1463        // Deferred-callback convergence loop (see the method doc). Each pass:
1464        // drain the flag, run the queued callbacks against real state, then
1465        // re-diff so this frame reflects them.
1466        let mut passes = 0usize;
1467        while crate::event::take_pending_result_flush() {
1468            if passes >= MAX_PENDING_RESULT_FLUSH_PASSES {
1469                // Cap reached. Put the flag back — the work is still owed — and
1470                // ask for one more frame instead of spinning inside this one.
1471                // `pending |= PAINT` is what the mobile frame gate reads
1472                // (`has_pending_change_flags`); `deferred_frame` is what surfaces
1473                // on the next `paint` as `needs_frame`, which is how the desktop
1474                // `ControlFlow::Wait` loop learns to wake.
1475                crate::event::mark_pending_result_flush();
1476                flags |= ChangeFlags::PAINT;
1477                self.deferred_frame = true;
1478                break;
1479            }
1480            // The dispatch's own outcome is load-bearing, not noise: a flushed
1481            // callback whose *only* effect is `EventCtx::request_redraw` (no
1482            // signal write, no state the next build-closure run reads) leaves the
1483            // re-diff below reporting `ChangeFlags::NONE`, so nothing else in
1484            // this method would ever mark the frame dirty and the requested
1485            // redraw would be dropped on the floor. Fold it into exactly the
1486            // wake the cap branch above raises: `PAINT` reaches `self.pending`,
1487            // which is what the mobile frame gate reads
1488            // (`has_pending_change_flags`), and `deferred_frame` surfaces on the
1489            // next `paint` as `needs_frame`, which is how the desktop
1490            // `ControlFlow::Wait` loop learns to schedule a frame. Both
1491            // Housekeeping producers need it (a navigator pop-result callback
1492            // and `frust-widgets`' gesture long-press latch), and without it a
1493            // redraw-only effect waits for whatever input happens to arrive
1494            // next — exactly the failure this mechanism exists to remove.
1495            //
1496            // Non-empty flags also bump the semantics generation below, which
1497            // is correct: the callback just mutated real `State` through a live
1498            // `EventCtx`, so the accessibility tree may genuinely have changed,
1499            // and every other paint-class path here bumps it the same way (a
1500            // spurious bump costs one recompute of an unchanged tree, a missed
1501            // one strands a stale tree).
1502            let outcome = self.event(state, &InputEvent::Housekeeping);
1503            if outcome.needs_redraw {
1504                flags |= ChangeFlags::PAINT;
1505                self.deferred_frame = true;
1506            }
1507            let view = build(state);
1508            flags |= self.rebuild_view(view);
1509            passes += 1;
1510        }
1511
1512        // Generic-unmount focus release. A reconciler that tears down (or
1513        // type-swaps, or clears the `focused` flag of) a child pod holding the
1514        // recorded focus path *on the live focus chain* has severed that path,
1515        // but runs over a `BuildCtx` with no `RenderRoot` in scope — so it raises
1516        // `mark_focus_orphaned` and this drain performs the release the
1517        // reconciler could not. "On the live chain" is what `rebuild_view`'s seed
1518        // buys: a mark means a live session lost its owner, never that some stale
1519        // flag deep in an already-blurred branch went away (see
1520        // `mark_focus_orphaned`). Without it the root's mirror stays standing over
1521        // a widget that no longer exists: `is_focus_active()` keeps reporting
1522        // true and `ime_state()` keeps handing the shell a surface for a dead
1523        // field, self-correcting only on the next event pass — which never
1524        // arrives on a screen the user has stopped touching (the pop-into-idle
1525        // case this whole seam exists for).
1526        //
1527        // Drained *after* the flush loop so one release covers every pass: a
1528        // flushed callback that navigates re-diffs, and either diff may orphan
1529        // the focus. `Housekeeping` claims no focus of its own (its root arm is
1530        // inert), so nothing the loop dispatched can be undone here.
1531        //
1532        // The release marks no `ChangeFlags` of its own: the structural change
1533        // that severed the path already flagged `LAYOUT | PAINT`, and the
1534        // generation bump is what wakes the mobile frame gate's
1535        // `focus_or_ime_changed` edge for the one repaint the release needs.
1536        if crate::event::take_focus_orphaned() {
1537            self.release_focus_session();
1538        }
1539
1540        // Generic-unmount hover release, the same shape one channel over: a diff
1541        // that dropped the `ChildPod` holding the live hover link has severed a
1542        // path the epoch mechanism cannot strand, because stranding needs a hover
1543        // pass and the dead claimant will never see another one. Without this the
1544        // mirror stands over a widget that no longer exists — `is_hover_active()`
1545        // reporting a link nothing holds — and every surviving ancestor of the
1546        // claimant keeps painting hover chrome off its own still-matching stamp
1547        // until some later `Move` re-derives, which never comes on a pointer the
1548        // user has stopped moving.
1549        //
1550        // The mark is raised by the pod's destructor rather than by the
1551        // reconcilers (the stamp has no setter for a container to cooperate
1552        // through — see `mark_hover_orphaned`), which is what makes this cover
1553        // every removal route, including hand-rolled containers outside this
1554        // workspace. That reach is also why the mark is qualified by
1555        // `root_identity`: a destructor fires whenever a pod happens to die, so
1556        // an unqualified mark could be a second root's on this thread. Drained
1557        // after the flush loop for the focus release's reason: any pass of the
1558        // loop may re-diff, and one end covers them all.
1559        //
1560        // Unlike that release this one flags `PAINT` of its own. The reconciler
1561        // that dropped the claimant usually reported `LAYOUT | PAINT` already,
1562        // but "usually" is not a contract this drain can rest on: the destructor
1563        // route deliberately covers containers outside this workspace (that is
1564        // its whole reason for existing), and one of those can drop a pod while
1565        // reporting whatever flags it likes. Ending a hover always changes what
1566        // paints, so the correction states its own need for the frame it rides
1567        // on — idempotent where the reconciler already said so.
1568        if crate::event::take_hover_orphaned(self.root_identity) && self.hover_active {
1569            self.end_hover_link();
1570            flags |= ChangeFlags::PAINT;
1571        }
1572
1573        self.pending |= flags;
1574        // A rebuild that changed layout/paint could have changed the semantics
1575        // tree (added/removed/relabelled nodes); bump the dirty gate a shell polls
1576        // via `semantics_if_changed`.
1577        if !flags.is_empty() {
1578            self.semantics_gen = self.semantics_gen.wrapping_add(1);
1579        }
1580        flags
1581    }
1582
1583    /// The rebuild body, split out so [`RenderRoot::rebuild`] can accumulate the
1584    /// result into [`RenderRoot::pending`] in one place.
1585    ///
1586    /// # Seeding the diff's focus chain
1587    ///
1588    /// The root is where the effective focus chain ([`BuildCtx::has_focus`])
1589    /// starts: the root widget sits in no `ChildPod`, so its "link above" is the
1590    /// root's own session mirror. A reconciler deep in the diff ANDs its pod's
1591    /// `focused` flag onto this seed and marks an orphan only if the whole chain
1592    /// holds — which is why the seed is "is there a session to lose" rather than
1593    /// `focus_active` alone: an active surface parked without the flag is still a
1594    /// live session `release_focus_session` would move. With neither set there is
1595    /// nothing to release, so the seed is `false` and the diff marks nothing.
1596    fn rebuild_view(&mut self, view: V) -> ChangeFlags {
1597        // Read before the `&mut self.next_id` borrow below (disjoint fields, but
1598        // spelled out for the reader).
1599        let session_live = self.focus_active || self.ime_state.is_some();
1600        match (self.root_id, self.prev_view.take()) {
1601            // Reconcile against the previous view of the same type.
1602            (Some(root_id), Some(prev)) => {
1603                let mut ctx = BuildCtx::new(&mut self.next_id);
1604                ctx.set_has_focus(session_live);
1605                let flags = {
1606                    let pod = self
1607                        .tree
1608                        .pod_mut(root_id)
1609                        .expect("root pod present when root_id is set");
1610                    let element = pod
1611                        .widget_mut()
1612                        .downcast_mut::<V::Element>()
1613                        .expect("root widget type matches its originating view");
1614                    view.rebuild(&prev, element, &mut ctx)
1615                };
1616                if let Some(pod) = self.tree.pod_mut(root_id) {
1617                    pod.merge_flags(flags);
1618                }
1619                self.prev_view = Some(view);
1620                flags
1621            }
1622            // First build: materialise the widget and insert it as the root.
1623            _ => {
1624                let mut ctx = BuildCtx::new(&mut self.next_id);
1625                // A first build tears nothing down, so the seed is moot — set it
1626                // anyway so the rule is "the root always seeds the chain", with no
1627                // arm exempt.
1628                ctx.set_has_focus(session_live);
1629                let id = ctx.alloc_id();
1630                let element = view.build(&mut ctx);
1631                // `new_typed` boxes the element exactly like `new` would, and
1632                // additionally records `V::Element`'s type name for
1633                // introspection — the concrete type is only nameable here.
1634                let pod = WidgetPod::new_typed(id, element);
1635                let root_id = self.tree.insert_root(pod);
1636                self.root_id = Some(root_id);
1637                self.prev_view = Some(view);
1638                ChangeFlags::LAYOUT | ChangeFlags::PAINT
1639            }
1640        }
1641    }
1642
1643    /// Lay out the root widget against `window_size` and record its geometry.
1644    ///
1645    /// The root receives loose constraints (zero up to the window size) and is
1646    /// placed at the origin. Returns the size the root chose. No text context is
1647    /// threaded in (use [`RenderRoot::layout_with_text`] when the tree contains
1648    /// text widgets); the stored theme, if any, is still threaded down.
1649    pub fn layout(&mut self, window_size: Size) -> Size {
1650        self.layout_inner(window_size, None)
1651    }
1652
1653    /// Lay out the root widget, threading a shared text-shaping context down to
1654    /// text widgets.
1655    ///
1656    /// `text_ctx` is the shell-owned `frust_text::TextContext`, passed
1657    /// type-erased so this crate needs no `frust-text` dependency. Text
1658    /// widgets recover it via [`crate::widget::LayoutCtx::text_context`]. The
1659    /// stored theme, if any, is threaded down alongside it.
1660    pub fn layout_with_text(&mut self, window_size: Size, text_ctx: &mut dyn Any) -> Size {
1661        self.layout_inner(window_size, Some(text_ctx))
1662    }
1663
1664    /// Shared layout body: hands the root loose window constraints, lends the
1665    /// optional text context and the stored theme into a [`LayoutCtx`], and
1666    /// records the size the root returns.
1667    fn layout_inner(&mut self, window_size: Size, text_ctx: Option<&mut dyn Any>) -> Size {
1668        // The window size is a global layout input (an overlay pod is laid out
1669        // against it, not against its owner's constraints): a change invalidates
1670        // every cached layout. Compared against what the last pass stored.
1671        if self.window_size != window_size {
1672            self.bump_layout_epoch();
1673        }
1674        self.window_size = window_size;
1675        // A floated surface dirtied outside its owner's pod chain (see
1676        // `overlay_pods`) is reachable only through a full pass.
1677        if self.overlay_pod_dirty() {
1678            self.bump_layout_epoch();
1679        }
1680        if self.root_id.is_none() {
1681            return Size::ZERO;
1682        }
1683        if !(self.layout_audit.enabled && self.layout_skip) {
1684            return self.layout_pass(window_size, text_ctx, !self.layout_skip);
1685        }
1686        // Audit: the normal (skipping) pass, then a forced one over the same
1687        // constraints and epoch; the forced geometry is what stays.
1688        let mut text_ctx = text_ctx;
1689        self.layout_pass(window_size, text_ctx.as_deref_mut(), false);
1690        let before = self.audit_snapshot();
1691        let size = self.layout_pass(window_size, text_ctx, true);
1692        let after = self.audit_snapshot();
1693        self.audit_compare(&before, &after);
1694        size
1695    }
1696
1697    /// Every node's `(id, type, label, origin relative to parent, size)`.
1698    fn audit_snapshot(&self) -> Vec<AuditGeom> {
1699        let nodes = self.tree.inspect();
1700        let origins: HashMap<WidgetId, Point> =
1701            nodes.iter().map(|n| (n.id, n.bounds.origin())).collect();
1702        nodes
1703            .into_iter()
1704            .map(|n| {
1705                let parent = n
1706                    .parent
1707                    .and_then(|p| origins.get(&p).copied())
1708                    .unwrap_or(Point::ZERO);
1709                AuditGeom {
1710                    id: n.id,
1711                    type_name: n.type_name,
1712                    label: n.debug_label.map(|l| l.into_owned()),
1713                    origin: Point::new(n.bounds.x0 - parent.x, n.bounds.y0 - parent.y),
1714                    size: n.bounds.size(),
1715                }
1716            })
1717            .collect()
1718    }
1719
1720    /// Report every pod that differs between the two snapshots.
1721    fn audit_compare(&mut self, before: &[AuditGeom], after: &[AuditGeom]) {
1722        const TOLERANCE: f64 = 1e-3;
1723        let frame = self.layout_audit.frame;
1724        self.layout_audit.frame += 1;
1725        let by_id: HashMap<WidgetId, &AuditGeom> = before.iter().map(|g| (g.id, g)).collect();
1726        for now in after {
1727            let Some(was) = by_id.get(&now.id) else {
1728                continue;
1729            };
1730            let differs = (was.origin.x - now.origin.x).abs() > TOLERANCE
1731                || (was.origin.y - now.origin.y).abs() > TOLERANCE
1732                || (was.size.width - now.size.width).abs() > TOLERANCE
1733                || (was.size.height - now.size.height).abs() > TOLERANCE;
1734            if !differs {
1735                continue;
1736            }
1737            self.layout_audit.mismatches += 1;
1738            let key = (now.type_name, now.label.clone());
1739            let due = self
1740                .layout_audit
1741                .last_logged
1742                .get(&key)
1743                .is_none_or(|last| frame.saturating_sub(*last) >= 60);
1744            if due {
1745                self.layout_audit.last_logged.insert(key, frame);
1746                log::warn!(
1747                    "layout-audit: {} (label {:?}) changed geometry without signalling layout: skipped pass origin {:?} size {:?}, forced pass origin {:?} size {:?}",
1748                    now.type_name,
1749                    now.label,
1750                    was.origin,
1751                    was.size,
1752                    now.origin,
1753                    now.size,
1754                );
1755            }
1756        }
1757    }
1758
1759    /// One layout pass over the root; `force` lays out every pod regardless of
1760    /// the skip rule.
1761    fn layout_pass(
1762        &mut self,
1763        window_size: Size,
1764        text_ctx: Option<&mut dyn Any>,
1765        force: bool,
1766    ) -> Size {
1767        let Some(root_id) = self.root_id else {
1768            return Size::ZERO;
1769        };
1770        let bc = BoxConstraints::loose(window_size);
1771        // Disjoint field borrows: the theme (immut) and the tree (mut) are
1772        // different fields of `self`, so both borrows coexist through the layout.
1773        let theme = self.theme.as_deref();
1774        // Copied out before the `&mut self.tree` borrow below (a disjoint,
1775        // `Copy` field read).
1776        let insets = self.insets;
1777        let layout_epoch = self.layout_epoch;
1778        let Some(pod) = self.tree.pod_mut(root_id) else {
1779            return Size::ZERO;
1780        };
1781        let mut ctx = LayoutCtx::with_resources(text_ctx, theme);
1782        // Thread the window insets down; one layout context reaches the whole
1783        // tree, so the global insets are set once here (see `crate::insets`).
1784        ctx.set_window_insets(insets);
1785        // Thread the window's own size down the same way — global and
1786        // origin-independent like the insets. A widget floating an overlay pod
1787        // lays it out against this rather than against its own constraints (see
1788        // `LayoutCtx::window_size`).
1789        ctx.set_window_size(window_size);
1790        // The layout skip's two global inputs: the epoch every pod compares its
1791        // own against, and the kill switch (see `ChildPod::layout_child`). The
1792        // root widget itself sits in no `ChildPod` and is always laid out.
1793        ctx.set_layout_epoch(layout_epoch);
1794        ctx.set_layout_force(force);
1795        let size = pod.widget_mut().layout(&mut ctx, &bc);
1796        pod.set_layout(Point::ZERO, size);
1797        size
1798    }
1799
1800    /// Paint the root widget into `scene`, returning whether the tree wants
1801    /// another frame to continue an animation.
1802    ///
1803    /// A widget whose paint advances animation state (e.g. a scroll fling) signals
1804    /// [`PaintCtx::request_frame`]; that flag bubbles up through the container
1805    /// [`ChildPod`](crate::widget::ChildPod)s and out here as
1806    /// [`PaintOutcome::needs_frame`], which the shell honors by scheduling the next
1807    /// frame (desktop `window.request_redraw()`; the mobile continuous loops
1808    /// already do so). Mirrors how [`RenderRoot::event`] surfaces `needs_redraw`.
1809    ///
1810    /// A widget whose animation changes its *layout* signals
1811    /// [`PaintCtx::request_layout`] instead (or as well); that bubbles up the same
1812    /// way and is folded here into the render root's pending [`ChangeFlags`]
1813    /// (`LAYOUT`), so the *next* frame's
1814    /// [`take_change_flags`](RenderRoot::take_change_flags)`().needs_layout()`
1815    /// reports it and the mobile intra-frame layout skip relayouts while the
1816    /// animation is in flight. It is also surfaced on the returned
1817    /// [`PaintOutcome::needs_layout`].
1818    ///
1819    /// `frame_time` is the shell's shared monotonic clock for this frame
1820    /// (time enters `frust-core` from the shell, never `Instant::now()` here). It
1821    /// is seeded onto the root [`PaintCtx`] and threaded unchanged to every child
1822    /// ([`crate::widget::ChildPod::paint_child`]), so an animating widget advances
1823    /// against one consistent timestamp — see [`PaintCtx::frame_time`].
1824    ///
1825    /// # The overlay post-pass
1826    ///
1827    /// Painting the main tree is only the first half. Widgets registering a
1828    /// floated surface during that walk ([`PaintCtx::register_overlay`]) are
1829    /// drained here and painted **after** it, in band order — which is the only
1830    /// way a popover, menu or tooltip escapes its owner's paint order and every
1831    /// ancestor's clip. Their routing rects are retained (see
1832    /// `RenderRoot::overlay_hits`) for the next event pass to hit-test first, and
1833    /// their paint outcomes merge into this pass's own, so an animating overlay
1834    /// keeps the frames coming exactly like an animating widget in the tree.
1835    pub fn paint(&mut self, scene: &mut dyn PaintScene, frame_time: FrameTime) -> PaintOutcome {
1836        // Open the two paint-pass channels for the whole pass. Entering CLEARS
1837        // each slot, which is what makes "the registry is empty at the start of
1838        // every paint" true by construction rather than by everyone remembering
1839        // to unregister; `Drop` hands an enclosing pass its own back.
1840        let overlay_pass = OverlayPaintPass::enter();
1841        let toolbar_pass = SelectionToolbarPass::enter();
1842        // Republish this root's focus session before anything reads it: the
1843        // channel mirrors one root, and the pods about to be visited must
1844        // compare their stamps against *this* root's session.
1845        self.publish_focus_session();
1846        // Number this pass (see `paint_serial`).
1847        self.paint_serial = self.paint_serial.wrapping_add(1);
1848        // A hover link or focus session that moved since the last paint changes
1849        // what pods read at paint time without having dirtied the pod that lost
1850        // it, so no subtree painted under the old links is clean (see
1851        // `PaintCtx::subtree_clean`).
1852        let links = (self.hover_link, self.focus_epoch);
1853        if links != self.painted_links {
1854            self.painted_links = links;
1855            self.paint_epoch = self.paint_epoch.wrapping_add(1);
1856        }
1857
1858        let (mut outcome, main_tree_ime) = self.paint_main_tree(scene, frame_time);
1859        outcome.paint_serial = self.paint_serial;
1860
1861        // Drain what the main tree registered and paint it above everything.
1862        // Sorting is stable, so the band decides and registration order breaks
1863        // ties within a band (see `crate::overlay::sort_into_paint_order`).
1864        let mut entries = overlay_pass.take();
1865        sort_into_paint_order(&mut entries);
1866        // Retain the routing half — never the pods — for the next event pass.
1867        self.overlay_hits = entries.iter().map(OverlayHit::of).collect();
1868        // ... and a weak handle to each pod, for the next layout pass's
1869        // dirty-surface check (see `overlay_pods`).
1870        self.overlay_pods = entries
1871            .iter()
1872            .map(|entry| Rc::downgrade(&entry.pod))
1873            .collect();
1874        let (holder, holder_ime) = self.paint_overlays(&entries, scene, frame_time, &mut outcome);
1875        // Release the owners' pod clones before the pass ends: the root holds no
1876        // overlay pod at rest, so a pod's lifetime stays exactly its owner's.
1877        drop(entries);
1878
1879        // Both halves have now spoken, so the one question neither of them can
1880        // answer alone — whose surface the shell is configured with — is settled
1881        // in one place, with both answers in hand.
1882        self.resolve_session_surface(main_tree_ime, holder, holder_ime);
1883
1884        // Resolve the selection-toolbar publish last, so a field that published
1885        // while painting *inside* a floated pod (a text input hosted in a
1886        // popover) is resolved by the same rule as one in the main tree.
1887        self.resolve_selection_toolbar(toolbar_pass.take());
1888
1889        outcome
1890    }
1891
1892    /// Settle which branch's IME surface the shell is configured with, from the
1893    /// two halves of the paint pass: what the main tree published, and what the
1894    /// floated surface holding the live focus link published (`holder`/
1895    /// `holder_ime`, both resolved from the pods' own stamps — see
1896    /// [`RenderRoot::paint_overlays`]).
1897    ///
1898    /// # Why it is decided here and not where the publish happens
1899    ///
1900    /// A publish is a claim about *the* session, and the session has exactly one
1901    /// owner. The main tree paints first, so at the moment it publishes, nothing
1902    /// yet knows whether a floated surface is about to prove that the session is
1903    /// no longer the tree's. Storing it there and correcting later is what
1904    /// produced a frame of lag with a secure field's surface standing in it —
1905    /// and the record the correction had to be driven from was one nothing could
1906    /// falsify. Deferring the decision by the width of one pass removes both.
1907    ///
1908    /// # The rules
1909    ///
1910    /// * the owner's own publish wins, whether that owner is a surface or the
1911    ///   tree; a publish from anywhere else was already dropped by the half that
1912    ///   collected it;
1913    /// * an **inactive** publish from the owner is the session ending, not a
1914    ///   value — the same reading every other release site gives it;
1915    /// * an owner that published **nothing** leaves a standing surface standing,
1916    ///   *unless* ownership moved this pass: then what stands belongs to the
1917    ///   branch that just lost the session, and goes with it rather than
1918    ///   remaining as the shell's idea of a live one. This is the rule that stops
1919    ///   a popover taking focus from leaving the platform keyboard configured for
1920    ///   the secure field underneath it — in both directions, since the session
1921    ///   coming back to the tree moves ownership just as much as it leaving.
1922    fn resolve_session_surface(
1923        &mut self,
1924        main_tree_ime: Option<ImeState>,
1925        holder: Option<OverlayKey>,
1926        holder_ime: Option<ImeState>,
1927    ) {
1928        let published = if holder.is_some() {
1929            holder_ime
1930        } else {
1931            main_tree_ime
1932        };
1933        let ownership_moved = holder != self.focus_surface;
1934        match published {
1935            Some(ime) if ime.active => self.store_ime_state(Some(ime)),
1936            Some(_) => self.release_focus_session(),
1937            None if ownership_moved => self.store_ime_state(None),
1938            None => {}
1939        }
1940        // Resolved from the links themselves, never from the focus request that
1941        // opened the session — see the field's doc.
1942        self.focus_surface = if self.focus_active { holder } else { None };
1943    }
1944
1945    /// Paint the main widget tree — everything [`RenderRoot::paint`] does before
1946    /// the floated overlay pods get their turn.
1947    ///
1948    /// Split out from [`RenderRoot::paint`] purely for borrow scoping: this body
1949    /// holds `&mut self.tree` (beside disjoint borrows of the theme) for its whole
1950    /// length, while painting an overlay pod needs the root's fields again to
1951    /// merge outcomes and extend the per-pass channels. Nothing about the pass
1952    /// itself changed when it moved here.
1953    ///
1954    /// Reports, beside the outcome, the IME surface the tree published this pass
1955    /// (if any) — handed back rather than stored, because whether the tree still
1956    /// owns the session is not knowable until the floated pods have been visited
1957    /// (see [`RenderRoot::resolve_session_surface`]).
1958    fn paint_main_tree(
1959        &mut self,
1960        scene: &mut dyn PaintScene,
1961        frame_time: FrameTime,
1962    ) -> (PaintOutcome, Option<ImeState>) {
1963        let Some(root_id) = self.root_id else {
1964            return (PaintOutcome::default(), None);
1965        };
1966        // Disjoint field borrows: the theme (immut) vs the tree (mut).
1967        let theme = self.theme.as_deref();
1968        // Copied out before the `&mut self.tree` borrow (a disjoint `Copy` read).
1969        let insets = self.insets;
1970        // Same disjoint `Copy` read: the presented-frame count threaded to widgets.
1971        let presented_frames = self.presented_frames;
1972        // Same disjoint `Copy` read: the surface-translucency flag the
1973        // platform-view hole-punch reads (see `PaintCtx::is_translucent`).
1974        let surface_translucent = self.surface_translucent;
1975        let (paint_epoch, retained_layers) = (self.paint_epoch, self.retained_layers);
1976        let paint_serial = self.paint_serial;
1977        let retained_ledger = &self.retained_ledger;
1978        if let Some(pod) = self.tree.pod_mut(root_id) {
1979            let mut ctx = PaintCtx::new(pod.origin(), pod.size());
1980            // Seed the shared shell clock so the whole paint pass sees one time.
1981            ctx.set_frame_time(frame_time);
1982            // Lend the stored theme (type-erased) into the paint pass; widgets
1983            // recover it via `PaintCtx::theme_as`.
1984            ctx.set_theme(theme);
1985            // Thread the window insets down (global — see `crate::insets`).
1986            ctx.set_window_insets(insets);
1987            // Thread the shell's presented-frame count down (global; a widget
1988            // measuring FPS differences it — see `PaintCtx::presented_frames`).
1989            ctx.set_presented_frames(presented_frames);
1990            // Thread the surface-translucency flag down (global; the
1991            // platform-view hole-punch gates its rect-clear on it — see
1992            // `PaintCtx::is_translucent`).
1993            ctx.set_translucent(surface_translucent);
1994            ctx.set_paint_state(
1995                paint_epoch,
1996                retained_layers,
1997                Some(retained_ledger),
1998                paint_serial,
1999            );
2000            // Seed the root widget's paint-time focus from the session mirror so
2001            // a leaf-root editable observes its own focus, and thread the live
2002            // session's identity down beside it: deeper focus is resolved
2003            // per-pod by `ChildPod::paint_child`, which counts a recorded link
2004            // only while its stamp names this session.
2005            //
2006            // The mirror alone, deliberately. Narrowing the seed by *which
2007            // branch* the root believes owns the session was the previous shape,
2008            // and it de-seeded the whole tree off a record no pass could
2009            // falsify: a link a floated pod recorded is reached by no container's
2010            // blur sweep, so once one existed the tree never got seeded again.
2011            // The epoch answers the same question where it can actually be
2012            // answered — at each link, against the session that link was
2013            // recorded for.
2014            //
2015            // The arena root has no recorded link of its own (it is a
2016            // `WidgetPod`, not a `ChildPod`, and carries no stamp), so a
2017            // *leaf-root* editable is still seeded from the bare mirror. Every
2018            // real tree puts a container there, and the first `ChildPod` below it
2019            // composes the stamp back in.
2020            ctx.set_has_focus(self.focus_active);
2021            ctx.set_focus_epoch(self.focus_epoch);
2022            // Thread the hover mirror + live epoch the same way: the root widget's
2023            // own hover comes from the mirror (a leaf root can claim hover itself),
2024            // and deeper links are resolved per-pod by `ChildPod::paint_child`
2025            // against this epoch.
2026            ctx.set_hovered(self.hover_active);
2027            ctx.set_hover_epoch(self.hover_epoch);
2028            pod.widget_mut().paint(&mut ctx, scene);
2029            pod.clear_flags();
2030            // A focused editable republishes its IME surface during paint (which
2031            // runs after every rebuild), so a controlled change applied by the
2032            // rebuild — e.g. a submit clearing the field — refreshes the
2033            // shell-facing `ime_state` that the event pass alone would leave
2034            // stale.
2035            //
2036            // Collected, not stored: what the tree published is only the
2037            // session's if the session is still the tree's, and the pods that
2038            // could say otherwise have not been visited yet.
2039            // `resolve_session_surface` decides once both halves have spoken,
2040            // and routes the store through the change guard so an *unchanged*
2041            // republish — the overwhelmingly common case, a focused field
2042            // re-publishing the same surface frame after frame — moves no
2043            // generation and therefore fires no `focus_or_ime_changed` edge at
2044            // the shell.
2045            //
2046            // An **inactive** publish is not a surface refresh at all: it is the
2047            // publishing widget saying "this session is over" — the navigator's
2048            // post-pop `cleared_ime_state`, `PatternSwitcher`'s equivalent, and
2049            // a `TextInput` turned disabled/read-only under a live focus are the
2050            // three shipped producers, and every one of them is an unmount or a
2051            // de-focus. Storing it as `Some(inactive)` and leaving `focus_active`
2052            // standing is what leaked the session after a pop: the shells' gate
2053            // saw `ime_state().is_some()`, `is_focus_active()` kept lying, and the
2054            // next real focus interaction started from a corrupt baseline. So the
2055            // resolver takes the *full* release instead — the same one a
2056            // blur-on-outside-tap `Down` performs, firing exactly one edge.
2057            //
2058            // The `self.focus_active` guard stays and is applied here, at the
2059            // point of collection: a publish arriving when no session is live
2060            // describes nothing, so a widget whose pod focus was just cleared by
2061            // a container-routed blur (but whose internal flag lags one frame)
2062            // can never resurrect the `ime_state` that blur dropped — even
2063            // before it observes the blur via `PaintCtx::has_focus`.
2064            //
2065            // Provenance below that guard is the chain's own job now: a link
2066            // reads focused only while its stamp names the live session, so a
2067            // branch the session has left publishes nothing to collect.
2068            let main_tree_ime = if self.focus_active {
2069                ctx.take_ime_state()
2070            } else {
2071                None
2072            };
2073            // Replace (never merge) the whole platform-view collection with
2074            // whatever this pass published — unlike `ime_state` above there is
2075            // no single "the" published instance to guard behind a focus
2076            // check, and a pass that publishes none must clear out every
2077            // stale frame from the previous one (see the field's doc comment).
2078            self.platform_view_frames = ctx.take_platform_views();
2079            // Same replace-per-pass discipline for the z-shield channel: a pass
2080            // whose shields stopped painting reports none, so a stale shield can
2081            // never keep stealing input from an interactive slot (see
2082            // `PaintCtx::report_input_shield`).
2083            self.input_shields = ctx.take_input_shields();
2084            // Fold every paint-time layout dirtying into `pending` so the *next*
2085            // frame relayouts. `pending` survives to the next frame and feeds both
2086            // the frame gate (`has_pending_change_flags`) and the shells' layout
2087            // gate (`take_change_flags().needs_layout()`), so no shell change is
2088            // needed on any platform. Three sources: a bubbled `request_layout`;
2089            // an unpaced frame request (an animating subtree's pods were marked,
2090            // the conservative rule — the pass that follows is mostly skipped);
2091            // and the path bit, which also carries the one trailing paint after
2092            // an animation lands. A paced-only frame (`CosmeticLoop` requests
2093            // alone) folds nothing: a pending flag would make the frame gate run
2094            // it every vsync, and the pods it marked are laid out by the next
2095            // pass that runs. The root widget has no pod to remember its own
2096            // trailing paint, so `painted_animating` does that here. Only the
2097            // first source is reported on the outcome's `needs_layout`.
2098            let needs_layout = ctx.needs_layout();
2099            let animating = needs_layout || ctx.needs_frame();
2100            let unpaced = needs_layout || (ctx.needs_frame() && !ctx.needs_frame_paced_only());
2101            let landing = self.painted_animating && !animating;
2102            if unpaced || landing || ctx.is_layout_path_dirty() {
2103                self.pending |= ChangeFlags::LAYOUT;
2104            }
2105            self.painted_animating = animating;
2106            // A rebuild that ran out of flush passes owes one more frame; surface
2107            // it here (and clear it) so a dirty-driven shell schedules the frame
2108            // that finishes the flush — see the `deferred_frame` field doc.
2109            let deferred_frame = std::mem::take(&mut self.deferred_frame);
2110            let outcome = PaintOutcome {
2111                needs_frame: ctx.needs_frame() || deferred_frame,
2112                needs_layout,
2113                // Aggregate tick class: paced-only iff a frame was requested and
2114                // every request was CosmeticLoop-class. The mobile frame gate
2115                // may throttle such a frame; any Transition request
2116                // (including the LAYOUT-implying `request_layout` above) leaves
2117                // this false so the frame runs every vsync.
2118                needs_frame_paced_only: ctx.needs_frame_paced_only(),
2119                // ...and, when it IS paceable, how fast it asked to be re-run:
2120                // the MIN over every paced request this pass (`Duration::ZERO`
2121                // / `None` meaning the theme's own cosmetic rate). The gate
2122                // resolves it against the live theme's cap — see
2123                // `PaintCtx::request_frame_paced_at`.
2124                paced_interval: ctx.paced_interval(),
2125                // Stamped by `paint` once both halves of the pass have run.
2126                paint_serial: 0,
2127            };
2128            (outcome, main_tree_ime)
2129        } else {
2130            (PaintOutcome::default(), None)
2131        }
2132    }
2133
2134    /// Paint the pods registered during this pass, above the main tree, and fold
2135    /// each one's paint outcome back into `outcome`.
2136    ///
2137    /// `entries` arrives in paint order (`Floating` band first, then `Tooltip`,
2138    /// registration order within each). Each pod is painted through a
2139    /// [`PaintCtx`] whose absolute origin is its own registered
2140    /// [`window_rect`](crate::overlay::OverlayEntry::window_rect) — not its
2141    /// owner's origin, which is the whole point of floating — carrying the same
2142    /// clock, theme, insets, presented count, translucency and focus/hover
2143    /// seeding the root pod's context carries, so a widget inside a pod cannot
2144    /// tell it is not in the tree.
2145    ///
2146    /// The pod borrow is taken per entry and released before the next: the root
2147    /// must never hold one across a pass boundary, nor across another entry's
2148    /// paint (two entries may belong to the same owner).
2149    ///
2150    /// It is also the one pass that can act on a floated pod's focus link at
2151    /// all, because it is the one pass holding the pods. Two things follow, and
2152    /// both happen here:
2153    ///
2154    /// * it **retires** a link the live session has already stranded
2155    ///   ([`ChildPod::retire_stale_focus_link`](crate::widget::ChildPod::retire_stale_focus_link)),
2156    ///   so the raw flag consumers that cannot consult an epoch — an owner's own
2157    ///   `is_focused()` read, a hand-written container's routing — stop seeing a
2158    ///   record of a session that has moved on;
2159    /// * it **resolves** which surface, if any, holds a link on the live session,
2160    ///   and collects that one surface's IME publish. Every other pod's publish
2161    ///   is dropped where it is taken: paint descends into every pod
2162    ///   unconditionally and the bubble up `paint_child` carries a published
2163    ///   surface whatever the publisher's link says, so without this the last pod
2164    ///   painted would decide what the shell is configured with — a surface
2165    ///   belonging to a field it has nothing to do with, secure-text
2166    ///   configuration and all.
2167    ///
2168    /// Returns `(holder, holder's publish)` for
2169    /// [`RenderRoot::resolve_session_surface`] to settle against the main tree's.
2170    /// **Two holders resolve to none:** a second live link is a contradiction the
2171    /// mechanism is supposed to make impossible (one claim, one chain, one
2172    /// stamp), and answering it by picking the topmost would let a surface speak
2173    /// for a session on evidence that has already failed. Reporting no holder
2174    /// instead means neither surface's publish is carried out of this pass —
2175    /// the resolver then reads the pass exactly as it reads one where nothing is
2176    /// floated at all.
2177    fn paint_overlays(
2178        &mut self,
2179        entries: &[OverlayEntry],
2180        scene: &mut dyn PaintScene,
2181        frame_time: FrameTime,
2182        outcome: &mut PaintOutcome,
2183    ) -> (Option<OverlayKey>, Option<ImeState>) {
2184        if entries.is_empty() {
2185            // Nothing is floated, so nothing floated owns the session.
2186            return (None, None);
2187        }
2188        // The same disjoint field borrows the main pass takes, for the same
2189        // reason: the theme is lent immutably into each context while other
2190        // fields of `self` are written.
2191        let (paint_epoch, retained_layers) = (self.paint_epoch, self.retained_layers);
2192        let paint_serial = self.paint_serial;
2193        let retained_ledger = &self.retained_ledger;
2194        let theme = self.theme.as_deref();
2195        let presented_frames = self.presented_frames;
2196        let surface_translucent = self.surface_translucent;
2197        let hover_active = self.hover_active;
2198        let hover_epoch = self.hover_epoch;
2199        let focus_active = self.focus_active;
2200        let focus_epoch = self.focus_epoch;
2201
2202        // The surface whose pod holds a link on the LIVE session, resolved as the
2203        // pods go past, together with the surface that pod published — one pair,
2204        // so the publish can never be attributed to an entry that did not make
2205        // it. A second live holder sets `contested` and the pair is discarded.
2206        let mut holder: Option<(OverlayKey, Option<ImeState>)> = None;
2207        let mut contested = false;
2208
2209        for entry in entries {
2210            let mut ctx = PaintCtx::new(entry.window_rect.origin(), entry.window_rect.size());
2211            ctx.set_frame_time(frame_time);
2212            ctx.set_theme(theme);
2213            ctx.set_window_insets(entry.insets);
2214            ctx.set_presented_frames(presented_frames);
2215            ctx.set_translucent(surface_translucent);
2216            ctx.set_paint_state(
2217                paint_epoch,
2218                retained_layers,
2219                Some(retained_ledger),
2220                paint_serial,
2221            );
2222            // Seeded from the root's own mirrors exactly as the root pod's
2223            // context is, so a focused editable inside a floated pod observes its
2224            // focus (and a hovered one its hover) through the ordinary
2225            // `ChildPod::paint_child` composition.
2226            //
2227            // The mirror alone, deliberately: the pod's own link is ANDed onto it
2228            // inside `paint_child`, which is the same composition that decides
2229            // the main tree's, so a pod that holds no link reads unfocused
2230            // whatever the mirror says.
2231            ctx.set_has_focus(focus_active);
2232            ctx.set_focus_epoch(focus_epoch);
2233            ctx.set_hovered(hover_active);
2234            ctx.set_hover_epoch(hover_epoch);
2235            // Retired and read before the paint, in the same borrow: nothing in a
2236            // paint pass moves the focus path, and the answer is what decides
2237            // whether this pod may describe the session below.
2238            let pod_holds_focus = {
2239                let mut pod = entry.pod.borrow_mut();
2240                pod.retire_stale_focus_link();
2241                let holds = pod.holds_live_focus();
2242                pod.paint_child(&mut ctx, scene);
2243                holds
2244            };
2245
2246            // Fold this pod's continuation-frame request into the frame's outcome
2247            // on the two lattices the tree's own aggregation uses: `needs_frame`
2248            // ORs, the class is a max-lattice (any unpaced request makes the whole
2249            // frame unpaced), and the paced interval is a MIN-lattice. The
2250            // standing aggregate's class is recovered from the outcome itself —
2251            // `needs_frame && !needs_frame_paced_only` is precisely "something
2252            // unpaced asked" — which also preserves the deferred-flush frame the
2253            // main pass may have folded in.
2254            let stood_unpaced = outcome.needs_frame && !outcome.needs_frame_paced_only;
2255            let entry_unpaced = ctx.needs_frame() && !ctx.needs_frame_paced_only();
2256            outcome.needs_frame |= ctx.needs_frame();
2257            outcome.needs_frame_paced_only =
2258                outcome.needs_frame && !(stood_unpaced || entry_unpaced);
2259            outcome.paced_interval = match (outcome.paced_interval, ctx.paced_interval()) {
2260                (Some(standing), Some(asked)) => Some(standing.min(asked)),
2261                (standing, asked) => standing.or(asked),
2262            };
2263            // A layout-animating widget inside a pod relayouts the next frame the
2264            // same way one in the tree does, and so does a pod this paint dirtied
2265            // (an unpaced frame request or a trailing landing paint — the path
2266            // bit, which `paint_child` already set for both). The floated pod's
2267            // own mark makes the next pass a full one (see `overlay_pod_dirty`).
2268            if ctx.needs_layout() {
2269                outcome.needs_layout = true;
2270                self.pending |= ChangeFlags::LAYOUT;
2271            }
2272            if ctx.is_layout_path_dirty() {
2273                self.pending |= ChangeFlags::LAYOUT;
2274            }
2275            // A focused editable inside a pod republishes its IME surface on every
2276            // paint, exactly like one in the tree, so the same rules apply
2277            // verbatim: collect a publish only while a session is actually active
2278            // (never resurrect a surface a blur cleared), and only from the pod
2279            // that holds a link on THAT session — the provenance a child of the
2280            // tree gets for free from its chain. A publish taken from any other
2281            // pod is dropped here, which is why the take is unconditional: the
2282            // per-entry context is about to be discarded either way, and leaving
2283            // a surface in it would only invite a later reader to trust it.
2284            //
2285            // An *inactive* publish is refused on exactly the same terms rather
2286            // than treated as a release: ending a session is a claim about it
2287            // too, and a pod that does not hold it makes neither.
2288            let published = ctx.take_ime_state();
2289            if focus_active && pod_holds_focus {
2290                if holder.is_some() {
2291                    // Two pods claiming one session. See the method doc: the
2292                    // contradiction is answered by attributing the session to
2293                    // nobody, not by ranking the claimants.
2294                    contested = true;
2295                } else {
2296                    holder = Some((entry.key, published));
2297                }
2298            }
2299            // EXTEND the two replace-per-pass channels rather than replacing them:
2300            // `paint_main_tree` already put this pass's tree-published frames and
2301            // shields there, and a platform-view slot or z-shield that happens to
2302            // paint inside a floated pod must survive beside them (see
2303            // `PaintCtx::publish_platform_view`).
2304            self.platform_view_frames.extend(ctx.take_platform_views());
2305            self.input_shields.extend(ctx.take_input_shields());
2306        }
2307
2308        match holder {
2309            Some((key, published)) if !contested => (Some(key), published),
2310            _ => (None, None),
2311        }
2312    }
2313
2314    /// Resolve this paint pass's selection-toolbar publish into the shell-facing
2315    /// slot, moving [`RenderRoot::selection_toolbar_generation`] only on a
2316    /// **menu edge**.
2317    ///
2318    /// The request carries two shapes of fact, and they are resolved differently
2319    /// (see [`SelectionToolbarRequest`]):
2320    ///
2321    /// * The whole request is stored as a **level**, newest wins. A shell reads
2322    ///   [`SelectionToolbarRequest::anchor`] on every tick it has a menu on
2323    ///   screen, so the stored anchor has to be the current one, not the one the
2324    ///   generation last moved for.
2325    /// * The generation moves on the **menu-significant** part alone:
2326    ///   [`SelectionToolbarRequest::present_menu`] and
2327    ///   [`SelectionToolbarRequest::actions`], with an absent request reading as
2328    ///   "no menu, no verbs" so appearing and disappearing are edges on the same
2329    ///   comparison. An anchor that merely moved is deliberately **not** an edge:
2330    ///   a focused field recomputes its anchor every painted frame, and a
2331    ///   selection dragged wider moves it on every touch sample — bumping there
2332    ///   would ask the platform to re-present its menu per sample.
2333    ///
2334    /// The publish is refused outright while no focus session is active, mirroring
2335    /// `paint`'s refusal to let a paint-time publish resurrect a cleared IME
2336    /// surface: the request describes the focused field, so one arriving after the
2337    /// blur describes a field that no longer holds anything.
2338    fn resolve_selection_toolbar(&mut self, published: Option<SelectionToolbarRequest>) {
2339        let next = if self.focus_active { published } else { None };
2340        // "Nothing published" is the same statement as "no menu wanted, no verbs
2341        // enabled" — which is what lets one comparison cover a change between two
2342        // requests, a first appearance, and a clearing alike.
2343        let menu_edge = |request: &Option<SelectionToolbarRequest>| {
2344            request.map_or((false, SelectionToolbarActions::default()), |request| {
2345                (request.present_menu, request.actions)
2346            })
2347        };
2348        if menu_edge(&self.selection_toolbar) != menu_edge(&next) {
2349            self.selection_toolbar_gen = self.selection_toolbar_gen.wrapping_add(1);
2350        }
2351        self.selection_toolbar = next;
2352    }
2353
2354    /// The selection-toolbar request the focused field published during the most
2355    /// recent [`RenderRoot::paint`], or `None` when no field is focused at all.
2356    ///
2357    /// The shell half of the platform edit-menu route
2358    /// ([`SelectionToolbarPolicy::Native`](crate::selection_toolbar::SelectionToolbarPolicy::Native)):
2359    /// a shell reads it beside [`RenderRoot::ime_state`], answers "may I offer
2360    /// this verb?" from [`SelectionToolbarRequest::actions`] whenever the platform
2361    /// asks, and presents the host's own menu at
2362    /// [`SelectionToolbarRequest::anchor`] when
2363    /// [`SelectionToolbarRequest::present_menu`] says so. A **level**, not an edge
2364    /// — re-read it as often as you like; pair it with
2365    /// [`RenderRoot::selection_toolbar_generation`] to notice the changes worth
2366    /// presenting or dismissing for.
2367    ///
2368    /// Present for a focused field with no selection at all, which is not a
2369    /// wasted answer: paste applies to a bare caret, and a platform asking
2370    /// whether it may offer one needs a reply before any bar exists.
2371    ///
2372    /// A field under the framework policy publishes this too (it floats its own
2373    /// toolbar through [`crate::overlay`] as well), so a shell that drives the
2374    /// platform menu must decide on the policy, not on the presence of a request.
2375    pub fn selection_toolbar(&self) -> Option<SelectionToolbarRequest> {
2376        self.selection_toolbar
2377    }
2378
2379    /// A monotonically-increasing generation bumped on every change to the
2380    /// **menu-significant** part of [`RenderRoot::selection_toolbar`] — its
2381    /// [`present_menu`](SelectionToolbarRequest::present_menu) flag and its
2382    /// [`actions`](SelectionToolbarRequest::actions) — including the clearing that
2383    /// a blur produces, so a shell sees the menu going away as an edge too.
2384    ///
2385    /// A moved [`anchor`](SelectionToolbarRequest::anchor) is **not** an edge: it
2386    /// is republished (and recomputed) every painted frame, so a shell re-reads it
2387    /// from [`RenderRoot::selection_toolbar`] rather than waiting for this to move
2388    /// — bumping on it would re-present a menu on every touch sample of a drag
2389    /// that widens a selection.
2390    ///
2391    /// The `focus_ime_generation` contract one channel over: a shell caches the
2392    /// last value it acted on and acts only when it moves, which is what keeps a
2393    /// standing selection — republished every single frame — from asking the
2394    /// platform to re-present its menu on every vsync.
2395    pub fn selection_toolbar_generation(&self) -> u64 {
2396        self.selection_toolbar_gen
2397    }
2398
2399    /// Collect the accessibility tree for the current frame,
2400    /// returning a [`SemanticsUpdate`] a platform adapter (`accesskit_*`)
2401    /// can consume.
2402    ///
2403    /// Pull-based and stateless: the shell calls this when a platform a11y client
2404    /// asks for the tree (or after a change), *never* per frame — this crate owns
2405    /// no scheduling. Must run **after** [`RenderRoot::layout`], since node bounds
2406    /// come from the pods' post-layout geometry.
2407    ///
2408    /// The result is always rooted at a synthetic [`accesskit::Role::Window`]
2409    /// node covering the window, whose children are whatever the root widget
2410    /// contributed. An unbuilt tree yields a bare window node with no children.
2411    pub fn semantics(&self) -> SemanticsUpdate {
2412        let mut ctx = SemanticsCtx::new(self.window_size, self.semantics_alloc.get());
2413        let window = self.window_size;
2414        let root_pod = self.root_id.and_then(|id| self.tree.pod(id));
2415        // The root pod is arena-backed (not a `ChildPod`), so it caches its stable
2416        // base id in `root_semantics_id` rather than in a pod — assigned on first
2417        // pass and reused thereafter, exactly like `ChildPod::semantics_base`.
2418        let root_widget_base = match self.root_semantics_id.get() {
2419            Some(id) => id,
2420            None => {
2421                let id = ctx.alloc_base();
2422                self.root_semantics_id.set(Some(id));
2423                id
2424            }
2425        };
2426        let root_node = ctx.push_container_with_id(
2427            ROOT_NODE_ID,
2428            accesskit::Role::Window,
2429            |node| {
2430                node.set_bounds(accesskit::Rect {
2431                    x0: 0.0,
2432                    y0: 0.0,
2433                    x1: window.width,
2434                    y1: window.height,
2435                });
2436            },
2437            |ctx| {
2438                if let Some(pod) = root_pod {
2439                    // The root pod sits at its recorded origin (ZERO today) with
2440                    // its laid-out size; descend into that geometry and its stable
2441                    // id scope, mirroring `ChildPod::semantics_child`.
2442                    ctx.descend_into_pod(
2443                        root_widget_base,
2444                        pod.origin().to_vec2(),
2445                        pod.size(),
2446                        |ctx| {
2447                            pod.widget().semantics(ctx);
2448                        },
2449                    );
2450                }
2451            },
2452        );
2453        // Persist the allocator's high-water mark so the next pass keeps handing
2454        // out fresh, never-reused bases to pods that first appear later.
2455        self.semantics_alloc.set(ctx.next_base());
2456        ctx.finish(root_node)
2457    }
2458
2459    /// The current semantics generation — bumped by every rebuild/theme swap that
2460    /// could have changed the accessibility tree (the semantics dirty gate).
2461    ///
2462    /// A shell records the value it last pushed and compares; see
2463    /// [`RenderRoot::semantics_if_changed`].
2464    pub fn semantics_generation(&self) -> u64 {
2465        self.semantics_gen
2466    }
2467
2468    /// Pull a fresh [`SemanticsUpdate`] **only if** the semantics tree may have
2469    /// changed since generation `last_seen`.
2470    ///
2471    /// Returns `None` when nothing relevant changed, letting a shell skip both the
2472    /// tree walk and the platform `accesskit_*` push. Call it post-layout (bounds
2473    /// must be valid). A shell threads its stored generation in and, on `Some`,
2474    /// updates it from [`RenderRoot::semantics_generation`]. v1 pushes the whole
2475    /// tree when it does recompute (stable ids make that valid); finer-grained
2476    /// diffing is a later optimization.
2477    pub fn semantics_if_changed(&self, last_seen: u64) -> Option<SemanticsUpdate> {
2478        (self.semantics_gen != last_seen).then(|| self.semantics())
2479    }
2480
2481    /// Deliver an input event to the widget tree, returning what happened.
2482    ///
2483    /// Builds a root [`EventCtx`] over the (type-erased) `state`, dispatches to
2484    /// the root widget — which routes the event down through its container
2485    /// children — and folds the result into an [`EventOutcome`]. The outcome's
2486    /// `needs_redraw` is set whenever a widget consumed the event or explicitly
2487    /// requested a redraw; the shell turns that into a `window.request_redraw()`.
2488    ///
2489    /// Root capture bookkeeping mirrors the per-container `active`-child model: a
2490    /// `Down` whose dispatch requested capture marks a gesture in flight and
2491    /// latches the contact that sent it as the gesture's claimant; the
2492    /// claimant's `Up` and `Cancel` release it (never a window-leave, and never
2493    /// another contact's release).
2494    ///
2495    /// Pointer contacts are gated **first**, before anything else runs: an
2496    /// [`InputEvent::PointerContact`] is unwrapped into the plain
2497    /// [`InputEvent::Pointer`] every widget matches on, with
2498    /// [`EventCtx::pointer_id`](crate::event::EventCtx::pointer_id) reporting its
2499    /// id, and a contact the multi-contact contract does not route — an
2500    /// additional contact with nothing captured, or one the captor did not opt
2501    /// into — is dropped here with an empty outcome (see that variant's
2502    /// *Multi-contact contract*). A bare `InputEvent::Pointer` is the mouse.
2503    ///
2504    /// Root hover bookkeeping is the third recorded path, and the one this pass
2505    /// *derives* rather than merely mirrors: an **uncaptured** `Move` opens a hover
2506    /// pass (widgets on the hit-tested path may claim it — see
2507    /// [`EventCtx::claim_hover`](crate::event::EventCtx::claim_hover)), a
2508    /// `Down`/`Up`/`Cancel` ends whatever hover stood, and every other event leaves
2509    /// it alone. There is nothing to release and no generation to bump: the epoch
2510    /// advance strands the previous claimant's path by itself, and the outcome's
2511    /// `needs_redraw` carries the one repaint **no widget can ask for** — a hover
2512    /// that ended with nothing taking it. A hover that *begins* or *moves from one
2513    /// claimant to another* is repainted by the new claimant's own change-gated
2514    /// `request_redraw`, which is why keeping an internal hover flag is part of the
2515    /// consumer contract rather than an optimization (see `claim_hover`).
2516    /// **No shell change is required for hover** — the desktop shell already
2517    /// dispatches a `Move` on every cursor move.
2518    ///
2519    /// The cursor is hover's sibling channel and the fourth thing this pass
2520    /// resolves: any pointer `Move` (captured included) re-resolves
2521    /// [`RenderRoot::cursor`] from the pass's last
2522    /// [`EventCtx::set_cursor`](crate::event::EventCtx::set_cursor), defaulting to
2523    /// [`CursorIcon::Default`] when nothing asked. It deliberately does **not**
2524    /// fold into the outcome's `needs_redraw`: applying a cursor is a platform
2525    /// call a desktop shell makes straight after this pass returns, with no frame
2526    /// involved, and folding it in would repaint the tree on every hover move.
2527    ///
2528    /// The **clipboard channel** rides the same bracket and is the fifth thing
2529    /// this pass resolves: whatever the dispatch asked for through
2530    /// [`EventCtx::write_clipboard`](crate::event::EventCtx::write_clipboard) and
2531    /// [`EventCtx::request_paste`](crate::event::EventCtx::request_paste) lands in
2532    /// [`RenderRoot::take_clipboard_write`] / [`RenderRoot::take_paste_request`],
2533    /// which a shell drains immediately after this returns, beside
2534    /// [`RenderRoot::cursor`] and [`RenderRoot::ime_state`]. Unlike the cursor,
2535    /// both commit on **every** pass rather than on a pointer `Move` alone — a
2536    /// copy can be answered from a key chord, a context-menu tap, or an
2537    /// [`InputEvent::EditCommand`] — and both are one-shot drains rather than
2538    /// standing levels. Like the cursor, neither folds into `needs_redraw`:
2539    /// talking to the host clipboard paints nothing (a `Cut` that mutates the
2540    /// document asks for its own redraw, for the mutation).
2541    ///
2542    /// # Reentrancy
2543    ///
2544    /// This pass **never rebuilds or repaints**. Event handlers mutate `state`
2545    /// synchronously through the context; the shell is expected to run a single
2546    /// [`RenderRoot::rebuild`] (then layout/paint) *after* the event pass returns,
2547    /// driven by the outcome. Rebuilding re-entrantly here would invalidate the
2548    /// widget references the dispatch still holds and turn the event→state→view
2549    /// feedback into recursion.
2550    ///
2551    /// [`RenderRoot::rebuild`] calls this itself with
2552    /// [`InputEvent::Housekeeping`] to flush deferred state-bearing callbacks.
2553    /// That is *sequential*, not re-entrant — the dispatch fully returns before
2554    /// the next diff starts — so the rule above is intact.
2555    ///
2556    /// # File-drag follow-up
2557    ///
2558    /// Every [`InputEvent::FileDrop`] whose phase is [`FileDropPhase::Drop`]
2559    /// or [`FileDropPhase::Cancel`] is followed, within this same call and
2560    /// whether or not its hit-tested dispatch was handled, by a
2561    /// [`FileDropPhase::Ended`] broadcast to the whole tree; the returned
2562    /// outcome folds both. A `Hover` and an `Ended` dispatched directly get
2563    /// no follow-up. See [`FileDropEvent`]'s *Broadcast follow-up* for why.
2564    pub fn event(&mut self, state: &mut State, event: &InputEvent) -> EventOutcome {
2565        let Some(root_id) = self.root_id else {
2566            return EventOutcome::default();
2567        };
2568
2569        // The multi-contact gate (`InputEvent::PointerContact`'s contract). A
2570        // contact is unwrapped into the plain `Pointer` every widget matches on,
2571        // so everything below sees one pointer shape; a bare `Pointer` is the
2572        // mouse. Any other event dispatches under the contact the enclosing pass
2573        // carries (the overlay pre-pass re-enters this method with a broadcast
2574        // that must keep its gesture's id), or the mouse at the top level.
2575        let unwrapped;
2576        let (event, pointer_id) = match event {
2577            InputEvent::PointerContact {
2578                pointer_id,
2579                event: pointer,
2580            } => {
2581                unwrapped = InputEvent::Pointer(*pointer);
2582                (&unwrapped, *pointer_id)
2583            }
2584            InputEvent::Pointer(_) => (event, PointerId::MOUSE),
2585            _ => (event, crate::event::current_pointer_id()),
2586        };
2587        let secondary = if matches!(event, InputEvent::Pointer(_)) {
2588            match self.contact_route(pointer_id) {
2589                Some(secondary) => secondary,
2590                // Rule (b), or rule (c) for a captor that did not opt in: the
2591                // contact reaches nothing and moves no root state.
2592                None => return EventOutcome::default(),
2593            }
2594        } else {
2595            false
2596        };
2597        // Published for the whole dispatch: `EventCtx::new` seeds
2598        // `pointer_id()` from it (so the id survives a component boundary),
2599        // `capture_contacts` records its opt-in into it, and `ChildPod::set_active`
2600        // reads its `secondary` mark to keep every container's active link on
2601        // the claimant. Restored on drop, so a nested pass scopes its own.
2602        let contact_pass = ContactPass::enter(pointer_id, secondary);
2603
2604        // The overlay pre-pass runs before every other thing this method does —
2605        // before the hover derivation, before the cursor bracket, before the
2606        // dispatch — because a pointer over a floated surface must reach none of
2607        // them: not the main tree's hit test, not its hover pass, and above all
2608        // not its blur rule. See `route_overlay`.
2609        let carried = match self.route_overlay(state, event) {
2610            OverlayRoute::Consumed(outcome) => return outcome,
2611            OverlayRoute::Continue(outcome) => outcome,
2612        };
2613
2614        // Open a candidate focus session for this dispatch, BEFORE anything is
2615        // routed. A claim recorded below is stamped with this new epoch, which
2616        // nothing older carries — so the branch the session is leaving is
2617        // stranded by arithmetic rather than by a clearing sweep that would have
2618        // to visit it, and a floated pod no container owns is retired on exactly
2619        // the same terms as a field in the tree.
2620        //
2621        // Candidate, not committed: `self.focus_epoch` is left alone and the
2622        // pass settles it below, because whether this dispatch recorded anything
2623        // is only known once it has run. A `Move` over a focused field, a
2624        // housekeeping broadcast, a press inside a surface that claims nothing —
2625        // none of those may disturb a standing link.
2626        //
2627        // Both epochs are published because a claim recorded on the way back up
2628        // has to be observable to the container still unwinding around it (a
2629        // blur sweep asking which child kept focus, a portal noticing its surface
2630        // took the session) while a link recorded *before* this dispatch still
2631        // has to read live. See `crate::widget::set_live_focus_session`.
2632        //
2633        // After `route_overlay`, deliberately: an event that belongs to a floated
2634        // surface is re-dispatched through the front door and opens its own
2635        // candidate session there, and this call returns that outcome untouched.
2636        let live_focus_epoch = self.focus_epoch;
2637        let candidate_focus_epoch = advance_focus_epoch(live_focus_epoch);
2638        crate::widget::set_live_focus_session(
2639            self.root_identity,
2640            live_focus_epoch,
2641            candidate_focus_epoch,
2642        );
2643        let focus_active_before = self.focus_active;
2644
2645        let Some(pod) = self.tree.pod_mut(root_id) else {
2646            // Nothing to dispatch into, but an outside-tap notification may
2647            // already have produced an outcome; returning it rather than the
2648            // default keeps that redraw.
2649            self.publish_focus_session();
2650            return carried;
2651        };
2652
2653        // Hover is derived per **uncaptured** pointer `Move`: that pass, and only
2654        // that pass, may record a claim, so a captured drag can never paint hover
2655        // under the pointer. Every other pointer phase — `Down`, `Up`, `Cancel` —
2656        // is an epoch-advancing pass that *ends* whatever hover stood without
2657        // opening a new one: a press is not a hover, a touch `Down` must not
2658        // inherit one, and a lift is the only signal a touch contact leaving the
2659        // screen ever produces (no further `Move` follows it, so a tint claimed
2660        // during an uncaptured touch drag would otherwise stand indefinitely).
2661        // Ending on `Up` costs a mouse the hover tint between a click's release
2662        // and its next motion — the same standing-until-next-move class as the
2663        // press-then-hold-still gap, and traded deliberately for a touch link that
2664        // cannot outlive the finger (`docs/LIMITATIONS.md`'s
2665        // `hover-window-leave-standing`). A scroll, key, IME, or the housekeeping
2666        // broadcast leaves a live hover exactly as it was.
2667        //
2668        // A non-claimant contact delivered down a live capture (`secondary`) is
2669        // neither: it is not a gesture of its own, so it leaves hover exactly as
2670        // the claimant's gesture had it.
2671        let hover_pass = matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Move)
2672            && self.capture_claimant.is_none();
2673        let hover_ends = !secondary
2674            && matches!(
2675                event,
2676                InputEvent::Pointer(p)
2677                    if matches!(
2678                        p.phase,
2679                        PointerPhase::Down | PointerPhase::Up | PointerPhase::Cancel
2680                    )
2681            );
2682        let hover_epoch = self.hover_epoch;
2683        let hover_was_active = self.hover_active;
2684        let root_identity = self.root_identity;
2685
2686        // The cursor pass is hover's pass widened by one case: **any** pointer
2687        // `Move`, captured included, because a captured `Move` routes only to the
2688        // capturing widget and that is exactly how a drag keeps its own cursor
2689        // while the pointer is outside its bounds. Every other pass leaves the
2690        // resolved cursor standing — notably `Down`/`Up`, whose handlers have no
2691        // reason to restate a cursor and whose reset would blink the shape back to
2692        // `Default` for the length of a click.
2693        //
2694        // The slot is cleared here rather than trusted to be empty: a `set_cursor`
2695        // from a dispatch no root drove (a reconciler's synthesized `Cancel`)
2696        // must not leak into this pass's resolution. The clear and the drain below
2697        // are one bracket (`CursorPass`) rather than two bare calls, so a dispatch
2698        // that re-entered this method could not silently eat the enclosing pass's
2699        // request — see that guard.
2700        //
2701        // The claimant's moves only: a second finger moving under a captured
2702        // drag says nothing about the drag's own cursor.
2703        let cursor_pass =
2704            !secondary && matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Move);
2705        // The same bracket carries the clipboard channel (a write and a paste
2706        // request), which differs only in when it commits: every pass, not the
2707        // pointer-move subset, since a copy can be answered from a key chord or an
2708        // `EditCommand` that never moved a pointer. See `RequestPass`.
2709        let request_slot = RequestPass::enter();
2710
2711        // Whether the root widget itself holds the live opt-in (rather than a
2712        // pod below it), and whether it opted in during this dispatch.
2713        let contacts_captor_is_root = self.capture_claimant.is_some()
2714            && self.capture_contacts
2715            && self.contacts_captor_is_root;
2716        let root_opted_in;
2717
2718        // The claimant pod this dispatch records, if any (see `hover_link`).
2719        crate::widget::clear_hover_claimant();
2720        let (handled, needs_redraw, captured, hover_claimed, focus_req, focus_rel, ime) = {
2721            let state_any: &mut dyn Any = state;
2722            let mut ctx = EventCtx::new(state_any, pod.origin(), pod.size());
2723            // Seed the root widget's focus flag so a leaf-root editable that holds
2724            // focus can observe `has_focus()`; deeper focus is threaded per-pod.
2725            ctx.set_has_focus(self.focus_active);
2726            // Same for the hover link, plus the live epoch every pod compares its
2727            // stamp against and the eligibility gate that decides whether a claim
2728            // is recordable at all this pass.
2729            ctx.set_hovered(hover_was_active);
2730            ctx.set_hover_epoch(hover_epoch);
2731            ctx.set_hover_eligible(hover_pass);
2732            // Stamped onto whichever pod records a claim, so that pod's
2733            // destructor can tell this root's link from another root's
2734            // identically-numbered epoch (see `root_identity`).
2735            ctx.set_hover_root(root_identity);
2736            // The root widget's own contact frame: attributes a
2737            // `capture_contacts` made by the root widget itself (not by a pod
2738            // below it) to the root, and marks the dispatch as running under the
2739            // live opt-in's holder when the root widget is that holder.
2740            let frame = ContactFrame::enter(contacts_captor_is_root);
2741            let result = if secondary && !contacts_captor_is_root {
2742                // A non-claimant contact walks the recorded active path
2743                // forward-only (see `ChildPod::event_child`): the root widget is
2744                // handed the inert carrier and the real event rides beside it,
2745                // so only the captor that opted in runs its pointer handling. A
2746                // walk that never reached a pod on the path (the root widget
2747                // does not forward broadcasts to its captured child) falls back
2748                // to the ordinary delivery.
2749                let widget = pod.widget_mut();
2750                let carrier = crate::event::secondary_walk_carrier();
2751                let (_, delivered) = crate::event::run_secondary_walk(event.clone(), || {
2752                    widget.event(&mut ctx, &carrier)
2753                });
2754                match delivered {
2755                    Some(result) => result,
2756                    None => crate::event::without_secondary_walk(|| widget.event(&mut ctx, event)),
2757                }
2758            } else {
2759                pod.widget_mut().event(&mut ctx, event)
2760            };
2761            root_opted_in = frame.close().0;
2762            let handled = matches!(result, EventResult::Handled);
2763            // The event fold: every dirtying channel a shell gating layout on the
2764            // pending flags must see (the mirror of `paint_main_tree`'s fold).
2765            // `EventCtx::request_layout`, and the path bit — a pod below the
2766            // root consumed the event or asked for a redraw, so it was marked and
2767            // only a pass that runs lays it out. The root widget sits in no pod,
2768            // so its own consumed event or redraw request is the same source.
2769            if ctx.needs_layout() || ctx.is_layout_path_dirty() || handled || ctx.needs_redraw() {
2770                self.pending |= ChangeFlags::LAYOUT;
2771            }
2772            (
2773                handled,
2774                ctx.needs_redraw() || handled,
2775                ctx.is_pointer_captured(),
2776                ctx.is_hover_claimed(),
2777                ctx.is_focus_requested(),
2778                ctx.is_focus_released(),
2779                ctx.take_ime_state(),
2780            )
2781        };
2782
2783        // A published IME surface refreshes the stored one (persists past this
2784        // event, survives rebuild) until a blur clears it below. `store_ime_state`
2785        // bumps the focus/IME edge generation only if the surface actually moved
2786        // (a keystroke that changes nothing observable is not an edge).
2787        //
2788        // An **inactive** publish carries the same release intent here as it does
2789        // in `paint` (see that method's take path): a widget that publishes
2790        // `active: false` is ending the session, not describing it, so it takes
2791        // the full release. Reachable from this pass too — every paint-time
2792        // producer of an inactive surface is a container/widget whose `event` arm
2793        // can run first — and the two passes must not disagree about what an
2794        // inactive surface means. No `focus_active` guard is needed (unlike
2795        // `paint`, which must refuse to *resurrect* a cleared surface): releasing
2796        // an already-released root moves nothing and fires no edge.
2797        //
2798        // Ordering: this runs before the focus match below, so a dispatch that
2799        // both published an inactive surface and requested focus still ends up
2800        // focused — the later, more specific claim wins.
2801        //
2802        // The provenance rule `paint_overlays` applies to a paint-time publish,
2803        // mirrored onto this pass — the two must not disagree about who is
2804        // allowed to speak for the session, so a change to either belongs in
2805        // both.
2806        //
2807        // What the root can attribute here is bounded by how the event was
2808        // routed. A hit-tested or focus-routed dispatch reaches a publisher
2809        // through the tree, and the tree's own chain is what gated it (a
2810        // well-behaved editable publishes only while `has_focus`, which is now
2811        // stamp-gated) — so the rule for those is the liveness half alone: a
2812        // session must exist, or be opening in this very dispatch, for a publish
2813        // to describe anything. An `InputEvent::Overlay` is broadcast to the
2814        // whole tree and reaches every floated pod whether or not it holds
2815        // anything, so there the addressed surface must be the recorded owner of
2816        // the session, or be claiming it now — the same "the publisher holds the
2817        // link" question `paint_overlays` answers from the pods themselves.
2818        //
2819        // The *inactive* publish is refused on the same terms and for the same
2820        // reason it is at paint: ending a session is a claim about it too, and a
2821        // branch that owns none makes neither.
2822        let publish_attributable = match event {
2823            InputEvent::Overlay(overlay) => focus_req || self.focus_surface == Some(overlay.key),
2824            _ => self.focus_active || focus_req,
2825        };
2826        match ime {
2827            Some(_) if !publish_attributable => {}
2828            Some(ime) if !ime.active => {
2829                self.release_focus_session();
2830            }
2831            Some(ime) => self.store_ime_state(Some(ime)),
2832            None => {}
2833        }
2834
2835        // Root-level capture path: a captured `Down` opens a gesture; `Up`/`Cancel`
2836        // close it. `Move` leaves the flag untouched so it survives the drag.
2837        //
2838        // Root-level focus path (the capture mirror): a `Down` that requested
2839        // focus opens the focus session; a `Down` that did not is a
2840        // blur-on-outside-tap and closes it (the per-container `focused` flags are
2841        // cleared by the routing helpers). Key/Ime/Scroll only adjust focus if the
2842        // dispatch explicitly requested or released it.
2843        //
2844        // Every arm mutates through `set_focus_active`/`store_ime_state` or the
2845        // paired `release_focus_session`, the change-guarded writers that own the
2846        // focus/IME edge generation: a `Down` on already-blurred chrome (the
2847        // commonest event of all) writes the same values back and must therefore
2848        // NOT fire an edge.
2849        //
2850        // Whether an arm below actually *honoured* a focus claim — the one thing
2851        // that makes the candidate epoch opened above the real one. Deliberately
2852        // not `focus_req` itself: the housekeeping arm ignores a claim by
2853        // contract, and a claim it ignored must leave the standing session (and
2854        // therefore the standing epoch) exactly where it was.
2855        let mut focus_claim_honoured = false;
2856        match event {
2857            // A non-claimant contact delivered down a live capture (rule (c) of
2858            // `InputEvent::PointerContact`'s contract): not a gesture of its own,
2859            // so it opens, moves and releases no capture and never blurs — its
2860            // `Up` must not end the claimant's gesture, and its `Down` is not a
2861            // tap outside anything. An explicit focus request or release from
2862            // its handler is honoured exactly as the scroll arm below honours
2863            // one.
2864            InputEvent::Pointer(_) if secondary => {
2865                if focus_req {
2866                    focus_claim_honoured = true;
2867                    self.set_focus_active(true);
2868                }
2869                if focus_rel {
2870                    self.release_focus_session();
2871                }
2872            }
2873            // Never reached: the gate at the top unwrapped every contact into a
2874            // plain `Pointer`, and routed it by the arms around this one.
2875            InputEvent::PointerContact { .. } => {}
2876            InputEvent::Pointer(pointer) => match pointer.phase {
2877                PointerPhase::Down => {
2878                    if captured {
2879                        // Rule (a): the contact whose `Down` captured is the
2880                        // claimant, and only its release ends the gesture. The
2881                        // opt-in is read from the pass rather than the root
2882                        // context's bubble so a captor below a component
2883                        // boundary (which mirrors capture but not the opt-in)
2884                        // is still heard.
2885                        self.capture_claimant = Some(pointer_id);
2886                        self.capture_contacts = contact_pass.contacts_requested();
2887                        self.contacts_captor_is_root = root_opted_in;
2888                    }
2889                    if focus_req {
2890                        focus_claim_honoured = true;
2891                        self.set_focus_active(true);
2892                        // A hit-tested press reached the claimant through the
2893                        // containers, so the session is the main tree's — the one
2894                        // attribution the event pass can make without help, and
2895                        // what makes the next paint seed the tree's links again
2896                        // the moment focus comes back to it (see `focus_surface`).
2897                        self.focus_surface = None;
2898                    } else {
2899                        // Blur: no widget on the tapped path took focus. The
2900                        // canonical release — flag and surface drop together, as
2901                        // one edge (see `release_focus_session_in`).
2902                        self.release_focus_session();
2903                    }
2904                }
2905                // Only ever the claimant's (or an uncaptured contact's) release:
2906                // any other contact's was routed to the `secondary` arm above or
2907                // dropped at the gate.
2908                PointerPhase::Up | PointerPhase::Cancel => {
2909                    self.capture_claimant = None;
2910                    self.capture_contacts = false;
2911                    self.contacts_captor_is_root = false;
2912                }
2913                PointerPhase::Move => {}
2914            },
2915            // A file drop is hit-tested exactly like `Scroll`/`Scale` above —
2916            // no capture, no claimant bookkeeping — so it joins their bucket
2917            // rather than getting one of its own: a target that calls
2918            // `request_focus`/`release_focus` from its `FileDrop` handler is
2919            // honoured on the same terms a scroll's would be, even though no
2920            // shipped drop target does today. Its `Ended` broadcast follow-up
2921            // lands here too: like an overlay event it is a broadcast that
2922            // stands for a real user gesture, so an explicit request is
2923            // honoured and nothing blurs (only a pointer `Down` blurs).
2924            InputEvent::Scroll { .. }
2925            | InputEvent::Scale(_)
2926            | InputEvent::FileDrop(_)
2927            | InputEvent::Key(_)
2928            | InputEvent::Ime(_)
2929            | InputEvent::EditCommand(_) => {
2930                if focus_req {
2931                    // The candidate epoch opened above becomes this claim's, so
2932                    // a focus move driven from the keyboard retires the branch it
2933                    // supersedes on exactly the terms a press does — including a
2934                    // floated surface's link, which this arm could otherwise
2935                    // never reach. `focus_surface` is deliberately left alone:
2936                    // which branch a focus-routed claim came from is not
2937                    // something the root can attribute, so the next paint
2938                    // re-resolves it from the pods' own live links.
2939                    focus_claim_honoured = true;
2940                    self.set_focus_active(true);
2941                }
2942                if focus_rel {
2943                    self.release_focus_session();
2944                }
2945            }
2946            // A broadcast is not user input: it opens no gesture, claims no
2947            // focus, and blurs nothing. Deliberately inert here — a housekeeping
2948            // pass that moved the root's capture/focus bookkeeping would change
2949            // what the *next* real event does, which is exactly what this
2950            // mechanism must not do (see `InputEvent::Housekeeping`).
2951            //
2952            // The one thing a broadcast *can* still move is the IME surface, via
2953            // the publish handling above (which is pass-agnostic by design, and
2954            // was before this arm existed): a widget that publishes while
2955            // flushing has said something about its session either way, and an
2956            // inactive publish releases it. No shipped widget does — `TextInput`
2957            // ignores a broadcast outright, and both cleared-surface publishers
2958            // are paint-time — so this is a contract note, not live behavior.
2959            InputEvent::Housekeeping => {}
2960            // A floated surface's own input: a broadcast at the root, but a real
2961            // user gesture underneath, so it moves *some* of what a hit-tested
2962            // event moves and deliberately none of the rest.
2963            InputEvent::Overlay(overlay) => {
2964                // Honoured: a text field inside a popover may claim focus, and the
2965                // session it opens is an ordinary one.
2966                //
2967                // What it must not do is stack on top of the session it
2968                // supersedes. A hit-tested press has the containers' blur sweep
2969                // under it, which clears every focused child but the one the
2970                // press kept; a press inside a floated surface reaches no
2971                // container's hit test, so nothing retires the chain the claim
2972                // replaces and two branches go on believing they are focused —
2973                // one of them still describing its own IME surface to the shell.
2974                // The root retires it in the two places it has standing to: here,
2975                // when its record already names a *different* surface as the
2976                // owner, and in `paint_overlays`, which resolves that record from
2977                // the pods themselves and stops seeding the main tree's links the
2978                // pass after the session leaves it.
2979                //
2980                // The other repair — running the blur sweep for this event, with
2981                // the claiming surface's owner kept — was not taken: that sweep
2982                // belongs to the containers, which run it over the children they
2983                // hold when a pointer `Down` passes through them. The root has no
2984                // mutable route to a `focused` link below its own pod, so from
2985                // here it is not a sweep at all but a change to how every
2986                // container routes a broadcast.
2987                //
2988                // The record is deliberately not *written* here: an overlay-pass
2989                // focus request cannot be attributed at the root (see
2990                // `focus_surface`) — a field in the tree re-claiming its own
2991                // session through a floated toolbar raises exactly the flag an
2992                // editable inside the surface raises to take it away.
2993                if focus_req {
2994                    if self.focus_surface.is_some_and(|owner| owner != overlay.key) {
2995                        // A session another surface owns, which *is* attributable:
2996                        // retire it whole rather than let the new claim inherit
2997                        // the surface the old one published.
2998                        self.release_focus_session();
2999                    }
3000                    focus_claim_honoured = true;
3001                    self.set_focus_active(true);
3002                }
3003                // Honoured: an **explicit** `EventCtx::release_focus` from inside
3004                // the surface ends the session it asked to end — the same rule the
3005                // `Scroll`/`Key`/`Ime` arm applies.
3006                if focus_rel {
3007                    self.release_focus_session();
3008                }
3009                // NEVER the blur branch. A `Down` that claims no focus blurs the
3010                // tree only when it was hit-tested *in* the tree; a press inside a
3011                // floated surface is the one press that must not, or tapping a
3012                // selection toolbar would drop the very selection the toolbar acts
3013                // on — which is the whole reason these two are routed apart.
3014                //
3015                // Nor does it advance the hover epoch: `hover_pass`/`hover_ends`
3016                // above match `InputEvent::Pointer` only, so a live hover in the
3017                // main tree survives an overlay pass by construction rather than by
3018                // a check here.
3019                //
3020                // A capture requested from inside the surface IS honoured, exactly
3021                // as a hit-tested `Down`'s is: the gesture then owns every
3022                // follow-up, and because a live capture short-circuits the overlay
3023                // pre-pass those follow-ups arrive as ordinary `Pointer` events
3024                // routed by the capture path — which is what lets a drag begun
3025                // inside a surface continue outside it, and what closes it on the
3026                // `Up` through the arm above.
3027                //
3028                // Whatever phase claimed it, not `Down` alone. The pods record
3029                // their own `active` path on any phase, so a capture claimed on a
3030                // `Move` left the surface latched and the root's mirror clear:
3031                // the pre-pass kept hit-testing, the follow-ups kept missing the
3032                // latched surface, and the `Up` that would have released it never
3033                // arrived — leaving the surface free to divert every later
3034                // pointer event its owner is reached by. Mirroring the claim on
3035                // any phase is what routes those follow-ups, and that `Up`, back
3036                // down the capture path to the surface that opened it.
3037                //
3038                // The claimant is the contact this overlay event was routed for —
3039                // the pre-pass re-enters this method under the gesture's own
3040                // contact pass, so `pointer_id` is that gesture's id.
3041                if captured {
3042                    self.capture_claimant = Some(pointer_id);
3043                    self.capture_contacts = contact_pass.contacts_requested();
3044                    self.contacts_captor_is_root = root_opted_in;
3045                }
3046                // ...and released on the phase that ends the gesture, in the
3047                // same door it was claimed through. The claim is mirrored on any
3048                // phase (see above), so the release has to be too: an `Up` or
3049                // `Cancel` that arrives *as an overlay event* — which is what
3050                // happens when the surface was not holding the capture when the
3051                // gesture began, so the pre-pass kept hit-testing it — would
3052                // otherwise leave the root's latch standing on a gesture that is
3053                // over, and a standing latch short-circuits the pre-pass for
3054                // every floated surface until some unrelated pointer release
3055                // happens along.
3056                //
3057                // A gesture whose claim *did* short-circuit the pre-pass ends on
3058                // the ordinary pointer arm above instead, because that is the
3059                // door its follow-ups come in by; the pod's own `active` link is
3060                // released by whichever of the two the release arrived through
3061                // (see `frust-widgets`' `OverlaySlot::forward`).
3062                if let OverlayEventKind::Pointer(pointer) = &overlay.kind
3063                    && matches!(pointer.phase, PointerPhase::Up | PointerPhase::Cancel)
3064                    && self
3065                        .capture_claimant
3066                        .is_none_or(|claimant| claimant == pointer_id)
3067                {
3068                    self.capture_claimant = None;
3069                    self.capture_contacts = false;
3070                    self.contacts_captor_is_root = false;
3071                }
3072            }
3073        }
3074
3075        // A takeover during the claimant's own dispatch: a container cancelled
3076        // the widget that opted into the gesture's other contacts and released
3077        // it from the active path (`EventCtx::release_captured_child`). The
3078        // opt-in goes with it — unless the container that took over opted in
3079        // itself afterwards, which the pass records — so the other contacts are
3080        // dropped from here on rather than routed to a captor that is gone. The
3081        // claimant latch is deliberately kept: the gesture is still the
3082        // claimant's, now held by the container that took it over (still on the
3083        // active path, as the released child's ancestor), and only the
3084        // claimant's own `Up`/`Cancel` ends it. Never on a non-claimant
3085        // contact's dispatch, in which no container can clear a recorded link
3086        // (`ChildPod::set_active`), so nothing could have left the path.
3087        if !secondary && self.capture_claimant.is_some() && contact_pass.capture_released() {
3088            self.capture_contacts = contact_pass.contacts_requested();
3089        }
3090
3091        // Settle the session's identity — the one write that decides which
3092        // recorded links survive this dispatch, in one place, from what the
3093        // dispatch actually did.
3094        //
3095        // * A **live session that ended** strands everything: the chain it ran
3096        //   through, a floated surface's link, and anything this very pass
3097        //   stamped (a claim the same dispatch then released). Past the candidate,
3098        //   therefore, not merely onto it — this is the clearing sweep no
3099        //   container can be asked to run.
3100        // * A **claim the root honoured** commits the candidate, so the chain it
3101        //   stamped is the only one that reads live and every older link is
3102        //   stranded by arithmetic.
3103        // * Anything else leaves the session exactly where it was, which is what
3104        //   makes a `Move`, a housekeeping broadcast, or a press inside a surface
3105        //   that claimed nothing incapable of disturbing a standing link — and
3106        //   what strands a claim an arm declined to honour, since the candidate
3107        //   it was stamped with is then never published again.
3108        let session_ended = focus_active_before && !self.focus_active;
3109        self.focus_epoch = if session_ended {
3110            advance_focus_epoch(candidate_focus_epoch)
3111        } else if focus_claim_honoured {
3112            candidate_focus_epoch
3113        } else {
3114            live_focus_epoch
3115        };
3116        self.publish_focus_session();
3117
3118        // Close the hover pass: advance the epoch (which strands every stamp this
3119        // pass did not renew, wherever in the tree it sits) and refresh the mirror.
3120        // A claim only counts on a `hover_pass` — an ineligible pass records none
3121        // anyway, but stating it here keeps the mirror true by construction rather
3122        // than by the eligibility gate alone.
3123        let hover_claimant = crate::widget::take_hover_claimant();
3124        if hover_pass || hover_ends {
3125            self.hover_epoch = self.hover_epoch.wrapping_add(1);
3126            self.hover_active = hover_pass && hover_claimed;
3127            // The link's identity, for the paint-epoch fold: the claimant pod,
3128            // or `0` for a claim the root widget made with no pod of its own.
3129            self.hover_link = self.hover_active.then(|| hover_claimant.unwrap_or(0));
3130            // Republish the link a dropping pod checks its stamp against, so a
3131            // rebuild that removes the claimant can report the severance the
3132            // epoch alone cannot strand (see `ChildPod`'s `Drop`). Epoch `0` while
3133            // nothing holds a link, which is what makes a stale stamp's drop —
3134            // the common case — cost one comparison and mark nothing. The
3135            // identity rides with it because epoch integers are per-root and
3136            // collide by construction (see `root_identity`).
3137            crate::event::set_live_hover_link(
3138                self.root_identity,
3139                if self.hover_active {
3140                    self.hover_epoch
3141                } else {
3142                    0
3143                },
3144            );
3145        }
3146
3147        // Close the cursor pass: the last request of the pass wins, and its
3148        // absence resolves to `Default` — which is what makes the request
3149        // stateless (a widget that stops asking needs no clearing) and what a
3150        // pointer moving off every requesting widget resolves to. Drained
3151        // unconditionally so a request made on a non-cursor pass cannot survive
3152        // into the next one; only a cursor pass commits it.
3153        let requested = request_slot.take();
3154        if cursor_pass {
3155            self.cursor = requested.cursor.unwrap_or_default();
3156        }
3157        // The clipboard half of the same drain, committed on **every** pass: a copy
3158        // is answered from whatever event decoded it, and there is no
3159        // "clipboard pass" the way there is a cursor pass. Both fields are
3160        // one-shot (see their docs) — a write from this pass supersedes one still
3161        // standing undrained, and a request is raised but never lowered here, so a
3162        // shell that skipped a drain answers late rather than losing the paste.
3163        if let Some(text) = requested.clipboard_write {
3164            self.pending_clipboard_write = Some(text);
3165        }
3166        self.pending_paste_request |= requested.paste_request;
3167        // A hover that ended with *nothing* taking it needs one repaint no widget
3168        // can ask for: the pointer moved onto empty chrome (or a press/lift/cancel
3169        // cleared the link), so the old claimant's `event()` was never called and
3170        // the new state has no claimant to speak for it. Every other edge is the
3171        // consumer's own: a hover that *began* or *moved from one claimant to
3172        // another* is repainted by the new claimant's change-gated
3173        // `request_redraw`, and because a repaint is global, that one frame is also
3174        // what lets the widget losing the link drop its overlay from
3175        // `PaintCtx::is_hovered` (a retained page holding the loser's hovered
3176        // body is not drawn on it: the moved `hover_link` bumps the paint epoch,
3177        // see `painted_links`). This mirror is identity-free, so an A→B handoff
3178        // is `true`→`true` here and manufactures nothing — which is exactly why the
3179        // consumer's internal flag is normative (see `EventCtx::claim_hover`).
3180        let needs_redraw = needs_redraw || (hover_was_active && !self.hover_active);
3181
3182        let mut outcome = EventOutcome {
3183            // Merge whatever the overlay pre-pass already produced: a
3184            // pass-through outside-tap notification ran before this dispatch and
3185            // its redraw is owed just as much as the dispatch's own.
3186            handled: handled || carried.handled,
3187            needs_redraw: needs_redraw || carried.needs_redraw,
3188        };
3189
3190        // An OS file drag that just ended (`Drop`/`Cancel`) is followed by the
3191        // broadcast form, handled or not, so every widget holding state the
3192        // drag opened — a drop target's external session — hears the drag is
3193        // over: one whose release resolved over a region with no drop target
3194        // in it, and one whose release a *different* widget handled (two
3195        // independent drop-target groups the drag hovered in turn). See
3196        // `FileDropEvent`'s *Broadcast follow-up*. Re-entered through the
3197        // front door for the reason `route_overlay` re-enters: the follow-up
3198        // gets the same request bracket and outcome folding as any dispatch.
3199        // Shallow by construction: `Ended` is not one of the two phases that
3200        // trigger it.
3201        if let InputEvent::FileDrop(drop) = event
3202            && matches!(drop.phase, FileDropPhase::Drop | FileDropPhase::Cancel)
3203        {
3204            let ended = InputEvent::FileDrop(FileDropEvent {
3205                phase: FileDropPhase::Ended,
3206                position: Point::ZERO,
3207                paths: Vec::new(),
3208            });
3209            let follow_up = self.event(state, &ended);
3210            outcome.handled |= follow_up.handled;
3211            outcome.needs_redraw |= follow_up.needs_redraw;
3212        }
3213        outcome
3214    }
3215
3216    /// Decide what one pointer contact does — the root half of
3217    /// [`InputEvent::PointerContact`]'s multi-contact contract.
3218    ///
3219    /// `Some(false)` routes it as the gesture's own pointer (rule (a): slot `0`
3220    /// with nothing captured, hit-tested; or the claimant of a live capture,
3221    /// down the captured path). `Some(true)` routes it as a **non-claimant**
3222    /// contact down a live capture's path, which happens only while the captor's
3223    /// opt-in stands (rule (c)): the dispatch then walks the recorded active
3224    /// path forward-only, so only the captor's own handler sees it (see
3225    /// [`crate::widget::ChildPod::event_child`]). `None` drops it: an additional
3226    /// contact with nothing captured (rule (b)), or one the captor did not opt
3227    /// into — or whose opt-in ended when a container took the gesture over from
3228    /// the captor ([`EventCtx::release_captured_child`]) — (rule (c)).
3229    fn contact_route(&self, pointer_id: PointerId) -> Option<bool> {
3230        match self.capture_claimant {
3231            None if pointer_id.slot == 0 => Some(false),
3232            None => None,
3233            Some(claimant) if claimant == pointer_id => Some(false),
3234            Some(_) if self.capture_contacts => Some(true),
3235            Some(_) => None,
3236        }
3237    }
3238
3239    /// Hit-test the surfaces the last paint floated, **before** the main tree
3240    /// sees an uncaptured pointer, scroll, or scale — the routing half of the
3241    /// overlay portal (see [`crate::overlay`]).
3242    ///
3243    /// # What it does
3244    ///
3245    /// Walks [`RenderRoot::overlay_hits`] topmost-first (the `Tooltip` band before
3246    /// `Floating`, later registration before earlier), skipping
3247    /// [`OverlayInput::Transparent`] entries, and on the first rect containing the
3248    /// event's position re-dispatches it as
3249    /// [`InputEvent::Overlay`] — a broadcast carrying the owner's key and a
3250    /// **window-space** payload — then returns
3251    /// [`OverlayRoute::Consumed`]. The main tree never sees the original event.
3252    ///
3253    /// If nothing was hit and the event is a primary `Down`, every `Interactive`
3254    /// entry registered [`OutsideTap::Notify`] is told, topmost-first, with
3255    /// [`OverlayEventKind::OutsideDown`]; the press is then consumed iff any of
3256    /// them asked to consume it, and otherwise continues into today's dispatch.
3257    ///
3258    /// # What it deliberately does not do
3259    ///
3260    /// A **live capture short-circuits it entirely**: a gesture that has captured
3261    /// the pointer owns every follow-up until it ends, and re-hit-testing a drag
3262    /// that wandered over a floated surface would hand it to the wrong widget
3263    /// mid-gesture. That is also what lets a drag *begun* inside a surface
3264    /// continue outside it — the capture the overlay `Down` opened routes the
3265    /// follow-ups by the ordinary captured path.
3266    ///
3267    /// Housekeeping, `Key`, `Ime`, `EditCommand` and an overlay event already
3268    /// being routed pass straight through: a broadcast and a focus-routed event
3269    /// each reach their target with no hit test, so there is nothing here to
3270    /// redirect.
3271    ///
3272    /// A `FileDrop` passes straight through too, but for a different reason:
3273    /// it is hit-tested (see [`InputEvent::FileDrop`]), yet there is no
3274    /// [`OverlayEventKind`] carrier for it to ride a redirect through. A
3275    /// native OS drag has no floated-surface concept on the platform side —
3276    /// winit reports it against the window, not a particular surface — so a
3277    /// drop target living inside a popover is unreachable by one in v1 (see
3278    /// `docs/LIMITATIONS.md`). Its [`FileDropPhase::Ended`] follow-up passes
3279    /// through as well, as the broadcast it is.
3280    fn route_overlay(&mut self, state: &mut State, event: &InputEvent) -> OverlayRoute {
3281        if self.capture_claimant.is_some() || self.overlay_hits.is_empty() {
3282            return OverlayRoute::Continue(EventOutcome::default());
3283        }
3284        let (position, kind) = match event {
3285            // A contact is routed for its position and phase exactly like a
3286            // plain pointer (the root's gate has already unwrapped one, so the
3287            // second pattern is for totality); the re-entered dispatch below
3288            // keeps its id through the contact pass it runs under.
3289            InputEvent::Pointer(pointer) | InputEvent::PointerContact { event: pointer, .. } => {
3290                (pointer.position, OverlayEventKind::Pointer(*pointer))
3291            }
3292            InputEvent::Scroll { position, delta } => (
3293                *position,
3294                OverlayEventKind::Scroll {
3295                    position: *position,
3296                    delta: *delta,
3297                },
3298            ),
3299            InputEvent::Scale(scale) => (
3300                scale.focal,
3301                OverlayEventKind::Scale {
3302                    focal: scale.focal,
3303                    phase: scale.phase,
3304                    scale_delta: scale.scale_delta,
3305                    velocity: scale.velocity,
3306                },
3307            ),
3308            InputEvent::FileDrop(_)
3309            | InputEvent::Key(_)
3310            | InputEvent::Ime(_)
3311            | InputEvent::EditCommand(_)
3312            | InputEvent::Housekeeping
3313            | InputEvent::Overlay(_) => {
3314                return OverlayRoute::Continue(EventOutcome::default());
3315            }
3316        };
3317
3318        // `overlay_hits` is in paint order, so walking it in reverse walks from
3319        // the surface painted last — the one the user sees on top — downward.
3320        // Cloned because each dispatch below needs `&mut self`; the list is one
3321        // small `Copy` struct per floated surface, and the clone never happens on
3322        // the overwhelmingly common no-overlays path (guarded above).
3323        let hits = self.overlay_hits.clone();
3324        for hit in hits.iter().rev() {
3325            // An `egui`-style transparent surface is painted above the app and
3326            // hit-tested by nothing: the pointer passes straight through to
3327            // whatever the main tree has underneath.
3328            if hit.input == OverlayInput::Transparent {
3329                continue;
3330            }
3331            if hit.contains(position) {
3332                let routed = InputEvent::Overlay(OverlayEvent {
3333                    key: hit.key,
3334                    kind: kind.clone(),
3335                });
3336                // Re-entering this method is deliberate and shallow: the routed
3337                // event is a broadcast, so it takes the branch above out
3338                // immediately and can never recurse further. Going back through
3339                // the front door is what gives the overlay dispatch the same
3340                // request bracket, focus bookkeeping and outcome folding every
3341                // other event gets, with one arm's worth of difference rather
3342                // than a second copy of the pass.
3343                return OverlayRoute::Consumed(self.event(state, &routed));
3344            }
3345        }
3346
3347        // Nothing floated was hit. Only a **primary** press is a light-dismiss
3348        // signal: a secondary press is a context gesture (see
3349        // `docs/CODE_STANDARDS.md`'s Interaction Semantics), and a `Move`, `Up` or
3350        // scroll outside a surface says nothing about dismissing it.
3351        let dismissing = matches!(
3352            event,
3353            InputEvent::Pointer(pointer) | InputEvent::PointerContact { event: pointer, .. }
3354                if pointer.phase == PointerPhase::Down
3355                    && pointer.button == PointerButton::Primary
3356        );
3357        if !dismissing {
3358            return OverlayRoute::Continue(EventOutcome::default());
3359        }
3360
3361        let mut carried = EventOutcome::default();
3362        let mut consumed = false;
3363        for hit in hits.iter().rev() {
3364            // A transparent surface takes no input at all, outside-taps included:
3365            // it is chrome the pointer does not know about.
3366            if hit.input == OverlayInput::Transparent {
3367                continue;
3368            }
3369            let OutsideTap::Notify { consume } = hit.outside_tap else {
3370                continue;
3371            };
3372            let routed = InputEvent::Overlay(OverlayEvent {
3373                key: hit.key,
3374                kind: OverlayEventKind::OutsideDown,
3375            });
3376            let outcome = self.event(state, &routed);
3377            carried.handled |= outcome.handled;
3378            carried.needs_redraw |= outcome.needs_redraw;
3379            // Every notified surface is told before any of them consumes:
3380            // dismissing one menu must not hide the press from a second surface
3381            // that also wanted to close.
3382            consumed |= consume;
3383        }
3384        if consumed {
3385            OverlayRoute::Consumed(carried)
3386        } else {
3387            OverlayRoute::Continue(carried)
3388        }
3389    }
3390
3391    /// Perform a platform accessibility action, returning the same
3392    /// [`EventOutcome`] the synthesized input produced.
3393    ///
3394    /// A platform `accesskit_*` adapter delivers an `ActionRequest(node_id,
3395    /// action)`; a shell forwards it here. v1 routes actions through the **normal
3396    /// event path** by synthesizing pointer events at the target node's absolute
3397    /// bounds center (recovered from a fresh semantics pass — the id → bounds map
3398    /// the pass produces), so *every* fire-on-up-inside widget is operable with
3399    /// **zero** widget-side changes:
3400    ///
3401    /// * [`accesskit::Action::Click`] → a `Down` then an `Up` at the center,
3402    ///   activating any button/switch/checkbox exactly as a real tap would.
3403    /// * [`accesskit::Action::Focus`] → a `Down` then a synthetic `Cancel` at
3404    ///   the center: the `Down` claims focus for a widget that opts in on `Down`
3405    ///   (the recorded-focus contract), and the `Cancel` releases the capture
3406    ///   that same `Down` opened without touching the recorded focus path — so
3407    ///   the action claims focus without leaving the widget permanently
3408    ///   capturing every later pointer event. A widget that does not claim focus
3409    ///   on `Down` is unaffected, and `Cancel` never fires an on-press callback.
3410    /// * any other action → ignored (a no-op [`EventOutcome`]); richer actions are
3411    ///   deferred.
3412    ///
3413    /// An unknown `node_id` (not in the current tree) is a benign no-op. Requires
3414    /// a prior [`RenderRoot::layout`] so the bounds are valid. Synthetic-pointer
3415    /// activation cannot drive widgets that require a real drag (e.g. a slider) —
3416    /// an accepted v1 limitation.
3417    pub fn perform_accessibility_action(
3418        &mut self,
3419        state: &mut State,
3420        node_id: accesskit::NodeId,
3421        action: accesskit::Action,
3422    ) -> EventOutcome {
3423        // Recover the node's absolute bounds from a fresh semantics pass (the
3424        // id → absolute-bounds map this action routing needs; recomputing keeps
3425        // it in step with the live tree without a stored cache).
3426        let update = self.semantics();
3427        let Some(bounds) = update
3428            .nodes
3429            .iter()
3430            .find(|(id, _)| *id == node_id)
3431            .and_then(|(_, node)| node.bounds())
3432        else {
3433            return EventOutcome::default();
3434        };
3435        let center = Point::new((bounds.x0 + bounds.x1) / 2.0, (bounds.y0 + bounds.y1) / 2.0);
3436        let synth = |phase| {
3437            InputEvent::Pointer(PointerEvent {
3438                phase,
3439                position: center,
3440                button: PointerButton::Primary,
3441            })
3442        };
3443        match action {
3444            accesskit::Action::Click => {
3445                let down = self.event(state, &synth(PointerPhase::Down));
3446                let up = self.event(state, &synth(PointerPhase::Up));
3447                EventOutcome {
3448                    handled: down.handled || up.handled,
3449                    needs_redraw: down.needs_redraw || up.needs_redraw,
3450                }
3451            }
3452            accesskit::Action::Focus => {
3453                // A `Down` claims focus for a widget that opts in on `Down`; a
3454                // synthetic `Cancel` then releases the capture that `Down` also
3455                // opened (mirroring a real gesture steal), leaving the recorded
3456                // focus path intact — `Cancel` clears both `active` and
3457                // `capture_claimant` while never touching `focused`/`focus_active`.
3458                // Without the `Cancel`, the `Down` alone would leave the widget
3459                // permanently capturing every subsequent pointer event.
3460                let down = self.event(state, &synth(PointerPhase::Down));
3461                let cancel = self.event(state, &synth(PointerPhase::Cancel));
3462                EventOutcome {
3463                    handled: down.handled || cancel.handled,
3464                    needs_redraw: down.needs_redraw || cancel.needs_redraw,
3465                }
3466            }
3467            // Other actions are not modelled in v1: ignore rather than guess.
3468            _ => EventOutcome::default(),
3469        }
3470    }
3471}
3472
3473impl<State: 'static, V: View<State>> Default for RenderRoot<State, V> {
3474    fn default() -> Self {
3475        Self::new()
3476    }
3477}
3478
3479#[cfg(test)]
3480mod tests {
3481    use super::*;
3482    use crate::event::{ImeContentType, ScrollDelta};
3483    use crate::overlay::OverlayKey;
3484
3485    /// Application state for the tests.
3486    #[derive(Default)]
3487    struct AppState {
3488        label: String,
3489    }
3490
3491    /// The retained widget produced by `MockTextView`: stores the current text
3492    /// and records what it painted.
3493    struct TextWidget {
3494        text: String,
3495    }
3496
3497    impl crate::widget::Widget for TextWidget {
3498        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3499            // A crude intrinsic size: width proportional to text length.
3500            let intrinsic = Size::new(self.text.len() as f64 * 8.0, 16.0);
3501            bc.constrain(intrinsic)
3502        }
3503        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3504            scene.draw_text(ctx.origin(), &self.text);
3505        }
3506    }
3507
3508    /// The task's `MockTextView`: a real `View` impl living in tests.
3509    struct MockTextView {
3510        text: String,
3511    }
3512
3513    impl View<AppState> for MockTextView {
3514        type Element = TextWidget;
3515
3516        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
3517            TextWidget {
3518                text: self.text.clone(),
3519            }
3520        }
3521
3522        fn rebuild(
3523            &self,
3524            prev: &Self,
3525            element: &mut Self::Element,
3526            _ctx: &mut BuildCtx<'_>,
3527        ) -> ChangeFlags {
3528            if prev.text != self.text {
3529                element.text = self.text.clone();
3530                // Text change: same size model would relayout, but the intrinsic
3531                // width can change, so signal PAINT here and let callers decide.
3532                ChangeFlags::PAINT
3533            } else {
3534                ChangeFlags::NONE
3535            }
3536        }
3537    }
3538
3539    /// A scene recorder for asserting paint output.
3540    #[derive(Default)]
3541    struct RecordingScene {
3542        texts: Vec<(Point, String)>,
3543    }
3544    impl PaintScene for RecordingScene {
3545        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: peniko::Color) {}
3546        fn draw_text(&mut self, origin: Point, text: &str) {
3547            self.texts.push((origin, text.to_string()));
3548        }
3549        fn draw_scene_texture(&mut self, _id: u64, _dest: Rect) {}
3550    }
3551
3552    fn build(state: &mut AppState) -> MockTextView {
3553        MockTextView {
3554            text: state.label.clone(),
3555        }
3556    }
3557
3558    #[test]
3559    fn build_inserts_widget_into_arena() {
3560        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3561        let mut state = AppState {
3562            label: "hello".to_string(),
3563        };
3564        let flags = root.rebuild(&mut build, &mut state);
3565        // First build dirties both passes.
3566        assert!(flags.needs_layout());
3567        assert!(flags.needs_paint());
3568        let id = root.root_id().expect("root built");
3569        let pod = root.tree().pod(id).expect("pod in arena");
3570        assert!(pod.widget().downcast_ref_is::<TextWidget>());
3571    }
3572
3573    #[test]
3574    fn rebuild_changed_data_yields_paint() {
3575        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3576        let mut state = AppState {
3577            label: "a".to_string(),
3578        };
3579        root.rebuild(&mut build, &mut state);
3580        state.label = "b".to_string();
3581        let flags = root.rebuild(&mut build, &mut state);
3582        assert_eq!(flags, ChangeFlags::PAINT);
3583    }
3584
3585    #[test]
3586    fn rebuild_unchanged_data_yields_none() {
3587        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3588        let mut state = AppState {
3589            label: "same".to_string(),
3590        };
3591        root.rebuild(&mut build, &mut state);
3592        let flags = root.rebuild(&mut build, &mut state);
3593        assert_eq!(flags, ChangeFlags::NONE);
3594    }
3595
3596    #[test]
3597    fn layout_stores_size_in_pod() {
3598        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3599        let mut state = AppState {
3600            label: "hi".to_string(),
3601        };
3602        root.rebuild(&mut build, &mut state);
3603        let size = root.layout(Size::new(800.0, 600.0));
3604        // "hi" -> 2 * 8 = 16 wide, 16 tall, within the window.
3605        assert_eq!(size, Size::new(16.0, 16.0));
3606        let id = root.root_id().unwrap();
3607        let pod = root.tree().pod(id).unwrap();
3608        assert_eq!(pod.origin(), Point::ZERO);
3609        assert_eq!(pod.size(), Size::new(16.0, 16.0));
3610    }
3611
3612    #[test]
3613    fn inspect_reports_the_laid_out_root() {
3614        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3615        let mut state = AppState {
3616            label: "hi".to_string(),
3617        };
3618        // Before the first build there is nothing to inspect.
3619        assert!(root.inspect().is_empty());
3620
3621        root.rebuild(&mut build, &mut state);
3622        root.layout(Size::new(800.0, 600.0));
3623
3624        let nodes = root.inspect();
3625        assert_eq!(nodes.len(), 1);
3626        let node = &nodes[0];
3627        assert_eq!(node.id, root.root_id().unwrap());
3628        assert_eq!(node.parent, None);
3629        assert_eq!(node.depth, 0);
3630        assert!(node.children.is_empty());
3631        // The concrete element type is captured, not the erased box.
3632        assert!(node.type_name.ends_with("TextWidget"), "{}", node.type_name);
3633        assert_eq!(node.debug_label, None);
3634        // Bounds match what the layout pass recorded on the pod.
3635        let pod = root.tree().pod(node.id).unwrap();
3636        assert_eq!(
3637            node.bounds,
3638            Rect::from_origin_size(pod.origin(), pod.size())
3639        );
3640        assert_eq!(node.bounds, Rect::new(0.0, 0.0, 16.0, 16.0));
3641    }
3642
3643    #[test]
3644    fn layout_clamps_to_window() {
3645        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3646        let mut state = AppState {
3647            label: "wwwwwwwwwww".to_string(), // 10 chars -> 80 wide intrinsic
3648        };
3649        root.rebuild(&mut build, &mut state);
3650        let size = root.layout(Size::new(40.0, 40.0));
3651        // Intrinsic width 80 is clamped to the 40-wide window.
3652        assert_eq!(size.width, 40.0);
3653    }
3654
3655    #[test]
3656    fn paint_emits_current_text() {
3657        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3658        let mut state = AppState {
3659            label: "one".to_string(),
3660        };
3661        root.rebuild(&mut build, &mut state);
3662        root.layout(Size::new(200.0, 200.0));
3663
3664        let mut scene = RecordingScene::default();
3665        root.paint(&mut scene, FrameTime::ZERO);
3666        assert_eq!(scene.texts, vec![(Point::ZERO, "one".to_string())]);
3667
3668        // Change data, rebuild, repaint -> new text.
3669        state.label = "two".to_string();
3670        root.rebuild(&mut build, &mut state);
3671        let mut scene2 = RecordingScene::default();
3672        root.paint(&mut scene2, FrameTime::ZERO);
3673        assert_eq!(scene2.texts, vec![(Point::ZERO, "two".to_string())]);
3674    }
3675
3676    /// A leaf widget that publishes a fixed [`PlatformViewFrame`] — and reports
3677    /// a z-shield rect over its own bounds — on every paint, unless
3678    /// `should_publish` is false (the widget-level toggle that simulates a slot
3679    /// no longer publishing between two rebuilds). Both channels ride the same
3680    /// toggle so one fixture covers both replace-per-pass contracts.
3681    struct PlatformViewProbeWidget {
3682        slot_id: u64,
3683        should_publish: bool,
3684    }
3685
3686    impl crate::widget::Widget for PlatformViewProbeWidget {
3687        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3688            bc.constrain(Size::new(10.0, 10.0))
3689        }
3690        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3691            if self.should_publish {
3692                ctx.publish_platform_view(PlatformViewFrame {
3693                    slot_id: self.slot_id,
3694                    view_type: "dev.frust.Probe".to_string(),
3695                    params_json: String::new(),
3696                    params_generation: 0,
3697                    rect: kurbo::Rect::from_origin_size(ctx.origin(), ctx.size()),
3698                    clip: None,
3699                    visible: true,
3700                    interactive: false,
3701                    shields: Vec::new(),
3702                    command_index: 0,
3703                });
3704                ctx.report_input_shield(kurbo::Rect::from_origin_size(ctx.origin(), ctx.size()));
3705            }
3706        }
3707    }
3708
3709    /// A root widget owning two independently toggleable [`ChildPod`]s (a
3710    /// minimal two-slot container) so a rebuild can flip either slot's
3711    /// `should_publish` — the fixture the "two slots in one pass" and
3712    /// "empty-pass clears stale frames" tests below need.
3713    struct PlatformViewRootWidget {
3714        a: crate::widget::ChildPod,
3715        b: crate::widget::ChildPod,
3716    }
3717
3718    impl crate::widget::Widget for PlatformViewRootWidget {
3719        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3720            self.a.layout_child(ctx, bc);
3721            self.b.layout_child(ctx, bc);
3722            bc.constrain(Size::new(10.0, 10.0))
3723        }
3724        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3725            self.a.paint_child(ctx, scene);
3726            self.b.paint_child(ctx, scene);
3727        }
3728    }
3729
3730    /// The `View` producing [`PlatformViewRootWidget`], reconciling each
3731    /// slot's `should_publish` flag on rebuild like any controlled widget.
3732    struct PlatformViewRootView {
3733        publish_a: bool,
3734        publish_b: bool,
3735    }
3736
3737    impl View<PvState> for PlatformViewRootView {
3738        type Element = PlatformViewRootWidget;
3739
3740        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
3741            PlatformViewRootWidget {
3742                a: crate::widget::ChildPod::new(Box::new(PlatformViewProbeWidget {
3743                    slot_id: 1,
3744                    should_publish: self.publish_a,
3745                })),
3746                b: crate::widget::ChildPod::new(Box::new(PlatformViewProbeWidget {
3747                    slot_id: 2,
3748                    should_publish: self.publish_b,
3749                })),
3750            }
3751        }
3752
3753        fn rebuild(
3754            &self,
3755            prev: &Self,
3756            element: &mut Self::Element,
3757            _ctx: &mut BuildCtx<'_>,
3758        ) -> ChangeFlags {
3759            if prev.publish_a != self.publish_a || prev.publish_b != self.publish_b {
3760                element
3761                    .a
3762                    .widget_mut()
3763                    .downcast_mut::<PlatformViewProbeWidget>()
3764                    .expect("slot a stays a PlatformViewProbeWidget")
3765                    .should_publish = self.publish_a;
3766                element
3767                    .b
3768                    .widget_mut()
3769                    .downcast_mut::<PlatformViewProbeWidget>()
3770                    .expect("slot b stays a PlatformViewProbeWidget")
3771                    .should_publish = self.publish_b;
3772                ChangeFlags::PAINT
3773            } else {
3774                ChangeFlags::NONE
3775            }
3776        }
3777    }
3778
3779    /// App state for the platform-view frame-channel tests.
3780    #[derive(Default)]
3781    struct PvState {
3782        publish_a: bool,
3783        publish_b: bool,
3784    }
3785
3786    fn platform_view_logic(state: &mut PvState) -> PlatformViewRootView {
3787        PlatformViewRootView {
3788            publish_a: state.publish_a,
3789            publish_b: state.publish_b,
3790        }
3791    }
3792
3793    #[test]
3794    fn platform_view_frames_arrive_in_order_and_clear_on_empty_pass() {
3795        let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3796        let mut state = PvState {
3797            publish_a: true,
3798            publish_b: true,
3799        };
3800        root.rebuild(&mut platform_view_logic, &mut state);
3801        root.layout(Size::new(200.0, 200.0));
3802
3803        let mut scene = RecordingScene::default();
3804        root.paint(&mut scene, FrameTime::ZERO);
3805
3806        // Two slots publishing in one pass both arrive, in paint order — the
3807        // regression test for the overwrite hazard (an Option-based `ime_state`
3808        // shape here would leave only the second slot's frame).
3809        let frames = root.platform_view_frames();
3810        assert_eq!(frames.len(), 2);
3811        assert_eq!(frames[0].slot_id, 1);
3812        assert_eq!(frames[1].slot_id, 2);
3813
3814        // Next pass: neither slot publishes (simulates both going away/culled).
3815        // The collection is REPLACED, so the previous pass's frames must not
3816        // survive as stale entries.
3817        state.publish_a = false;
3818        state.publish_b = false;
3819        root.rebuild(&mut platform_view_logic, &mut state);
3820        root.layout(Size::new(200.0, 200.0));
3821        root.paint(&mut scene, FrameTime::ZERO);
3822        assert!(
3823            root.platform_view_frames().is_empty(),
3824            "a paint pass with no publishers must yield an empty slice"
3825        );
3826    }
3827
3828    #[test]
3829    fn input_shields_arrive_in_order_and_clear_on_empty_pass() {
3830        // The shield channel's half of the contract above:
3831        // two shields reported in one pass both survive (the `Vec`
3832        // extend, not an `Option` overwrite), and a pass that reports none
3833        // replaces the collection rather than merging — a stale shield must
3834        // never keep stealing input from an interactive slot.
3835        let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3836        let mut state = PvState {
3837            publish_a: true,
3838            publish_b: true,
3839        };
3840        root.rebuild(&mut platform_view_logic, &mut state);
3841        root.layout(Size::new(200.0, 200.0));
3842
3843        let mut scene = RecordingScene::default();
3844        root.paint(&mut scene, FrameTime::ZERO);
3845        assert_eq!(root.input_shields().len(), 2);
3846        assert_eq!(
3847            root.input_shields()[0],
3848            Rect::from_origin_size(Point::ZERO, Size::new(10.0, 10.0)),
3849            "a shield is reported in absolute paint coordinates"
3850        );
3851
3852        state.publish_a = false;
3853        state.publish_b = false;
3854        root.rebuild(&mut platform_view_logic, &mut state);
3855        root.layout(Size::new(200.0, 200.0));
3856        root.paint(&mut scene, FrameTime::ZERO);
3857        assert!(
3858            root.input_shields().is_empty(),
3859            "a paint pass reporting no shields must yield an empty slice"
3860        );
3861    }
3862
3863    #[test]
3864    fn retired_platform_views_drain_exactly_once() {
3865        // The prompt-teardown channel: a reported slot
3866        // id is handed to the shell once and then gone, mirroring
3867        // `take_change_flags`. Serialized against the other test touching the
3868        // process-wide list (see `RETIRE_TEST_LOCK`).
3869        let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3870        let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3871        let _ = root.take_retired_platform_views(); // clear anything a sibling left
3872
3873        crate::widget::report_retired_slot(7);
3874        crate::widget::report_retired_slot(9);
3875        assert_eq!(root.take_retired_platform_views(), vec![7, 9]);
3876        assert!(
3877            root.take_retired_platform_views().is_empty(),
3878            "draining is destructive — a second drain reports nothing"
3879        );
3880    }
3881
3882    #[test]
3883    fn retired_platform_views_are_capped_dropping_the_oldest() {
3884        // A shell that never drains (desktop: no native compositor) must not
3885        // grow this list forever; past the cap the OLDEST id is dropped and the
3886        // differ's missing-streak backstop covers it.
3887        let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3888        let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3889        let _ = root.take_retired_platform_views();
3890
3891        for slot_id in 0..1_000u64 {
3892            crate::widget::report_retired_slot(slot_id);
3893        }
3894        let drained = root.take_retired_platform_views();
3895        assert!(drained.len() <= 256, "the pending list stays bounded");
3896        assert_eq!(
3897            *drained.last().expect("non-empty"),
3898            999,
3899            "the newest report always survives"
3900        );
3901        assert!(
3902            !drained.contains(&0),
3903            "the oldest reports are the ones dropped"
3904        );
3905    }
3906
3907    /// Serializes the two tests that drive the process-wide retire list
3908    /// (`crate::widget::report_retired_slot`), which `cargo test`'s parallel
3909    /// threads would otherwise interleave — the same shape
3910    /// `frust-shell-common::theme_override`'s tests use for its global slot.
3911    static RETIRE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3912
3913    /// A root widget that advances no state but requests a continuation frame on
3914    /// every paint — stands in for an animating widget (e.g. a scroll fling).
3915    struct FrameWidget;
3916    impl crate::widget::Widget for FrameWidget {
3917        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3918            bc.constrain(Size::new(10.0, 10.0))
3919        }
3920        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3921            ctx.request_frame();
3922        }
3923    }
3924
3925    struct FrameView;
3926    impl View<AppState> for FrameView {
3927        type Element = FrameWidget;
3928        fn build(&self, _ctx: &mut BuildCtx<'_>) -> FrameWidget {
3929            FrameWidget
3930        }
3931        fn rebuild(
3932            &self,
3933            _prev: &Self,
3934            _element: &mut FrameWidget,
3935            _ctx: &mut BuildCtx<'_>,
3936        ) -> ChangeFlags {
3937            ChangeFlags::NONE
3938        }
3939    }
3940
3941    #[test]
3942    fn paint_reports_needs_frame_from_animating_root() {
3943        // A still root reports no continuation frame.
3944        let mut still: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3945        let mut state = AppState {
3946            label: "x".to_string(),
3947        };
3948        still.rebuild(&mut build, &mut state);
3949        still.layout(Size::new(100.0, 100.0));
3950        let mut scene = RecordingScene::default();
3951        assert!(!still.paint(&mut scene, FrameTime::ZERO).needs_frame);
3952
3953        // An animating root bubbles request_frame out as PaintOutcome::needs_frame.
3954        let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
3955        anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
3956        anim.layout(Size::new(100.0, 100.0));
3957        let mut scene2 = RecordingScene::default();
3958        assert!(anim.paint(&mut scene2, FrameTime::ZERO).needs_frame);
3959    }
3960
3961    /// A root widget whose animation changes its layout: it requests a layout
3962    /// re-run on every paint — stands in for an expanding accordion.
3963    struct LayoutFrameWidget;
3964    impl crate::widget::Widget for LayoutFrameWidget {
3965        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3966            bc.constrain(Size::new(10.0, 10.0))
3967        }
3968        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3969            ctx.request_layout();
3970        }
3971    }
3972
3973    struct LayoutFrameView;
3974    impl View<AppState> for LayoutFrameView {
3975        type Element = LayoutFrameWidget;
3976        fn build(&self, _ctx: &mut BuildCtx<'_>) -> LayoutFrameWidget {
3977            LayoutFrameWidget
3978        }
3979        fn rebuild(
3980            &self,
3981            _prev: &Self,
3982            _element: &mut LayoutFrameWidget,
3983            _ctx: &mut BuildCtx<'_>,
3984        ) -> ChangeFlags {
3985            ChangeFlags::NONE
3986        }
3987    }
3988
3989    #[test]
3990    fn paint_folds_request_layout_into_pending_change_flags() {
3991        let mut state = AppState {
3992            label: "x".to_string(),
3993        };
3994
3995        // A root calling `request_layout` in paint surfaces it on the outcome AND
3996        // folds LAYOUT into `pending`, so the NEXT frame's `take_change_flags`
3997        // reports `needs_layout()`.
3998        let mut anim: RenderRoot<AppState, LayoutFrameView> = RenderRoot::new();
3999        anim.rebuild(&mut |_s: &mut AppState| LayoutFrameView, &mut state);
4000        anim.layout(Size::new(100.0, 100.0));
4001        // Drain any rebuild/layout dirtiness so we observe only paint's fold.
4002        let _ = anim.take_change_flags();
4003        let mut scene = RecordingScene::default();
4004        let outcome = anim.paint(&mut scene, FrameTime::ZERO);
4005        assert!(outcome.needs_layout, "outcome reports needs_layout");
4006        // `request_layout` implies `request_frame`, so the animation still runs.
4007        assert!(outcome.needs_frame, "request_layout implies needs_frame");
4008        assert!(
4009            anim.has_pending_change_flags(),
4010            "the fold survives to the next frame"
4011        );
4012        assert!(
4013            anim.take_change_flags().needs_layout(),
4014            "next frame's take_change_flags reports needs_layout"
4015        );
4016    }
4017
4018    #[test]
4019    fn paint_request_frame_only_folds_layout_into_pending() {
4020        let mut state = AppState {
4021            label: "x".to_string(),
4022        };
4023
4024        // A paint-only animation (request_frame, no request_layout) dirties its
4025        // path conservatively, so it folds LAYOUT into pending — a shell gating
4026        // its layout pass on the flags must run the (mostly skipped) pass that
4027        // reaches the marked pods. It does not report `needs_layout` on the
4028        // outcome: that stays the `request_layout` channel.
4029        let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
4030        anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
4031        anim.layout(Size::new(100.0, 100.0));
4032        let _ = anim.take_change_flags();
4033        let mut scene = RecordingScene::default();
4034        let outcome = anim.paint(&mut scene, FrameTime::ZERO);
4035        assert!(outcome.needs_frame);
4036        assert!(
4037            !outcome.needs_layout,
4038            "request_frame alone: no needs_layout on the outcome"
4039        );
4040        assert!(
4041            anim.take_change_flags().needs_layout(),
4042            "request_frame alone folds LAYOUT into pending"
4043        );
4044    }
4045
4046    /// A root whose paint requests a *pacable* cosmetic-loop frame — stands in
4047    /// for a skeleton shimmer whose cadence the mobile frame gate may throttle.
4048    struct PacedFrameWidget;
4049    impl crate::widget::Widget for PacedFrameWidget {
4050        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4051            bc.constrain(Size::new(10.0, 10.0))
4052        }
4053        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4054            ctx.request_frame_paced();
4055        }
4056    }
4057
4058    struct PacedFrameView;
4059    impl View<AppState> for PacedFrameView {
4060        type Element = PacedFrameWidget;
4061        fn build(&self, _ctx: &mut BuildCtx<'_>) -> PacedFrameWidget {
4062            PacedFrameWidget
4063        }
4064        fn rebuild(
4065            &self,
4066            _prev: &Self,
4067            _element: &mut PacedFrameWidget,
4068            _ctx: &mut BuildCtx<'_>,
4069        ) -> ChangeFlags {
4070            ChangeFlags::NONE
4071        }
4072    }
4073
4074    #[test]
4075    fn paint_surfaces_paced_only_tick_class_on_outcome() {
4076        let mut state = AppState {
4077            label: "x".to_string(),
4078        };
4079
4080        // A paced-only root surfaces `needs_frame_paced_only` on the outcome so
4081        // the mobile frame gate may throttle its cadence.
4082        let mut paced: RenderRoot<AppState, PacedFrameView> = RenderRoot::new();
4083        paced.rebuild(&mut |_s: &mut AppState| PacedFrameView, &mut state);
4084        paced.layout(Size::new(100.0, 100.0));
4085        let mut scene = RecordingScene::default();
4086        let outcome = paced.paint(&mut scene, FrameTime::ZERO);
4087        assert!(outcome.needs_frame);
4088        assert!(
4089            outcome.needs_frame_paced_only,
4090            "a purely-cosmetic frame surfaces as paced-only"
4091        );
4092        assert_eq!(
4093            outcome.paced_interval,
4094            Some(std::time::Duration::ZERO),
4095            "a bare `request_frame_paced` names no interval (the theme's own rate)"
4096        );
4097
4098        // A Transition-class (`request_frame`) root is never paced-only, keeping
4099        // today's every-vsync behavior for existing callers.
4100        let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
4101        anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
4102        anim.layout(Size::new(100.0, 100.0));
4103        let mut scene2 = RecordingScene::default();
4104        let outcome2 = anim.paint(&mut scene2, FrameTime::ZERO);
4105        assert!(outcome2.needs_frame);
4106        assert!(
4107            !outcome2.needs_frame_paced_only,
4108            "request_frame stays unpaced (Transition)"
4109        );
4110        assert_eq!(
4111            outcome2.paced_interval, None,
4112            "an unpaced frame names no paced interval"
4113        );
4114    }
4115
4116    /// A root widget whose decorative loop names its own slow cadence — the
4117    /// `request_frame_paced_at` counterpart of [`PacedFrameWidget`].
4118    struct SlowPacedFrameWidget;
4119    impl crate::widget::Widget for SlowPacedFrameWidget {
4120        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4121            bc.constrain(Size::new(10.0, 10.0))
4122        }
4123        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4124            ctx.request_frame_paced_at(std::time::Duration::from_millis(500));
4125        }
4126    }
4127    struct SlowPacedFrameView;
4128    impl View<AppState> for SlowPacedFrameView {
4129        type Element = SlowPacedFrameWidget;
4130        fn build(&self, _ctx: &mut BuildCtx<'_>) -> SlowPacedFrameWidget {
4131            SlowPacedFrameWidget
4132        }
4133        fn rebuild(
4134            &self,
4135            _prev: &Self,
4136            _element: &mut SlowPacedFrameWidget,
4137            _ctx: &mut BuildCtx<'_>,
4138        ) -> ChangeFlags {
4139            ChangeFlags::NONE
4140        }
4141    }
4142
4143    #[test]
4144    fn paint_surfaces_the_requested_paced_interval_on_outcome() {
4145        // The end-to-end core half of the per-request pacing seam: a widget's
4146        // `request_frame_paced_at` reaches the shell on `PaintOutcome`, which is
4147        // what the mobile gate latches into `FramePacing`.
4148        let mut state = AppState {
4149            label: "x".to_string(),
4150        };
4151        let mut root: RenderRoot<AppState, SlowPacedFrameView> = RenderRoot::new();
4152        root.rebuild(&mut |_s: &mut AppState| SlowPacedFrameView, &mut state);
4153        root.layout(Size::new(100.0, 100.0));
4154        let mut scene = RecordingScene::default();
4155        let outcome = root.paint(&mut scene, FrameTime::ZERO);
4156        assert!(outcome.needs_frame_paced_only);
4157        assert_eq!(
4158            outcome.paced_interval,
4159            Some(std::time::Duration::from_millis(500))
4160        );
4161    }
4162
4163    /// A root widget that records the `frame_time` its paint observed, so a test
4164    /// can prove the shell-injected clock reaches `PaintCtx::frame_time()`.
4165    struct ClockWidget {
4166        seen: std::rc::Rc<std::cell::Cell<Option<FrameTime>>>,
4167    }
4168    impl crate::widget::Widget for ClockWidget {
4169        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4170            bc.constrain(Size::new(10.0, 10.0))
4171        }
4172        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4173            self.seen.set(Some(ctx.frame_time()));
4174        }
4175    }
4176
4177    struct ClockView {
4178        seen: std::rc::Rc<std::cell::Cell<Option<FrameTime>>>,
4179    }
4180    impl View<AppState> for ClockView {
4181        type Element = ClockWidget;
4182        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ClockWidget {
4183            ClockWidget {
4184                seen: self.seen.clone(),
4185            }
4186        }
4187        fn rebuild(
4188            &self,
4189            _prev: &Self,
4190            _element: &mut ClockWidget,
4191            _ctx: &mut BuildCtx<'_>,
4192        ) -> ChangeFlags {
4193            ChangeFlags::NONE
4194        }
4195    }
4196
4197    #[test]
4198    fn paint_threads_injected_frame_time_to_widget() {
4199        let seen = std::rc::Rc::new(std::cell::Cell::new(None));
4200        let mut root: RenderRoot<AppState, ClockView> = RenderRoot::new();
4201        let mut state = AppState::default();
4202        let seen_for_view = seen.clone();
4203        root.rebuild(
4204            &mut move |_s: &mut AppState| ClockView {
4205                seen: seen_for_view.clone(),
4206            },
4207            &mut state,
4208        );
4209        root.layout(Size::new(100.0, 100.0));
4210
4211        // Two paints with distinct injected times: the widget observes each one,
4212        // proving the clock is shell-fed (not read from an ambient `Instant`).
4213        let mut scene = RecordingScene::default();
4214        root.paint(&mut scene, FrameTime::from_nanos(1_000));
4215        assert_eq!(seen.get(), Some(FrameTime::from_nanos(1_000)));
4216        root.paint(&mut scene, FrameTime::from_nanos(17_000));
4217        assert_eq!(seen.get(), Some(FrameTime::from_nanos(17_000)));
4218    }
4219
4220    // --- Theme threading: a dummy theme recovered during paint/layout. ---
4221
4222    /// A dummy theme type standing in for `frust_theme::Theme` — `frust-core`
4223    /// never names the real one, so this proves the type-erased slot works for
4224    /// any `'static` type.
4225    #[derive(Debug, Clone, PartialEq)]
4226    struct TestTheme {
4227        accent: u32,
4228    }
4229
4230    /// A root widget recording the theme accent it recovered during paint (and
4231    /// during layout), or `None` when no theme was threaded in.
4232    struct ThemeWidget {
4233        seen_paint: std::rc::Rc<std::cell::Cell<Option<u32>>>,
4234        seen_layout: std::rc::Rc<std::cell::Cell<Option<u32>>>,
4235    }
4236    impl crate::widget::Widget for ThemeWidget {
4237        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4238            self.seen_layout
4239                .set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
4240            bc.constrain(Size::new(10.0, 10.0))
4241        }
4242        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4243            self.seen_paint
4244                .set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
4245        }
4246    }
4247
4248    struct ThemeView {
4249        seen_paint: std::rc::Rc<std::cell::Cell<Option<u32>>>,
4250        seen_layout: std::rc::Rc<std::cell::Cell<Option<u32>>>,
4251    }
4252    impl View<AppState> for ThemeView {
4253        type Element = ThemeWidget;
4254        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ThemeWidget {
4255            ThemeWidget {
4256                seen_paint: self.seen_paint.clone(),
4257                seen_layout: self.seen_layout.clone(),
4258            }
4259        }
4260        fn rebuild(
4261            &self,
4262            _prev: &Self,
4263            _element: &mut ThemeWidget,
4264            _ctx: &mut BuildCtx<'_>,
4265        ) -> ChangeFlags {
4266            ChangeFlags::NONE
4267        }
4268    }
4269
4270    fn drive_theme_root(theme: Option<TestTheme>) -> (Option<u32>, Option<u32>) {
4271        let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
4272        let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
4273        let mut root: RenderRoot<AppState, ThemeView> = RenderRoot::new();
4274        if let Some(theme) = theme {
4275            root.set_theme(Box::new(theme));
4276        }
4277        let mut state = AppState::default();
4278        let sp = seen_paint.clone();
4279        let sl = seen_layout.clone();
4280        root.rebuild(
4281            &mut move |_s: &mut AppState| ThemeView {
4282                seen_paint: sp.clone(),
4283                seen_layout: sl.clone(),
4284            },
4285            &mut state,
4286        );
4287        root.layout(Size::new(100.0, 100.0));
4288        let mut scene = RecordingScene::default();
4289        root.paint(&mut scene, FrameTime::ZERO);
4290        (seen_layout.get(), seen_paint.get())
4291    }
4292
4293    #[test]
4294    fn set_theme_threads_into_layout_and_paint() {
4295        let (layout, paint) = drive_theme_root(Some(TestTheme { accent: 5 }));
4296        assert_eq!(layout, Some(5));
4297        assert_eq!(paint, Some(5));
4298    }
4299
4300    #[test]
4301    fn no_theme_yields_none_in_layout_and_paint() {
4302        let (layout, paint) = drive_theme_root(None);
4303        assert_eq!(layout, None);
4304        assert_eq!(paint, None);
4305    }
4306
4307    #[test]
4308    fn set_theme_replaces_the_previous_theme() {
4309        // A second `set_theme` (a live dark-mode flip on desktop) wins on the
4310        // next paint.
4311        let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
4312        let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
4313        let mut root: RenderRoot<AppState, ThemeView> = RenderRoot::new();
4314        root.set_theme(Box::new(TestTheme { accent: 1 }));
4315        let mut state = AppState::default();
4316        let sp = seen_paint.clone();
4317        let sl = seen_layout.clone();
4318        root.rebuild(
4319            &mut move |_s: &mut AppState| ThemeView {
4320                seen_paint: sp.clone(),
4321                seen_layout: sl.clone(),
4322            },
4323            &mut state,
4324        );
4325        root.layout(Size::new(100.0, 100.0));
4326        let mut scene = RecordingScene::default();
4327        root.paint(&mut scene, FrameTime::ZERO);
4328        assert_eq!(seen_paint.get(), Some(1));
4329
4330        // Flip the theme, repaint — the new accent is observed.
4331        root.set_theme(Box::new(TestTheme { accent: 2 }));
4332        root.layout(Size::new(100.0, 100.0));
4333        root.paint(&mut scene, FrameTime::ZERO);
4334        assert_eq!(seen_paint.get(), Some(2));
4335    }
4336
4337    // --- Event-pass fixtures: a widget that mutates state on pointer-down. ---
4338
4339    #[derive(Default)]
4340    struct ClickState {
4341        clicks: u32,
4342    }
4343
4344    struct ButtonWidget;
4345    impl crate::widget::Widget for ButtonWidget {
4346        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4347            bc.constrain(Size::new(40.0, 20.0))
4348        }
4349        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4350        fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4351            if let InputEvent::Pointer(p) = event {
4352                match p.phase {
4353                    PointerPhase::Down => {
4354                        ctx.state_mut::<ClickState>().clicks += 1;
4355                        ctx.request_redraw();
4356                        ctx.capture_pointer();
4357                        return EventResult::Handled;
4358                    }
4359                    PointerPhase::Up | PointerPhase::Cancel => return EventResult::Handled,
4360                    PointerPhase::Move => {}
4361                }
4362            }
4363            EventResult::Ignored
4364        }
4365    }
4366
4367    struct ButtonView;
4368    impl View<ClickState> for ButtonView {
4369        type Element = ButtonWidget;
4370        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ButtonWidget {
4371            ButtonWidget
4372        }
4373        fn rebuild(
4374            &self,
4375            _prev: &Self,
4376            _element: &mut ButtonWidget,
4377            _ctx: &mut BuildCtx<'_>,
4378        ) -> ChangeFlags {
4379            ChangeFlags::NONE
4380        }
4381    }
4382
4383    fn button_logic(_state: &mut ClickState) -> ButtonView {
4384        ButtonView
4385    }
4386
4387    fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
4388        InputEvent::Pointer(crate::event::PointerEvent {
4389            phase,
4390            position: Point::new(x, y),
4391            button: crate::event::PointerButton::Primary,
4392        })
4393    }
4394
4395    #[test]
4396    fn event_reaches_root_widget_and_mutates_state() {
4397        let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
4398        let mut state = ClickState::default();
4399        root.rebuild(&mut button_logic, &mut state);
4400        root.layout(Size::new(200.0, 200.0));
4401
4402        let outcome = root.event(&mut state, &pointer(PointerPhase::Down, 5.0, 5.0));
4403        assert!(outcome.handled);
4404        assert!(outcome.needs_redraw);
4405        assert_eq!(state.clicks, 1);
4406        // A captured Down opens the root gesture.
4407        assert!(root.is_pointer_captured());
4408    }
4409
4410    #[test]
4411    fn event_before_build_is_a_benign_no_op() {
4412        let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
4413        let mut state = ClickState::default();
4414        let outcome = root.event(&mut state, &pointer(PointerPhase::Down, 1.0, 1.0));
4415        assert_eq!(outcome, EventOutcome::default());
4416        assert_eq!(state.clicks, 0);
4417    }
4418
4419    #[test]
4420    fn capture_releases_on_pointer_up() {
4421        let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
4422        let mut state = ClickState::default();
4423        root.rebuild(&mut button_logic, &mut state);
4424        root.layout(Size::new(200.0, 200.0));
4425
4426        root.event(&mut state, &pointer(PointerPhase::Down, 5.0, 5.0));
4427        assert!(root.is_pointer_captured());
4428        root.event(&mut state, &pointer(PointerPhase::Up, 5.0, 5.0));
4429        assert!(!root.is_pointer_captured());
4430    }
4431
4432    #[test]
4433    fn take_change_flags_drains_accumulated_dirtiness() {
4434        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4435        let mut state = AppState {
4436            label: "x".to_string(),
4437        };
4438        root.rebuild(&mut build, &mut state);
4439        // First build accumulated LAYOUT|PAINT.
4440        let flags = root.take_change_flags();
4441        assert!(flags.needs_layout());
4442        // Draining leaves it empty until the next rebuild.
4443        assert!(root.take_change_flags().is_empty());
4444    }
4445
4446    #[test]
4447    fn has_pending_change_flags_peeks_without_draining() {
4448        // The frame-gate peek: observe pending dirtiness without
4449        // clearing it, so a skipped frame preserves the flags for the next
4450        // frame that actually runs.
4451        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4452        assert!(
4453            !root.has_pending_change_flags(),
4454            "a fresh root has nothing pending"
4455        );
4456        let mut state = AppState {
4457            label: "x".to_string(),
4458        };
4459        root.rebuild(&mut build, &mut state);
4460        // First build accumulated LAYOUT|PAINT — the peek sees it...
4461        assert!(root.has_pending_change_flags());
4462        // ...and repeated peeks do NOT drain it.
4463        assert!(root.has_pending_change_flags());
4464        // Only `take_change_flags` drains.
4465        assert!(!root.take_change_flags().is_empty());
4466        assert!(!root.has_pending_change_flags());
4467    }
4468
4469    #[test]
4470    fn set_theme_marks_layout_and_paint_pending() {
4471        // `set_theme` alone (no rebuild) must dirty layout/paint so a shell
4472        // gating on `take_change_flags` doesn't skip re-resolving theme-baked
4473        // widget state (e.g. Text's themed glyph color) on a bare theme swap.
4474        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4475        root.set_theme(Box::new(TestTheme { accent: 1 }));
4476        let flags = root.take_change_flags();
4477        assert!(flags.needs_layout());
4478        assert!(flags.needs_paint());
4479
4480        // Draining clears it until the next `set_theme`/rebuild.
4481        assert!(root.take_change_flags().is_empty());
4482        root.set_theme(Box::new(TestTheme { accent: 2 }));
4483        let flags = root.take_change_flags();
4484        assert!(flags.needs_layout());
4485        assert!(flags.needs_paint());
4486    }
4487
4488    // --- Window insets: pushed value reaches layout/paint contexts. ---
4489
4490    use crate::insets::{EdgeInsets, WindowInsets};
4491
4492    /// A root widget recording the `WindowInsets` it observed during layout and
4493    /// paint, proving the shell-pushed value threads through both contexts.
4494    struct InsetsWidget {
4495        seen_layout: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4496        seen_paint: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4497    }
4498    impl crate::widget::Widget for InsetsWidget {
4499        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4500            self.seen_layout.set(Some(ctx.window_insets()));
4501            bc.constrain(Size::new(10.0, 10.0))
4502        }
4503        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4504            self.seen_paint.set(Some(ctx.window_insets()));
4505        }
4506    }
4507
4508    struct InsetsView {
4509        seen_layout: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4510        seen_paint: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4511    }
4512    impl View<AppState> for InsetsView {
4513        type Element = InsetsWidget;
4514        fn build(&self, _ctx: &mut BuildCtx<'_>) -> InsetsWidget {
4515            InsetsWidget {
4516                seen_layout: self.seen_layout.clone(),
4517                seen_paint: self.seen_paint.clone(),
4518            }
4519        }
4520        fn rebuild(
4521            &self,
4522            _prev: &Self,
4523            _element: &mut InsetsWidget,
4524            _ctx: &mut BuildCtx<'_>,
4525        ) -> ChangeFlags {
4526            ChangeFlags::NONE
4527        }
4528    }
4529
4530    fn drive_insets_root(
4531        insets: Option<WindowInsets>,
4532    ) -> (Option<WindowInsets>, Option<WindowInsets>) {
4533        let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
4534        let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
4535        let mut root: RenderRoot<AppState, InsetsView> = RenderRoot::new();
4536        if let Some(insets) = insets {
4537            root.set_insets(insets);
4538        }
4539        let mut state = AppState::default();
4540        let sl = seen_layout.clone();
4541        let sp = seen_paint.clone();
4542        root.rebuild(
4543            &mut move |_s: &mut AppState| InsetsView {
4544                seen_layout: sl.clone(),
4545                seen_paint: sp.clone(),
4546            },
4547            &mut state,
4548        );
4549        root.layout(Size::new(100.0, 100.0));
4550        let mut scene = RecordingScene::default();
4551        root.paint(&mut scene, FrameTime::ZERO);
4552        (seen_layout.get(), seen_paint.get())
4553    }
4554
4555    #[test]
4556    fn set_insets_threads_into_layout_and_paint() {
4557        let insets = WindowInsets::new(
4558            EdgeInsets::new(0.0, 24.0, 0.0, 34.0),
4559            EdgeInsets::new(0.0, 0.0, 0.0, 0.0),
4560        );
4561        let (layout, paint) = drive_insets_root(Some(insets));
4562        assert_eq!(layout, Some(insets));
4563        assert_eq!(paint, Some(insets));
4564    }
4565
4566    #[test]
4567    fn no_insets_yields_zero_in_layout_and_paint() {
4568        let (layout, paint) = drive_insets_root(None);
4569        assert_eq!(layout, Some(WindowInsets::default()));
4570        assert_eq!(paint, Some(WindowInsets::default()));
4571    }
4572
4573    #[test]
4574    fn set_insets_marks_layout_and_paint_pending() {
4575        // Mirrors `set_theme_marks_layout_and_paint_pending`: a bare inset push
4576        // (no rebuild) must dirty layout/paint so a shell gating on
4577        // `take_change_flags` relayouts a `SafeArea` when the insets move.
4578        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4579        root.set_insets(WindowInsets::new(
4580            EdgeInsets::new(0.0, 24.0, 0.0, 0.0),
4581            EdgeInsets::ZERO,
4582        ));
4583        let flags = root.take_change_flags();
4584        assert!(flags.needs_layout());
4585        assert!(flags.needs_paint());
4586        // Drained until the next change.
4587        assert!(root.take_change_flags().is_empty());
4588    }
4589
4590    #[test]
4591    fn set_insets_no_op_when_unchanged_marks_nothing() {
4592        // The `PartialEq` no-op guard: re-pushing the current insets dirties
4593        // nothing, so a shell that forwards the platform insets every frame
4594        // never forces a needless relayout.
4595        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4596        let insets = WindowInsets::new(EdgeInsets::new(0.0, 24.0, 0.0, 34.0), EdgeInsets::ZERO);
4597        root.set_insets(insets);
4598        assert!(!root.take_change_flags().is_empty());
4599        // Same value again: no dirtiness.
4600        root.set_insets(insets);
4601        assert!(root.take_change_flags().is_empty());
4602        // A different value dirties again.
4603        root.set_insets(WindowInsets::default());
4604        assert!(!root.take_change_flags().is_empty());
4605    }
4606
4607    #[test]
4608    fn set_insets_round_trips_corner_insets() {
4609        use crate::insets::{CornerInset, CornerInsets};
4610        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4611        let corners = CornerInsets::new(
4612            CornerInset::ZERO,
4613            CornerInset::new(72.0, 24.0),
4614            CornerInset::ZERO,
4615            CornerInset::ZERO,
4616        );
4617        let insets = WindowInsets::default().with_corner_insets(corners);
4618        root.set_insets(insets);
4619        let flags = root.take_change_flags();
4620        assert!(flags.needs_layout());
4621        assert!(flags.needs_paint());
4622        assert_eq!(root.insets().corner_insets, corners);
4623        // Identical re-push marks nothing.
4624        root.set_insets(insets);
4625        assert!(root.take_change_flags().is_empty());
4626        // A push differing only in corners dirties again.
4627        let moved = insets.with_corner_insets(CornerInsets::new(
4628            CornerInset::new(72.0, 24.0),
4629            CornerInset::ZERO,
4630            CornerInset::ZERO,
4631            CornerInset::ZERO,
4632        ));
4633        root.set_insets(moved);
4634        let flags = root.take_change_flags();
4635        assert!(flags.needs_layout());
4636        assert!(flags.needs_paint());
4637    }
4638
4639    // --- Presented-frame count: pushed value reaches the paint context, unset
4640    //     yields `None`, and — unlike theme/insets — the setter dirties nothing. ---
4641
4642    /// A root widget recording the `presented_frames` count it observed during
4643    /// paint, proving the shell-pushed value threads through `PaintCtx`.
4644    struct PresentedWidget {
4645        seen_paint: std::rc::Rc<std::cell::Cell<Option<Option<u64>>>>,
4646    }
4647    impl crate::widget::Widget for PresentedWidget {
4648        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4649            bc.constrain(Size::new(10.0, 10.0))
4650        }
4651        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4652            self.seen_paint.set(Some(ctx.presented_frames()));
4653        }
4654    }
4655
4656    struct PresentedView {
4657        seen_paint: std::rc::Rc<std::cell::Cell<Option<Option<u64>>>>,
4658    }
4659    impl View<AppState> for PresentedView {
4660        type Element = PresentedWidget;
4661        fn build(&self, _ctx: &mut BuildCtx<'_>) -> PresentedWidget {
4662            PresentedWidget {
4663                seen_paint: self.seen_paint.clone(),
4664            }
4665        }
4666        fn rebuild(
4667            &self,
4668            _prev: &Self,
4669            _element: &mut PresentedWidget,
4670            _ctx: &mut BuildCtx<'_>,
4671        ) -> ChangeFlags {
4672            ChangeFlags::NONE
4673        }
4674    }
4675
4676    fn drive_presented_root(presented: Option<u64>) -> Option<u64> {
4677        let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
4678        let mut root: RenderRoot<AppState, PresentedView> = RenderRoot::new();
4679        if let Some(presented) = presented {
4680            root.set_presented_frames(presented);
4681        }
4682        let mut state = AppState::default();
4683        let sp = seen_paint.clone();
4684        root.rebuild(
4685            &mut move |_s: &mut AppState| PresentedView {
4686                seen_paint: sp.clone(),
4687            },
4688            &mut state,
4689        );
4690        root.layout(Size::new(100.0, 100.0));
4691        let mut scene = RecordingScene::default();
4692        root.paint(&mut scene, FrameTime::ZERO);
4693        // Unwrap the "did paint run" outer Option; the inner is what the widget saw.
4694        seen_paint.get().expect("paint ran")
4695    }
4696
4697    #[test]
4698    fn set_presented_frames_threads_into_paint() {
4699        assert_eq!(drive_presented_root(Some(12)), Some(12));
4700    }
4701
4702    #[test]
4703    fn unset_presented_frames_yields_none_in_paint() {
4704        assert_eq!(drive_presented_root(None), None);
4705    }
4706
4707    #[test]
4708    fn set_presented_frames_marks_no_change_flags() {
4709        // Unlike `set_theme`/`set_insets`, a presented-count push is a paint-only
4710        // observation — it must dirty NOTHING, so a monotonically ticking counter
4711        // never forces a relayout or (on mobile) keeps the frame gate perpetually
4712        // "Run" (the menu-idle behavior depends on this).
4713        let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4714        let gen_before = root.semantics_generation();
4715        root.set_presented_frames(1);
4716        assert!(root.take_change_flags().is_empty());
4717        assert!(!root.has_pending_change_flags());
4718        // A second, changed push still dirties nothing.
4719        root.set_presented_frames(2);
4720        assert!(root.take_change_flags().is_empty());
4721        // And bumps no semantics generation (mirrors the no-dirty contract).
4722        assert_eq!(root.semantics_generation(), gen_before);
4723    }
4724
4725    // Small test helper: does the boxed widget downcast to `W`?
4726    trait DowncastRefIs {
4727        fn downcast_ref_is<W: crate::widget::Widget>(&self) -> bool;
4728    }
4729    impl DowncastRefIs for dyn crate::widget::Widget {
4730        fn downcast_ref_is<W: crate::widget::Widget>(&self) -> bool {
4731            (self as &dyn std::any::Any).is::<W>()
4732        }
4733    }
4734
4735    // --- Focus / IME surface fixtures: a root editable that focuses + publishes
4736    //     an IME surface on a `Down` in its left half, and blurs (no focus) on a
4737    //     `Down` in its right half. ---
4738
4739    use std::cell::RefCell;
4740    use std::rc::Rc;
4741
4742    use crate::event::{EditingState, ImeState};
4743
4744    struct ImeWidget;
4745    impl crate::widget::Widget for ImeWidget {
4746        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4747            bc.constrain(Size::new(100.0, 100.0))
4748        }
4749        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4750        fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4751            if let InputEvent::Pointer(p) = event {
4752                if p.phase == PointerPhase::Down && p.position.x < 50.0 {
4753                    ctx.request_focus();
4754                    ctx.publish_ime_state(ImeState {
4755                        active: true,
4756                        editing: EditingState {
4757                            text: "abc".to_string(),
4758                            selection_base: 3,
4759                            selection_extent: 3,
4760                            composing_base: -1,
4761                            composing_extent: -1,
4762                        },
4763                        caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
4764                        content_type: Default::default(),
4765                        suppress_soft_keyboard: false,
4766                    });
4767                    return EventResult::Handled;
4768                }
4769                if p.phase == PointerPhase::Down {
4770                    // Right-half tap: a blur (no focus request).
4771                    return EventResult::Handled;
4772                }
4773            }
4774            EventResult::Ignored
4775        }
4776    }
4777
4778    struct ImeView;
4779    impl View<ClickState> for ImeView {
4780        type Element = ImeWidget;
4781        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeWidget {
4782            ImeWidget
4783        }
4784        fn rebuild(
4785            &self,
4786            _prev: &Self,
4787            _element: &mut ImeWidget,
4788            _ctx: &mut BuildCtx<'_>,
4789        ) -> ChangeFlags {
4790            ChangeFlags::NONE
4791        }
4792    }
4793
4794    fn ime_logic(_state: &mut ClickState) -> ImeView {
4795        ImeView
4796    }
4797
4798    #[test]
4799    fn focus_and_ime_state_surface_and_clear_on_blur() {
4800        let mut root: RenderRoot<ClickState, ImeView> = RenderRoot::new();
4801        let mut state = ClickState::default();
4802        root.rebuild(&mut ime_logic, &mut state);
4803        root.layout(Size::new(100.0, 100.0));
4804
4805        // No focus / no IME surface initially.
4806        assert!(!root.is_focus_active());
4807        assert!(root.ime_state().is_none());
4808
4809        // A left-half Down focuses the widget and publishes an IME surface.
4810        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4811        assert!(root.is_focus_active());
4812        let ime = root
4813            .ime_state()
4814            .expect("focused widget published an IME surface");
4815        assert!(ime.active);
4816        assert_eq!(ime.editing.text, "abc");
4817
4818        // The published surface survives a rebuild (shell can query it between
4819        // frames).
4820        root.rebuild(&mut ime_logic, &mut state);
4821        assert!(root.ime_state().is_some());
4822
4823        // A right-half Down is a blur: focus and the IME surface both clear.
4824        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
4825        assert!(!root.is_focus_active());
4826        assert!(root.ime_state().is_none());
4827    }
4828
4829    // --- Session release: the focus session must die with its owner -----------
4830    //
4831    // Two routes end a session without any user input reaching the root:
4832    //
4833    //  (a) a widget publishes an INACTIVE IME surface (the navigator's post-pop
4834    //      `cleared_ime_state`, `PatternSwitcher`'s equivalent, a `TextInput`
4835    //      turned disabled under a live focus), and
4836    //  (b) a reconciler tears the focused pod out of the tree (any generic
4837    //      unmount — `frust-widgets`' `cancel_active_children`/`teardown_child`),
4838    //      which raises `mark_focus_orphaned` because it has no `RenderRoot` to
4839    //      reach from a `BuildCtx` pass.
4840    //
4841    // Both must perform the SAME full release a blur does. Leaving either half
4842    // standing — `focus_active` true, or `ime_state` parked at `Some(inactive)` —
4843    // is what stranded a popped screen: `is_focus_active()` kept lying, the
4844    // shell's IME poll kept seeing a surface, and the next real focus
4845    // interaction started from a corrupt baseline.
4846
4847    /// The navigator's cleared surface, spelled out here so the fixture below
4848    /// publishes exactly the shape `nav::navigator::cleared_ime_state` does
4849    /// (`frust-core` cannot name it — `frust-widgets` sits above this crate).
4850    fn cleared_surface() -> ImeState {
4851        ImeState {
4852            active: false,
4853            editing: EditingState {
4854                text: String::new(),
4855                selection_base: -1,
4856                selection_extent: -1,
4857                composing_base: -1,
4858                composing_extent: -1,
4859            },
4860            caret: None,
4861            content_type: Default::default(),
4862            suppress_soft_keyboard: false,
4863        }
4864    }
4865
4866    /// A focused editable that publishes its active surface from paint (like a
4867    /// real field), and — once `clear` is raised — publishes the *inactive*
4868    /// surface from paint instead: the container-after-a-pop shape.
4869    struct PopImeWidget {
4870        clear: Rc<Cell<bool>>,
4871    }
4872    impl crate::widget::Widget for PopImeWidget {
4873        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4874            bc.constrain(Size::new(100.0, 100.0))
4875        }
4876        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4877            if self.clear.get() {
4878                ctx.publish_ime_state(cleared_surface());
4879            } else if ctx.has_focus() {
4880                ctx.publish_ime_state(PaintImeWidget::surface("abc"));
4881            }
4882        }
4883        fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4884            match event {
4885                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
4886                    ctx.request_focus();
4887                    ctx.publish_ime_state(PaintImeWidget::surface("abc"));
4888                    EventResult::Handled
4889                }
4890                _ => EventResult::Ignored,
4891            }
4892        }
4893    }
4894
4895    struct PopImeView {
4896        clear: Rc<Cell<bool>>,
4897    }
4898    impl View<ClickState> for PopImeView {
4899        type Element = PopImeWidget;
4900        fn build(&self, _ctx: &mut BuildCtx<'_>) -> PopImeWidget {
4901            PopImeWidget {
4902                clear: self.clear.clone(),
4903            }
4904        }
4905        fn rebuild(
4906            &self,
4907            _prev: &Self,
4908            _element: &mut PopImeWidget,
4909            _ctx: &mut BuildCtx<'_>,
4910        ) -> ChangeFlags {
4911            ChangeFlags::NONE
4912        }
4913    }
4914
4915    #[test]
4916    fn inactive_paint_publish_releases_the_whole_session() {
4917        // (a) The pop shape. Before this, the paint take stored `Some(inactive)`
4918        // and never touched `focus_active`, so the session outlived the page.
4919        let clear = Rc::new(Cell::new(false));
4920        let mut build = {
4921            let clear = clear.clone();
4922            move |_state: &mut ClickState| PopImeView {
4923                clear: clear.clone(),
4924            }
4925        };
4926        let mut root: RenderRoot<ClickState, PopImeView> = RenderRoot::new();
4927        let mut state = ClickState::default();
4928        root.rebuild(&mut build, &mut state);
4929        root.layout(Size::new(100.0, 100.0));
4930
4931        let mut scene = RecordingScene::default();
4932        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4933        root.paint(&mut scene, FrameTime::ZERO);
4934        assert!(root.is_focus_active());
4935        assert!(root.ime_state().is_some_and(|s| s.active));
4936        let focused = root.focus_ime_generation();
4937
4938        // The "pop": the next paint publishes the cleared surface.
4939        clear.set(true);
4940        root.paint(&mut scene, FrameTime::ZERO);
4941        assert!(
4942            !root.is_focus_active(),
4943            "an inactive publish ends the session, not just the surface"
4944        );
4945        assert_eq!(
4946            root.ime_state(),
4947            None,
4948            "the surface is dropped, never parked at Some(inactive)"
4949        );
4950        assert_eq!(
4951            root.focus_ime_generation(),
4952            focused.wrapping_add(1),
4953            "one release is exactly one edge"
4954        );
4955
4956        // The widget keeps publishing the cleared surface every frame (a real
4957        // one-shot flag would not, but an idle screen must survive the worst
4958        // case): the paint take's `focus_active` guard makes each a no-op, so
4959        // the released session neither resurrects nor spins the edge.
4960        let released = root.focus_ime_generation();
4961        for _ in 0..30 {
4962            root.paint(&mut scene, FrameTime::ZERO);
4963        }
4964        assert!(!root.is_focus_active());
4965        assert!(root.ime_state().is_none());
4966        assert_eq!(
4967            root.focus_ime_generation(),
4968            released,
4969            "an inactive publish against an already-released root is inert"
4970        );
4971    }
4972
4973    /// A view whose rebuild raises the generic-unmount orphan mark on demand —
4974    /// standing in for `frust-widgets`' reconcilers, which clear a focused
4975    /// `ChildPod` mid-diff and raise exactly this flag (this crate has no
4976    /// multi-child container of its own to diff).
4977    struct UnmountView {
4978        orphan: Rc<Cell<bool>>,
4979    }
4980    impl View<ClickState> for UnmountView {
4981        type Element = ImeWidget;
4982        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeWidget {
4983            ImeWidget
4984        }
4985        fn rebuild(
4986            &self,
4987            _prev: &Self,
4988            _element: &mut ImeWidget,
4989            _ctx: &mut BuildCtx<'_>,
4990        ) -> ChangeFlags {
4991            if self.orphan.get() {
4992                crate::event::mark_focus_orphaned();
4993                return ChangeFlags::LAYOUT | ChangeFlags::PAINT;
4994            }
4995            ChangeFlags::NONE
4996        }
4997    }
4998
4999    #[test]
5000    fn generic_unmount_orphan_releases_the_whole_session() {
5001        // (b) The child-list-diff shape: no publish, no event — the focused
5002        // widget simply stops existing. Nothing self-corrects this on an idle
5003        // screen, which is why the reconciler's mark is drained here.
5004        let _ = crate::event::take_focus_orphaned();
5005        let orphan = Rc::new(Cell::new(false));
5006        let mut build = {
5007            let orphan = orphan.clone();
5008            move |_state: &mut ClickState| UnmountView {
5009                orphan: orphan.clone(),
5010            }
5011        };
5012        let mut root: RenderRoot<ClickState, UnmountView> = RenderRoot::new();
5013        let mut state = ClickState::default();
5014        root.rebuild(&mut build, &mut state);
5015        root.layout(Size::new(100.0, 100.0));
5016
5017        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
5018        assert!(root.is_focus_active());
5019        assert!(root.ime_state().is_some());
5020        let focused = root.focus_ime_generation();
5021
5022        // The unmount rebuild.
5023        orphan.set(true);
5024        root.rebuild(&mut build, &mut state);
5025        assert!(
5026            !root.is_focus_active(),
5027            "the root's focus mirror does not outlive the widget it mirrors"
5028        );
5029        assert!(root.ime_state().is_none());
5030        assert_eq!(
5031            root.focus_ime_generation(),
5032            focused.wrapping_add(1),
5033            "one orphaned focus path is exactly one edge"
5034        );
5035
5036        // The mark was drained, so an ordinary rebuild afterwards is inert...
5037        let released = root.focus_ime_generation();
5038        orphan.set(false);
5039        root.rebuild(&mut build, &mut state);
5040        assert_eq!(root.focus_ime_generation(), released);
5041
5042        // ...and re-marking against an already-released root fires no edge
5043        // either (a stale `focused` flag torn down later must not spin it).
5044        orphan.set(true);
5045        root.rebuild(&mut build, &mut state);
5046        assert_eq!(root.focus_ime_generation(), released);
5047        assert!(!root.is_focus_active());
5048    }
5049
5050    // --- The focus/IME EDGE generation ---------------------------------------
5051    //
5052    // `focus_ime_generation` is the shell-facing edge behind the mobile frame
5053    // gate's `FrameInputs::focus_or_ime_changed`: a shell caches the value and
5054    // runs a frame when it moves. Two properties make that safe, and both are
5055    // pinned below: EVERY real transition moves it (or a focus change strands
5056    // unpainted), and NO same-value write moves it (or a focused screen forces
5057    // a frame every vsync — the level-input behavior this replaced, measured at
5058    // 62–120 fps on a static focused screen).
5059
5060    /// A widget that focuses on `Down`, releases focus on any `Key`, and — the
5061    /// point of the fixture — re-publishes an IME surface from its **paint**
5062    /// pass on every frame, reading the text from a shared cell so a test can
5063    /// make a republish genuinely change (or genuinely not).
5064    struct PaintImeWidget {
5065        published: Rc<RefCell<String>>,
5066    }
5067    impl PaintImeWidget {
5068        fn surface(text: &str) -> ImeState {
5069            ImeState {
5070                active: true,
5071                editing: EditingState {
5072                    text: text.to_string(),
5073                    selection_base: 0,
5074                    selection_extent: 0,
5075                    composing_base: -1,
5076                    composing_extent: -1,
5077                },
5078                caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
5079                content_type: Default::default(),
5080                suppress_soft_keyboard: false,
5081            }
5082        }
5083    }
5084    impl crate::widget::Widget for PaintImeWidget {
5085        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5086            bc.constrain(Size::new(100.0, 100.0))
5087        }
5088        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
5089            ctx.publish_ime_state(Self::surface(&self.published.borrow()));
5090        }
5091        fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
5092            match event {
5093                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
5094                    ctx.request_focus();
5095                    EventResult::Handled
5096                }
5097                InputEvent::Key(_) => {
5098                    ctx.release_focus();
5099                    EventResult::Handled
5100                }
5101                _ => EventResult::Ignored,
5102            }
5103        }
5104    }
5105
5106    struct PaintImeView {
5107        published: Rc<RefCell<String>>,
5108    }
5109    impl View<ClickState> for PaintImeView {
5110        type Element = PaintImeWidget;
5111        fn build(&self, _ctx: &mut BuildCtx<'_>) -> PaintImeWidget {
5112            PaintImeWidget {
5113                published: self.published.clone(),
5114            }
5115        }
5116        fn rebuild(
5117            &self,
5118            _prev: &Self,
5119            _element: &mut PaintImeWidget,
5120            _ctx: &mut BuildCtx<'_>,
5121        ) -> ChangeFlags {
5122            ChangeFlags::NONE
5123        }
5124    }
5125
5126    fn key_event() -> InputEvent {
5127        InputEvent::Key(crate::event::KeyEvent {
5128            key: crate::event::Key::Named(crate::event::NamedKey::Enter),
5129            modifiers: crate::event::Modifiers::default(),
5130            repeat: false,
5131        })
5132    }
5133
5134    #[test]
5135    fn focus_ime_generation_moves_on_every_pointer_transition_only() {
5136        let mut root: RenderRoot<ClickState, ImeView> = RenderRoot::new();
5137        let mut state = ClickState::default();
5138        root.rebuild(&mut ime_logic, &mut state);
5139        root.layout(Size::new(100.0, 100.0));
5140
5141        // Idle: a rebuild/layout touches neither focus nor the IME surface.
5142        let idle = root.focus_ime_generation();
5143        root.rebuild(&mut ime_logic, &mut state);
5144        assert_eq!(
5145            root.focus_ime_generation(),
5146            idle,
5147            "a rebuild is not an edge"
5148        );
5149
5150        // Focus gained + IME surface published (one transition for a shell,
5151        // however many field writes it took).
5152        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
5153        let focused = root.focus_ime_generation();
5154        assert_ne!(
5155            focused, idle,
5156            "focus + IME publish must move the generation"
5157        );
5158
5159        // The SAME tap again, on the already-focused widget publishing the
5160        // identical surface: no state moved, so no edge. This is the case that
5161        // decides whether a live text field forces a frame per vsync.
5162        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
5163        assert_eq!(
5164            root.focus_ime_generation(),
5165            focused,
5166            "a same-value focus/IME write must not spin the edge"
5167        );
5168
5169        // Blur: focus cleared and the surface dropped — a real transition.
5170        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
5171        let blurred = root.focus_ime_generation();
5172        assert_ne!(blurred, focused, "a blur must move the generation");
5173
5174        // Blur while already blurred (a tap on inert chrome — the commonest
5175        // event there is) writes `false`/`None` back over `false`/`None`.
5176        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 20.0));
5177        assert_eq!(
5178            root.focus_ime_generation(),
5179            blurred,
5180            "blurring an already-blurred root must not move the generation"
5181        );
5182    }
5183
5184    #[test]
5185    fn focus_ime_generation_ignores_an_unchanged_paint_republish() {
5186        // The paint pass re-publishes the focused widget's IME surface on EVERY
5187        // frame (that is how a rebuild-applied controlled change refreshes the
5188        // shell-facing state). If that unconditional write moved the
5189        // generation, the frame gate's edge would fire every single frame for
5190        // the whole life of a focus session — exactly the per-vsync forcing the
5191        // edge exists to remove.
5192        let published = Rc::new(RefCell::new("abc".to_string()));
5193        let mut build = {
5194            let published = published.clone();
5195            move |_state: &mut ClickState| PaintImeView {
5196                published: published.clone(),
5197            }
5198        };
5199        let mut root: RenderRoot<ClickState, PaintImeView> = RenderRoot::new();
5200        let mut state = ClickState::default();
5201        root.rebuild(&mut build, &mut state);
5202        root.layout(Size::new(100.0, 100.0));
5203
5204        // Focus the field, then let it paint: the first paint publishes.
5205        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
5206        let mut scene = RecordingScene::default();
5207        root.paint(&mut scene, FrameTime::ZERO);
5208        let steady = root.focus_ime_generation();
5209        assert_eq!(
5210            root.ime_state(),
5211            Some(PaintImeWidget::surface("abc")),
5212            "the paint pass published the focused widget's surface"
5213        );
5214
5215        // 120 further frames of the same focused, unchanged field: the caret
5216        // blinks, nothing else moves. Not one edge.
5217        for _ in 0..120 {
5218            root.paint(&mut scene, FrameTime::ZERO);
5219        }
5220        assert_eq!(
5221            root.focus_ime_generation(),
5222            steady,
5223            "an unchanged paint republish must never move the generation"
5224        );
5225
5226        // A real change (the app applied a controlled edit) publishes a
5227        // different surface: exactly one edge, then quiet again.
5228        *published.borrow_mut() = "abcd".to_string();
5229        root.paint(&mut scene, FrameTime::ZERO);
5230        let edited = root.focus_ime_generation();
5231        assert_ne!(edited, steady, "a changed republish IS an edge");
5232        for _ in 0..10 {
5233            root.paint(&mut scene, FrameTime::ZERO);
5234        }
5235        assert_eq!(
5236            root.focus_ime_generation(),
5237            edited,
5238            "the session goes quiet again at the new value"
5239        );
5240    }
5241
5242    #[test]
5243    fn focus_ime_generation_moves_on_a_focus_release_only_once() {
5244        // The Key/Ime/Scroll arm of the root focus path: a dispatch that
5245        // RELEASES focus clears both the flag and the published surface.
5246        let published = Rc::new(RefCell::new("abc".to_string()));
5247        let mut build = {
5248            let published = published.clone();
5249            move |_state: &mut ClickState| PaintImeView {
5250                published: published.clone(),
5251            }
5252        };
5253        let mut root: RenderRoot<ClickState, PaintImeView> = RenderRoot::new();
5254        let mut state = ClickState::default();
5255        root.rebuild(&mut build, &mut state);
5256        root.layout(Size::new(100.0, 100.0));
5257        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
5258        let mut scene = RecordingScene::default();
5259        root.paint(&mut scene, FrameTime::ZERO);
5260        let focused = root.focus_ime_generation();
5261        assert!(root.is_focus_active());
5262
5263        // A key that releases focus: one edge.
5264        root.event(&mut state, &key_event());
5265        let released = root.focus_ime_generation();
5266        assert!(!root.is_focus_active());
5267        assert!(root.ime_state().is_none());
5268        assert_ne!(
5269            released, focused,
5270            "a focus release must move the generation"
5271        );
5272
5273        // A second release against an already-released root: no edge. (The
5274        // paint pass republishes nothing now — the paint-take arm only accepts
5275        // a publish while focus is active.)
5276        root.event(&mut state, &key_event());
5277        root.paint(&mut scene, FrameTime::ZERO);
5278        assert_eq!(
5279            root.focus_ime_generation(),
5280            released,
5281            "releasing an already-released focus must not move the generation"
5282        );
5283    }
5284
5285    // --- The focus session's IDENTITY, beside the edge generation ------------
5286    //
5287    // `focus_epoch` answers a question `focus_ime_generation` cannot: "is this
5288    // still the session that asked?". A caller that binds a slow, asynchronous
5289    // answer to the field that asked for it needs an identity, and a counter
5290    // over the published surface's *value* is not one — two fields publish
5291    // equal surfaces, and a field is free to take focus and publish nothing at
5292    // all.
5293    //
5294    // Each test below asserts what BOTH counters did at the same moment. The
5295    // `focus_ime_generation` assertions are the point rather than decoration:
5296    // they are what states, in a form the compiler checks, that the edge
5297    // generation stands still exactly where the identity moves.
5298
5299    /// The text both fields publish from a press, so neither can be told from
5300    /// the other by the published value alone.
5301    const SHARED_FIELD_TEXT: &str = "shared";
5302
5303    /// One field of the two-field fixture.
5304    ///
5305    /// Takes the focus session on any press inside itself; publishes an IME
5306    /// surface on that press only when built to; and treats an `Ime` event as an
5307    /// edit — the text changes and the surface is republished, but nothing
5308    /// re-claims a session the field already holds.
5309    ///
5310    /// `publishes: false` is not a contrivance: the baseline text input claims
5311    /// focus and republishes nothing for a press that lands inside text it
5312    /// already had selected (such a press moves no caret and collapses no
5313    /// selection), and any app-authored focusable that publishes no IME surface
5314    /// of its own behaves the same way.
5315    ///
5316    /// A press publishes the *shared* surface, so the two fields are
5317    /// indistinguishable to anything reading the published value — that is the
5318    /// case under test. An edit publishes the field's own `name` instead, which
5319    /// is how a test proves which field a focus-routed event actually reached.
5320    struct SessionField {
5321        name: &'static str,
5322        publishes: bool,
5323    }
5324
5325    impl SessionField {
5326        /// The surface a field publishes. Deliberately carries nothing that
5327        /// tells one field from another: `ImeState` is
5328        /// `{active, editing, caret, content_type}` and names no widget, so two
5329        /// fields holding the same text and caret publish equal values — which
5330        /// is the ordinary shape of two empty fields, or two overlapping ones
5331        /// mid-transition.
5332        fn surface(text: &str) -> ImeState {
5333            ImeState {
5334                active: true,
5335                editing: EditingState {
5336                    text: text.to_string(),
5337                    selection_base: 0,
5338                    selection_extent: 0,
5339                    composing_base: -1,
5340                    composing_extent: -1,
5341                },
5342                caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
5343                content_type: Default::default(),
5344                suppress_soft_keyboard: false,
5345            }
5346        }
5347    }
5348
5349    impl crate::widget::Widget for SessionField {
5350        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5351            bc.constrain(Size::new(100.0, 20.0))
5352        }
5353        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5354        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5355            match event {
5356                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
5357                    ctx.request_focus();
5358                    if self.publishes {
5359                        ctx.publish_ime_state(Self::surface(SHARED_FIELD_TEXT));
5360                    }
5361                    EventResult::Handled
5362                }
5363                InputEvent::Ime(_) => {
5364                    ctx.publish_ime_state(Self::surface(self.name));
5365                    EventResult::Handled
5366                }
5367                _ => EventResult::Ignored,
5368            }
5369        }
5370    }
5371
5372    /// The fixture's root: two stacked fields, with a pointer event hit-tested
5373    /// to the one under it and a focus-routed event forwarded down the recorded
5374    /// focus path without a hit test — the routing every real container does.
5375    struct TwoFields {
5376        top: crate::widget::ChildPod,
5377        bottom: crate::widget::ChildPod,
5378    }
5379
5380    impl crate::widget::Widget for TwoFields {
5381        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5382            self.top.layout_child(ctx, bc);
5383            self.top.set_origin(Point::new(0.0, 0.0));
5384            self.bottom.layout_child(ctx, bc);
5385            self.bottom.set_origin(Point::new(0.0, 50.0));
5386            bc.max()
5387        }
5388        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
5389            self.top.paint_child(ctx, scene);
5390            self.bottom.paint_child(ctx, scene);
5391        }
5392        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5393            if matches!(event, InputEvent::Pointer(_)) {
5394                let pos = event.position();
5395                if self.top.contains(pos) {
5396                    return self.top.event_child(ctx, event);
5397                }
5398                if self.bottom.contains(pos) {
5399                    return self.bottom.event_child(ctx, event);
5400                }
5401                return EventResult::Ignored;
5402            }
5403            if self.top.holds_live_focus() {
5404                return self.top.event_child(ctx, event);
5405            }
5406            if self.bottom.holds_live_focus() {
5407                return self.bottom.event_child(ctx, event);
5408            }
5409            EventResult::Ignored
5410        }
5411        fn semantics(&self, ctx: &mut SemanticsCtx) {
5412            self.top.semantics_child(ctx);
5413            self.bottom.semantics_child(ctx);
5414        }
5415    }
5416
5417    struct TwoFieldsView {
5418        bottom_publishes: bool,
5419    }
5420
5421    impl View<ClickState> for TwoFieldsView {
5422        type Element = TwoFields;
5423        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TwoFields {
5424            TwoFields {
5425                top: crate::widget::ChildPod::new(Box::new(SessionField {
5426                    name: "top",
5427                    publishes: true,
5428                })),
5429                bottom: crate::widget::ChildPod::new(Box::new(SessionField {
5430                    name: "bottom",
5431                    publishes: self.bottom_publishes,
5432                })),
5433            }
5434        }
5435        fn rebuild(
5436            &self,
5437            _prev: &Self,
5438            _element: &mut TwoFields,
5439            _ctx: &mut BuildCtx<'_>,
5440        ) -> ChangeFlags {
5441            ChangeFlags::NONE
5442        }
5443    }
5444
5445    /// An edit pushed down the focus path by the platform IME: it claims no
5446    /// focus, so only the field already holding the session sees it — which is
5447    /// what makes the surface it republishes name that field.
5448    fn edit_event() -> InputEvent {
5449        InputEvent::Ime(crate::event::ImeEvent::ApplyEditingState(
5450            SessionField::surface(SHARED_FIELD_TEXT).editing,
5451        ))
5452    }
5453
5454    /// Mount the two-field fixture and press the top field, returning the root
5455    /// with a live session on it.
5456    fn two_fields_focused(
5457        bottom_publishes: bool,
5458    ) -> (RenderRoot<ClickState, TwoFieldsView>, ClickState) {
5459        let mut root: RenderRoot<ClickState, TwoFieldsView> = RenderRoot::new();
5460        let mut state = ClickState::default();
5461        root.rebuild(&mut |_| TwoFieldsView { bottom_publishes }, &mut state);
5462        root.layout(Size::new(200.0, 200.0));
5463        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
5464        assert!(root.is_focus_active(), "the top field opened a session");
5465        (root, state)
5466    }
5467
5468    #[test]
5469    fn focus_epoch_moves_when_focus_crosses_two_fields_publishing_alike() {
5470        let (mut root, mut state) = two_fields_focused(true);
5471        let first_session = root.focus_epoch();
5472        let steady_edge = root.focus_ime_generation();
5473        assert_eq!(
5474            root.ime_state(),
5475            Some(SessionField::surface(SHARED_FIELD_TEXT))
5476        );
5477
5478        // Press the bottom field. Focus really does move — and nothing
5479        // observable about the published surface moves with it: claiming while
5480        // some field is already focused writes `true` over `true`, and the
5481        // surface the second field publishes compares equal to the first's.
5482        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
5483        assert!(root.is_focus_active());
5484        assert_eq!(
5485            root.focus_ime_generation(),
5486            steady_edge,
5487            "the edge generation is blind to this move, which is why a caller \
5488             asking 'is this the same session?' must not be built on it"
5489        );
5490        assert_ne!(
5491            root.focus_epoch(),
5492            first_session,
5493            "the session identity must move when focus crosses to another field"
5494        );
5495
5496        // Not merely "some counter moved": the focus PATH is the bottom
5497        // field's now, which a focus-routed event proves by reaching it.
5498        root.event(&mut state, &edit_event());
5499        assert_eq!(
5500            root.ime_state(),
5501            Some(SessionField::surface("bottom")),
5502            "the second field is the one holding the session"
5503        );
5504    }
5505
5506    #[test]
5507    fn focus_epoch_moves_when_the_field_taking_focus_publishes_nothing() {
5508        let (mut root, mut state) = two_fields_focused(false);
5509        let first_session = root.focus_epoch();
5510        let steady_edge = root.focus_ime_generation();
5511
5512        // Press the bottom field, which takes the session and publishes no
5513        // surface of its own. A publish-nothing dispatch leaves the standing
5514        // surface standing, so what the shell still sees describes the field
5515        // the user just left — for as long as this session lasts, not merely
5516        // until the next frame.
5517        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
5518        assert!(root.is_focus_active());
5519        assert_eq!(
5520            root.ime_state(),
5521            Some(SessionField::surface(SHARED_FIELD_TEXT)),
5522            "the field that lost focus is still the one the surface describes"
5523        );
5524        assert_eq!(
5525            root.focus_ime_generation(),
5526            steady_edge,
5527            "no published value moved, so the edge generation cannot have"
5528        );
5529        assert_ne!(
5530            root.focus_epoch(),
5531            first_session,
5532            "an honoured claim moves the session identity whether or not the \
5533             claimant publishes anything"
5534        );
5535
5536        // And again, the move is a real one: the focus path now ends at the
5537        // field that published nothing.
5538        root.event(&mut state, &edit_event());
5539        assert_eq!(
5540            root.ime_state(),
5541            Some(SessionField::surface("bottom")),
5542            "the second field is the one holding the session"
5543        );
5544    }
5545
5546    #[test]
5547    fn focus_epoch_ignores_an_edit_inside_one_session() {
5548        // The converse direction, and the reason the two counters are kept
5549        // apart rather than merged: a caller holding an identity can let a
5550        // harmless edit ride, where a caller comparing the published value has
5551        // to treat every keystroke as a reason to give up.
5552        let (mut root, mut state) = two_fields_focused(true);
5553        let session = root.focus_epoch();
5554        let before_edit = root.focus_ime_generation();
5555
5556        root.event(&mut state, &edit_event());
5557        assert_eq!(
5558            root.ime_state(),
5559            Some(SessionField::surface("top")),
5560            "the focused field applied the edit and republished"
5561        );
5562        assert_ne!(
5563            root.focus_ime_generation(),
5564            before_edit,
5565            "a changed surface IS an edge"
5566        );
5567        assert_eq!(
5568            root.focus_epoch(),
5569            session,
5570            "an edit does not end or restart the session it lands in"
5571        );
5572
5573        // A release ends the identity too, so a stale one can never come back
5574        // round to matching by standing still.
5575        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 90.0));
5576        assert!(!root.is_focus_active());
5577        assert_ne!(
5578            root.focus_epoch(),
5579            session,
5580            "a release retires the session's identity"
5581        );
5582    }
5583
5584    // --- Semantics: stable ids + accessibility action routing ---
5585    //
5586    // Fixtures: an accessibility-visible button (fire-on-up-inside, contributes a
5587    // `Role::Button` node) and a checkbox variant (`Role::CheckBox`), plus a
5588    // labelled leaf used to prove id stability survives a pod relocation.
5589
5590    use accesskit::{Action, NodeId, Role};
5591
5592    /// A fire-on-up-inside button that also contributes a semantics node — the
5593    /// end-to-end target for `perform_accessibility_action(Click)`.
5594    struct A11yButtonWidget;
5595    impl crate::widget::Widget for A11yButtonWidget {
5596        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5597            bc.constrain(Size::new(40.0, 20.0))
5598        }
5599        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5600        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5601            if let InputEvent::Pointer(p) = event {
5602                match p.phase {
5603                    PointerPhase::Down => {
5604                        ctx.capture_pointer();
5605                        return EventResult::Handled;
5606                    }
5607                    PointerPhase::Up => {
5608                        let size = ctx.size();
5609                        let inside = p.position.x >= 0.0
5610                            && p.position.y >= 0.0
5611                            && p.position.x <= size.width
5612                            && p.position.y <= size.height;
5613                        if inside {
5614                            ctx.state_mut::<ClickState>().clicks += 1;
5615                            ctx.request_redraw();
5616                        }
5617                        return EventResult::Handled;
5618                    }
5619                    _ => {}
5620                }
5621            }
5622            EventResult::Ignored
5623        }
5624        fn semantics(&self, ctx: &mut SemanticsCtx) {
5625            ctx.push_node(Role::Button, |n| n.set_label("Go"));
5626        }
5627    }
5628
5629    struct A11yButtonView;
5630    impl View<ClickState> for A11yButtonView {
5631        type Element = A11yButtonWidget;
5632        fn build(&self, _ctx: &mut BuildCtx<'_>) -> A11yButtonWidget {
5633            A11yButtonWidget
5634        }
5635        fn rebuild(
5636            &self,
5637            _prev: &Self,
5638            _el: &mut A11yButtonWidget,
5639            _ctx: &mut BuildCtx<'_>,
5640        ) -> ChangeFlags {
5641            ChangeFlags::NONE
5642        }
5643    }
5644
5645    fn a11y_button_logic(_state: &mut ClickState) -> A11yButtonView {
5646        A11yButtonView
5647    }
5648
5649    fn button_node_id(update: &SemanticsUpdate, role: Role) -> NodeId {
5650        update
5651            .nodes
5652            .iter()
5653            .find(|(_, n)| n.role() == role)
5654            .map(|(id, _)| *id)
5655            .unwrap_or_else(|| panic!("a {role:?} node is present"))
5656    }
5657
5658    #[test]
5659    fn semantics_ids_are_stable_across_frames() {
5660        let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5661        let mut state = ClickState::default();
5662        root.rebuild(&mut a11y_button_logic, &mut state);
5663        root.layout(Size::new(200.0, 200.0));
5664
5665        let first = button_node_id(&root.semantics(), Role::Button);
5666        // Re-run rebuild+layout+semantics several times: the button keeps its id.
5667        for _ in 0..3 {
5668            root.rebuild(&mut a11y_button_logic, &mut state);
5669            root.layout(Size::new(200.0, 200.0));
5670            assert_eq!(
5671                button_node_id(&root.semantics(), Role::Button),
5672                first,
5673                "the same widget must keep its NodeId across frames"
5674            );
5675        }
5676        // The window root is the reserved constant id.
5677        assert_eq!(root.semantics().root, ROOT_NODE_ID);
5678    }
5679
5680    #[test]
5681    fn semantics_ids_survive_a_pod_relocation() {
5682        // A keyed reorder relocates the whole `ChildPod` (preserving its cached
5683        // semantics id); simulate that here by swapping two pods in place and
5684        // asserting each labelled node keeps its id despite changing position.
5685        struct LabeledLeaf {
5686            label: &'static str,
5687        }
5688        impl crate::widget::Widget for LabeledLeaf {
5689            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5690                bc.constrain(Size::new(10.0, 10.0))
5691            }
5692            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5693            fn semantics(&self, ctx: &mut SemanticsCtx) {
5694                let label = self.label;
5695                ctx.push_node(Role::Label, |n| n.set_label(label));
5696            }
5697        }
5698
5699        let mut pods = vec![
5700            crate::widget::ChildPod::new(Box::new(LabeledLeaf { label: "A" })),
5701            crate::widget::ChildPod::new(Box::new(LabeledLeaf { label: "B" })),
5702        ];
5703
5704        // Collect (label -> id) for a given pod order. `SemanticsCtx` is
5705        // crate-private, so this drives the pods directly — the same allocation
5706        // path `RenderRoot::semantics` uses.
5707        let collect = |pods: &[crate::widget::ChildPod]| {
5708            let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), 2);
5709            for pod in pods {
5710                pod.semantics_child(&mut ctx);
5711            }
5712            let update = ctx.finish(ROOT_NODE_ID);
5713            update
5714                .nodes
5715                .iter()
5716                .filter(|(id, _)| *id != ROOT_NODE_ID)
5717                .map(|(id, n)| (n.label().unwrap().to_string(), *id))
5718                .collect::<Vec<_>>()
5719        };
5720
5721        let before = collect(&pods);
5722        // Relocate: swap the pods (the pods themselves, with their cached ids,
5723        // move — mirroring the keyed reconciler's `take`-and-reorder).
5724        pods.swap(0, 1);
5725        let after = collect(&pods);
5726
5727        for (label, id) in &before {
5728            let relocated = after.iter().find(|(l, _)| l == label).unwrap().1;
5729            assert_eq!(
5730                *id, relocated,
5731                "widget {label:?} must keep its NodeId across the reorder"
5732            );
5733        }
5734        // And the reorder actually changed positions (A now second).
5735        assert_eq!(after[0].0, "B");
5736        assert_eq!(after[1].0, "A");
5737    }
5738
5739    #[test]
5740    fn semantics_full_update_assembles_window_and_child() {
5741        let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5742        let mut state = ClickState::default();
5743        root.rebuild(&mut a11y_button_logic, &mut state);
5744        root.layout(Size::new(200.0, 200.0));
5745
5746        let update = root.semantics();
5747        // Window root + the button.
5748        assert_eq!(update.nodes.len(), 2);
5749        assert_eq!(update.root, ROOT_NODE_ID);
5750        let root_node = update
5751            .nodes
5752            .iter()
5753            .find(|(id, _)| *id == update.root)
5754            .unwrap();
5755        assert_eq!(root_node.1.role(), Role::Window);
5756        let button = button_node_id(&update, Role::Button);
5757        assert_eq!(
5758            root_node.1.children(),
5759            &[button],
5760            "the button attaches under the window root"
5761        );
5762        // Nothing focused → the adapter-facing focus id defaults to the root.
5763        assert!(update.focus.is_none());
5764        assert_eq!(update.focus_id(), ROOT_NODE_ID);
5765    }
5766
5767    #[test]
5768    fn perform_click_action_activates_a_button() {
5769        let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5770        let mut state = ClickState::default();
5771        root.rebuild(&mut a11y_button_logic, &mut state);
5772        root.layout(Size::new(200.0, 200.0));
5773
5774        let button = button_node_id(&root.semantics(), Role::Button);
5775        let outcome = root.perform_accessibility_action(&mut state, button, Action::Click);
5776        assert!(outcome.handled, "the synthesized Down+Up was handled");
5777        assert!(outcome.needs_redraw);
5778        assert_eq!(state.clicks, 1, "Click synthesized a real up-inside tap");
5779
5780        // An unknown node id is a benign no-op.
5781        let outcome = root.perform_accessibility_action(&mut state, NodeId(999_999), Action::Click);
5782        assert_eq!(outcome, EventOutcome::default());
5783        assert_eq!(state.clicks, 1);
5784
5785        // An unmodelled action is ignored.
5786        let outcome = root.perform_accessibility_action(&mut state, button, Action::ScrollDown);
5787        assert_eq!(outcome, EventOutcome::default());
5788        assert_eq!(state.clicks, 1);
5789    }
5790
5791    #[test]
5792    fn perform_click_action_toggles_a_checkbox() {
5793        // A checkbox-shaped widget (`Role::CheckBox`) reached through the same
5794        // synthetic-pointer path — proving Click drives any fire-on-up-inside
5795        // control, not just buttons.
5796        struct CheckboxWidget;
5797        impl crate::widget::Widget for CheckboxWidget {
5798            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5799                bc.constrain(Size::new(24.0, 24.0))
5800            }
5801            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5802            fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5803                if let InputEvent::Pointer(p) = event {
5804                    match p.phase {
5805                        PointerPhase::Down => {
5806                            ctx.capture_pointer();
5807                            return EventResult::Handled;
5808                        }
5809                        PointerPhase::Up => {
5810                            let size = ctx.size();
5811                            if p.position.x >= 0.0
5812                                && p.position.y >= 0.0
5813                                && p.position.x <= size.width
5814                                && p.position.y <= size.height
5815                            {
5816                                ctx.state_mut::<ClickState>().clicks += 1;
5817                            }
5818                            return EventResult::Handled;
5819                        }
5820                        _ => {}
5821                    }
5822                }
5823                EventResult::Ignored
5824            }
5825            fn semantics(&self, ctx: &mut SemanticsCtx) {
5826                ctx.push_node(Role::CheckBox, |n| n.set_label("Agree"));
5827            }
5828        }
5829        struct CheckboxView;
5830        impl View<ClickState> for CheckboxView {
5831            type Element = CheckboxWidget;
5832            fn build(&self, _ctx: &mut BuildCtx<'_>) -> CheckboxWidget {
5833                CheckboxWidget
5834            }
5835            fn rebuild(
5836                &self,
5837                _p: &Self,
5838                _e: &mut CheckboxWidget,
5839                _c: &mut BuildCtx<'_>,
5840            ) -> ChangeFlags {
5841                ChangeFlags::NONE
5842            }
5843        }
5844
5845        let mut root: RenderRoot<ClickState, CheckboxView> = RenderRoot::new();
5846        let mut state = ClickState::default();
5847        root.rebuild(&mut |_| CheckboxView, &mut state);
5848        root.layout(Size::new(200.0, 200.0));
5849
5850        let cb = button_node_id(&root.semantics(), Role::CheckBox);
5851        root.perform_accessibility_action(&mut state, cb, Action::Click);
5852        assert_eq!(state.clicks, 1, "Click toggled the checkbox once");
5853    }
5854
5855    #[test]
5856    fn perform_focus_action_claims_focus_and_clears_capture() {
5857        // Two focus-claiming, fire-on-up-inside buttons in a container. A11y
5858        // `Focus` on B must claim focus for B *and* release the capture the
5859        // synthesized `Down` opened — the CRITICAL leak this regresses: without
5860        // the trailing `Cancel`, B stayed captured and swallowed every later
5861        // pointer event, so a tap on A never reached A.
5862
5863        #[derive(Default)]
5864        struct FocusState {
5865            a_press: u32,
5866            b_press: u32,
5867            b_move: u32,
5868        }
5869
5870        #[derive(Clone, Copy)]
5871        enum Btn {
5872            A,
5873            B,
5874        }
5875
5876        /// A button that opts into both recorded paths (capture + focus) on
5877        /// `Down` and fires its press only on `Up`-inside — never on `Cancel`.
5878        struct FocusButton {
5879            id: Btn,
5880        }
5881        impl crate::widget::Widget for FocusButton {
5882            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5883                bc.constrain(Size::new(40.0, 20.0))
5884            }
5885            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5886            fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5887                let InputEvent::Pointer(p) = event else {
5888                    return EventResult::Ignored;
5889                };
5890                match p.phase {
5891                    PointerPhase::Down => {
5892                        ctx.capture_pointer();
5893                        ctx.request_focus();
5894                        EventResult::Handled
5895                    }
5896                    PointerPhase::Move => {
5897                        if let Btn::B = self.id {
5898                            ctx.state_mut::<FocusState>().b_move += 1;
5899                        }
5900                        EventResult::Handled
5901                    }
5902                    PointerPhase::Up => {
5903                        let size = ctx.size();
5904                        let inside = p.position.x >= 0.0
5905                            && p.position.y >= 0.0
5906                            && p.position.x <= size.width
5907                            && p.position.y <= size.height;
5908                        if inside {
5909                            match self.id {
5910                                Btn::A => ctx.state_mut::<FocusState>().a_press += 1,
5911                                Btn::B => ctx.state_mut::<FocusState>().b_press += 1,
5912                            }
5913                        }
5914                        EventResult::Handled
5915                    }
5916                    // A `Cancel` clears without firing on_press and never touches
5917                    // state — the contract the Focus action's trailing Cancel rides.
5918                    PointerPhase::Cancel => EventResult::Handled,
5919                }
5920            }
5921            fn semantics(&self, ctx: &mut SemanticsCtx) {
5922                let label = match self.id {
5923                    Btn::A => "A",
5924                    Btn::B => "B",
5925                };
5926                ctx.push_node(Role::Button, |n| n.set_label(label));
5927            }
5928        }
5929
5930        /// A minimal two-child container mirroring `frust-widgets`'
5931        /// `route_event`: a captured gesture goes straight to the active child
5932        /// (auto-released on `Up`/`Cancel`), otherwise the event is hit-tested to
5933        /// the child under it.
5934        struct TwoButtons {
5935            a: crate::widget::ChildPod,
5936            b: crate::widget::ChildPod,
5937        }
5938        impl crate::widget::Widget for TwoButtons {
5939            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5940                self.a.layout_child(ctx, bc);
5941                self.a.set_origin(Point::new(0.0, 0.0));
5942                self.b.layout_child(ctx, bc);
5943                self.b.set_origin(Point::new(0.0, 30.0));
5944                bc.max()
5945            }
5946            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
5947                self.a.paint_child(ctx, scene);
5948                self.b.paint_child(ctx, scene);
5949            }
5950            fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5951                let releases = matches!(
5952                    event,
5953                    InputEvent::Pointer(p)
5954                        if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
5955                );
5956                // Capture fast-path: a recorded active child receives every event
5957                // until it releases on Up/Cancel, bypassing the hit test entirely.
5958                if self.a.is_active() {
5959                    let r = self.a.event_child(ctx, event);
5960                    if releases {
5961                        self.a.set_active(false);
5962                    }
5963                    return r;
5964                }
5965                if self.b.is_active() {
5966                    let r = self.b.event_child(ctx, event);
5967                    if releases {
5968                        self.b.set_active(false);
5969                    }
5970                    return r;
5971                }
5972                // Fresh event: route to the child under the point.
5973                let pos = event.position();
5974                if self.a.contains(pos) {
5975                    return self.a.event_child(ctx, event);
5976                }
5977                if self.b.contains(pos) {
5978                    return self.b.event_child(ctx, event);
5979                }
5980                EventResult::Ignored
5981            }
5982            fn semantics(&self, ctx: &mut SemanticsCtx) {
5983                self.a.semantics_child(ctx);
5984                self.b.semantics_child(ctx);
5985            }
5986        }
5987
5988        struct TwoButtonsView;
5989        impl View<FocusState> for TwoButtonsView {
5990            type Element = TwoButtons;
5991            fn build(&self, _ctx: &mut BuildCtx<'_>) -> TwoButtons {
5992                TwoButtons {
5993                    a: crate::widget::ChildPod::new(Box::new(FocusButton { id: Btn::A })),
5994                    b: crate::widget::ChildPod::new(Box::new(FocusButton { id: Btn::B })),
5995                }
5996            }
5997            fn rebuild(
5998                &self,
5999                _p: &Self,
6000                _e: &mut TwoButtons,
6001                _c: &mut BuildCtx<'_>,
6002            ) -> ChangeFlags {
6003                ChangeFlags::NONE
6004            }
6005        }
6006
6007        let mut root: RenderRoot<FocusState, TwoButtonsView> = RenderRoot::new();
6008        let mut state = FocusState::default();
6009        root.rebuild(&mut |_| TwoButtonsView, &mut state);
6010        root.layout(Size::new(200.0, 200.0));
6011
6012        // B's semantics node (label "B") is the a11y Focus target.
6013        let b_id = root
6014            .semantics()
6015            .nodes
6016            .iter()
6017            .find(|(_, n)| n.label().is_some_and(|l| l == "B"))
6018            .map(|(id, _)| *id)
6019            .expect("button B contributes a semantics node");
6020
6021        // A11y `Focus` on B: claims the focus session, fires no on_press, and —
6022        // crucially — leaves nothing captured (the Down+Cancel shape).
6023        root.perform_accessibility_action(&mut state, b_id, Action::Focus);
6024        assert!(root.is_focus_active(), "Focus opened the focus session");
6025        assert!(
6026            !root.is_pointer_captured(),
6027            "the trailing Cancel released the capture the Focus Down opened"
6028        );
6029        assert_eq!(
6030            state.b_press, 0,
6031            "Focus (Down+Cancel) must not fire B's on_press"
6032        );
6033
6034        // B is not stuck-captured: a `Move` outside both buttons is ignored. Were
6035        // B still captured, the capture fast-path would route this to B regardless
6036        // of position (b_move would tick).
6037        let outside = InputEvent::Pointer(PointerEvent {
6038            phase: PointerPhase::Move,
6039            position: Point::new(100.0, 100.0),
6040            button: PointerButton::Primary,
6041        });
6042        root.event(&mut state, &outside);
6043        assert_eq!(state.b_move, 0, "no leaked capture: B saw no stray Move");
6044
6045        // A real Down+Up on A activates A exactly once and never reaches B.
6046        let at_a = |phase| {
6047            InputEvent::Pointer(PointerEvent {
6048                phase,
6049                position: Point::new(20.0, 10.0),
6050                button: PointerButton::Primary,
6051            })
6052        };
6053        root.event(&mut state, &at_a(PointerPhase::Down));
6054        root.event(&mut state, &at_a(PointerPhase::Up));
6055        assert_eq!(state.a_press, 1, "A fired once from its own tap");
6056        assert_eq!(
6057            state.b_press, 0,
6058            "B never fired — its capture never leaked onto A's tap"
6059        );
6060    }
6061
6062    #[test]
6063    fn semantics_if_changed_gates_on_generation() {
6064        let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
6065        let mut state = ClickState::default();
6066        // First build bumps the generation from 0.
6067        root.rebuild(&mut a11y_button_logic, &mut state);
6068        root.layout(Size::new(200.0, 200.0));
6069
6070        let generation = root.semantics_generation();
6071        assert!(generation > 0);
6072        // A shell that already pushed `generation` sees no change.
6073        assert!(root.semantics_if_changed(generation).is_none());
6074        // A stale generation triggers a fresh pull.
6075        assert!(root.semantics_if_changed(generation - 1).is_some());
6076
6077        // A theme swap marks the tree semantics-dirty.
6078        root.set_theme(Box::new(0u32));
6079        assert!(root.semantics_generation() > generation);
6080        assert!(root.semantics_if_changed(generation).is_some());
6081    }
6082
6083    #[test]
6084    fn orientation_from_size_is_portrait_when_taller_than_wide() {
6085        assert_eq!(
6086            Orientation::from_size(Size::new(400.0, 800.0)),
6087            Orientation::Portrait
6088        );
6089    }
6090
6091    #[test]
6092    fn orientation_from_size_is_landscape_when_wider_than_tall() {
6093        assert_eq!(
6094            Orientation::from_size(Size::new(800.0, 400.0)),
6095            Orientation::Landscape
6096        );
6097    }
6098
6099    #[test]
6100    fn orientation_from_size_square_reads_as_portrait() {
6101        // Height >= width is the derivation rule (see `Orientation::from_size`'s
6102        // doc); an exact square satisfies `>=` and must not panic/ambiguously
6103        // resolve, so this is pinned explicitly rather than left implicit.
6104        assert_eq!(
6105            Orientation::from_size(Size::new(500.0, 500.0)),
6106            Orientation::Portrait
6107        );
6108    }
6109
6110    #[test]
6111    fn window_metrics_new_derives_orientation_from_size() {
6112        let insets = WindowInsets::default();
6113        let portrait = WindowMetrics::new(Size::new(390.0, 844.0), 3.0, insets);
6114        assert_eq!(portrait.orientation, Orientation::Portrait);
6115        assert_eq!(portrait.size, Size::new(390.0, 844.0));
6116        assert_eq!(portrait.scale, 3.0);
6117        assert_eq!(portrait.insets, insets);
6118
6119        let landscape = WindowMetrics::new(Size::new(844.0, 390.0), 3.0, insets);
6120        assert_eq!(landscape.orientation, Orientation::Landscape);
6121
6122        let square = WindowMetrics::new(Size::new(500.0, 500.0), 2.0, insets);
6123        assert_eq!(square.orientation, Orientation::Portrait);
6124    }
6125
6126    // --- Deferred state-bearing callbacks: the `InputEvent::Housekeeping` flush
6127    //     `RenderRoot::rebuild` dispatches. ---
6128
6129    /// App state for the flush tests.
6130    #[derive(Default)]
6131    struct FlushState {
6132        /// How many deferred callbacks have run.
6133        flushes: u32,
6134        /// How many more times a running callback re-queues itself — the knob the
6135        /// chained/capped tests turn.
6136        chain_left: u32,
6137        /// The `flushes` value each build-closure run observed, in order. This is
6138        /// what proves the rebuild re-runs the build closure *after* a flush rather than
6139        /// shipping the now-stale pre-flush view.
6140        observed: Vec<u32>,
6141    }
6142
6143    /// The navigator's deferred-callback shape reduced to one leaf: a shared
6144    /// `Rc<Cell<u32>>` op queue (the `NavigatorController` analog) is drained
6145    /// during the state-free [`View::rebuild`], which can therefore only *queue*
6146    /// the callback and raise the flush mark; [`crate::widget::Widget::event`]
6147    /// runs it when the broadcast arrives, where `&mut State` finally exists.
6148    struct FlushView {
6149        ops: std::rc::Rc<Cell<u32>>,
6150    }
6151
6152    struct FlushWidget {
6153        ops: std::rc::Rc<Cell<u32>>,
6154        queued: u32,
6155    }
6156
6157    impl FlushWidget {
6158        fn drain_ops(&mut self) {
6159            let ops = self.ops.replace(0);
6160            if ops > 0 {
6161                self.queued += ops;
6162                crate::event::mark_pending_result_flush();
6163            }
6164        }
6165    }
6166
6167    impl View<FlushState> for FlushView {
6168        type Element = FlushWidget;
6169        fn build(&self, _ctx: &mut BuildCtx<'_>) -> FlushWidget {
6170            let mut widget = FlushWidget {
6171                ops: self.ops.clone(),
6172                queued: 0,
6173            };
6174            widget.drain_ops();
6175            widget
6176        }
6177        fn rebuild(
6178            &self,
6179            _prev: &Self,
6180            element: &mut FlushWidget,
6181            _ctx: &mut BuildCtx<'_>,
6182        ) -> ChangeFlags {
6183            element.ops = self.ops.clone();
6184            element.drain_ops();
6185            ChangeFlags::NONE
6186        }
6187    }
6188
6189    impl crate::widget::Widget for FlushWidget {
6190        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6191            bc.constrain(Size::new(10.0, 10.0))
6192        }
6193        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
6194        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6195            if !event.is_broadcast() || self.queued == 0 {
6196                return EventResult::Ignored;
6197            }
6198            let queued = std::mem::take(&mut self.queued);
6199            let state = ctx.state_mut::<FlushState>();
6200            for _ in 0..queued {
6201                state.flushes += 1;
6202                if state.chain_left > 0 {
6203                    state.chain_left -= 1;
6204                    self.ops.set(self.ops.get() + 1);
6205                }
6206            }
6207            EventResult::Ignored
6208        }
6209    }
6210
6211    fn flush_logic(ops: std::rc::Rc<Cell<u32>>) -> impl FnMut(&mut FlushState) -> FlushView {
6212        move |state: &mut FlushState| {
6213            state.observed.push(state.flushes);
6214            FlushView { ops: ops.clone() }
6215        }
6216    }
6217
6218    #[test]
6219    fn a_queued_callback_flushes_and_re_diffs_inside_one_rebuild() {
6220        let ops = std::rc::Rc::new(Cell::new(0u32));
6221        let mut root: RenderRoot<FlushState, FlushView> = RenderRoot::new();
6222        let mut app = flush_logic(ops.clone());
6223        let mut state = FlushState::default();
6224
6225        root.rebuild(&mut app, &mut state);
6226        assert_eq!(state.flushes, 0);
6227        assert_eq!(
6228            state.observed,
6229            vec![0],
6230            "nothing queued ⇒ exactly one build run, no broadcast"
6231        );
6232
6233        // Queue one op — the `NavigatorController::pop_with_result` analog.
6234        state.observed.clear();
6235        ops.set(1);
6236        root.rebuild(&mut app, &mut state);
6237        assert_eq!(
6238            state.flushes, 1,
6239            "the queued callback ran inside this rebuild — no event was dispatched \
6240             by anyone but the rebuild itself"
6241        );
6242        assert_eq!(
6243            state.observed,
6244            vec![0, 1],
6245            "the build closure re-ran after the flush and saw the post-callback state, so \
6246             the view this frame ships is not the stale pre-flush one"
6247        );
6248        assert!(
6249            !crate::event::take_pending_result_flush(),
6250            "the mark was consumed; nothing is owed to a later frame"
6251        );
6252    }
6253
6254    #[test]
6255    fn a_runaway_callback_chain_is_capped_and_deferred_to_the_next_frame() {
6256        let ops = std::rc::Rc::new(Cell::new(0u32));
6257        let mut root: RenderRoot<FlushState, FlushView> = RenderRoot::new();
6258        let mut app = flush_logic(ops.clone());
6259        // Far more chaining than the cap allows: unbounded, this rebuild would
6260        // never return. Reaching the assertions below at all is the no-spin proof.
6261        let mut state = FlushState {
6262            chain_left: 100,
6263            ..Default::default()
6264        };
6265        root.rebuild(&mut app, &mut state);
6266
6267        ops.set(1);
6268        root.rebuild(&mut app, &mut state);
6269        assert_eq!(
6270            state.flushes, MAX_PENDING_RESULT_FLUSH_PASSES as u32,
6271            "exactly the cap's worth of flush passes, then stop"
6272        );
6273
6274        // The remainder is owed, not lost: the mark still stands and the next
6275        // paint asks for the follow-up frame that will finish it.
6276        root.layout(Size::new(50.0, 50.0));
6277        let mut scene = RecordingScene::default();
6278        let outcome = root.paint(&mut scene, FrameTime::ZERO);
6279        assert!(
6280            outcome.needs_frame,
6281            "hitting the cap requests one more frame, so a dirty-driven shell \
6282             wakes instead of waiting for input"
6283        );
6284        assert!(
6285            !outcome.needs_frame_paced_only,
6286            "a deferred flush is not a cosmetic loop — the mobile frame gate must \
6287             not throttle it"
6288        );
6289
6290        // That next frame picks up exactly where the capped one left off.
6291        root.rebuild(&mut app, &mut state);
6292        assert_eq!(
6293            state.flushes,
6294            2 * MAX_PENDING_RESULT_FLUSH_PASSES as u32,
6295            "the deferred remainder resumed on the following frame"
6296        );
6297
6298        // Leave this thread's flag clean for anything else in the binary.
6299        let _ = crate::event::take_pending_result_flush();
6300    }
6301
6302    /// The redraw-only flush shape: a widget that queues a callback exactly like
6303    /// [`FlushView`] above, but whose broadcast handler touches **no** state at
6304    /// all — it only calls [`EventCtx::request_redraw`]. `frust-widgets`' gesture
6305    /// long-press latch is the shipped instance (its `on_long_press` consumer may
6306    /// mutate nothing the view diff can see), and the widget's own
6307    /// `ctx.request_redraw()` after firing is then the whole wake signal.
6308    struct RedrawOnlyView {
6309        ops: std::rc::Rc<Cell<u32>>,
6310    }
6311
6312    struct RedrawOnlyWidget {
6313        ops: std::rc::Rc<Cell<u32>>,
6314        queued: bool,
6315    }
6316
6317    impl RedrawOnlyWidget {
6318        fn drain_ops(&mut self) {
6319            if self.ops.replace(0) > 0 {
6320                self.queued = true;
6321                crate::event::mark_pending_result_flush();
6322            }
6323        }
6324    }
6325
6326    impl View<()> for RedrawOnlyView {
6327        type Element = RedrawOnlyWidget;
6328        fn build(&self, _ctx: &mut BuildCtx<'_>) -> RedrawOnlyWidget {
6329            let mut widget = RedrawOnlyWidget {
6330                ops: self.ops.clone(),
6331                queued: false,
6332            };
6333            widget.drain_ops();
6334            widget
6335        }
6336        fn rebuild(
6337            &self,
6338            _prev: &Self,
6339            element: &mut RedrawOnlyWidget,
6340            _ctx: &mut BuildCtx<'_>,
6341        ) -> ChangeFlags {
6342            element.ops = self.ops.clone();
6343            element.drain_ops();
6344            // The whole point: the re-diff after the flush reports nothing, so
6345            // the dispatch's own outcome is the only wake signal there is.
6346            ChangeFlags::NONE
6347        }
6348    }
6349
6350    impl crate::widget::Widget for RedrawOnlyWidget {
6351        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6352            bc.constrain(Size::new(10.0, 10.0))
6353        }
6354        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
6355        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6356            if event.is_broadcast() && std::mem::take(&mut self.queued) {
6357                // No `state_mut`, no signal, no view-visible change — a repaint
6358                // request and nothing else.
6359                ctx.request_redraw();
6360            }
6361            EventResult::Ignored
6362        }
6363    }
6364
6365    #[test]
6366    fn a_redraw_only_flushed_callback_wakes_both_loop_styles() {
6367        let ops = std::rc::Rc::new(Cell::new(0u32));
6368        let mut root: RenderRoot<(), RedrawOnlyView> = RenderRoot::new();
6369        let mut app = |_state: &mut ()| RedrawOnlyView { ops: ops.clone() };
6370        let mut state = ();
6371
6372        // Settle the first build so the assertions below observe only the flush.
6373        root.rebuild(&mut app, &mut state);
6374        root.layout(Size::new(50.0, 50.0));
6375        let mut scene = RecordingScene::default();
6376        let settled = root.paint(&mut scene, FrameTime::ZERO);
6377        assert!(!settled.needs_frame, "nothing queued ⇒ the tree is at rest");
6378        let _ = root.take_change_flags();
6379
6380        // Queue the redraw-only callback (the gesture long-press latch analog: a
6381        // prior pass marks, this rebuild flushes).
6382        ops.set(1);
6383        let flags = root.rebuild(&mut app, &mut state);
6384        assert!(
6385            flags.needs_paint(),
6386            "the broadcast's `needs_redraw` folds into the rebuild's flags even \
6387             though the re-diff saw no view change"
6388        );
6389        assert!(
6390            root.has_pending_change_flags(),
6391            "PAINT reached `pending`, which is the input the mobile frame gate \
6392             reads to decide the next tick runs at all"
6393        );
6394
6395        let outcome = root.paint(&mut scene, FrameTime::ZERO);
6396        assert!(
6397            outcome.needs_frame,
6398            "the same wake surfaces as `needs_frame`, which is how the desktop \
6399             `ControlFlow::Wait` loop schedules a frame with no input pending"
6400        );
6401        assert!(
6402            !outcome.needs_frame_paced_only,
6403            "a flushed callback's repaint is not a cosmetic loop — the mobile \
6404             frame gate must not throttle it"
6405        );
6406        assert!(
6407            !crate::event::take_pending_result_flush(),
6408            "the mark was consumed; nothing is owed to a later frame"
6409        );
6410
6411        // And it settles: the next frame asks for nothing, so neither loop spins.
6412        let _ = root.take_change_flags();
6413        root.rebuild(&mut app, &mut state);
6414        let settled = root.paint(&mut scene, FrameTime::ZERO);
6415        assert!(
6416            !settled.needs_frame,
6417            "one wake, not a perpetual one — the flush is over"
6418        );
6419        assert!(!root.has_pending_change_flags());
6420    }
6421
6422    // --- Hover: the claim pipeline -------------------------------------------
6423    //
6424    // Hover has no Enter/Leave phase to lean on (adding one to `PointerPhase`
6425    // would break every out-of-tree exhaustive match). It is instead an opt-in
6426    // claim a widget makes from its uncaptured `Move` arm, recorded as an epoch
6427    // stamp down the pod chain — so the fixture below is deliberately shaped like
6428    // a real container: two hit-tested children, a capture fast-path, and paint
6429    // recording what `PaintCtx::is_hovered` reported.
6430
6431    /// Which of the fixture's two leaves an assertion is about.
6432    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
6433    enum Leaf {
6434        Top,
6435        Bottom,
6436    }
6437
6438    /// What one hover leaf observed, shared out of the widget tree.
6439    #[derive(Default)]
6440    struct HoverProbe {
6441        /// `PaintCtx::is_hovered()` as of the last paint.
6442        painted_hovered: Cell<bool>,
6443        /// `EventCtx::is_hovered()` as of the last event dispatch that reached it.
6444        event_hovered: Cell<bool>,
6445    }
6446
6447    /// A leaf that claims hover on any `Move` landing inside its own bounds — the
6448    /// canonical opt-in shape — and optionally captures the pointer on `Down` (the
6449    /// drag fixture: a captured pointer must never create hover).
6450    ///
6451    /// With `latches` set it follows the whole consumer contract: the same hit test
6452    /// updates an internal flag, `request_redraw` is gated on that flag changing,
6453    /// and `paint` self-corrects the flag from the authoritative
6454    /// `PaintCtx::is_hovered`. Clearing `latches` is a deliberate negative control —
6455    /// a claimant that keeps no flag — used to pin which frames the pipeline itself
6456    /// does and does not manufacture.
6457    struct HoverLeaf {
6458        captures: bool,
6459        latches: bool,
6460        hovered: bool,
6461        probe: Rc<HoverProbe>,
6462    }
6463
6464    impl crate::widget::Widget for HoverLeaf {
6465        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6466            bc.constrain(Size::new(100.0, 30.0))
6467        }
6468        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
6469            self.probe.painted_hovered.set(ctx.is_hovered());
6470            if self.latches {
6471                // The self-correction half of the contract: authoritative here,
6472                // whatever the event arm last recorded.
6473                self.hovered = ctx.is_hovered();
6474            }
6475        }
6476        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6477            self.probe.event_hovered.set(ctx.is_hovered());
6478            let InputEvent::Pointer(p) = event else {
6479                return EventResult::Ignored;
6480            };
6481            match p.phase {
6482                PointerPhase::Move => {
6483                    let size = ctx.size();
6484                    let inside = p.position.x >= 0.0
6485                        && p.position.y >= 0.0
6486                        && p.position.x < size.width
6487                        && p.position.y < size.height;
6488                    if inside {
6489                        ctx.claim_hover();
6490                    }
6491                    if self.latches && self.hovered != inside {
6492                        self.hovered = inside;
6493                        ctx.request_redraw();
6494                    }
6495                    // Deliberately `Ignored`: a hovering widget does not consume a
6496                    // move it merely watched (the shipped `ListItem` shape).
6497                    EventResult::Ignored
6498                }
6499                PointerPhase::Down => {
6500                    if self.captures {
6501                        ctx.capture_pointer();
6502                    }
6503                    EventResult::Handled
6504                }
6505                _ => EventResult::Ignored,
6506            }
6507        }
6508    }
6509
6510    /// Whether the container claims hover for itself, and when relative to routing
6511    /// the move into its child — the ordering the claim contract binds a container
6512    /// to.
6513    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
6514    enum GroupClaim {
6515        /// Never claims — the transparent container, hovered only via the path.
6516        Never,
6517        /// Claims *after* routing: the contract-following container, whose claim is
6518        /// a fallback the child's claim beats.
6519        AfterRouting,
6520        /// Claims *before* routing: the documented anti-pattern, kept as a
6521        /// negative control.
6522        BeforeRouting,
6523    }
6524
6525    /// A container wrapping one hover leaf, recording what its **own**
6526    /// `PaintCtx::is_hovered`/`EventCtx::is_hovered` reported — the
6527    /// ancestor-on-the-claim-path case, which the two sibling leaves alone cannot
6528    /// show. With `claims` set it also wants hover chrome of its own, claiming
6529    /// either side of the route to exercise the ordering rule.
6530    ///
6531    /// The child is an `Option` so a rebuild can *remove* it — the unmount case,
6532    /// where the claimant stops existing between hover passes.
6533    struct HoverGroup {
6534        probe: Rc<HoverProbe>,
6535        claims: GroupClaim,
6536        child: Option<crate::widget::ChildPod>,
6537    }
6538
6539    impl HoverGroup {
6540        /// Whether this event is an uncaptured-move-shaped pass landing inside the
6541        /// container's own bounds — the same local hit test a leaf claims on.
6542        fn claims_on(&self, ctx: &EventCtx, event: &InputEvent) -> bool {
6543            let InputEvent::Pointer(p) = event else {
6544                return false;
6545            };
6546            let size = ctx.size();
6547            matches!(p.phase, PointerPhase::Move)
6548                && p.position.x >= 0.0
6549                && p.position.y >= 0.0
6550                && p.position.x < size.width
6551                && p.position.y < size.height
6552        }
6553    }
6554
6555    impl crate::widget::Widget for HoverGroup {
6556        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6557            match &mut self.child {
6558                Some(child) => {
6559                    let size = child.layout_child(ctx, bc);
6560                    child.set_origin(Point::ZERO);
6561                    size
6562                }
6563                // The same box with nothing in it, so removing the claimant
6564                // changes what is under the pointer without moving the container.
6565                None => bc.constrain(Size::new(100.0, 30.0)),
6566            }
6567        }
6568        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
6569            self.probe.painted_hovered.set(ctx.is_hovered());
6570            if let Some(child) = &mut self.child {
6571                child.paint_child(ctx, scene);
6572            }
6573        }
6574        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6575            self.probe.event_hovered.set(ctx.is_hovered());
6576            let claims = self.claims != GroupClaim::Never && self.claims_on(ctx, event);
6577            if claims && self.claims == GroupClaim::BeforeRouting {
6578                ctx.claim_hover();
6579            }
6580            let result = match &mut self.child {
6581                Some(child) => child.event_child(ctx, event),
6582                None => EventResult::Ignored,
6583            };
6584            if claims && self.claims == GroupClaim::AfterRouting {
6585                ctx.claim_hover();
6586            }
6587            result
6588        }
6589    }
6590
6591    /// Two stacked hover leaves with a hit-tested route and a capture fast-path —
6592    /// the minimum container that can show a claim moving between siblings.
6593    struct HoverPair {
6594        top: crate::widget::ChildPod,
6595        bottom: crate::widget::ChildPod,
6596    }
6597
6598    impl crate::widget::Widget for HoverPair {
6599        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6600            self.top.layout_child(ctx, bc);
6601            self.top.set_origin(Point::ZERO);
6602            self.bottom.layout_child(ctx, bc);
6603            self.bottom.set_origin(Point::new(0.0, 30.0));
6604            bc.max()
6605        }
6606        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
6607            self.top.paint_child(ctx, scene);
6608            self.bottom.paint_child(ctx, scene);
6609        }
6610        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6611            let releases = matches!(
6612                event,
6613                InputEvent::Pointer(p)
6614                    if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
6615            );
6616            for pod in [&mut self.top, &mut self.bottom] {
6617                if pod.is_active() {
6618                    let r = pod.event_child(ctx, event);
6619                    if releases {
6620                        pod.set_active(false);
6621                    }
6622                    return r;
6623                }
6624            }
6625            let pos = event.position();
6626            for pod in [&mut self.top, &mut self.bottom] {
6627                if pod.contains(pos) {
6628                    return pod.event_child(ctx, event);
6629                }
6630            }
6631            EventResult::Ignored
6632        }
6633    }
6634
6635    /// How one `HoverHarness` is shaped.
6636    #[derive(Clone, Copy)]
6637    struct HoverFixture {
6638        /// Leaves capture the pointer on `Down` (the drag case).
6639        captures: bool,
6640        /// Leaves follow the consumer contract (latched flag + change-gated redraw
6641        /// + paint-time self-correction).
6642        latches: bool,
6643        /// Wrap the top leaf in a [`HoverGroup`], so the claim path has an
6644        /// ancestor pod between the claimant and the root.
6645        nested: bool,
6646        /// Whether (and when) that container claims hover for itself.
6647        group_claims: GroupClaim,
6648        /// Rebuild the container without its child: the unmount case, where the
6649        /// pod holding the hover link is dropped by the view diff.
6650        drop_claimant: bool,
6651        /// Report [`ChangeFlags::NONE`] from that removal — the hand-rolled
6652        /// container outside this workspace, which drops a pod while reporting
6653        /// whatever it likes. The in-tree reconcilers report `LAYOUT | PAINT`,
6654        /// which is what the release used to lean on instead of flagging its own.
6655        silent_reconciler: bool,
6656    }
6657
6658    struct HoverPairView {
6659        top: Rc<HoverProbe>,
6660        bottom: Rc<HoverProbe>,
6661        group: Rc<HoverProbe>,
6662        fixture: HoverFixture,
6663    }
6664
6665    impl HoverPairView {
6666        fn leaf(&self, probe: &Rc<HoverProbe>) -> Box<dyn crate::widget::Widget> {
6667            Box::new(HoverLeaf {
6668                captures: self.fixture.captures,
6669                latches: self.fixture.latches,
6670                hovered: false,
6671                probe: probe.clone(),
6672            })
6673        }
6674    }
6675
6676    impl View<()> for HoverPairView {
6677        type Element = HoverPair;
6678        fn build(&self, _ctx: &mut BuildCtx<'_>) -> HoverPair {
6679            let top: Box<dyn crate::widget::Widget> = if self.fixture.nested {
6680                Box::new(HoverGroup {
6681                    probe: self.group.clone(),
6682                    claims: self.fixture.group_claims,
6683                    child: Some(crate::widget::ChildPod::new(self.leaf(&self.top))),
6684                })
6685            } else {
6686                self.leaf(&self.top)
6687            };
6688            HoverPair {
6689                top: crate::widget::ChildPod::new(top),
6690                bottom: crate::widget::ChildPod::new(self.leaf(&self.bottom)),
6691            }
6692        }
6693        fn rebuild(&self, p: &Self, e: &mut HoverPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
6694            // The only structural op this fixture performs: drop the nested
6695            // container's child pod, the way a real reconciler drops a truncated
6696            // or conditionally-removed child.
6697            let removes_child = self.fixture.drop_claimant && !p.fixture.drop_claimant;
6698            if !removes_child {
6699                return ChangeFlags::NONE;
6700            }
6701            let group = e
6702                .top
6703                .widget_mut()
6704                .downcast_mut::<HoverGroup>()
6705                .expect("the drop-claimant fixture is the nested one");
6706            group.child = None;
6707            if self.fixture.silent_reconciler {
6708                ChangeFlags::NONE
6709            } else {
6710                ChangeFlags::LAYOUT | ChangeFlags::PAINT
6711            }
6712        }
6713    }
6714
6715    /// A `RenderRoot` over the hover fixture, plus its probes.
6716    struct HoverHarness {
6717        root: RenderRoot<(), HoverPairView>,
6718        top: Rc<HoverProbe>,
6719        bottom: Rc<HoverProbe>,
6720        group: Rc<HoverProbe>,
6721        state: (),
6722        /// The shape the next rebuild re-states, so
6723        /// [`HoverHarness::rebuild_without_claimant`] can flip one flag without
6724        /// restating the rest.
6725        fixture: HoverFixture,
6726    }
6727
6728    impl HoverHarness {
6729        /// The contract-following fixture: two flat leaves that latch their own
6730        /// hover flag, optionally capturing on `Down`.
6731        fn new(captures: bool) -> Self {
6732            Self::build(HoverFixture {
6733                captures,
6734                latches: true,
6735                nested: false,
6736                group_claims: GroupClaim::Never,
6737                drop_claimant: false,
6738                silent_reconciler: false,
6739            })
6740        }
6741
6742        /// The negative control: leaves that claim hover but keep no flag of their
6743        /// own, so only frames the pipeline manufactures show up.
6744        fn without_consumer_flag() -> Self {
6745            Self::build(HoverFixture {
6746                captures: false,
6747                latches: false,
6748                nested: false,
6749                group_claims: GroupClaim::Never,
6750                drop_claimant: false,
6751                silent_reconciler: false,
6752            })
6753        }
6754
6755        /// The top leaf wrapped in a container pod, for the path-semantics case.
6756        fn nested() -> Self {
6757            Self::build(HoverFixture {
6758                captures: false,
6759                latches: true,
6760                nested: true,
6761                group_claims: GroupClaim::Never,
6762                drop_claimant: false,
6763                silent_reconciler: false,
6764            })
6765        }
6766
6767        /// The same nesting, with the container claiming hover for itself the way
6768        /// the contract requires: after routing the move into its child.
6769        fn nested_group_claiming(claims: GroupClaim) -> Self {
6770            Self::build(HoverFixture {
6771                captures: false,
6772                latches: true,
6773                nested: true,
6774                group_claims: claims,
6775                drop_claimant: false,
6776                silent_reconciler: false,
6777            })
6778        }
6779
6780        /// The same nesting, with a container that drops its child while
6781        /// reporting no flags — the hand-rolled container outside this workspace
6782        /// the destructor route exists to cover.
6783        fn nested_group_with_silent_reconciler() -> Self {
6784            Self::build(HoverFixture {
6785                captures: false,
6786                latches: true,
6787                nested: true,
6788                group_claims: GroupClaim::AfterRouting,
6789                drop_claimant: false,
6790                silent_reconciler: true,
6791            })
6792        }
6793
6794        fn build(fixture: HoverFixture) -> Self {
6795            let top = Rc::new(HoverProbe::default());
6796            let bottom = Rc::new(HoverProbe::default());
6797            let group = Rc::new(HoverProbe::default());
6798            let mut root: RenderRoot<(), HoverPairView> = RenderRoot::new();
6799            let mut state = ();
6800            let (t, b, g) = (top.clone(), bottom.clone(), group.clone());
6801            root.rebuild(
6802                &mut move |_: &mut ()| HoverPairView {
6803                    top: t.clone(),
6804                    bottom: b.clone(),
6805                    group: g.clone(),
6806                    fixture,
6807                },
6808                &mut state,
6809            );
6810            root.layout(Size::new(100.0, 60.0));
6811            HoverHarness {
6812                root,
6813                top,
6814                bottom,
6815                group,
6816                state,
6817                fixture,
6818            }
6819        }
6820
6821        /// Rebuild with the nested container's child removed — the claimant
6822        /// unmounting between hover passes — and re-lay out, returning what the
6823        /// diff reported.
6824        fn rebuild_without_claimant(&mut self) -> ChangeFlags {
6825            self.fixture.drop_claimant = true;
6826            self.rebuild_current()
6827        }
6828
6829        /// Rebuild restating the shape already on screen: nothing of this root's
6830        /// own is severed, so any hover end it performs came from elsewhere.
6831        fn rebuild_unchanged(&mut self) -> ChangeFlags {
6832            self.rebuild_current()
6833        }
6834
6835        /// Re-run the diff against the fixture as it currently stands, then
6836        /// re-lay out, returning what the diff reported.
6837        fn rebuild_current(&mut self) -> ChangeFlags {
6838            let fixture = self.fixture;
6839            let (t, b, g) = (self.top.clone(), self.bottom.clone(), self.group.clone());
6840            let flags = self.root.rebuild(
6841                &mut move |_: &mut ()| HoverPairView {
6842                    top: t.clone(),
6843                    bottom: b.clone(),
6844                    group: g.clone(),
6845                    fixture,
6846                },
6847                &mut self.state,
6848            );
6849            self.root.layout(Size::new(100.0, 60.0));
6850            flags
6851        }
6852
6853        /// Dispatch a pointer event at `(x, y)` in window space.
6854        fn dispatch(&mut self, phase: PointerPhase, x: f64, y: f64) -> EventOutcome {
6855            let event = InputEvent::Pointer(PointerEvent {
6856                phase,
6857                position: Point::new(x, y),
6858                button: PointerButton::Primary,
6859            });
6860            self.root.event(&mut self.state, &event)
6861        }
6862
6863        /// Move the pointer over the given leaf's middle.
6864        fn move_over(&mut self, leaf: Leaf) -> EventOutcome {
6865            match leaf {
6866                Leaf::Top => self.dispatch(PointerPhase::Move, 50.0, 15.0),
6867                Leaf::Bottom => self.dispatch(PointerPhase::Move, 50.0, 45.0),
6868            }
6869        }
6870
6871        /// Paint the tree, refreshing both probes' recorded hover state.
6872        fn paint(&mut self) {
6873            let mut scene = RecordingScene::default();
6874            self.root.paint(&mut scene, FrameTime::ZERO);
6875        }
6876
6877        /// `(top, bottom)` hover as the last paint reported it.
6878        fn painted(&mut self) -> (bool, bool) {
6879            self.paint();
6880            (
6881                self.top.painted_hovered.get(),
6882                self.bottom.painted_hovered.get(),
6883            )
6884        }
6885    }
6886
6887    #[test]
6888    fn an_uncaptured_move_claims_hover_and_paint_reports_it() {
6889        let mut h = HoverHarness::new(false);
6890        assert!(!h.root.is_hover_active(), "nothing is hovered at rest");
6891        assert_eq!(h.painted(), (false, false));
6892
6893        let outcome = h.move_over(Leaf::Top);
6894        assert!(h.root.is_hover_active(), "the claim reached the root");
6895        assert!(
6896            !outcome.handled,
6897            "a hovering widget need not consume the move"
6898        );
6899        assert_eq!(
6900            h.painted(),
6901            (true, false),
6902            "the claimant reads as hovered, its sibling does not"
6903        );
6904
6905        // A second move within the same leaf keeps the link (the claim is
6906        // re-recorded every pass) without re-reporting a change.
6907        h.dispatch(PointerPhase::Move, 60.0, 20.0);
6908        assert_eq!(h.painted(), (true, false));
6909    }
6910
6911    #[test]
6912    fn a_second_widgets_claim_clears_the_first_and_asks_for_a_repaint() {
6913        let mut h = HoverHarness::new(false);
6914        h.move_over(Leaf::Top);
6915        assert_eq!(h.painted(), (true, false));
6916
6917        // The pointer moves onto the sibling. The container never has to clear
6918        // anything: the epoch advance strands the top pod's stamp.
6919        h.move_over(Leaf::Bottom);
6920        assert!(h.root.is_hover_active());
6921        assert_eq!(
6922            h.painted(),
6923            (false, true),
6924            "the previous claimant lost its link when the new one recorded"
6925        );
6926
6927        // Both widgets need a repaint, and a repaint is global — one request
6928        // covers them. The *losing* side is what the root itself must guarantee:
6929        // moving onto a leaf that claims nothing still repaints.
6930        let outcome = h.dispatch(PointerPhase::Move, 50.0, 200.0);
6931        assert!(
6932            !h.root.is_hover_active(),
6933            "a move claiming nothing ends the hover"
6934        );
6935        assert!(
6936            outcome.needs_redraw,
6937            "the widget that lost hover cannot ask for the repaint itself"
6938        );
6939        assert_eq!(h.painted(), (false, false));
6940
6941        // ...and the same move repeated is not a change any more.
6942        let settled = h.dispatch(PointerPhase::Move, 50.0, 200.0);
6943        assert!(
6944            !settled.needs_redraw,
6945            "an already-hoverless move requests nothing"
6946        );
6947    }
6948
6949    #[test]
6950    fn a_captured_move_cannot_claim_hover() {
6951        let mut h = HoverHarness::new(true);
6952        // Press the top leaf: it captures, and the `Down` itself ends any hover.
6953        h.dispatch(PointerPhase::Down, 50.0, 15.0);
6954        assert!(h.root.is_pointer_captured());
6955        assert!(!h.root.is_hover_active());
6956
6957        // Drag: every one of these moves routes to the captured leaf, whose `Move`
6958        // arm hit-tests inside and calls `claim_hover()` — and must record nothing.
6959        h.dispatch(PointerPhase::Move, 50.0, 16.0);
6960        assert!(
6961            !h.root.is_hover_active(),
6962            "a captured pointer never creates hover"
6963        );
6964        assert_eq!(h.painted(), (false, false));
6965
6966        // Dragging outside the leaf keeps routing to it (capture), still no hover.
6967        h.dispatch(PointerPhase::Move, 50.0, 45.0);
6968        assert!(!h.root.is_hover_active());
6969        assert_eq!(h.painted(), (false, false));
6970
6971        // Release, then a fresh uncaptured move: hover is claimable again.
6972        h.dispatch(PointerPhase::Up, 50.0, 15.0);
6973        h.move_over(Leaf::Top);
6974        assert!(h.root.is_hover_active());
6975        assert_eq!(h.painted(), (true, false));
6976    }
6977
6978    #[test]
6979    fn a_down_up_or_cancel_ends_the_hover() {
6980        // Every pointer phase other than an uncaptured `Move` ends the link. `Up`
6981        // is in here for touch: a lifted finger sends no further move, so a tint
6982        // claimed during an uncaptured touch drag would otherwise stand for good.
6983        for ending in [PointerPhase::Down, PointerPhase::Up, PointerPhase::Cancel] {
6984            let mut h = HoverHarness::new(false);
6985            h.move_over(Leaf::Top);
6986            assert!(h.root.is_hover_active());
6987
6988            let outcome = h.dispatch(ending, 50.0, 15.0);
6989            assert!(
6990                !h.root.is_hover_active(),
6991                "{ending:?} ends the hover link outright"
6992            );
6993            assert!(
6994                outcome.needs_redraw,
6995                "{ending:?} that dropped a hover asks for the repaint"
6996            );
6997            assert_eq!(h.painted(), (false, false));
6998        }
6999    }
7000
7001    #[test]
7002    fn a_non_pointer_pass_leaves_a_live_hover_standing() {
7003        let mut h = HoverHarness::new(false);
7004        h.move_over(Leaf::Top);
7005        assert_eq!(h.painted(), (true, false));
7006
7007        // Neither a scroll, a key, nor the housekeeping broadcast is a hover pass:
7008        // the pointer has not moved, so the link must survive them untouched.
7009        h.root.event(
7010            &mut h.state,
7011            &InputEvent::Scroll {
7012                position: Point::new(50.0, 15.0),
7013                delta: crate::event::ScrollDelta::Lines(0.0, 1.0),
7014            },
7015        );
7016        assert!(h.root.is_hover_active());
7017        h.root.event(&mut h.state, &InputEvent::Housekeeping);
7018        assert!(h.root.is_hover_active());
7019        assert_eq!(h.painted(), (true, false));
7020    }
7021
7022    #[test]
7023    fn a_container_on_the_claim_path_reads_hovered_and_a_sibling_does_not() {
7024        // The recorded thing is a path, so hover is `:hover`-shaped: the claimant
7025        // and every ancestor enclosing it read hovered, nothing off the path does.
7026        let mut h = HoverHarness::nested();
7027        h.move_over(Leaf::Top);
7028        h.paint();
7029        assert!(h.top.painted_hovered.get(), "the claimant itself");
7030        assert!(
7031            h.group.painted_hovered.get(),
7032            "the container enclosing the claimant is on the path too"
7033        );
7034        assert!(
7035            !h.bottom.painted_hovered.get(),
7036            "a sibling leaf is off the path"
7037        );
7038
7039        // Move onto the sibling: the container goes unhovered with its child, and
7040        // both reads agree about it on the next pass.
7041        h.move_over(Leaf::Bottom);
7042        h.paint();
7043        assert!(!h.group.painted_hovered.get());
7044        assert!(!h.top.painted_hovered.get());
7045        assert!(h.bottom.painted_hovered.get());
7046        h.move_over(Leaf::Top);
7047        assert!(
7048            !h.group.event_hovered.get(),
7049            "the event read reports the previous pass, like the leaf's"
7050        );
7051        h.move_over(Leaf::Top);
7052        assert!(
7053            h.group.event_hovered.get(),
7054            "the container observes the link its child holds"
7055        );
7056    }
7057
7058    #[test]
7059    fn an_ancestor_claiming_after_routing_loses_to_its_child_and_still_reads_hovered() {
7060        // A container that wants hover chrome of its own claims after routing the
7061        // move into its child. Only one claim per pass is recorded and the first one
7062        // recorded wins, so the child's claim is the one that lands; the container's
7063        // own late call is a silent no-op, and it reads hovered through the stamped
7064        // path anyway — which is what makes this ordering correct in every case.
7065        let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
7066        let gain = h.move_over(Leaf::Top);
7067        h.paint();
7068        assert!(
7069            h.top.painted_hovered.get(),
7070            "the child under the pointer holds the link"
7071        );
7072        assert!(
7073            h.group.painted_hovered.get(),
7074            "the container is on that path, so it reads hovered too"
7075        );
7076        assert!(
7077            !h.bottom.painted_hovered.get(),
7078            "a sibling leaf is off the path"
7079        );
7080        assert!(gain.needs_redraw, "hover gain repaints");
7081
7082        // The child's latched flag now agrees with the authoritative paint read, so
7083        // wandering on within the same widget settles instead of repainting.
7084        let settled = h.dispatch(PointerPhase::Move, 60.0, 20.0);
7085        assert!(!settled.needs_redraw, "an unchanged flag asks for nothing");
7086        h.paint();
7087        assert!(h.top.painted_hovered.get());
7088        assert!(h.group.painted_hovered.get());
7089    }
7090
7091    #[test]
7092    fn an_ancestor_claiming_before_routing_starves_its_subtree() {
7093        // The negative control for the ordering rule above, pinning the trap it
7094        // exists to prevent: a container that claims *before* forwarding is recorded
7095        // first, which closes the pass to every descendant. The child under the
7096        // pointer can never read hovered, so its hover chrome never appears — and
7097        // because its latched flag is corrected back to `false` at paint time, it
7098        // flips and asks for a frame again on every single move.
7099        let mut h = HoverHarness::nested_group_claiming(GroupClaim::BeforeRouting);
7100        h.move_over(Leaf::Top);
7101        h.paint();
7102        assert!(
7103            !h.top.painted_hovered.get(),
7104            "the ancestor's earlier claim made its child ineligible"
7105        );
7106        assert!(
7107            h.group.painted_hovered.get(),
7108            "the outermost claimant is the one holding the link here"
7109        );
7110        assert!(!h.bottom.painted_hovered.get());
7111
7112        // Repaint-per-move: the flag never converges, because the event arm and the
7113        // authoritative paint read permanently disagree.
7114        let again = h.dispatch(PointerPhase::Move, 60.0, 20.0);
7115        assert!(
7116            again.needs_redraw,
7117            "the starved child re-flips its flag on every move"
7118        );
7119        h.paint();
7120        assert!(!h.top.painted_hovered.get(), "and still paints no chrome");
7121    }
7122
7123    #[test]
7124    fn hover_gain_is_repainted_by_the_consumers_own_flag() {
7125        let mut h = HoverHarness::new(false);
7126        // Entering a widget: the root manufactures nothing here (its mirror went
7127        // `false` → `true`, and it cannot know which widget cares), so the frame
7128        // comes from the claimant's own change-gated request.
7129        let gain = h.move_over(Leaf::Top);
7130        assert!(gain.needs_redraw, "hover gain repaints");
7131        assert_eq!(h.painted(), (true, false));
7132
7133        // Wandering within the same widget claims again but changes nothing, so it
7134        // must not repaint per event.
7135        let settled = h.dispatch(PointerPhase::Move, 60.0, 20.0);
7136        assert!(!settled.needs_redraw, "an unchanged flag asks for nothing");
7137
7138        // A handoff repaints both sides at once: the arriving leaf's flag changed
7139        // (it asks), and because a repaint is global that same frame is what lets
7140        // the departing leaf drop its chrome from the authoritative paint read.
7141        let handoff = h.move_over(Leaf::Bottom);
7142        assert!(handoff.needs_redraw, "a claimant handoff repaints");
7143        assert_eq!(
7144            h.painted(),
7145            (false, true),
7146            "one frame settles both the loss and the gain"
7147        );
7148    }
7149
7150    #[test]
7151    fn a_claimant_without_its_own_flag_gets_only_the_loss_frame() {
7152        // The negative control for the contract above: leaves that claim hover but
7153        // keep no flag of their own. Gain and handoff are invisible to the root
7154        // (its hover mirror is identity-free — `false` → `true` and `true` →
7155        // `true`), so nothing repaints for them, which is exactly why the
7156        // consumer's latched flag is normative rather than an optimization.
7157        let mut h = HoverHarness::without_consumer_flag();
7158        let gain = h.move_over(Leaf::Top);
7159        assert!(h.root.is_hover_active());
7160        assert!(!gain.needs_redraw, "no widget asked, and the root cannot");
7161
7162        let handoff = h.move_over(Leaf::Bottom);
7163        assert!(h.root.is_hover_active());
7164        assert!(!handoff.needs_redraw, "a handoff is `true` → `true` here");
7165
7166        // Loss is the one edge the root does cover, since no widget can see it.
7167        let loss = h.dispatch(PointerPhase::Move, 50.0, 200.0);
7168        assert!(!h.root.is_hover_active());
7169        assert!(loss.needs_redraw, "the root manufactures the loss frame");
7170    }
7171
7172    #[test]
7173    fn a_rebuild_that_removes_the_claimant_ends_the_hover() {
7174        // The one severance the epoch cannot strand: the claimant is dropped by a
7175        // view diff, so it will never see the `Move` that would have re-derived
7176        // the link. Its pod reports the drop and `rebuild` ends the hover.
7177        let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
7178        h.move_over(Leaf::Top);
7179        h.paint();
7180        assert!(h.root.is_hover_active());
7181        assert!(h.top.painted_hovered.get(), "the claimant holds the link");
7182        assert!(
7183            h.group.painted_hovered.get(),
7184            "its container is on the path"
7185        );
7186
7187        let flags = h.rebuild_without_claimant();
7188        assert!(
7189            !h.root.is_hover_active(),
7190            "the mirror cannot outlive the widget it described"
7191        );
7192        assert!(
7193            flags.contains(ChangeFlags::PAINT),
7194            "the correction states its own need for a frame, whatever the \
7195             reconciler that dropped the claimant reported"
7196        );
7197
7198        // The survivor is the ancestor that was on the claim path: its own stamp
7199        // still names the epoch the claim recorded, so nothing but the epoch
7200        // advance keeps it from painting hover chrome for a child that is gone.
7201        h.paint();
7202        assert!(
7203            !h.group.painted_hovered.get(),
7204            "the surviving ancestor lost the link with its child"
7205        );
7206        assert!(
7207            !h.bottom.painted_hovered.get(),
7208            "and the sibling never had it"
7209        );
7210
7211        // And the pipeline is not wedged: the next move over the same spot claims
7212        // cleanly, now for the container itself. (No frame is manufactured for
7213        // that gain — this container keeps no latched flag of its own, which is
7214        // the documented consumer-side half of the contract, not a pipeline job.)
7215        h.move_over(Leaf::Top);
7216        assert!(h.root.is_hover_active(), "the next move re-claims");
7217        h.paint();
7218        assert!(
7219            h.group.painted_hovered.get(),
7220            "the container is the claimant now"
7221        );
7222    }
7223
7224    #[test]
7225    fn a_silent_reconcilers_removal_still_carries_its_own_repaint() {
7226        // The reason the release flags `PAINT` itself rather than trusting the
7227        // diff to have reported one. The destructor route deliberately reaches
7228        // containers this workspace never sees, and such a container can drop the
7229        // claimant while reporting nothing — leaving the hover correctly ended but
7230        // the frame that shows it unrequested, on a pointer the user has stopped
7231        // moving.
7232        let mut h = HoverHarness::nested_group_with_silent_reconciler();
7233        h.move_over(Leaf::Top);
7234        assert!(h.root.is_hover_active());
7235
7236        let flags = h.rebuild_without_claimant();
7237        assert!(!h.root.is_hover_active(), "the link ends either way");
7238        assert_eq!(
7239            flags,
7240            ChangeFlags::PAINT,
7241            "and the frame it needs comes from the release, not from the diff"
7242        );
7243    }
7244
7245    #[test]
7246    fn a_rebuild_that_keeps_the_claimant_leaves_the_hover_standing() {
7247        // The negative control for the release above, and the reason the mark is
7248        // gated on the *live* epoch rather than on "some pod with a stamp died":
7249        // an ordinary rebuild — including one that drops pods carrying stale
7250        // stamps — must not touch a link the pointer still rests on.
7251        let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
7252        // Hover the top leaf, then hand the link to the sibling. The top pod chain
7253        // keeps its (now stale) stamp, which is what the next rebuild drops.
7254        h.move_over(Leaf::Top);
7255        h.move_over(Leaf::Bottom);
7256        assert!(h.root.is_hover_active());
7257
7258        h.rebuild_without_claimant();
7259        assert!(
7260            h.root.is_hover_active(),
7261            "dropping a stale stamp is not a severance"
7262        );
7263        h.paint();
7264        assert!(
7265            h.bottom.painted_hovered.get(),
7266            "the widget actually under the pointer keeps its chrome"
7267        );
7268    }
7269
7270    #[test]
7271    fn another_roots_dying_claimant_cannot_end_this_roots_hover() {
7272        // Two roots on one thread. Each has run exactly one hover pass, so their
7273        // epoch counters hold the identical integer — the collision the root
7274        // identity exists to break. Without it, the first root's pods dying (its
7275        // window closing, a page tearing down) raise a mark the second root's
7276        // next rebuild drains, ending a hover the pointer is still resting on.
7277        let mut first = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
7278        let mut second = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
7279        first.move_over(Leaf::Top);
7280        second.move_over(Leaf::Top);
7281        assert_eq!(
7282            first.root.hover_epoch, second.root.hover_epoch,
7283            "the two roots' epochs collide, which is the whole premise"
7284        );
7285        assert!(first.root.is_hover_active());
7286        assert!(second.root.is_hover_active());
7287
7288        // Drop the first root outright: every pod it owns runs the destructor
7289        // that reports a severed hover link, including the claimant's.
7290        drop(first);
7291
7292        second.rebuild_unchanged();
7293        assert!(
7294            second.root.is_hover_active(),
7295            "a mark another root raised is none of this root's business"
7296        );
7297        second.paint();
7298        assert!(
7299            second.top.painted_hovered.get(),
7300            "the widget under the pointer keeps its chrome"
7301        );
7302    }
7303
7304    #[test]
7305    fn event_ctx_hover_reports_the_previous_pass_not_this_ones_claim() {
7306        let mut h = HoverHarness::new(false);
7307        // First move over the top leaf: it was not hovered when its handler ran.
7308        h.move_over(Leaf::Top);
7309        assert!(
7310            !h.top.event_hovered.get(),
7311            "a fresh claim does not retroactively flip `is_hovered`"
7312        );
7313        // Second move over the same leaf: now it observes the link it holds.
7314        h.move_over(Leaf::Top);
7315        assert!(
7316            h.top.event_hovered.get(),
7317            "the link recorded last pass is visible to this pass's handler"
7318        );
7319    }
7320
7321    // --- Cursor: the per-pass request channel ---------------------------------
7322    //
7323    // The cursor is hover's sibling and is deliberately not derived from it (the
7324    // root's hover mirror is identity-free), so it gets its own fixture: two
7325    // leaves that ask for different shapes, a container that can speak before or
7326    // after routing (the last-writer case), and a capture fast-path (the drag
7327    // case, where the shape must survive the pointer leaving the widget).
7328
7329    /// How the cursor fixture's container speaks relative to its children.
7330    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7331    enum ContainerCursor {
7332        /// Says nothing at all — the ordinary container.
7333        Silent,
7334        /// Asks *before* routing, so whatever the child asks for comes later.
7335        BeforeRouting(CursorIcon),
7336        /// Asks *after* routing, deliberately overriding its child.
7337        AfterRouting(CursorIcon),
7338    }
7339
7340    /// A leaf that asks for one cursor while the pointer is over it and another
7341    /// while it holds the capture — the two shapes a real draggable control wants.
7342    struct CursorLeaf {
7343        hover_icon: Option<CursorIcon>,
7344        drag_icon: Option<CursorIcon>,
7345        captures: bool,
7346        captured: bool,
7347    }
7348
7349    impl crate::widget::Widget for CursorLeaf {
7350        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7351            bc.constrain(Size::new(100.0, 30.0))
7352        }
7353        // A cursor is resolved entirely in the event pass — this fixture never
7354        // paints, unlike the hover one (whose authoritative read is at paint time).
7355        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7356        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7357            let InputEvent::Pointer(p) = event else {
7358                return EventResult::Ignored;
7359            };
7360            match p.phase {
7361                PointerPhase::Move => {
7362                    if self.captured {
7363                        // The captured pass belongs to this widget wherever the
7364                        // pointer has gone — re-asking here is what keeps the drag
7365                        // shape alive outside its own bounds.
7366                        if let Some(icon) = self.drag_icon {
7367                            ctx.set_cursor(icon);
7368                        }
7369                        return EventResult::Handled;
7370                    }
7371                    let size = ctx.size();
7372                    let inside = p.position.x >= 0.0
7373                        && p.position.y >= 0.0
7374                        && p.position.x < size.width
7375                        && p.position.y < size.height;
7376                    if inside && let Some(icon) = self.hover_icon {
7377                        ctx.set_cursor(icon);
7378                    }
7379                    EventResult::Ignored
7380                }
7381                PointerPhase::Down => {
7382                    if self.captures {
7383                        ctx.capture_pointer();
7384                        self.captured = true;
7385                    }
7386                    EventResult::Handled
7387                }
7388                PointerPhase::Up | PointerPhase::Cancel => {
7389                    self.captured = false;
7390                    EventResult::Ignored
7391                }
7392            }
7393        }
7394    }
7395
7396    /// Two stacked cursor leaves routed exactly like [`HoverPair`], plus the
7397    /// container's own optional request.
7398    struct CursorPair {
7399        top: crate::widget::ChildPod,
7400        bottom: crate::widget::ChildPod,
7401        own: ContainerCursor,
7402    }
7403
7404    impl crate::widget::Widget for CursorPair {
7405        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7406            self.top.layout_child(ctx, bc);
7407            self.top.set_origin(Point::ZERO);
7408            self.bottom.layout_child(ctx, bc);
7409            self.bottom.set_origin(Point::new(0.0, 30.0));
7410            bc.max()
7411        }
7412        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7413        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7414            if let ContainerCursor::BeforeRouting(icon) = self.own {
7415                ctx.set_cursor(icon);
7416            }
7417            let result = self.route(ctx, event);
7418            if let ContainerCursor::AfterRouting(icon) = self.own {
7419                ctx.set_cursor(icon);
7420            }
7421            result
7422        }
7423    }
7424
7425    impl CursorPair {
7426        /// The capture-first, then hit-test routing every real container does.
7427        fn route(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7428            let releases = matches!(
7429                event,
7430                InputEvent::Pointer(p)
7431                    if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
7432            );
7433            for pod in [&mut self.top, &mut self.bottom] {
7434                if pod.is_active() {
7435                    let r = pod.event_child(ctx, event);
7436                    if releases {
7437                        pod.set_active(false);
7438                    }
7439                    return r;
7440                }
7441            }
7442            let pos = event.position();
7443            for pod in [&mut self.top, &mut self.bottom] {
7444                if pod.contains(pos) {
7445                    return pod.event_child(ctx, event);
7446                }
7447            }
7448            EventResult::Ignored
7449        }
7450    }
7451
7452    #[derive(Clone, Copy)]
7453    struct CursorPairView {
7454        own: ContainerCursor,
7455        captures: bool,
7456    }
7457
7458    impl View<()> for CursorPairView {
7459        type Element = CursorPair;
7460        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CursorPair {
7461            CursorPair {
7462                top: crate::widget::ChildPod::new(Box::new(CursorLeaf {
7463                    hover_icon: Some(CursorIcon::Pointer),
7464                    drag_icon: Some(CursorIcon::Grabbing),
7465                    captures: self.captures,
7466                    captured: false,
7467                })),
7468                bottom: crate::widget::ChildPod::new(Box::new(CursorLeaf {
7469                    hover_icon: Some(CursorIcon::Text),
7470                    drag_icon: None,
7471                    captures: false,
7472                    captured: false,
7473                })),
7474                own: self.own,
7475            }
7476        }
7477        fn rebuild(&self, _p: &Self, _e: &mut CursorPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
7478            ChangeFlags::NONE
7479        }
7480    }
7481
7482    /// A `RenderRoot` over the cursor fixture.
7483    struct CursorHarness {
7484        root: RenderRoot<(), CursorPairView>,
7485        state: (),
7486    }
7487
7488    impl CursorHarness {
7489        fn new(own: ContainerCursor, captures: bool) -> Self {
7490            let mut root: RenderRoot<(), CursorPairView> = RenderRoot::new();
7491            let mut state = ();
7492            root.rebuild(
7493                &mut move |_: &mut ()| CursorPairView { own, captures },
7494                &mut state,
7495            );
7496            root.layout(Size::new(100.0, 60.0));
7497            CursorHarness { root, state }
7498        }
7499
7500        fn dispatch(&mut self, phase: PointerPhase, x: f64, y: f64) -> EventOutcome {
7501            let event = InputEvent::Pointer(PointerEvent {
7502                phase,
7503                position: Point::new(x, y),
7504                button: PointerButton::Primary,
7505            });
7506            self.root.event(&mut self.state, &event)
7507        }
7508
7509        fn move_over(&mut self, leaf: Leaf) {
7510            match leaf {
7511                Leaf::Top => self.dispatch(PointerPhase::Move, 50.0, 15.0),
7512                Leaf::Bottom => self.dispatch(PointerPhase::Move, 50.0, 45.0),
7513            };
7514        }
7515
7516        fn cursor(&self) -> CursorIcon {
7517            self.root.cursor()
7518        }
7519    }
7520
7521    #[test]
7522    fn a_move_resolves_the_requested_cursor_and_absence_resolves_default() {
7523        let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7524        assert_eq!(
7525            h.cursor(),
7526            CursorIcon::Default,
7527            "nothing has asked for anything yet"
7528        );
7529
7530        h.move_over(Leaf::Top);
7531        assert_eq!(
7532            h.cursor(),
7533            CursorIcon::Pointer,
7534            "the request reached the root"
7535        );
7536
7537        // Moving onto the sibling re-resolves to *its* shape with nothing cleared:
7538        // the pass simply has a different last writer.
7539        h.move_over(Leaf::Bottom);
7540        assert_eq!(h.cursor(), CursorIcon::Text);
7541
7542        // Moving off both: the next pass has no writer at all, and absence is the
7543        // default rather than a stale value — the whole point of a stateless
7544        // request.
7545        h.dispatch(PointerPhase::Move, 50.0, 200.0);
7546        assert_eq!(
7547            h.cursor(),
7548            CursorIcon::Default,
7549            "a widget that stops asking falls back with nothing to clear"
7550        );
7551    }
7552
7553    #[test]
7554    fn a_cursor_change_does_not_ask_for_a_repaint() {
7555        // Applying a cursor is a platform call the shell makes after the pass, with
7556        // no frame involved; folding it into `needs_redraw` would repaint the whole
7557        // tree on every hover move.
7558        let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7559        let outcome = h.dispatch(PointerPhase::Move, 50.0, 15.0);
7560        assert_eq!(h.cursor(), CursorIcon::Pointer);
7561        assert!(
7562            !outcome.needs_redraw,
7563            "a cursor request alone never schedules a frame"
7564        );
7565    }
7566
7567    #[test]
7568    fn the_last_writer_on_the_routed_path_wins() {
7569        // A container that asks before routing loses to its child: the child's
7570        // handler runs later in the same pass, which is what makes a specific
7571        // control override the generic surface behind it.
7572        let mut h =
7573            CursorHarness::new(ContainerCursor::BeforeRouting(CursorIcon::ColResize), false);
7574        h.move_over(Leaf::Top);
7575        assert_eq!(
7576            h.cursor(),
7577            CursorIcon::Pointer,
7578            "the innermost widget the route reached spoke last"
7579        );
7580
7581        // ...and the container's own request still resolves where no child asks
7582        // (moving off both leaves leaves the container as the only writer).
7583        h.dispatch(PointerPhase::Move, 50.0, 200.0);
7584        assert_eq!(h.cursor(), CursorIcon::ColResize);
7585
7586        // The deliberate override is the mirror case: asking *after* routing beats
7587        // the child.
7588        let mut h =
7589            CursorHarness::new(ContainerCursor::AfterRouting(CursorIcon::NotAllowed), false);
7590        h.move_over(Leaf::Top);
7591        assert_eq!(
7592            h.cursor(),
7593            CursorIcon::NotAllowed,
7594            "a container overriding its children asks after routing"
7595        );
7596    }
7597
7598    #[test]
7599    fn only_a_pointer_move_re_resolves_the_cursor() {
7600        let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7601        h.move_over(Leaf::Top);
7602        assert_eq!(h.cursor(), CursorIcon::Pointer);
7603
7604        // A press/release says nothing about the cursor, and must not blink it back
7605        // to `Default` for the duration of a click.
7606        h.dispatch(PointerPhase::Down, 50.0, 15.0);
7607        assert_eq!(h.cursor(), CursorIcon::Pointer, "a Down leaves it standing");
7608        h.dispatch(PointerPhase::Up, 50.0, 15.0);
7609        assert_eq!(h.cursor(), CursorIcon::Pointer, "an Up leaves it standing");
7610
7611        // Neither does a scroll, a key, or the housekeeping broadcast.
7612        h.root.event(
7613            &mut h.state,
7614            &InputEvent::Scroll {
7615                position: Point::new(50.0, 15.0),
7616                delta: crate::event::ScrollDelta::Lines(0.0, 1.0),
7617            },
7618        );
7619        assert_eq!(h.cursor(), CursorIcon::Pointer);
7620        h.root.event(&mut h.state, &InputEvent::Housekeeping);
7621        assert_eq!(h.cursor(), CursorIcon::Pointer);
7622    }
7623
7624    #[test]
7625    fn a_captured_drag_keeps_the_capturing_widgets_cursor() {
7626        let mut h = CursorHarness::new(ContainerCursor::Silent, true);
7627        h.move_over(Leaf::Top);
7628        assert_eq!(h.cursor(), CursorIcon::Pointer);
7629
7630        // Press the top leaf: it captures. The `Down` itself resolves nothing.
7631        h.dispatch(PointerPhase::Down, 50.0, 15.0);
7632        assert!(h.root.is_pointer_captured());
7633        assert_eq!(h.cursor(), CursorIcon::Pointer);
7634
7635        // Drag inside, then well outside its own bounds and over the sibling: every
7636        // one of these moves routes to the captured leaf alone, so its drag shape is
7637        // the only request in the pass — the sibling's `Text` never gets a say.
7638        h.dispatch(PointerPhase::Move, 50.0, 16.0);
7639        assert_eq!(h.cursor(), CursorIcon::Grabbing);
7640        h.dispatch(PointerPhase::Move, 50.0, 45.0);
7641        assert_eq!(
7642            h.cursor(),
7643            CursorIcon::Grabbing,
7644            "a captured drag keeps its own cursor outside its bounds"
7645        );
7646        h.dispatch(PointerPhase::Move, 50.0, 500.0);
7647        assert_eq!(h.cursor(), CursorIcon::Grabbing);
7648
7649        // Release, then a fresh uncaptured move: the ordinary hit-tested resolution
7650        // is back, and the drag shape is gone with nothing cleared.
7651        h.dispatch(PointerPhase::Up, 50.0, 45.0);
7652        h.move_over(Leaf::Bottom);
7653        assert_eq!(h.cursor(), CursorIcon::Text);
7654    }
7655
7656    use crate::event::{EditCommand, Key, KeyEvent, Modifiers, NamedKey};
7657
7658    // --- Clipboard: the per-pass write / paste-request channels ---------------
7659    //
7660    // The cursor's two siblings, resolved by the same bracket but committed on
7661    // every pass rather than on a pointer `Move` alone, and drained one-shot
7662    // rather than read as a standing level. The fixture is the cursor pair's
7663    // shape with focus in place of hit testing, because a clipboard verb is
7664    // focus-routed: two stacked leaves, a container that can speak before or
7665    // after routing (the last-writer case) or ask for a paste of its own (the
7666    // two-channels-in-one-pass case).
7667
7668    /// How the clipboard fixture's container speaks relative to its children.
7669    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7670    enum ContainerClipboard {
7671        /// Says nothing at all — the ordinary container.
7672        Silent,
7673        /// Writes *before* routing, so whatever the child writes comes later.
7674        WritesBeforeRouting(&'static str),
7675        /// Writes *after* routing, deliberately overriding its child.
7676        WritesAfterRouting(&'static str),
7677        /// Asks for a paste before routing — a toolbar refreshing whether its
7678        /// paste button should be enabled, which is how a write and a request
7679        /// legitimately ride one pass.
7680        AsksForPaste,
7681    }
7682
7683    /// A focusable editable stand-in: claims focus on a `Down`, answers a
7684    /// copy/cut by writing its "selection", and asks for the clipboard when it
7685    /// sees the hardware [`NamedKey::Paste`] key it decoded itself.
7686    struct ClipboardLeaf {
7687        /// What this leaf would copy.
7688        text: &'static str,
7689        /// Every [`EditCommand`] this leaf was handed, in order — the proof of
7690        /// who the focus routing actually reached.
7691        seen: Rc<RefCell<Vec<EditCommand>>>,
7692    }
7693
7694    impl crate::widget::Widget for ClipboardLeaf {
7695        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7696            bc.constrain(Size::new(100.0, 30.0))
7697        }
7698        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7699        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7700            match event {
7701                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
7702                    ctx.request_focus();
7703                    EventResult::Handled
7704                }
7705                InputEvent::EditCommand(cmd) => {
7706                    self.seen.borrow_mut().push(cmd.clone());
7707                    match cmd {
7708                        // The widget owns the selection, so it is the only thing
7709                        // that can say what "copy" means.
7710                        EditCommand::Copy | EditCommand::Cut => {
7711                            ctx.write_clipboard(self.text.to_string());
7712                        }
7713                        // A paste arrives with its text already read by the shell,
7714                        // and select-all touches no clipboard at all.
7715                        EditCommand::Paste(_) | EditCommand::SelectAll => {}
7716                    }
7717                    EventResult::Handled
7718                }
7719                // The hardware clipboard key, decoded by the widget rather than by
7720                // the shell: it carries no text, so the widget asks for some.
7721                InputEvent::Key(k) if k.key == Key::Named(NamedKey::Paste) => {
7722                    ctx.request_paste();
7723                    EventResult::Handled
7724                }
7725                _ => EventResult::Ignored,
7726            }
7727        }
7728    }
7729
7730    /// Two stacked clipboard leaves, routed by focus for a focus-routed event and
7731    /// by hit test for a pointer one, plus the container's own optional request.
7732    struct ClipboardPair {
7733        top: crate::widget::ChildPod,
7734        bottom: crate::widget::ChildPod,
7735        own: ContainerClipboard,
7736    }
7737
7738    impl crate::widget::Widget for ClipboardPair {
7739        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7740            self.top.layout_child(ctx, bc);
7741            self.top.set_origin(Point::ZERO);
7742            self.bottom.layout_child(ctx, bc);
7743            self.bottom.set_origin(Point::new(0.0, 30.0));
7744            bc.max()
7745        }
7746        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7747        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7748            match self.own {
7749                ContainerClipboard::WritesBeforeRouting(text) => {
7750                    ctx.write_clipboard(text.to_string())
7751                }
7752                ContainerClipboard::AsksForPaste => ctx.request_paste(),
7753                _ => {}
7754            }
7755            let result = self.route(ctx, event);
7756            if let ContainerClipboard::WritesAfterRouting(text) = self.own {
7757                ctx.write_clipboard(text.to_string());
7758            }
7759            result
7760        }
7761    }
7762
7763    impl ClipboardPair {
7764        /// Focus routing for a focus-routed event, hit testing for the rest —
7765        /// the two branches every real container has.
7766        fn route(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7767            if event.is_focus_routed() {
7768                for pod in [&mut self.top, &mut self.bottom] {
7769                    if pod.is_focused() {
7770                        return pod.event_child(ctx, event);
7771                    }
7772                }
7773                return EventResult::Ignored;
7774            }
7775            let pos = event.position();
7776            let blurs = matches!(
7777                event,
7778                InputEvent::Pointer(p) if p.phase == PointerPhase::Down
7779            );
7780            let mut result = EventResult::Ignored;
7781            let mut hit = false;
7782            for pod in [&mut self.top, &mut self.bottom] {
7783                if !hit && pod.contains(pos) {
7784                    hit = true;
7785                    result = pod.event_child(ctx, event);
7786                } else if blurs {
7787                    // A `Down` that lands elsewhere blurs the chain, exactly as
7788                    // `frust-widgets`' routers do.
7789                    pod.set_focused(false);
7790                }
7791            }
7792            result
7793        }
7794    }
7795
7796    /// The fixture's view. It carries the two `seen` logs rather than letting the
7797    /// harness reach into the built tree for them: `Widget` has no downcast, and
7798    /// a shared handle is the same way the hover fixture's probe is watched.
7799    #[derive(Clone)]
7800    struct ClipboardPairView {
7801        own: ContainerClipboard,
7802        top_seen: Rc<RefCell<Vec<EditCommand>>>,
7803        bottom_seen: Rc<RefCell<Vec<EditCommand>>>,
7804    }
7805
7806    impl View<()> for ClipboardPairView {
7807        type Element = ClipboardPair;
7808        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ClipboardPair {
7809            ClipboardPair {
7810                top: crate::widget::ChildPod::new(Box::new(ClipboardLeaf {
7811                    text: "top selection",
7812                    seen: self.top_seen.clone(),
7813                })),
7814                bottom: crate::widget::ChildPod::new(Box::new(ClipboardLeaf {
7815                    text: "bottom selection",
7816                    seen: self.bottom_seen.clone(),
7817                })),
7818                own: self.own,
7819            }
7820        }
7821        fn rebuild(&self, _p: &Self, _e: &mut ClipboardPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
7822            ChangeFlags::NONE
7823        }
7824    }
7825
7826    /// A `RenderRoot` over the clipboard fixture, plus the two leaves' `seen`
7827    /// logs (cloned out of the built widgets, which the root owns).
7828    struct ClipboardHarness {
7829        root: RenderRoot<(), ClipboardPairView>,
7830        state: (),
7831        top_seen: Rc<RefCell<Vec<EditCommand>>>,
7832        bottom_seen: Rc<RefCell<Vec<EditCommand>>>,
7833    }
7834
7835    impl ClipboardHarness {
7836        fn new(own: ContainerClipboard) -> Self {
7837            let mut root: RenderRoot<(), ClipboardPairView> = RenderRoot::new();
7838            let mut state = ();
7839            let view = ClipboardPairView {
7840                own,
7841                top_seen: Rc::new(RefCell::new(Vec::new())),
7842                bottom_seen: Rc::new(RefCell::new(Vec::new())),
7843            };
7844            let (top_seen, bottom_seen) = (view.top_seen.clone(), view.bottom_seen.clone());
7845            root.rebuild(&mut move |_: &mut ()| view.clone(), &mut state);
7846            root.layout(Size::new(100.0, 60.0));
7847            ClipboardHarness {
7848                root,
7849                state,
7850                top_seen,
7851                bottom_seen,
7852            }
7853        }
7854
7855        /// Focus a leaf the way a user does: a `Down` inside its bounds.
7856        fn focus(&mut self, leaf: Leaf) {
7857            let y = match leaf {
7858                Leaf::Top => 15.0,
7859                Leaf::Bottom => 45.0,
7860            };
7861            self.root
7862                .event(&mut self.state, &pointer(PointerPhase::Down, 50.0, y));
7863        }
7864
7865        fn dispatch(&mut self, event: &InputEvent) -> EventOutcome {
7866            self.root.event(&mut self.state, event)
7867        }
7868    }
7869
7870    #[test]
7871    fn a_copy_reaches_the_focused_leaf_alone_and_its_text_drains_once() {
7872        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7873        h.focus(Leaf::Top);
7874        assert!(
7875            h.root.take_clipboard_write().is_none(),
7876            "a focusing tap writes nothing"
7877        );
7878
7879        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7880        assert_eq!(
7881            h.top_seen.borrow().as_slice(),
7882            &[EditCommand::Copy],
7883            "the focused leaf received the command"
7884        );
7885        assert!(
7886            h.bottom_seen.borrow().is_empty(),
7887            "and the unfocused sibling never saw it — focus routing, not hit testing"
7888        );
7889        assert_eq!(
7890            h.root.take_clipboard_write().as_deref(),
7891            Some("top selection")
7892        );
7893        assert_eq!(
7894            h.root.take_clipboard_write(),
7895            None,
7896            "the drain is one-shot: a clipboard write is an edge, not a level"
7897        );
7898    }
7899
7900    #[test]
7901    fn a_copy_follows_the_focus_when_it_moves() {
7902        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7903        h.focus(Leaf::Top);
7904        h.focus(Leaf::Bottom);
7905        h.dispatch(&InputEvent::EditCommand(EditCommand::Cut));
7906        assert!(
7907            h.top_seen.borrow().is_empty(),
7908            "the blurred leaf is out of the routed path"
7909        );
7910        assert_eq!(h.bottom_seen.borrow().as_slice(), &[EditCommand::Cut]);
7911        assert_eq!(
7912            h.root.take_clipboard_write().as_deref(),
7913            Some("bottom selection")
7914        );
7915    }
7916
7917    #[test]
7918    fn a_paste_request_drains_once_and_its_answer_is_an_ordinary_dispatch() {
7919        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7920        h.focus(Leaf::Top);
7921        assert!(
7922            !h.root.take_paste_request(),
7923            "a focusing tap asks for nothing"
7924        );
7925
7926        // The widget decoded the hardware Paste key itself and asked the shell.
7927        h.dispatch(&InputEvent::Key(KeyEvent {
7928            key: Key::Named(NamedKey::Paste),
7929            modifiers: Modifiers::default(),
7930            repeat: false,
7931        }));
7932        assert!(h.root.take_paste_request());
7933        assert!(
7934            !h.root.take_paste_request(),
7935            "the flag is one-shot, so a shell answers a request once"
7936        );
7937
7938        // The shell's answer is a new, focus-routed dispatch carrying the text.
7939        h.dispatch(&InputEvent::EditCommand(EditCommand::Paste(
7940            "from the host".to_string(),
7941        )));
7942        assert_eq!(
7943            h.top_seen.borrow().as_slice(),
7944            &[EditCommand::Paste("from the host".to_string())]
7945        );
7946        assert!(
7947            !h.root.take_paste_request(),
7948            "answering a request does not raise a new one"
7949        );
7950        assert!(
7951            h.root.take_clipboard_write().is_none(),
7952            "and a paste writes nothing back to the host clipboard"
7953        );
7954    }
7955
7956    #[test]
7957    fn a_paste_answered_after_a_blur_reaches_nobody() {
7958        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7959        h.focus(Leaf::Top);
7960        h.dispatch(&InputEvent::Key(KeyEvent {
7961            key: Key::Named(NamedKey::Paste),
7962            modifiers: Modifiers::default(),
7963            repeat: false,
7964        }));
7965        assert!(h.root.take_paste_request());
7966
7967        // Focus is released while the shell's clipboard read is in flight: a
7968        // `Down` on empty chrome past both leaves blurs the chain.
7969        h.dispatch(&pointer(PointerPhase::Down, 50.0, 100.0));
7970        assert!(!h.root.is_focus_active(), "the tap blurred the field");
7971
7972        // The shell answers anyway — it never has to track who asked, because the
7973        // answer is focus-routed and simply reaches no widget.
7974        h.dispatch(&InputEvent::EditCommand(EditCommand::Paste(
7975            "from the host".to_string(),
7976        )));
7977        assert!(h.top_seen.borrow().is_empty());
7978        assert!(h.bottom_seen.borrow().is_empty());
7979    }
7980
7981    #[test]
7982    fn the_last_clipboard_write_of_a_pass_wins_at_the_root() {
7983        // A container that writes BEFORE routing yields to its child, exactly as
7984        // `set_cursor` does: the innermost widget the route reaches speaks last.
7985        let mut h = ClipboardHarness::new(ContainerClipboard::WritesBeforeRouting("container"));
7986        h.focus(Leaf::Top);
7987        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7988        assert_eq!(
7989            h.root.take_clipboard_write().as_deref(),
7990            Some("top selection")
7991        );
7992
7993        // A container that writes AFTER routing deliberately overrides it.
7994        let mut h = ClipboardHarness::new(ContainerClipboard::WritesAfterRouting("container"));
7995        h.focus(Leaf::Top);
7996        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7997        assert_eq!(h.root.take_clipboard_write().as_deref(), Some("container"));
7998    }
7999
8000    #[test]
8001    fn a_write_and_a_paste_request_ride_one_pass_independently() {
8002        let mut h = ClipboardHarness::new(ContainerClipboard::AsksForPaste);
8003        h.focus(Leaf::Top);
8004        // The focusing tap already carried the container's ask; drain it so the
8005        // assertion below is about the copy pass alone.
8006        assert!(h.root.take_paste_request());
8007
8008        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
8009        assert_eq!(
8010            h.root.take_clipboard_write().as_deref(),
8011            Some("top selection"),
8012            "the leaf's write landed"
8013        );
8014        assert!(
8015            h.root.take_paste_request(),
8016            "and the container's request landed in the same pass"
8017        );
8018    }
8019
8020    #[test]
8021    fn a_pass_that_asks_for_neither_leaves_both_empty() {
8022        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
8023        h.focus(Leaf::Top);
8024        // A select-all is a real clipboard verb that touches no clipboard, and a
8025        // pointer move touches nothing at all.
8026        h.dispatch(&InputEvent::EditCommand(EditCommand::SelectAll));
8027        h.dispatch(&pointer(PointerPhase::Move, 50.0, 15.0));
8028        assert_eq!(h.top_seen.borrow().as_slice(), &[EditCommand::SelectAll]);
8029        assert_eq!(h.root.take_clipboard_write(), None);
8030        assert!(!h.root.take_paste_request());
8031    }
8032
8033    #[test]
8034    fn an_edit_command_moves_focus_only_when_the_dispatch_asks() {
8035        // The bookkeeping arm `EditCommand` shares with `Key`/`Ime`: unlike a
8036        // `Down`, it neither claims nor blurs by itself.
8037        let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
8038        h.focus(Leaf::Top);
8039        assert!(h.root.is_focus_active());
8040        let gen_before = h.root.focus_ime_generation();
8041        h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
8042        assert!(
8043            h.root.is_focus_active(),
8044            "a clipboard verb leaves the focus session exactly as it found it"
8045        );
8046        assert_eq!(h.root.focus_ime_generation(), gen_before);
8047    }
8048
8049    // ---------------------------------------------------------------------
8050    // Overlay portal: an owner that floats a pod, a sibling painted after it,
8051    // and a root that routes like any hand-written container.
8052    // ---------------------------------------------------------------------
8053
8054    /// What the owner does when a `Down` arrives inside one of its surfaces.
8055    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
8056    enum OwnerReaction {
8057        /// Nothing — the commonest shape (a menu item acts on `Up`).
8058        Nothing,
8059        /// Capture the pointer, the drag-from-inside-a-surface shape.
8060        Capture,
8061        /// Claim focus, the text-field-inside-a-popover shape.
8062        Focus,
8063    }
8064
8065    /// One floated surface the fixture's owner registers.
8066    #[derive(Clone, Debug, PartialEq)]
8067    struct SurfaceSpec {
8068        key: OverlayKey,
8069        label: &'static str,
8070        rect: Rect,
8071        band: crate::overlay::OverlayBand,
8072        input: OverlayInput,
8073        outside_tap: OutsideTap,
8074        /// Whether the owner registers it at all this frame — the fixture for
8075        /// "stop registering and the surface stops existing".
8076        register: bool,
8077        on_down: OwnerReaction,
8078        /// The same, for a `Move` inside the surface — a drag threshold latching
8079        /// a capture mid-gesture, the shape that claims one on no `Down` at all.
8080        on_move: OwnerReaction,
8081        /// The same again, for an `Up` — the phase on which a capture claim has
8082        /// nothing left to own, and the one that reaches the surface as an
8083        /// overlay event precisely because no capture was standing to divert it.
8084        on_up: OwnerReaction,
8085        /// Whether the leaf INSIDE the pod claims focus on a `Down` of its own —
8086        /// the editable-in-a-popover shape, distinct from `on_down`'s claim,
8087        /// which the owner makes on the surface's behalf.
8088        pod_claims_focus: bool,
8089        /// Whether that leaf republishes an active IME surface on every paint.
8090        /// Stated unconditionally, so the fixture can publish from a pod holding
8091        /// no focus link at all — the provenance case.
8092        pod_publishes_ime: bool,
8093        /// The same publish, from the leaf's `event` handler instead of its
8094        /// paint — the event-route half of the provenance case, and equally
8095        /// ungated.
8096        pod_publishes_ime_on_event: bool,
8097    }
8098
8099    impl SurfaceSpec {
8100        fn floating(label: &'static str, rect: Rect) -> Self {
8101            Self {
8102                key: OverlayKey::next(),
8103                label,
8104                rect,
8105                band: crate::overlay::OverlayBand::Floating,
8106                input: OverlayInput::Interactive,
8107                outside_tap: OutsideTap::Ignore,
8108                register: true,
8109                on_down: OwnerReaction::Nothing,
8110                on_move: OwnerReaction::Nothing,
8111                on_up: OwnerReaction::Nothing,
8112                pod_claims_focus: false,
8113                pod_publishes_ime: false,
8114                pod_publishes_ime_on_event: false,
8115            }
8116        }
8117    }
8118
8119    /// Everything the overlay fixture recorded, shared between the widgets and
8120    /// the test.
8121    #[derive(Default)]
8122    struct OverlayLog {
8123        /// Events the owner received, in the order they arrived.
8124        owner: Vec<InputEvent>,
8125        /// `(surface label, event)` for everything a floated pod's content saw —
8126        /// positions here are the pod's own local space.
8127        pod: Vec<(&'static str, InputEvent)>,
8128        /// Events the main-tree sibling received.
8129        sibling: Vec<InputEvent>,
8130        /// What the sibling read from `EventCtx::has_focus` on each event.
8131        sibling_focus: Vec<bool>,
8132        /// What the pod's leaf read from `PaintCtx::has_focus` on each paint.
8133        pod_paint_focus: Vec<bool>,
8134        /// The same read for the main-tree sibling. The two together answer
8135        /// "which branches believe they are focused".
8136        sibling_paint_focus: Vec<bool>,
8137        /// The window size the owner observed in its own (child) layout.
8138        child_window_size: Option<Size>,
8139    }
8140
8141    type Log = std::rc::Rc<std::cell::RefCell<OverlayLog>>;
8142
8143    /// The leaf inside a floated pod: paints its label at the pod's absolute
8144    /// origin and records what reaches it.
8145    ///
8146    /// Optionally an editable — it claims focus on a `Down` of its own and
8147    /// republishes an IME surface on every paint. The publish is deliberately
8148    /// ungated: a pod describing a session it holds no focus link for is
8149    /// precisely what the root has to answer for.
8150    struct OverlayContentWidget {
8151        label: &'static str,
8152        log: Log,
8153        claims_focus: bool,
8154        publishes_ime: bool,
8155        publishes_ime_on_event: bool,
8156    }
8157
8158    impl OverlayContentWidget {
8159        /// What such a pod publishes: an ordinary field's surface, carrying no
8160        /// secure-text configuration of its own.
8161        fn surface() -> ImeState {
8162            ImeState {
8163                active: true,
8164                content_type: ImeContentType::Normal,
8165                ..Default::default()
8166            }
8167        }
8168    }
8169
8170    impl crate::widget::Widget for OverlayContentWidget {
8171        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
8172            bc.constrain(Size::new(80.0, 30.0))
8173        }
8174        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
8175            scene.draw_text(ctx.origin(), self.label);
8176            self.log.borrow_mut().pod_paint_focus.push(ctx.has_focus());
8177            if self.publishes_ime {
8178                ctx.publish_ime_state(Self::surface());
8179            }
8180        }
8181        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
8182            self.log.borrow_mut().pod.push((self.label, event.clone()));
8183            if self.publishes_ime_on_event {
8184                ctx.publish_ime_state(Self::surface());
8185            }
8186            if self.claims_focus
8187                && let InputEvent::Pointer(pointer) = event
8188                && pointer.phase == PointerPhase::Down
8189            {
8190                ctx.request_focus();
8191            }
8192            EventResult::Handled
8193        }
8194    }
8195
8196    /// One live surface: its spec plus the pod the owner keeps.
8197    struct OwnedSurface {
8198        spec: SurfaceSpec,
8199        pod: crate::overlay::OverlayPod,
8200    }
8201
8202    /// The overlay owner: hosts the pods, registers them from `paint`, and
8203    /// forwards the broadcasts the root routes back to it into the right pod.
8204    struct OverlayOwnerWidget {
8205        surfaces: Vec<OwnedSurface>,
8206        log: Log,
8207    }
8208
8209    impl OverlayOwnerWidget {
8210        fn new(specs: &[SurfaceSpec], log: Log) -> Self {
8211            let mut owner = Self {
8212                surfaces: Vec::new(),
8213                log,
8214            };
8215            owner.sync(specs);
8216            owner
8217        }
8218
8219        /// Reconcile the live surfaces against `specs`, keeping each pod alive
8220        /// across a rebuild (a real owner keeps its popover's widget state).
8221        fn sync(&mut self, specs: &[SurfaceSpec]) {
8222            self.surfaces
8223                .retain(|s| specs.iter().any(|n| n.key == s.spec.key));
8224            for spec in specs {
8225                match self.surfaces.iter_mut().find(|s| s.spec.key == spec.key) {
8226                    Some(live) => live.spec = spec.clone(),
8227                    None => {
8228                        let log = std::rc::Rc::clone(&self.log);
8229                        self.surfaces.push(OwnedSurface {
8230                            spec: spec.clone(),
8231                            pod: std::rc::Rc::new(std::cell::RefCell::new(
8232                                crate::widget::ChildPod::new(Box::new(OverlayContentWidget {
8233                                    label: spec.label,
8234                                    log,
8235                                    claims_focus: spec.pod_claims_focus,
8236                                    publishes_ime: spec.pod_publishes_ime,
8237                                    publishes_ime_on_event: spec.pod_publishes_ime_on_event,
8238                                })),
8239                            )),
8240                        });
8241                    }
8242                }
8243            }
8244        }
8245    }
8246
8247    impl crate::widget::Widget for OverlayOwnerWidget {
8248        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
8249            // The window size reaches a CHILD's layout unchanged — this widget is
8250            // a `ChildPod` of the fixture's root.
8251            self.log.borrow_mut().child_window_size = Some(ctx.window_size());
8252            // A floated pod is laid out against the WINDOW, never against the
8253            // owner's own constraints: it escapes the owner's box entirely.
8254            let window = BoxConstraints::loose(ctx.window_size());
8255            for surface in &mut self.surfaces {
8256                surface.pod.borrow_mut().layout_child(ctx, &window);
8257            }
8258            bc.constrain(Size::new(40.0, 20.0))
8259        }
8260
8261        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
8262            scene.draw_text(ctx.origin(), "owner");
8263            for surface in &self.surfaces {
8264                if !surface.spec.register {
8265                    continue;
8266                }
8267                // Registered, never painted here: the root paints it last.
8268                ctx.register_overlay(OverlayEntry {
8269                    key: surface.spec.key,
8270                    band: surface.spec.band,
8271                    input: surface.spec.input,
8272                    outside_tap: surface.spec.outside_tap,
8273                    window_rect: surface.spec.rect,
8274                    pod: std::rc::Rc::clone(&surface.pod),
8275                    insets: ctx.window_insets(),
8276                });
8277            }
8278        }
8279
8280        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
8281            let InputEvent::Overlay(overlay) = event else {
8282                self.log.borrow_mut().owner.push(event.clone());
8283                return EventResult::Ignored;
8284            };
8285            let Some(surface) = self.surfaces.iter_mut().find(|s| s.spec.key == overlay.key) else {
8286                // Another owner's surface: the broadcast reached us, and we
8287                // ignore it. This fall-through is the whole addressing rule.
8288                return EventResult::Ignored;
8289            };
8290            self.log.borrow_mut().owner.push(event.clone());
8291            match &overlay.kind {
8292                OverlayEventKind::Pointer(pointer) => {
8293                    let reaction = match pointer.phase {
8294                        PointerPhase::Down => Some(surface.spec.on_down),
8295                        PointerPhase::Move => Some(surface.spec.on_move),
8296                        PointerPhase::Up => Some(surface.spec.on_up),
8297                        _ => None,
8298                    };
8299                    match reaction {
8300                        Some(OwnerReaction::Capture) => ctx.capture_pointer(),
8301                        Some(OwnerReaction::Focus) => ctx.request_focus(),
8302                        Some(OwnerReaction::Nothing) | None => {}
8303                    }
8304                    // Window space → the pod's own space is one subtraction: the
8305                    // registered rect's origin.
8306                    let local = InputEvent::Pointer(PointerEvent {
8307                        position: pointer.position - surface.spec.rect.origin().to_vec2(),
8308                        ..*pointer
8309                    });
8310                    surface.pod.borrow_mut().event_child(ctx, &local);
8311                }
8312                OverlayEventKind::Scroll { position, delta } => {
8313                    let local = InputEvent::Scroll {
8314                        position: *position - surface.spec.rect.origin().to_vec2(),
8315                        delta: *delta,
8316                    };
8317                    surface.pod.borrow_mut().event_child(ctx, &local);
8318                }
8319                OverlayEventKind::Scale {
8320                    focal,
8321                    phase,
8322                    scale_delta,
8323                    velocity,
8324                } => {
8325                    let local = InputEvent::Scale(crate::event::ScaleEvent {
8326                        phase: *phase,
8327                        scale_delta: *scale_delta,
8328                        focal: *focal - surface.spec.rect.origin().to_vec2(),
8329                        velocity: *velocity,
8330                    });
8331                    surface.pod.borrow_mut().event_child(ctx, &local);
8332                }
8333                OverlayEventKind::OutsideDown => {}
8334            }
8335            // A broadcast is never consumed, whatever the pod returned.
8336            EventResult::Ignored
8337        }
8338    }
8339
8340    /// The main-tree sibling: painted AFTER the owner, claims focus on a `Down`
8341    /// inside it, and records what it sees.
8342    ///
8343    /// It stands in for a secure text field: the surface it publishes carries
8344    /// [`ImeContentType::Password`], and it republishes that surface on every
8345    /// paint for as long as the pass seeds it focused — the shipped field's
8346    /// republish-and-self-correct shape, in miniature.
8347    struct SiblingLeafWidget {
8348        log: Log,
8349        /// The selection-toolbar request to publish each paint, if any.
8350        publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
8351    }
8352
8353    impl SiblingLeafWidget {
8354        /// The secure surface this field describes its session with.
8355        fn surface() -> ImeState {
8356            ImeState {
8357                active: true,
8358                content_type: ImeContentType::Password,
8359                ..Default::default()
8360            }
8361        }
8362    }
8363
8364    impl crate::widget::Widget for SiblingLeafWidget {
8365        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
8366            bc.constrain(Size::new(60.0, 40.0))
8367        }
8368        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
8369            scene.draw_text(ctx.origin(), "sibling");
8370            self.log
8371                .borrow_mut()
8372                .sibling_paint_focus
8373                .push(ctx.has_focus());
8374            if ctx.has_focus() {
8375                ctx.publish_ime_state(Self::surface());
8376            }
8377            if let Some(request) = self.publish {
8378                ctx.publish_selection_toolbar(request);
8379            }
8380        }
8381        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
8382            {
8383                let mut log = self.log.borrow_mut();
8384                log.sibling.push(event.clone());
8385                log.sibling_focus.push(ctx.has_focus());
8386            }
8387            match event {
8388                InputEvent::Pointer(pointer) if pointer.phase == PointerPhase::Down => {
8389                    ctx.request_focus();
8390                    ctx.publish_ime_state(Self::surface());
8391                    EventResult::Handled
8392                }
8393                // A scroll is hit-tested like a press but travels the root's
8394                // keyboard-class focus arm (`Scroll | Key | Ime | EditCommand`),
8395                // so claiming focus here is how the fixture expresses a focus
8396                // move that is not a pointer `Down`.
8397                InputEvent::Scroll { .. } => {
8398                    ctx.request_focus();
8399                    ctx.publish_ime_state(Self::surface());
8400                    EventResult::Handled
8401                }
8402                _ => EventResult::Ignored,
8403            }
8404        }
8405    }
8406
8407    /// The fixture's root: a hand-written two-child container routing exactly
8408    /// like `frust-widgets`' helpers — broadcast first, then capture, then focus,
8409    /// then a topmost-first hit test.
8410    struct OverlayRootWidget {
8411        owner: crate::widget::ChildPod,
8412        sibling: crate::widget::ChildPod,
8413    }
8414
8415    impl crate::widget::Widget for OverlayRootWidget {
8416        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
8417            self.owner.layout_child(ctx, bc);
8418            self.owner.set_origin(Point::new(0.0, 0.0));
8419            self.sibling.layout_child(ctx, bc);
8420            self.sibling.set_origin(Point::new(0.0, 100.0));
8421            bc.constrain(Size::new(200.0, 200.0))
8422        }
8423
8424        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
8425            // The owner paints FIRST, the sibling after it — so an overlay pod
8426            // landing after both proves the root's post-pass really is last.
8427            self.owner.paint_child(ctx, scene);
8428            self.sibling.paint_child(ctx, scene);
8429        }
8430
8431        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
8432            if event.is_broadcast() {
8433                self.owner.event_child(ctx, event);
8434                self.sibling.event_child(ctx, event);
8435                return EventResult::Ignored;
8436            }
8437            let ends = matches!(
8438                event,
8439                InputEvent::Pointer(p)
8440                    if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
8441            );
8442            for (pod, _) in [(&mut self.owner, 0), (&mut self.sibling, 1)] {
8443                if pod.is_active() {
8444                    let result = pod.event_child(ctx, event);
8445                    if ends {
8446                        pod.set_active(false);
8447                    }
8448                    return result;
8449                }
8450            }
8451            if event.is_focus_routed() {
8452                // `holds_live_focus`, not `is_focused` — the same read
8453                // `frust-widgets`' `route_event` makes, and the reason this
8454                // fixture can stand in for it: both children can carry a
8455                // recorded link at once, and only one of them can carry the live
8456                // session's.
8457                if self.sibling.holds_live_focus() {
8458                    return self.sibling.event_child(ctx, event);
8459                }
8460                if self.owner.holds_live_focus() {
8461                    return self.owner.event_child(ctx, event);
8462                }
8463                return EventResult::Ignored;
8464            }
8465            // Topmost-first: the sibling paints last, so it hit-tests first.
8466            let position = event.position();
8467            if self.sibling.contains(position) {
8468                return self.sibling.event_child(ctx, event);
8469            }
8470            if self.owner.contains(position) {
8471                return self.owner.event_child(ctx, event);
8472            }
8473            EventResult::Ignored
8474        }
8475    }
8476
8477    /// The view producing the fixture, reconciling the surface specs in place.
8478    struct OverlayRootView {
8479        specs: Vec<SurfaceSpec>,
8480        publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
8481        log: Log,
8482    }
8483
8484    impl View<OverlayState> for OverlayRootView {
8485        type Element = OverlayRootWidget;
8486
8487        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
8488            OverlayRootWidget {
8489                owner: crate::widget::ChildPod::new(Box::new(OverlayOwnerWidget::new(
8490                    &self.specs,
8491                    std::rc::Rc::clone(&self.log),
8492                ))),
8493                sibling: crate::widget::ChildPod::new(Box::new(SiblingLeafWidget {
8494                    log: std::rc::Rc::clone(&self.log),
8495                    publish: self.publish,
8496                })),
8497            }
8498        }
8499
8500        fn rebuild(
8501            &self,
8502            prev: &Self,
8503            element: &mut Self::Element,
8504            _ctx: &mut BuildCtx<'_>,
8505        ) -> ChangeFlags {
8506            element
8507                .owner
8508                .widget_mut()
8509                .downcast_mut::<OverlayOwnerWidget>()
8510                .expect("the owner keeps its type")
8511                .sync(&self.specs);
8512            element
8513                .sibling
8514                .widget_mut()
8515                .downcast_mut::<SiblingLeafWidget>()
8516                .expect("the sibling keeps its type")
8517                .publish = self.publish;
8518            if prev.specs != self.specs || prev.publish != self.publish {
8519                ChangeFlags::PAINT
8520            } else {
8521                ChangeFlags::NONE
8522            }
8523        }
8524    }
8525
8526    /// App state for the overlay fixture: the surface specs the next rebuild
8527    /// applies, plus the shared log.
8528    struct OverlayState {
8529        specs: Vec<SurfaceSpec>,
8530        publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
8531        log: Log,
8532    }
8533
8534    fn overlay_logic(state: &mut OverlayState) -> OverlayRootView {
8535        OverlayRootView {
8536            specs: state.specs.clone(),
8537            publish: state.publish,
8538            log: std::rc::Rc::clone(&state.log),
8539        }
8540    }
8541
8542    /// Drives the overlay fixture the way a shell does: rebuild, layout, paint,
8543    /// then dispatch.
8544    struct OverlayHarness {
8545        root: RenderRoot<OverlayState, OverlayRootView>,
8546        state: OverlayState,
8547        log: Log,
8548    }
8549
8550    impl OverlayHarness {
8551        fn new(specs: Vec<SurfaceSpec>) -> Self {
8552            let log: Log = std::rc::Rc::new(std::cell::RefCell::new(OverlayLog::default()));
8553            let mut harness = Self {
8554                root: RenderRoot::new(),
8555                state: OverlayState {
8556                    specs,
8557                    publish: None,
8558                    log: std::rc::Rc::clone(&log),
8559                },
8560                log,
8561            };
8562            harness.frame();
8563            harness
8564        }
8565
8566        /// One full frame: rebuild, layout at a 200x200 window, paint.
8567        fn frame(&mut self) -> RecordingScene {
8568            self.root.rebuild(&mut overlay_logic, &mut self.state);
8569            self.root.layout(Size::new(200.0, 200.0));
8570            let mut scene = RecordingScene::default();
8571            self.root.paint(&mut scene, FrameTime::ZERO);
8572            scene
8573        }
8574
8575        fn dispatch(&mut self, event: &InputEvent) -> EventOutcome {
8576            self.root.event(&mut self.state, event)
8577        }
8578
8579        fn down(&mut self, x: f64, y: f64) -> EventOutcome {
8580            self.dispatch(&pointer(PointerPhase::Down, x, y))
8581        }
8582
8583        fn clear_log(&mut self) {
8584            let mut log = self.log.borrow_mut();
8585            log.owner.clear();
8586            log.pod.clear();
8587            log.sibling.clear();
8588            log.sibling_focus.clear();
8589            log.pod_paint_focus.clear();
8590            log.sibling_paint_focus.clear();
8591        }
8592
8593        /// What the pod's leaf and the main-tree sibling each read from
8594        /// `PaintCtx::has_focus` on the most recent frame — "which branches
8595        /// believe they are focused", read from the branches themselves.
8596        fn branch_focus(&self) -> (bool, bool) {
8597            let log = self.log.borrow();
8598            (
8599                *log.pod_paint_focus
8600                    .last()
8601                    .expect("the pod painted at least once"),
8602                *log.sibling_paint_focus
8603                    .last()
8604                    .expect("the sibling painted at least once"),
8605            )
8606        }
8607
8608        /// The overlay events the owner received, as `(key, kind)`.
8609        fn owner_overlays(&self) -> Vec<(OverlayKey, OverlayEventKind)> {
8610            self.log
8611                .borrow()
8612                .owner
8613                .iter()
8614                .filter_map(|event| match event {
8615                    InputEvent::Overlay(o) => Some((o.key, o.kind.clone())),
8616                    _ => None,
8617                })
8618                .collect()
8619        }
8620
8621        /// Pointer events the sibling saw — the "did the main tree get it?" read.
8622        fn sibling_pointers(&self) -> Vec<PointerEvent> {
8623            self.log
8624                .borrow()
8625                .sibling
8626                .iter()
8627                .filter_map(|event| match event {
8628                    InputEvent::Pointer(p) => Some(*p),
8629                    _ => None,
8630                })
8631                .collect()
8632        }
8633    }
8634
8635    /// A rect well clear of the sibling (which sits at y >= 100).
8636    fn floating_rect() -> Rect {
8637        Rect::new(120.0, 10.0, 200.0, 60.0)
8638    }
8639
8640    #[test]
8641    fn a_registered_pod_paints_after_a_later_sibling_at_its_window_rect() {
8642        let spec = SurfaceSpec::floating("popover", floating_rect());
8643        let mut h = OverlayHarness::new(vec![spec.clone()]);
8644        let scene = h.frame();
8645
8646        assert_eq!(
8647            scene
8648                .texts
8649                .iter()
8650                .map(|(_, text)| text.as_str())
8651                .collect::<Vec<_>>(),
8652            vec!["owner", "sibling", "popover"],
8653            "the floated pod paints after the owner AND after the sibling painted \
8654             later than the owner — escaping paint order is the whole point"
8655        );
8656        assert_eq!(
8657            scene.texts[2].0,
8658            floating_rect().origin(),
8659            "and it paints at its registered window rect, not at its owner's origin"
8660        );
8661        assert_eq!(h.root.overlay_hits.len(), 1);
8662        assert_eq!(h.root.overlay_hits[0].key, spec.key);
8663        assert_eq!(h.root.overlay_hits[0].window_rect, floating_rect());
8664    }
8665
8666    #[test]
8667    fn two_bands_paint_floating_then_tooltip_whatever_the_registration_order() {
8668        // Registered tooltip-first, so registration order and band order
8669        // disagree: the band must win.
8670        let mut tooltip = SurfaceSpec::floating("tooltip", Rect::new(0.0, 0.0, 40.0, 20.0));
8671        tooltip.band = crate::overlay::OverlayBand::Tooltip;
8672        tooltip.input = OverlayInput::Transparent;
8673        let floating = SurfaceSpec::floating("floating", floating_rect());
8674        let mut h = OverlayHarness::new(vec![tooltip, floating]);
8675        let scene = h.frame();
8676
8677        assert_eq!(
8678            scene
8679                .texts
8680                .iter()
8681                .map(|(_, text)| text.as_str())
8682                .collect::<Vec<_>>(),
8683            vec!["owner", "sibling", "floating", "tooltip"],
8684            "Floating paints below Tooltip regardless of who registered first"
8685        );
8686    }
8687
8688    #[test]
8689    fn the_registry_is_empty_at_the_start_of_every_paint() {
8690        let spec = SurfaceSpec::floating("popover", floating_rect());
8691        let mut h = OverlayHarness::new(vec![spec.clone()]);
8692        h.frame();
8693        assert_eq!(h.root.overlay_hits.len(), 1);
8694
8695        // Paint again with nothing changed: the entry is re-registered, not
8696        // accumulated — an owner registering every frame must not grow the table.
8697        let scene = h.frame();
8698        assert_eq!(h.root.overlay_hits.len(), 1);
8699        assert_eq!(scene.texts.len(), 3);
8700
8701        // The owner stops registering (its popover closed). There is nothing to
8702        // unregister: the next paint simply does not see it.
8703        h.state.specs[0].register = false;
8704        let scene = h.frame();
8705        assert!(
8706            h.root.overlay_hits.is_empty(),
8707            "a surface nobody registers stops existing after the next paint"
8708        );
8709        assert_eq!(
8710            scene
8711                .texts
8712                .iter()
8713                .map(|(_, text)| text.as_str())
8714                .collect::<Vec<_>>(),
8715            vec!["owner", "sibling"],
8716            "and stops painting"
8717        );
8718
8719        // A `Down` inside where it used to be now reaches the main tree.
8720        h.clear_log();
8721        h.down(150.0, 30.0);
8722        assert!(h.owner_overlays().is_empty());
8723    }
8724
8725    #[test]
8726    fn the_window_size_reaches_a_childs_layout() {
8727        let h = OverlayHarness::new(vec![SurfaceSpec::floating("popover", floating_rect())]);
8728        assert_eq!(
8729            h.log.borrow().child_window_size,
8730            Some(Size::new(200.0, 200.0)),
8731            "a child lays out knowing the window, which is what an overlay pod is \
8732             sized against"
8733        );
8734    }
8735
8736    #[test]
8737    fn a_down_inside_a_floating_surface_reaches_only_its_owner_and_never_blurs() {
8738        let spec = SurfaceSpec::floating("popover", floating_rect());
8739        let mut h = OverlayHarness::new(vec![spec.clone()]);
8740
8741        // A field elsewhere in the main tree takes focus first.
8742        h.down(30.0, 120.0);
8743        assert!(
8744            h.root.is_focus_active(),
8745            "the sibling holds a focus session"
8746        );
8747        let ime_before = h.root.ime_state();
8748        assert!(ime_before.is_some());
8749        let focus_gen_before = h.root.focus_ime_generation();
8750        h.clear_log();
8751
8752        // Now press inside the floated surface.
8753        h.down(150.0, 30.0);
8754
8755        assert_eq!(
8756            h.owner_overlays(),
8757            vec![(
8758                spec.key,
8759                OverlayEventKind::Pointer(PointerEvent {
8760                    phase: PointerPhase::Down,
8761                    position: Point::new(150.0, 30.0),
8762                    button: PointerButton::Primary,
8763                })
8764            )],
8765            "the owner is reached by key, with a WINDOW-space payload"
8766        );
8767        assert_eq!(
8768            h.log.borrow().pod,
8769            vec![(
8770                "popover",
8771                InputEvent::Pointer(PointerEvent {
8772                    phase: PointerPhase::Down,
8773                    // 150-120, 30-10: the owner's one subtraction, the rect origin.
8774                    position: Point::new(30.0, 20.0),
8775                    button: PointerButton::Primary,
8776                })
8777            )],
8778            "and the owner forwards it into the pod in the pod's own space"
8779        );
8780        assert!(
8781            h.sibling_pointers().is_empty(),
8782            "the main tree never saw the press"
8783        );
8784        assert!(
8785            h.root.is_focus_active(),
8786            "and the press did NOT blur the field the surface belongs to"
8787        );
8788        assert_eq!(
8789            h.root.ime_state(),
8790            ime_before,
8791            "nor disturb its IME surface"
8792        );
8793        assert_eq!(
8794            h.root.focus_ime_generation(),
8795            focus_gen_before,
8796            "so no focus/IME edge fires at the shell either"
8797        );
8798        assert_eq!(
8799            h.log.borrow().sibling_focus.last(),
8800            Some(&true),
8801            "the focused leaf still reads as focused when the broadcast reaches it"
8802        );
8803    }
8804
8805    #[test]
8806    fn a_down_inside_a_transparent_surface_reaches_the_main_tree_normally() {
8807        // The transparent surface covers the sibling exactly.
8808        let mut spec = SurfaceSpec::floating("tooltip", Rect::new(0.0, 100.0, 60.0, 140.0));
8809        spec.input = OverlayInput::Transparent;
8810        spec.outside_tap = OutsideTap::Notify { consume: true };
8811        let mut h = OverlayHarness::new(vec![spec]);
8812        h.frame();
8813        h.clear_log();
8814
8815        h.down(30.0, 120.0);
8816
8817        assert!(
8818            h.owner_overlays().is_empty(),
8819            "a transparent surface is never hit-tested — not even for OutsideDown"
8820        );
8821        assert_eq!(
8822            h.sibling_pointers().len(),
8823            1,
8824            "the pointer passed straight through to the widget underneath"
8825        );
8826        assert!(h.root.is_focus_active(), "which claimed focus as usual");
8827    }
8828
8829    #[test]
8830    fn a_scroll_inside_a_surface_routes_to_its_owner_in_window_space() {
8831        let spec = SurfaceSpec::floating("popover", floating_rect());
8832        let mut h = OverlayHarness::new(vec![spec.clone()]);
8833        h.clear_log();
8834
8835        h.dispatch(&InputEvent::Scroll {
8836            position: Point::new(150.0, 30.0),
8837            delta: ScrollDelta::Lines(0.0, 3.0),
8838        });
8839
8840        assert_eq!(
8841            h.owner_overlays(),
8842            vec![(
8843                spec.key,
8844                OverlayEventKind::Scroll {
8845                    position: Point::new(150.0, 30.0),
8846                    delta: ScrollDelta::Lines(0.0, 3.0),
8847                }
8848            )]
8849        );
8850        assert_eq!(
8851            h.log.borrow().pod,
8852            vec![(
8853                "popover",
8854                InputEvent::Scroll {
8855                    position: Point::new(30.0, 20.0),
8856                    delta: ScrollDelta::Lines(0.0, 3.0),
8857                }
8858            )]
8859        );
8860    }
8861
8862    #[test]
8863    fn a_capture_from_inside_a_surface_routes_the_next_move_by_the_capture_path() {
8864        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8865        spec.on_down = OwnerReaction::Capture;
8866        let mut h = OverlayHarness::new(vec![spec.clone()]);
8867        h.clear_log();
8868
8869        h.down(150.0, 30.0);
8870        assert!(
8871            h.root.is_pointer_captured(),
8872            "a capture bubbled from an overlay Down opens a gesture exactly like a \
8873             hit-tested one"
8874        );
8875        h.clear_log();
8876
8877        // Still INSIDE the surface's own rect — which is the case that
8878        // discriminates: the pre-pass would happily hit-test this one and route it
8879        // as another broadcast, and it must not, because the gesture is captured.
8880        h.dispatch(&pointer(PointerPhase::Move, 160.0, 45.0));
8881        assert!(
8882            h.owner_overlays().is_empty(),
8883            "a live capture short-circuits the overlay pre-pass even inside the \
8884             surface's own rect"
8885        );
8886        assert_eq!(
8887            h.log.borrow().owner,
8888            vec![InputEvent::Pointer(PointerEvent {
8889                phase: PointerPhase::Move,
8890                position: Point::new(160.0, 45.0),
8891                button: PointerButton::Primary,
8892            })],
8893            "the move reaches the owner by the ordinary captured path instead"
8894        );
8895        h.clear_log();
8896
8897        // And the drag may wander far outside the surface — over the sibling, in
8898        // fact — without the sibling ever hearing about it.
8899        h.dispatch(&pointer(PointerPhase::Move, 30.0, 120.0));
8900
8901        assert!(h.owner_overlays().is_empty());
8902        assert_eq!(
8903            h.log.borrow().owner,
8904            vec![InputEvent::Pointer(PointerEvent {
8905                phase: PointerPhase::Move,
8906                position: Point::new(30.0, 120.0),
8907                button: PointerButton::Primary,
8908            })],
8909            "which is what lets a drag begun inside a floated surface continue \
8910             outside it"
8911        );
8912        assert!(
8913            h.sibling_pointers().is_empty(),
8914            "and never reaches the widget it passed over"
8915        );
8916
8917        // The `Up` closes the gesture through the ordinary pointer arm.
8918        h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
8919        assert!(!h.root.is_pointer_captured());
8920    }
8921
8922    /// A capture claimed on a phase other than `Down` is a capture all the
8923    /// same. The pods record their own active path on any phase, so a root that
8924    /// mirrored the `Down` alone left the surface latched with no `Up` able to
8925    /// reach it — and a latched surface diverts every later pointer event its
8926    /// owner is reached by.
8927    #[test]
8928    fn a_capture_claimed_on_a_move_inside_a_surface_reaches_the_root() {
8929        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8930        spec.on_move = OwnerReaction::Capture;
8931        let mut h = OverlayHarness::new(vec![spec]);
8932
8933        h.dispatch(&pointer(PointerPhase::Move, 150.0, 30.0));
8934        assert!(
8935            h.root.is_pointer_captured(),
8936            "the root mirrors a claim the surface made mid-gesture"
8937        );
8938
8939        // Which is what ends it: the mirrored capture short-circuits the overlay
8940        // pre-pass, so the `Up` routes down the capture path — outside the
8941        // surface's own rect, where a hit test would never have delivered it.
8942        h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
8943        assert!(
8944            !h.root.is_pointer_captured(),
8945            "and the gesture closes on the ordinary pointer arm"
8946        );
8947    }
8948
8949    /// The release side of the root's overlay capture mirror. The claim is
8950    /// mirrored on any phase, so the release has to be bounded on any phase too:
8951    /// an `Up` reaches a surface as an overlay event exactly when no capture was
8952    /// standing to divert it, and a claim made there has no gesture left to own.
8953    /// A latch left standing short-circuits the overlay pre-pass, which stops
8954    /// every floated surface taking input until some unrelated pointer release
8955    /// happens along.
8956    #[test]
8957    fn a_capture_claimed_on_an_overlay_up_does_not_outlive_the_gesture() {
8958        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8959        spec.on_up = OwnerReaction::Capture;
8960        let mut h = OverlayHarness::new(vec![spec]);
8961
8962        h.down(150.0, 30.0);
8963        h.dispatch(&pointer(PointerPhase::Up, 150.0, 30.0));
8964
8965        assert!(
8966            !h.root.is_pointer_captured(),
8967            "the phase that ends a gesture releases the mirror it just set"
8968        );
8969
8970        // Which is what keeps the surfaces routable: a press inside one still
8971        // reaches its owner rather than being diverted down a capture path.
8972        h.clear_log();
8973        h.down(150.0, 30.0);
8974        assert_eq!(
8975            h.owner_overlays().len(),
8976            1,
8977            "the overlay pre-pass is still hit-testing"
8978        );
8979    }
8980
8981    #[test]
8982    fn a_focus_request_from_inside_a_surface_opens_a_session() {
8983        let mut spec = SurfaceSpec::floating("popover", floating_rect());
8984        spec.on_down = OwnerReaction::Focus;
8985        let mut h = OverlayHarness::new(vec![spec]);
8986        assert!(!h.root.is_focus_active());
8987
8988        h.down(150.0, 30.0);
8989
8990        assert!(
8991            h.root.is_focus_active(),
8992            "a text field inside a popover may claim focus — the overlay arm \
8993             honours the request even though it refuses the blur"
8994        );
8995    }
8996
8997    /// The case the test above cannot express: the claim arrives while ANOTHER
8998    /// branch already holds the session. Honouring it without retiring what it
8999    /// supersedes leaves two branches believing they are focused — and leaves
9000    /// the root describing, to the shell, a field that no longer owns anything.
9001    #[test]
9002    fn a_pod_focus_claim_retires_the_branch_it_supersedes() {
9003        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9004        spec.pod_claims_focus = true;
9005        let mut h = OverlayHarness::new(vec![spec]);
9006
9007        // A secure field in the main tree takes the session first.
9008        h.down(30.0, 120.0);
9009        h.frame();
9010        assert_eq!(
9011            h.branch_focus(),
9012            (false, true),
9013            "only the field's own branch reads focused while it holds the session"
9014        );
9015        assert_eq!(
9016            h.root.ime_state().map(|ime| ime.content_type),
9017            Some(ImeContentType::Password),
9018            "and the surface the shell configures its keyboard from is the \
9019             secure one that field published"
9020        );
9021
9022        // Now the editable inside the floated surface claims focus.
9023        h.down(150.0, 30.0);
9024        assert!(h.root.is_focus_active(), "the claim is honoured");
9025
9026        // The claim stamped its own chain with a session identity nothing older
9027        // carries, so the field's link stops counting on the very next pass —
9028        // no convergence frame, and nothing had to visit the branch the session
9029        // left in order to clear it.
9030        h.frame();
9031        assert_eq!(
9032            h.branch_focus(),
9033            (true, false),
9034            "exactly one branch believes it is focused: the one that claimed it"
9035        );
9036        assert_eq!(
9037            h.root.ime_state(),
9038            None,
9039            "and the surface that field published for the session it just lost \
9040             does NOT stand: the branch that took the session described none of \
9041             its own, so the shell is left configuring nothing rather than a \
9042             secure field nobody is in"
9043        );
9044
9045        h.frame();
9046        assert_eq!(
9047            h.branch_focus(),
9048            (true, false),
9049            "which is a settled state, not a frame of transition"
9050        );
9051        assert!(
9052            h.root.is_focus_active(),
9053            "and the session the pod opened is still the live one"
9054        );
9055    }
9056
9057    /// The return direction, which the test above never exercises: the session
9058    /// goes to a floated surface and the user then presses the field in the main
9059    /// tree again.
9060    ///
9061    /// A press outside every floated rect is hit-tested through the containers
9062    /// like any other, so the claim it produces is the main tree's by
9063    /// construction — and the surface's own recorded link has to stop counting
9064    /// the moment that claim lands, or the tree is de-seeded for the rest of the
9065    /// pod's life and the secure surface the field publishes is thrown away
9066    /// every frame.
9067    #[test]
9068    fn a_hit_tested_claim_takes_the_session_back_from_a_floated_surface() {
9069        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9070        spec.pod_claims_focus = true;
9071        let mut h = OverlayHarness::new(vec![spec]);
9072
9073        // The field takes the session, then the editable inside the surface
9074        // takes it away.
9075        h.down(30.0, 120.0);
9076        h.frame();
9077        h.down(150.0, 30.0);
9078        h.frame();
9079        assert_eq!(
9080            h.branch_focus(),
9081            (true, false),
9082            "the surface holds the session and the field's link no longer counts"
9083        );
9084
9085        // The user presses the field again, outside `floating_rect()`, so the
9086        // press routes through the containers exactly as the first one did.
9087        h.down(30.0, 120.0);
9088        h.frame();
9089
9090        assert_eq!(
9091            h.branch_focus(),
9092            (false, true),
9093            "the session is the field's again and the surface's link is retired"
9094        );
9095        assert_eq!(
9096            h.root.ime_state().map(|ime| ime.content_type),
9097            Some(ImeContentType::Password),
9098            "and the secure surface that field publishes reaches the shell"
9099        );
9100    }
9101
9102    /// The same move, driven from the root's keyboard-class arm
9103    /// (`Scroll | Key | Ime | EditCommand`) rather than a pointer `Down`. That
9104    /// arm honours the claim and nothing else, so a mechanism that only retires
9105    /// a surface's link on a press leaves the session split here.
9106    #[test]
9107    fn a_keyboard_class_claim_in_the_tree_retires_a_surfaces_link() {
9108        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9109        spec.pod_claims_focus = true;
9110        let mut h = OverlayHarness::new(vec![spec]);
9111
9112        h.down(150.0, 30.0);
9113        h.frame();
9114        assert!(h.branch_focus().0, "the surface holds the session");
9115
9116        // A scroll over the field, outside every floated rect: hit-tested into
9117        // the main tree, but routed through the root's non-pointer focus arm.
9118        h.dispatch(&InputEvent::Scroll {
9119            position: Point::new(30.0, 120.0),
9120            delta: ScrollDelta::Lines(0.0, 3.0),
9121        });
9122        h.frame();
9123
9124        assert_eq!(
9125            h.branch_focus(),
9126            (false, true),
9127            "a claim that arrives without a pointer `Down` retires the surface's \
9128             link just the same"
9129        );
9130        assert_eq!(
9131            h.root.ime_state().map(|ime| ime.content_type),
9132            Some(ImeContentType::Password),
9133        );
9134    }
9135
9136    /// A pod dropped while it holds the link takes the session with it. Nothing
9137    /// else can end that session: the widget that owned it no longer exists, so
9138    /// no later pass can reach it to release it, and the root would otherwise go
9139    /// on reporting a live focus session to the shell for a surface nobody can
9140    /// see.
9141    #[test]
9142    fn a_pod_dropped_while_it_holds_the_link_ends_the_session_it_owned() {
9143        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9144        spec.pod_claims_focus = true;
9145        let mut h = OverlayHarness::new(vec![spec]);
9146
9147        h.down(150.0, 30.0);
9148        h.frame();
9149        assert!(h.root.is_focus_active(), "the surface holds the session");
9150
9151        // The owner stops owning the surface: the spec goes, the pod is dropped
9152        // by the next rebuild.
9153        h.state.specs.clear();
9154        h.frame();
9155
9156        assert!(
9157            !h.root.is_focus_active(),
9158            "the session dies with the widget that held it"
9159        );
9160        assert_eq!(h.root.ime_state(), None);
9161
9162        // And the tree is not left de-seeded: the next press focuses normally.
9163        h.down(30.0, 120.0);
9164        h.frame();
9165        assert!(
9166            h.branch_focus().1,
9167            "a field in the tree takes the session as if no surface had existed"
9168        );
9169        assert_eq!(
9170            h.root.ime_state().map(|ime| ime.content_type),
9171            Some(ImeContentType::Password),
9172        );
9173    }
9174
9175    /// The event-route half of the provenance rule. `paint_overlays` refuses a
9176    /// paint-time publish from a pod holding no link; the dispatch that carries
9177    /// an overlay broadcast has to refuse the same publish, or a surface the
9178    /// user merely touched reconfigures the platform keyboard for a secure field
9179    /// it has nothing to do with.
9180    #[test]
9181    fn a_pod_publish_from_an_event_cannot_overwrite_a_field_it_does_not_own() {
9182        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9183        // Publishes from its `event`, and never claims focus.
9184        spec.pod_publishes_ime_on_event = true;
9185        let mut h = OverlayHarness::new(vec![spec]);
9186
9187        h.down(30.0, 120.0);
9188        assert_eq!(
9189            h.root.ime_state().map(|ime| ime.content_type),
9190            Some(ImeContentType::Password),
9191            "the focused field's own surface"
9192        );
9193
9194        // A press inside the surface. The pod publishes, holds no focus link,
9195        // and claims none.
9196        h.down(150.0, 30.0);
9197
9198        assert_eq!(
9199            h.root.ime_state().map(|ime| ime.content_type),
9200            Some(ImeContentType::Password),
9201            "a publish from a branch that owns no session describes nobody's"
9202        );
9203        assert!(
9204            h.root.is_focus_active(),
9205            "and the field it did not own still holds the session"
9206        );
9207    }
9208
9209    /// A pod publishing an IME surface it holds no focus link for describes
9210    /// nobody's session. Accepting it would let a surface overwrite a focused
9211    /// field's — including the content type that configures the platform
9212    /// keyboard as a secure one.
9213    #[test]
9214    fn a_pod_publish_cannot_overwrite_the_surface_of_a_field_it_does_not_own() {
9215        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9216        // Publishes on every paint, and never claims focus.
9217        spec.pod_publishes_ime = true;
9218        let mut h = OverlayHarness::new(vec![spec]);
9219
9220        h.down(30.0, 120.0);
9221        assert_eq!(
9222            h.root.ime_state().map(|ime| ime.content_type),
9223            Some(ImeContentType::Password),
9224            "the focused field's own surface"
9225        );
9226
9227        h.frame();
9228
9229        assert!(
9230            h.root.is_focus_active(),
9231            "the field still holds the session"
9232        );
9233        assert_eq!(
9234            h.root.ime_state().map(|ime| ime.content_type),
9235            Some(ImeContentType::Password),
9236            "and it still describes it: the publishing pod holds no focus link, \
9237             so its surface is not the session's"
9238        );
9239    }
9240
9241    /// The provenance rule is a check, not a refusal: a pod that DOES hold the
9242    /// recorded focus path publishes exactly like a field in the tree.
9243    #[test]
9244    fn a_pod_that_holds_the_focus_path_publishes_its_own_surface() {
9245        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9246        spec.pod_claims_focus = true;
9247        spec.pod_publishes_ime = true;
9248        let mut h = OverlayHarness::new(vec![spec]);
9249
9250        h.down(30.0, 120.0);
9251        h.down(150.0, 30.0);
9252        h.frame();
9253
9254        assert!(h.root.is_focus_active());
9255        assert_eq!(
9256            h.root.ime_state().map(|ime| ime.content_type),
9257            Some(ImeContentType::Normal),
9258            "the surface published by the branch that owns the session"
9259        );
9260    }
9261
9262    #[test]
9263    fn an_outside_press_notifies_a_consuming_surface_and_stops_there() {
9264        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9265        spec.outside_tap = OutsideTap::Notify { consume: true };
9266        let mut h = OverlayHarness::new(vec![spec.clone()]);
9267        h.clear_log();
9268
9269        // Inside the sibling, outside every floated rect.
9270        h.down(30.0, 120.0);
9271
9272        assert_eq!(
9273            h.owner_overlays(),
9274            vec![(spec.key, OverlayEventKind::OutsideDown)],
9275            "the light-dismiss notification carries no position"
9276        );
9277        assert!(
9278            h.sibling_pointers().is_empty(),
9279            "and the press that dismissed the menu did not also activate what was \
9280             underneath it"
9281        );
9282        assert!(
9283            !h.root.is_focus_active(),
9284            "the main tree saw no Down at all, so nothing claimed focus"
9285        );
9286    }
9287
9288    #[test]
9289    fn a_pass_through_outside_press_notifies_and_still_reaches_the_main_tree() {
9290        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9291        spec.outside_tap = OutsideTap::Notify { consume: false };
9292        let mut h = OverlayHarness::new(vec![spec.clone()]);
9293        h.clear_log();
9294
9295        let outcome = h.down(30.0, 120.0);
9296
9297        assert_eq!(
9298            h.owner_overlays(),
9299            vec![(spec.key, OverlayEventKind::OutsideDown)]
9300        );
9301        assert_eq!(
9302            h.sibling_pointers().len(),
9303            1,
9304            "consume: false means both — the owner hears, and the press continues"
9305        );
9306        assert!(h.root.is_focus_active(), "so the tapped field took focus");
9307        assert!(
9308            outcome.handled,
9309            "and the main dispatch's own outcome survives"
9310        );
9311    }
9312
9313    #[test]
9314    fn an_ignoring_surface_hears_nothing_about_an_outside_press() {
9315        // `Ignore` is the default; state it explicitly.
9316        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9317        spec.outside_tap = OutsideTap::Ignore;
9318        let mut h = OverlayHarness::new(vec![spec]);
9319        h.clear_log();
9320
9321        h.down(30.0, 120.0);
9322
9323        assert!(
9324            h.owner_overlays().is_empty(),
9325            "a surface that dismisses some other way is never told"
9326        );
9327        assert_eq!(h.sibling_pointers().len(), 1);
9328    }
9329
9330    #[test]
9331    fn only_a_primary_press_dismisses() {
9332        let mut spec = SurfaceSpec::floating("popover", floating_rect());
9333        spec.outside_tap = OutsideTap::Notify { consume: true };
9334        let mut h = OverlayHarness::new(vec![spec]);
9335        h.clear_log();
9336
9337        // A secondary press is a context gesture, not a dismissal.
9338        h.dispatch(&InputEvent::Pointer(PointerEvent {
9339            phase: PointerPhase::Down,
9340            position: Point::new(30.0, 120.0),
9341            button: PointerButton::Secondary,
9342        }));
9343        assert!(h.owner_overlays().is_empty());
9344        assert_eq!(h.sibling_pointers().len(), 1, "and it reaches the tree");
9345
9346        // Nor does a move or a lift outside the surface.
9347        h.clear_log();
9348        h.dispatch(&pointer(PointerPhase::Move, 30.0, 120.0));
9349        h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
9350        assert!(h.owner_overlays().is_empty());
9351    }
9352
9353    #[test]
9354    fn an_overlay_press_leaves_the_main_trees_hover_standing() {
9355        let spec = SurfaceSpec::floating("popover", floating_rect());
9356        let mut h = OverlayHarness::new(vec![spec]);
9357        let epoch_before = h.root.hover_epoch;
9358
9359        h.down(150.0, 30.0);
9360
9361        assert_eq!(
9362            h.root.hover_epoch, epoch_before,
9363            "an overlay pass advances no hover epoch, so a live hover in the main \
9364             tree is not stranded by a press on a floated surface"
9365        );
9366    }
9367
9368    // ---------------------------------------------------------------------
9369    // Selection toolbar: publish, generation, clear.
9370    // ---------------------------------------------------------------------
9371
9372    /// A request with the menu up, anchored at `x` — the shape a field
9373    /// publishes while its bar stands.
9374    fn toolbar_request(x: f64) -> crate::selection_toolbar::SelectionToolbarRequest {
9375        crate::selection_toolbar::SelectionToolbarRequest {
9376            anchor: Rect::new(x, 100.0, x + 50.0, 120.0),
9377            actions: crate::selection_toolbar::SelectionToolbarActions {
9378                copy: true,
9379                cut: true,
9380                paste: false,
9381                select_all: true,
9382            },
9383            present_menu: true,
9384        }
9385    }
9386
9387    /// The same request with no menu wanted — what a focused field publishes
9388    /// with nothing on screen, so the platform can still answer "may I offer
9389    /// Copy?" for a hardware shortcut.
9390    fn toolbar_level(x: f64) -> crate::selection_toolbar::SelectionToolbarRequest {
9391        crate::selection_toolbar::SelectionToolbarRequest {
9392            present_menu: false,
9393            ..toolbar_request(x)
9394        }
9395    }
9396
9397    #[test]
9398    fn a_selection_toolbar_publish_resolves_and_only_a_menu_edge_moves_the_generation() {
9399        let mut h = OverlayHarness::new(vec![]);
9400        // A toolbar describes the FOCUSED field's selection, so open a session
9401        // first — a publish with nothing focused describes nothing.
9402        h.down(30.0, 120.0);
9403        assert!(h.root.is_focus_active());
9404
9405        h.state.publish = Some(toolbar_request(10.0));
9406        h.frame();
9407        assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(10.0)));
9408        let first_gen = h.root.selection_toolbar_generation();
9409        assert!(first_gen > 0, "appearing is an edge");
9410
9411        // The field republishes the same request every frame its selection
9412        // stands: that must not ask the shell to re-present the menu per vsync.
9413        h.frame();
9414        h.frame();
9415        assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(10.0)));
9416        assert_eq!(h.root.selection_toolbar_generation(), first_gen);
9417
9418        // A moved anchor is stored — a shell re-reads it to place a menu it
9419        // already has on screen — but it is NOT a menu edge. This assertion used
9420        // to read `first_gen + 1`: the anchor is recomputed every painted frame
9421        // and a drag that widens a selection moves it on every touch sample, so
9422        // bumping here asked the platform to re-present its menu per sample.
9423        h.state.publish = Some(toolbar_request(60.0));
9424        h.frame();
9425        assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(60.0)));
9426        assert_eq!(
9427            h.root.selection_toolbar_generation(),
9428            first_gen,
9429            "an anchor following the selection is not a reason to re-present"
9430        );
9431
9432        // A verb changing IS: the menu's own contents just changed.
9433        let mut fewer_verbs = toolbar_request(60.0);
9434        fewer_verbs.actions.select_all = false;
9435        h.state.publish = Some(fewer_verbs);
9436        h.frame();
9437        assert_eq!(h.root.selection_toolbar_generation(), first_gen + 1);
9438
9439        // The field blurs: it stops publishing, and the menu goes away with
9440        // nothing retracted.
9441        h.state.publish = None;
9442        h.frame();
9443        assert_eq!(h.root.selection_toolbar(), None);
9444        assert_eq!(
9445            h.root.selection_toolbar_generation(),
9446            first_gen + 2,
9447            "going away is an edge too, or a shell never learns to dismiss"
9448        );
9449
9450        // ...and staying away is not.
9451        h.frame();
9452        assert_eq!(h.root.selection_toolbar_generation(), first_gen + 2);
9453    }
9454
9455    #[test]
9456    fn only_the_menu_flag_going_up_asks_a_shell_to_present() {
9457        let mut h = OverlayHarness::new(vec![]);
9458        h.down(30.0, 120.0);
9459        assert!(h.root.is_focus_active());
9460
9461        // A focused field with no bar up publishes all the same: the verbs are
9462        // the answer a platform responder chain needs for a hardware shortcut
9463        // that arrives with nothing on screen.
9464        h.state.publish = Some(toolbar_level(10.0));
9465        h.frame();
9466        assert_eq!(h.root.selection_toolbar(), Some(toolbar_level(10.0)));
9467        let level_gen = h.root.selection_toolbar_generation();
9468
9469        // Republishing the same level is not an edge, however many frames it
9470        // stands for.
9471        h.frame();
9472        h.frame();
9473        assert_eq!(h.root.selection_toolbar_generation(), level_gen);
9474
9475        // The gesture fires and the field asks for a menu: THAT is the edge.
9476        h.state.publish = Some(toolbar_request(10.0));
9477        h.frame();
9478        assert_eq!(
9479            h.root.selection_toolbar_generation(),
9480            level_gen + 1,
9481            "the flag going false to true is what presents the menu"
9482        );
9483
9484        // And dropping it again is the dismiss edge, with the field still
9485        // focused and still publishing its verbs.
9486        h.state.publish = Some(toolbar_level(10.0));
9487        h.frame();
9488        assert!(h.root.selection_toolbar().is_some());
9489        assert_eq!(h.root.selection_toolbar_generation(), level_gen + 2);
9490    }
9491
9492    #[test]
9493    fn a_pass_that_publishes_nothing_still_moves_the_generation() {
9494        // `RenderRoot::paint` resolves a pass nobody published in to `None`,
9495        // which reads as "no menu, no verbs" and must differ from whatever
9496        // stood — otherwise a shell holding a presented menu never learns to
9497        // put it away.
9498        let mut h = OverlayHarness::new(vec![]);
9499        h.down(30.0, 120.0);
9500        h.state.publish = Some(toolbar_request(10.0));
9501        h.frame();
9502        let standing = h.root.selection_toolbar_generation();
9503
9504        h.state.publish = None;
9505        h.frame();
9506        assert_eq!(h.root.selection_toolbar(), None);
9507        assert_eq!(h.root.selection_toolbar_generation(), standing + 1);
9508    }
9509
9510    #[test]
9511    fn a_blur_clears_the_selection_toolbar_on_the_event_pass() {
9512        let mut h = OverlayHarness::new(vec![]);
9513        h.down(30.0, 120.0);
9514        h.state.publish = Some(toolbar_request(10.0));
9515        h.frame();
9516        assert!(h.root.selection_toolbar().is_some());
9517        let gen_before = h.root.selection_toolbar_generation();
9518
9519        // A press on chrome that claims no focus ends the session — and the menu
9520        // must go with it immediately, not a frame later.
9521        h.down(150.0, 30.0);
9522        assert!(!h.root.is_focus_active());
9523        assert_eq!(
9524            h.root.selection_toolbar(),
9525            None,
9526            "a toolbar cannot outlive the focus session its selection belonged to"
9527        );
9528        assert_eq!(h.root.selection_toolbar_generation(), gen_before + 1);
9529
9530        // And a field still publishing into the blurred session cannot resurrect
9531        // it — the same refusal the IME republish makes.
9532        h.frame();
9533        assert_eq!(h.root.selection_toolbar(), None);
9534    }
9535}
9536
9537/// The root half of the multi-contact contract (`InputEvent::PointerContact`):
9538/// how a contact's id, the claimant latch and the `capture_contacts` opt-in
9539/// decide where an event goes.
9540#[cfg(test)]
9541mod contact_tests {
9542    use super::*;
9543
9544    /// What every probe saw: which probe, which contact, which phase.
9545    #[derive(Default)]
9546    struct Log {
9547        seen: Vec<(char, PointerId, PointerPhase)>,
9548    }
9549
9550    /// A leaf that logs every pointer event it receives with the contact id its
9551    /// context reports, and on a `Down` captures (optionally opting into the
9552    /// gesture's other contacts) and claims focus.
9553    struct Probe {
9554        tag: char,
9555        opt_in: bool,
9556    }
9557    impl crate::widget::Widget for Probe {
9558        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9559            bc.constrain(Size::new(40.0, 20.0))
9560        }
9561        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
9562        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9563            let InputEvent::Pointer(p) = event else {
9564                return EventResult::Ignored;
9565            };
9566            let id = ctx.pointer_id();
9567            ctx.state_mut::<Log>().seen.push((self.tag, id, p.phase));
9568            if p.phase == PointerPhase::Down {
9569                ctx.capture_pointer();
9570                if self.opt_in {
9571                    ctx.capture_contacts();
9572                }
9573                ctx.request_focus();
9574            }
9575            EventResult::Handled
9576        }
9577    }
9578
9579    /// Two probes stacked vertically (A at y 0..20, B at y 30..50) behind the
9580    /// standard recorded-path routing: a captured gesture goes straight to the
9581    /// active child and releases it on `Up`/`Cancel`; anything else is
9582    /// hit-tested, and a `Down` that hits nothing blurs both.
9583    struct Pair {
9584        a: crate::widget::ChildPod,
9585        b: crate::widget::ChildPod,
9586    }
9587    impl crate::widget::Widget for Pair {
9588        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9589            self.a.layout_child(ctx, bc);
9590            self.a.set_origin(Point::new(0.0, 0.0));
9591            self.b.layout_child(ctx, bc);
9592            self.b.set_origin(Point::new(0.0, 30.0));
9593            bc.max()
9594        }
9595        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9596            self.a.paint_child(ctx, scene);
9597            self.b.paint_child(ctx, scene);
9598        }
9599        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9600            let releases = matches!(
9601                event,
9602                InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
9603            );
9604            for pod in [&mut self.a, &mut self.b] {
9605                if pod.is_active() {
9606                    let result = pod.event_child(ctx, event);
9607                    if releases {
9608                        pod.set_active(false);
9609                    }
9610                    return result;
9611                }
9612            }
9613            let pos = event.position();
9614            let down = matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Down);
9615            for pod in [&mut self.a, &mut self.b] {
9616                if pod.contains(pos) {
9617                    return pod.event_child(ctx, event);
9618                }
9619            }
9620            if down {
9621                self.a.set_focused(false);
9622                self.b.set_focused(false);
9623            }
9624            EventResult::Ignored
9625        }
9626    }
9627
9628    struct PairView {
9629        opt_in: bool,
9630    }
9631    impl View<Log> for PairView {
9632        type Element = Pair;
9633        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Pair {
9634            Pair {
9635                a: crate::widget::ChildPod::new(Box::new(Probe {
9636                    tag: 'A',
9637                    opt_in: self.opt_in,
9638                })),
9639                b: crate::widget::ChildPod::new(Box::new(Probe {
9640                    tag: 'B',
9641                    opt_in: self.opt_in,
9642                })),
9643            }
9644        }
9645        fn rebuild(&self, _p: &Self, _e: &mut Pair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
9646            ChangeFlags::NONE
9647        }
9648    }
9649
9650    fn root_with(opt_in: bool) -> (RenderRoot<Log, PairView>, Log) {
9651        let mut root: RenderRoot<Log, PairView> = RenderRoot::new();
9652        let mut log = Log::default();
9653        root.rebuild(&mut |_| PairView { opt_in }, &mut log);
9654        root.layout(Size::new(200.0, 200.0));
9655        (root, log)
9656    }
9657
9658    fn ev(phase: PointerPhase, x: f64, y: f64) -> PointerEvent {
9659        PointerEvent {
9660            phase,
9661            position: Point::new(x, y),
9662            button: PointerButton::Primary,
9663        }
9664    }
9665
9666    fn mouse(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
9667        InputEvent::Pointer(ev(phase, x, y))
9668    }
9669
9670    fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
9671        InputEvent::PointerContact {
9672            pointer_id: PointerId::touch(slot),
9673            event: ev(phase, x, y),
9674        }
9675    }
9676
9677    /// Positions: over A, over B, and over neither.
9678    const A: (f64, f64) = (10.0, 10.0);
9679    const B: (f64, f64) = (10.0, 40.0);
9680    const NOWHERE: (f64, f64) = (150.0, 150.0);
9681
9682    #[test]
9683    fn slot_zero_touch_takes_the_same_path_as_a_plain_pointer() {
9684        use PointerPhase::{Down, Move, Up};
9685        let script = [
9686            (Down, A),
9687            (Move, NOWHERE),
9688            (Up, NOWHERE),
9689            (Down, NOWHERE),
9690            (Move, B),
9691            (Down, B),
9692            (Up, B),
9693        ];
9694        let (mut via_pointer, mut pointer_log) = root_with(false);
9695        let (mut via_contact, mut contact_log) = root_with(false);
9696        for (phase, (x, y)) in script {
9697            let a = via_pointer.event(&mut pointer_log, &mouse(phase, x, y));
9698            let b = via_contact.event(&mut contact_log, &touch(0, phase, x, y));
9699            assert_eq!(a, b, "{phase:?} at ({x}, {y}): same outcome");
9700            assert_eq!(
9701                via_pointer.is_pointer_captured(),
9702                via_contact.is_pointer_captured()
9703            );
9704            assert_eq!(via_pointer.is_focus_active(), via_contact.is_focus_active());
9705            assert_eq!(via_pointer.is_hover_active(), via_contact.is_hover_active());
9706            assert_eq!(
9707                via_pointer.focus_ime_generation(),
9708                via_contact.focus_ime_generation()
9709            );
9710        }
9711        // Same widgets, same phases; only the reported id differs.
9712        let strip = |log: &Log| -> Vec<(char, PointerPhase)> {
9713            log.seen.iter().map(|(t, _, p)| (*t, *p)).collect()
9714        };
9715        assert_eq!(strip(&pointer_log), strip(&contact_log));
9716        assert!(
9717            pointer_log
9718                .seen
9719                .iter()
9720                .all(|(_, id, _)| *id == PointerId::MOUSE)
9721        );
9722        assert!(
9723            contact_log
9724                .seen
9725                .iter()
9726                .all(|(_, id, _)| *id == PointerId::touch(0))
9727        );
9728        // The capture the contact's Down took named it as the claimant.
9729        via_contact.event(&mut contact_log, &touch(0, Down, A.0, A.1));
9730        assert_eq!(
9731            via_contact.pointer_capture_claimant(),
9732            Some(PointerId::touch(0))
9733        );
9734    }
9735
9736    #[test]
9737    fn an_additional_contact_with_nothing_captured_is_dropped() {
9738        let (mut root, mut log) = root_with(true);
9739        let outcome = root.event(&mut log, &touch(1, PointerPhase::Down, A.0, A.1));
9740        assert_eq!(outcome, EventOutcome::default());
9741        assert!(log.seen.is_empty(), "no widget saw the slot-1 contact");
9742        assert!(!root.is_pointer_captured());
9743        assert!(!root.is_focus_active(), "a dropped contact claims nothing");
9744    }
9745
9746    #[test]
9747    fn an_opted_in_captor_receives_the_other_contacts_on_the_captured_path() {
9748        use PointerPhase::{Down, Move, Up};
9749        let (mut root, mut log) = root_with(true);
9750        root.event(&mut log, &touch(0, Down, A.0, A.1));
9751        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9752
9753        // The second finger lands over B, yet travels the captured path to A.
9754        let t1 = PointerId::touch(1);
9755        assert!(root.event(&mut log, &touch(1, Down, B.0, B.1)).handled);
9756        root.event(&mut log, &touch(1, Move, NOWHERE.0, NOWHERE.1));
9757        root.event(&mut log, &touch(1, Up, NOWHERE.0, NOWHERE.1));
9758        assert_eq!(
9759            log.seen[1..],
9760            [('A', t1, Down), ('A', t1, Move), ('A', t1, Up)],
9761            "A saw touch(1)'s whole contact, B saw nothing"
9762        );
9763        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9764    }
9765
9766    #[test]
9767    fn a_captor_that_did_not_opt_in_never_sees_the_other_contacts() {
9768        use PointerPhase::{Down, Move, Up};
9769        let (mut root, mut log) = root_with(false);
9770        root.event(&mut log, &touch(0, Down, A.0, A.1));
9771        for phase in [Down, Move, Up] {
9772            let outcome = root.event(&mut log, &touch(1, phase, B.0, B.1));
9773            assert_eq!(
9774                outcome,
9775                EventOutcome::default(),
9776                "touch(1) {phase:?} dropped"
9777            );
9778        }
9779        assert_eq!(log.seen, [('A', PointerId::touch(0), Down)]);
9780        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9781    }
9782
9783    #[test]
9784    fn a_touch_release_never_ends_a_mouse_capture_and_vice_versa() {
9785        use PointerPhase::{Cancel, Down, Move, Up};
9786        // A mouse drag holds A; a finger tapping elsewhere must not break it.
9787        let (mut root, mut log) = root_with(false);
9788        root.event(&mut log, &mouse(Down, A.0, A.1));
9789        root.event(&mut log, &touch(0, Down, B.0, B.1));
9790        root.event(&mut log, &touch(0, Up, B.0, B.1));
9791        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::MOUSE));
9792        root.event(&mut log, &mouse(Move, NOWHERE.0, NOWHERE.1));
9793        assert_eq!(
9794            log.seen.last(),
9795            Some(&('A', PointerId::MOUSE, Move)),
9796            "the drag still reaches its captor"
9797        );
9798        assert!(log.seen.iter().all(|(tag, _, _)| *tag == 'A'));
9799        root.event(&mut log, &mouse(Up, NOWHERE.0, NOWHERE.1));
9800        assert!(!root.is_pointer_captured());
9801
9802        // The mirror image: a touch drag holds B; the mouse cannot end it.
9803        let (mut root, mut log) = root_with(false);
9804        root.event(&mut log, &touch(0, Down, B.0, B.1));
9805        root.event(&mut log, &mouse(Up, A.0, A.1));
9806        root.event(&mut log, &mouse(Cancel, A.0, A.1));
9807        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9808        root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9809        assert_eq!(log.seen.last(), Some(&('B', PointerId::touch(0), Move)));
9810        root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9811        assert!(!root.is_pointer_captured());
9812    }
9813
9814    #[test]
9815    fn only_the_claimants_release_clears_the_latch() {
9816        use PointerPhase::{Cancel, Down, Move, Up};
9817        let (mut root, mut log) = root_with(true);
9818        root.event(&mut log, &touch(0, Down, A.0, A.1));
9819        // Another contact ends twice over — delivered, yet nothing releases:
9820        // not the root latch, and not the container's active link either.
9821        root.event(&mut log, &touch(1, Down, A.0, A.1));
9822        root.event(&mut log, &touch(1, Up, A.0, A.1));
9823        root.event(&mut log, &touch(2, Down, A.0, A.1));
9824        root.event(&mut log, &touch(2, Cancel, A.0, A.1));
9825        assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9826        // Still on the captured path: a claimant move far outside A reaches A.
9827        root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9828        assert_eq!(log.seen.last(), Some(&('A', PointerId::touch(0), Move)));
9829
9830        // The claimant's own release ends it; later contacts fall to rule (b).
9831        root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9832        assert!(!root.is_pointer_captured());
9833        let before = log.seen.len();
9834        root.event(&mut log, &touch(1, Move, A.0, A.1));
9835        assert_eq!(
9836            log.seen.len(),
9837            before,
9838            "touch(1) dropped once the gesture ended"
9839        );
9840        // And the container's link went with it: a fresh contact is hit-tested.
9841        root.event(&mut log, &touch(0, Down, B.0, B.1));
9842        assert_eq!(log.seen.last(), Some(&('B', PointerId::touch(0), Down)));
9843    }
9844
9845    #[test]
9846    fn a_non_claimant_contact_never_blurs_or_moves_hover() {
9847        use PointerPhase::{Down, Up};
9848        let (mut root, mut log) = root_with(true);
9849        root.event(&mut log, &touch(0, Down, A.0, A.1));
9850        assert!(root.is_focus_active());
9851        let generation = root.focus_ime_generation();
9852        root.event(&mut log, &touch(1, Down, NOWHERE.0, NOWHERE.1));
9853        root.event(&mut log, &touch(1, Up, NOWHERE.0, NOWHERE.1));
9854        assert!(
9855            root.is_focus_active(),
9856            "a second finger is not a tap outside"
9857        );
9858        assert_eq!(root.focus_ime_generation(), generation);
9859    }
9860
9861    /// A container in front of [`Pair`] with pointer handling of its own that
9862    /// another contact must never reach: it logs every pointer event it routes
9863    /// (tag `'G'`) and treats a `Down` outside its child as a tap outside — an
9864    /// explicit focus release. With `forwards_broadcasts` unset it drops every
9865    /// broadcast instead of forwarding it (a container the forward-only walk
9866    /// cannot pass).
9867    struct Guard {
9868        inner: crate::widget::ChildPod,
9869        forwards_broadcasts: bool,
9870    }
9871    impl crate::widget::Widget for Guard {
9872        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9873            self.inner
9874                .layout_child(ctx, &BoxConstraints::tight(Size::new(100.0, 60.0)));
9875            self.inner.set_origin(Point::ZERO);
9876            bc.max()
9877        }
9878        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9879            self.inner.paint_child(ctx, scene);
9880        }
9881        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9882            if event.is_broadcast() {
9883                if self.forwards_broadcasts {
9884                    self.inner.event_child(ctx, event);
9885                }
9886                return EventResult::Ignored;
9887            }
9888            let InputEvent::Pointer(p) = event else {
9889                return EventResult::Ignored;
9890            };
9891            let id = ctx.pointer_id();
9892            ctx.state_mut::<Log>().seen.push(('G', id, p.phase));
9893            let inside = self.inner.contains(p.position);
9894            let result = if self.inner.is_active() || inside {
9895                self.inner.event_child(ctx, event)
9896            } else {
9897                EventResult::Ignored
9898            };
9899            if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel) {
9900                self.inner.set_active(false);
9901            }
9902            if p.phase == PointerPhase::Down && !inside {
9903                ctx.release_focus();
9904            }
9905            result
9906        }
9907    }
9908
9909    struct GuardView {
9910        forwards_broadcasts: bool,
9911    }
9912    impl View<Log> for GuardView {
9913        type Element = Guard;
9914        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Guard {
9915            Guard {
9916                inner: crate::widget::ChildPod::new(Box::new(Pair {
9917                    a: crate::widget::ChildPod::new(Box::new(Probe {
9918                        tag: 'A',
9919                        opt_in: true,
9920                    })),
9921                    b: crate::widget::ChildPod::new(Box::new(Probe {
9922                        tag: 'B',
9923                        opt_in: true,
9924                    })),
9925                })),
9926                forwards_broadcasts: self.forwards_broadcasts,
9927            }
9928        }
9929        fn rebuild(&self, _p: &Self, _e: &mut Guard, _c: &mut BuildCtx<'_>) -> ChangeFlags {
9930            ChangeFlags::NONE
9931        }
9932    }
9933
9934    fn guarded_root(forwards_broadcasts: bool) -> (RenderRoot<Log, GuardView>, Log) {
9935        let mut root: RenderRoot<Log, GuardView> = RenderRoot::new();
9936        let mut log = Log::default();
9937        root.rebuild(
9938            &mut |_| GuardView {
9939                forwards_broadcasts,
9940            },
9941            &mut log,
9942        );
9943        root.layout(Size::new(200.0, 200.0));
9944        (root, log)
9945    }
9946
9947    #[test]
9948    fn another_contact_reaches_only_the_captor_and_moves_no_hover_or_focus() {
9949        use PointerPhase::{Down, Move, Up};
9950        let (mut root, mut log) = guarded_root(true);
9951        let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
9952        root.event(&mut log, &touch(0, Down, A.0, A.1));
9953        assert!(root.is_focus_active());
9954        assert_eq!(root.pointer_capture_claimant(), Some(t0));
9955        assert!(root.pointer_capture_contacts());
9956        let (focus_generation, hover) = (root.focus_ime_generation(), root.is_hover_active());
9957
9958        // A second finger landing outside the guard's child: delivered to A down
9959        // the captured path, while the guard — whose own `Down` handling would
9960        // release focus on a tap outside — never runs on it.
9961        assert!(
9962            root.event(&mut log, &touch(1, Down, NOWHERE.0, NOWHERE.1))
9963                .handled
9964        );
9965        root.event(&mut log, &touch(1, Move, B.0, B.1));
9966        root.event(&mut log, &touch(1, Up, B.0, B.1));
9967        assert_eq!(
9968            log.seen,
9969            [
9970                ('G', t0, Down),
9971                ('A', t0, Down),
9972                ('A', t1, Down),
9973                ('A', t1, Move),
9974                ('A', t1, Up),
9975            ],
9976            "only the captor saw touch(1)"
9977        );
9978        assert!(root.is_focus_active(), "no tap outside was seen");
9979        assert_eq!(root.focus_ime_generation(), focus_generation);
9980        assert_eq!(root.is_hover_active(), hover);
9981        assert_eq!(root.pointer_capture_claimant(), Some(t0));
9982        assert!(root.pointer_capture_contacts());
9983
9984        // The claimant itself still takes the ordinary path through the guard.
9985        root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9986        assert_eq!(
9987            log.seen[5..],
9988            [('G', t0, Move), ('A', t0, Move)],
9989            "the claimant's own move runs every handler on the path"
9990        );
9991        root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9992        assert!(!root.is_pointer_captured());
9993        assert!(!root.pointer_capture_contacts());
9994    }
9995
9996    #[test]
9997    fn a_walk_a_container_cannot_pass_falls_back_to_the_ordinary_delivery() {
9998        use PointerPhase::Down;
9999        let (mut root, mut log) = guarded_root(false);
10000        let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
10001        root.event(&mut log, &touch(0, Down, A.0, A.1));
10002        // The guard drops the carrier, so it is handed the real event instead —
10003        // and the captor still receives it, down the captured path.
10004        assert!(root.event(&mut log, &touch(1, Down, B.0, B.1)).handled);
10005        assert_eq!(
10006            log.seen,
10007            [
10008                ('G', t0, Down),
10009                ('A', t0, Down),
10010                ('G', t1, Down),
10011                ('A', t1, Down),
10012            ]
10013        );
10014        assert_eq!(root.pointer_capture_claimant(), Some(t0));
10015    }
10016
10017    /// A root container that takes a gesture over from its child once the
10018    /// claimant moves more than 10 px: it cancels the child and releases it
10019    /// through [`EventCtx::release_captured_child`]. Logs every pointer event it
10020    /// sees (tag `'T'`); with `opt_in` it also opts into the gesture's other
10021    /// contacts itself, ahead of its child, on the `Down`.
10022    struct TakeOver {
10023        inner: crate::widget::ChildPod,
10024        opt_in: bool,
10025        down_at: Option<Point>,
10026        released_seen: bool,
10027    }
10028    impl crate::widget::Widget for TakeOver {
10029        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
10030            self.inner.layout_child(ctx, bc);
10031            self.inner.set_origin(Point::ZERO);
10032            bc.max()
10033        }
10034        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
10035            self.inner.paint_child(ctx, scene);
10036        }
10037        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
10038            if event.is_broadcast() {
10039                self.inner.event_child(ctx, event);
10040                return EventResult::Ignored;
10041            }
10042            let InputEvent::Pointer(p) = event else {
10043                return EventResult::Ignored;
10044            };
10045            let id = ctx.pointer_id();
10046            ctx.state_mut::<Log>().seen.push(('T', id, p.phase));
10047            match p.phase {
10048                PointerPhase::Down if self.down_at.is_none() => {
10049                    self.down_at = Some(p.position);
10050                    ctx.capture_pointer();
10051                    if self.opt_in {
10052                        ctx.capture_contacts();
10053                    }
10054                    if self.inner.contains(p.position) {
10055                        self.inner.event_child(ctx, event);
10056                    }
10057                }
10058                PointerPhase::Move => {
10059                    let moved = self.down_at.map(|at| (p.position - at).hypot());
10060                    if self.inner.is_active() && moved.is_some_and(|d| d > 10.0) {
10061                        let cancel = InputEvent::Pointer(PointerEvent {
10062                            phase: PointerPhase::Cancel,
10063                            ..*p
10064                        });
10065                        self.inner.event_child(ctx, &cancel);
10066                        ctx.release_captured_child(&mut self.inner);
10067                        self.released_seen = ctx.is_capture_released();
10068                    } else if self.inner.is_active() {
10069                        self.inner.event_child(ctx, event);
10070                    }
10071                }
10072                PointerPhase::Up | PointerPhase::Cancel => {
10073                    if self.inner.is_active() {
10074                        self.inner.event_child(ctx, event);
10075                        self.inner.set_active(false);
10076                    }
10077                    self.down_at = None;
10078                }
10079                PointerPhase::Down => {}
10080            }
10081            EventResult::Handled
10082        }
10083    }
10084
10085    struct TakeOverView {
10086        opt_in: bool,
10087    }
10088    impl View<Log> for TakeOverView {
10089        type Element = TakeOver;
10090        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TakeOver {
10091            TakeOver {
10092                inner: crate::widget::ChildPod::new(Box::new(Probe {
10093                    tag: 'A',
10094                    opt_in: true,
10095                })),
10096                opt_in: self.opt_in,
10097                down_at: None,
10098                released_seen: false,
10099            }
10100        }
10101        fn rebuild(&self, _p: &Self, _e: &mut TakeOver, _c: &mut BuildCtx<'_>) -> ChangeFlags {
10102            ChangeFlags::NONE
10103        }
10104    }
10105
10106    fn take_over_root(opt_in: bool) -> (RenderRoot<Log, TakeOverView>, Log) {
10107        let mut root: RenderRoot<Log, TakeOverView> = RenderRoot::new();
10108        let mut log = Log::default();
10109        root.rebuild(&mut |_| TakeOverView { opt_in }, &mut log);
10110        root.layout(Size::new(200.0, 200.0));
10111        (root, log)
10112    }
10113
10114    fn take_over_widget(root: &mut RenderRoot<Log, TakeOverView>) -> &mut TakeOver {
10115        let id = root.root_id().expect("root built");
10116        root.tree
10117            .pod_mut(id)
10118            .expect("root pod")
10119            .widget_mut()
10120            .downcast_mut::<TakeOver>()
10121            .expect("root is a TakeOver")
10122    }
10123
10124    #[test]
10125    fn a_takeover_ends_the_contact_opt_in_but_keeps_the_claimant() {
10126        use PointerPhase::{Cancel, Down, Move, Up};
10127        let (mut root, mut log) = take_over_root(false);
10128        let t0 = PointerId::touch(0);
10129        root.event(&mut log, &touch(0, Down, A.0, A.1));
10130        assert_eq!(root.pointer_capture_claimant(), Some(t0));
10131        assert!(root.pointer_capture_contacts(), "A opted in");
10132
10133        // The claimant moves past the container's threshold: it cancels A and
10134        // releases it, and the release reaches the root.
10135        root.event(&mut log, &touch(0, Move, A.0, A.1 + 30.0));
10136        assert!(take_over_widget(&mut root).released_seen);
10137        assert_eq!(log.seen[3], ('A', t0, Cancel));
10138        assert_eq!(
10139            root.pointer_capture_claimant(),
10140            Some(t0),
10141            "the gesture is still the claimant's, now held by the container"
10142        );
10143        assert!(
10144            !root.pointer_capture_contacts(),
10145            "the widget that asked for the other contacts is gone"
10146        );
10147
10148        // Another finger is dropped from here on — neither the container nor
10149        // the cancelled captor hears it.
10150        let before = log.seen.len();
10151        let outcome = root.event(&mut log, &touch(1, Down, A.0, A.1));
10152        assert_eq!(outcome, EventOutcome::default());
10153        assert_eq!(log.seen.len(), before);
10154
10155        // The claimant keeps driving the container, and its release ends it.
10156        root.event(&mut log, &touch(0, Move, A.0, A.1 + 60.0));
10157        assert_eq!(log.seen.last(), Some(&('T', t0, Move)));
10158        root.event(&mut log, &touch(0, Up, A.0, A.1 + 60.0));
10159        assert!(!root.is_pointer_captured());
10160    }
10161
10162    #[test]
10163    fn a_takeover_by_the_widget_holding_the_opt_in_keeps_it() {
10164        use PointerPhase::{Down, Move};
10165        let (mut root, mut log) = take_over_root(true);
10166        let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
10167        root.event(&mut log, &touch(0, Down, A.0, A.1));
10168        root.event(&mut log, &touch(0, Move, A.0, A.1 + 30.0));
10169        assert!(
10170            !take_over_widget(&mut root).released_seen,
10171            "the container itself holds the opt-in, so releasing its child ends nothing"
10172        );
10173        assert!(root.pointer_capture_contacts());
10174        // The container is the captor: another finger reaches it directly.
10175        assert!(root.event(&mut log, &touch(1, Down, A.0, A.1)).handled);
10176        assert_eq!(log.seen.last(), Some(&('T', t1, Down)));
10177        assert_eq!(root.pointer_capture_claimant(), Some(t0));
10178    }
10179}
10180
10181/// [`InputEvent::Scale`]: hit-tested by [`ScaleEvent::focal`] exactly like
10182/// [`InputEvent::Scroll`], translated down the tree the same way, and bubbles
10183/// topmost-first until a widget reports [`EventResult::Handled`].
10184#[cfg(test)]
10185mod scale_tests {
10186    use super::*;
10187    use crate::event::{ScaleEvent, ScalePhase};
10188
10189    /// What every leaf saw: which leaf, which event.
10190    #[derive(Default)]
10191    struct Log {
10192        seen: Vec<(char, ScaleEvent)>,
10193    }
10194
10195    /// Logs every [`InputEvent::Scale`] it receives, in its own local space,
10196    /// and reports `Handled` only when `handles` is set — everything else
10197    /// (including a non-`Scale` event) is ignored.
10198    struct ScaleLeaf {
10199        tag: char,
10200        handles: bool,
10201    }
10202    impl crate::widget::Widget for ScaleLeaf {
10203        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
10204            bc.constrain(Size::new(40.0, 40.0))
10205        }
10206        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
10207        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
10208            let InputEvent::Scale(scale) = event else {
10209                return EventResult::Ignored;
10210            };
10211            ctx.state_mut::<Log>().seen.push((self.tag, *scale));
10212            if self.handles {
10213                EventResult::Handled
10214            } else {
10215                EventResult::Ignored
10216            }
10217        }
10218    }
10219
10220    /// Two leaves at identical bounds `(0, 0)..(40, 40)` — `top` painted (and
10221    /// hit-tested) before `bottom`, mirroring `frust-widgets::route_event`'s
10222    /// topmost-first, fall-through-on-`Ignored` hit test: a `Scale` landing in
10223    /// the shared rect reaches `top` first, and only reaches `bottom` if `top`
10224    /// ignores it.
10225    struct Overlapping {
10226        top: crate::widget::ChildPod,
10227        bottom: crate::widget::ChildPod,
10228    }
10229    impl crate::widget::Widget for Overlapping {
10230        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
10231            self.top.layout_child(ctx, bc);
10232            self.top.set_origin(Point::ZERO);
10233            self.bottom.layout_child(ctx, bc);
10234            self.bottom.set_origin(Point::ZERO);
10235            bc.max()
10236        }
10237        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
10238            self.bottom.paint_child(ctx, scene);
10239            self.top.paint_child(ctx, scene);
10240        }
10241        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
10242            let pos = event.position();
10243            for pod in [&mut self.top, &mut self.bottom] {
10244                if pod.contains(pos) && pod.event_child(ctx, event) == EventResult::Handled {
10245                    return EventResult::Handled;
10246                }
10247            }
10248            EventResult::Ignored
10249        }
10250    }
10251
10252    /// Wraps [`Overlapping`] one container deeper, offset by `(10, 20)` — so
10253    /// the fixture also proves [`InputEvent::translated`] shifts a `Scale`
10254    /// event's focal point correctly across a container boundary.
10255    struct Offset {
10256        inner: crate::widget::ChildPod,
10257    }
10258    impl crate::widget::Widget for Offset {
10259        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
10260            self.inner.layout_child(ctx, bc);
10261            self.inner.set_origin(Point::new(10.0, 20.0));
10262            bc.max()
10263        }
10264        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
10265            self.inner.paint_child(ctx, scene);
10266        }
10267        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
10268            let pos = event.position();
10269            if self.inner.contains(pos) {
10270                return self.inner.event_child(ctx, event);
10271            }
10272            EventResult::Ignored
10273        }
10274    }
10275
10276    struct OffsetView {
10277        top_handles: bool,
10278    }
10279    impl View<Log> for OffsetView {
10280        type Element = Offset;
10281        fn build(&self, _ctx: &mut BuildCtx<'_>) -> Offset {
10282            Offset {
10283                inner: crate::widget::ChildPod::new(Box::new(Overlapping {
10284                    top: crate::widget::ChildPod::new(Box::new(ScaleLeaf {
10285                        tag: 'T',
10286                        handles: self.top_handles,
10287                    })),
10288                    bottom: crate::widget::ChildPod::new(Box::new(ScaleLeaf {
10289                        tag: 'B',
10290                        handles: true,
10291                    })),
10292                })),
10293            }
10294        }
10295        fn rebuild(
10296            &self,
10297            _prev: &Self,
10298            _element: &mut Offset,
10299            _ctx: &mut BuildCtx<'_>,
10300        ) -> ChangeFlags {
10301            ChangeFlags::NONE
10302        }
10303    }
10304
10305    fn root_with(top_handles: bool) -> (RenderRoot<Log, OffsetView>, Log) {
10306        let mut root: RenderRoot<Log, OffsetView> = RenderRoot::new();
10307        let mut log = Log::default();
10308        root.rebuild(&mut |_| OffsetView { top_handles }, &mut log);
10309        root.layout(Size::new(200.0, 200.0));
10310        (root, log)
10311    }
10312
10313    fn scale(phase: ScalePhase, scale_delta: f64, x: f64, y: f64) -> InputEvent {
10314        InputEvent::Scale(ScaleEvent {
10315            phase,
10316            scale_delta,
10317            focal: Point::new(x, y),
10318            velocity: 0.0,
10319        })
10320    }
10321
10322    #[test]
10323    fn scale_hit_tests_by_focal_point_and_translates_through_a_container() {
10324        let (mut root, mut log) = root_with(true);
10325
10326        // Window (5, 5): outside the offset container entirely (it starts at
10327        // (10, 20)) — nothing is hit, nothing logged.
10328        let outside = root.event(&mut log, &scale(ScalePhase::Begin, 1.1, 5.0, 5.0));
10329        assert!(
10330            log.seen.is_empty(),
10331            "a focal point outside every pod hits nothing"
10332        );
10333        assert!(!outside.handled);
10334
10335        // Window (30, 40): inside the container, local (20, 20) once the
10336        // Offset container's (10, 20) origin is subtracted by
10337        // `InputEvent::translated` — squarely inside both overlapping 40x40
10338        // leaves, so the topmost one (`top`) is the one that sees it.
10339        let inside = root.event(&mut log, &scale(ScalePhase::Update, 1.2, 30.0, 40.0));
10340        assert!(inside.handled);
10341        assert_eq!(log.seen.len(), 1, "the topmost leaf alone handled it");
10342        let (tag, seen) = log.seen[0];
10343        assert_eq!(tag, 'T');
10344        assert_eq!(
10345            seen.focal,
10346            Point::new(20.0, 20.0),
10347            "translated() shifted the focal point into the container's local space"
10348        );
10349        assert_eq!(seen.scale_delta, 1.2);
10350        assert_eq!(seen.phase, ScalePhase::Update);
10351    }
10352
10353    #[test]
10354    fn an_ignored_scale_bubbles_to_the_next_hit_widget() {
10355        // `top` ignores every `Scale` it sees; `bottom`, at the identical
10356        // bounds, still handles it — proving the hit test falls through to
10357        // the next topmost-first candidate instead of stopping (and
10358        // swallowing the event) at the first hit.
10359        let (mut root, mut log) = root_with(false);
10360        let outcome = root.event(&mut log, &scale(ScalePhase::Begin, 0.9, 30.0, 40.0));
10361        assert!(outcome.handled, "the bottom leaf still handled it");
10362        assert_eq!(
10363            log.seen.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
10364            vec!['T', 'B'],
10365            "the ignoring top leaf saw it first, and bottom is what it bubbled to"
10366        );
10367    }
10368}
10369
10370/// The [`RenderRoot::event`] file-drag follow-up: every `Drop`/`Cancel`,
10371/// handled or not, is followed by an `Ended` broadcast.
10372#[cfg(test)]
10373mod file_drop_tests {
10374    use super::*;
10375
10376    /// Every `FileDrop` phase a leaf saw, in order, and whether the leaf
10377    /// handles the hit-tested phases.
10378    #[derive(Default)]
10379    struct DropLog {
10380        handles: bool,
10381        seen: Vec<(char, FileDropPhase)>,
10382    }
10383
10384    struct DropLeaf {
10385        tag: char,
10386    }
10387    impl crate::widget::Widget for DropLeaf {
10388        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
10389            bc.constrain(Size::new(40.0, 40.0))
10390        }
10391        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
10392        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
10393            let InputEvent::FileDrop(drop) = event else {
10394                return EventResult::Ignored;
10395            };
10396            let log = ctx.state_mut::<DropLog>();
10397            log.seen.push((self.tag, drop.phase));
10398            if log.handles && !event.is_broadcast() {
10399                EventResult::Handled
10400            } else {
10401                EventResult::Ignored
10402            }
10403        }
10404    }
10405
10406    /// One 40x40 leaf at the origin of a 200x200 root that routes like
10407    /// `frust-widgets`' helpers: a broadcast to the child unconditionally,
10408    /// anything else only on a hit.
10409    struct DropRoot {
10410        leaf: crate::widget::ChildPod,
10411    }
10412    impl crate::widget::Widget for DropRoot {
10413        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
10414            self.leaf.layout_child(ctx, bc);
10415            self.leaf.set_origin(Point::ZERO);
10416            bc.max()
10417        }
10418        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
10419            self.leaf.paint_child(ctx, scene);
10420        }
10421        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
10422            if event.is_broadcast() {
10423                self.leaf.event_child(ctx, event);
10424                return EventResult::Ignored;
10425            }
10426            if self.leaf.contains(event.position()) {
10427                return self.leaf.event_child(ctx, event);
10428            }
10429            EventResult::Ignored
10430        }
10431    }
10432
10433    struct DropRootView;
10434    impl View<DropLog> for DropRootView {
10435        type Element = DropRoot;
10436        fn build(&self, _ctx: &mut BuildCtx<'_>) -> DropRoot {
10437            DropRoot {
10438                leaf: crate::widget::ChildPod::new(Box::new(DropLeaf { tag: 'L' })),
10439            }
10440        }
10441        fn rebuild(
10442            &self,
10443            _prev: &Self,
10444            _element: &mut DropRoot,
10445            _ctx: &mut BuildCtx<'_>,
10446        ) -> ChangeFlags {
10447            ChangeFlags::NONE
10448        }
10449    }
10450
10451    fn drop_root(handles: bool) -> (RenderRoot<DropLog, DropRootView>, DropLog) {
10452        let mut root: RenderRoot<DropLog, DropRootView> = RenderRoot::new();
10453        let mut log = DropLog {
10454            handles,
10455            seen: Vec::new(),
10456        };
10457        root.rebuild(&mut |_| DropRootView, &mut log);
10458        root.layout(Size::new(200.0, 200.0));
10459        (root, log)
10460    }
10461
10462    fn file_drop(phase: FileDropPhase, x: f64, y: f64) -> InputEvent {
10463        InputEvent::FileDrop(FileDropEvent {
10464            phase,
10465            position: Point::new(x, y),
10466            paths: Vec::new(),
10467        })
10468    }
10469
10470    #[test]
10471    fn an_unhandled_file_drop_end_is_re_broadcast_to_the_whole_tree() {
10472        for phase in [FileDropPhase::Drop, FileDropPhase::Cancel] {
10473            let (mut root, mut log) = drop_root(false);
10474            // Far outside the leaf: the hit-tested dispatch reaches nothing.
10475            let outcome = root.event(&mut log, &file_drop(phase, 150.0, 150.0));
10476            assert!(!outcome.handled, "a broadcast is never consumed");
10477            assert_eq!(
10478                log.seen,
10479                vec![('L', FileDropPhase::Ended)],
10480                "the {phase:?} that missed the leaf is followed by an Ended broadcast it hears"
10481            );
10482
10483            // Over the leaf but ignored by it: the leaf hears the hit-tested
10484            // phase, then Ended.
10485            log.seen.clear();
10486            root.event(&mut log, &file_drop(phase, 10.0, 10.0));
10487            assert_eq!(log.seen, vec![('L', phase), ('L', FileDropPhase::Ended)]);
10488        }
10489    }
10490
10491    #[test]
10492    fn a_handled_file_drop_end_is_still_re_broadcast_to_the_whole_tree() {
10493        let (mut root, mut log) = drop_root(true);
10494        for phase in [FileDropPhase::Drop, FileDropPhase::Cancel] {
10495            log.seen.clear();
10496            let outcome = root.event(&mut log, &file_drop(phase, 10.0, 10.0));
10497            assert!(outcome.handled, "the hit-tested {phase:?} was handled");
10498            assert_eq!(
10499                log.seen,
10500                vec![('L', phase), ('L', FileDropPhase::Ended)],
10501                "and is still followed by the Ended broadcast"
10502            );
10503        }
10504    }
10505
10506    #[test]
10507    fn a_hover_and_a_direct_ended_get_no_follow_up() {
10508        let (mut root, mut log) = drop_root(false);
10509        root.event(&mut log, &file_drop(FileDropPhase::Hover, 150.0, 150.0));
10510        assert!(log.seen.is_empty(), "a Hover is never re-broadcast");
10511
10512        root.event(&mut log, &file_drop(FileDropPhase::Ended, 0.0, 0.0));
10513        assert_eq!(
10514            log.seen,
10515            vec![('L', FileDropPhase::Ended)],
10516            "an Ended dispatched directly is delivered once, not followed again"
10517        );
10518    }
10519}