frust_core/app.rs
1//! The render root: the object each platform shell drives each frame.
2//!
3//! It owns the widget [`WidgetTree`] and the previous [`View`], and exposes the
4//! three framework passes in Masonry order (the subset relevant to v0):
5//!
6//! * [`RenderRoot::rebuild`] — run the root build closure, diff against the previous view,
7//! producing/mutating the retained widget.
8//! * [`RenderRoot::layout`] — hand the root widget window-sized constraints and
9//! record the size it returns.
10//! * [`RenderRoot::paint`] — emit the root widget's draw commands into a scene.
11//!
12//! v0 is single-root: the root component's build closure returns one `impl View<State>` whose concrete
13//! type is fixed, so the root's previous view and element are stored typed.
14//! ViewSequence / multiple children are explicitly out of scope for now.
15
16use std::any::Any;
17use std::cell::Cell;
18use std::num::NonZeroU64;
19use std::sync::atomic::{AtomicU64, Ordering};
20
21use kurbo::{Point, Rect, Size};
22
23use crate::anim::FrameTime;
24use crate::event::{
25 ContactFrame, ContactPass, CursorIcon, EventCtx, EventOutcome, EventResult, ImeState,
26 InputEvent, OverlayEvent, OverlayEventKind, PointerButton, PointerEvent, PointerId,
27 PointerPhase, RequestPass,
28};
29use crate::insets::WindowInsets;
30use crate::layout::BoxConstraints;
31use crate::overlay::{
32 OutsideTap, OverlayEntry, OverlayHit, OverlayInput, OverlayKey, OverlayPaintPass,
33 sort_into_paint_order,
34};
35use crate::selection_toolbar::{
36 SelectionToolbarActions, SelectionToolbarPass, SelectionToolbarRequest,
37};
38use crate::semantics::{ROOT_NODE_ID, SemanticsCtx, SemanticsUpdate};
39use crate::tree::{InspectNode, WidgetPod, WidgetTree};
40use crate::view::{BuildCtx, ChangeFlags, View, WidgetId};
41use crate::widget::{LayoutCtx, PaintCtx, PaintOutcome, PaintScene, PlatformViewFrame};
42
43/// The window's shape and platform-occlusion state, delivered to app code as a
44/// plain [`provide_context`](reactive_graph::owner::provide_context)-carried
45/// value — logical size, device-pixel scale, a
46/// [derived](Orientation::from_size) orientation, and the current
47/// [`WindowInsets`].
48///
49/// # Plain value, not a signal
50///
51/// `WindowMetrics` is delivered exactly like `Theme` and [`WindowInsets`]
52/// already are: a shell calls `provide_context` with a freshly-built value on
53/// change, and app code recovers it with `use_context::<WindowMetrics>()`
54/// inside `Component::build`. It is **not** an `RwSignal` — only `deep_link`
55/// and `back` are true signals in `frust-reactive`; every other host-signal
56/// carrier (theme, insets, and now this) is a re-provided plain value.
57///
58/// # Alongside `WindowInsets`, not superseding it
59///
60/// `WindowInsets` already reaches `Component::build` on Android and iOS today
61/// (each shell's `push_insets` calls `provide_context(insets)` independently
62/// of anything here — desktop has no such arm for either value yet).
63/// `WindowMetrics` is additive: a shell that starts providing it keeps
64/// providing the standalone `WindowInsets` context too, so an existing
65/// `use_context::<WindowInsets>()` call site never breaks. `insets` on this
66/// type is a **copy** of that same value for convenience (a widget laying
67/// itself out around window shape wants size/scale/orientation/insets
68/// together), not a replacement for the independent context.
69///
70/// # Orientation is derived, not platform-sourced
71///
72/// No platform callback in either mobile shell carries an orientation enum —
73/// Android's `nativeOnSurfaceChanged` and iOS's `frust_resize` each hand the
74/// shell only a `(width, height, scale)` triple. [`Orientation`] is therefore
75/// always computed from `size` via [`Orientation::from_size`]
76/// (portrait when `height >= width`, so an exact square reads as portrait);
77/// it never tracks a device orientation-lock setting or a platform rotation
78/// event directly.
79///
80/// # Context is not reactive
81///
82/// `provide_context` is a plain insert into the owner's context map — it
83/// notifies nothing — and `use_context` inside `Component::build` (or the
84/// root build closure) creates no subscription, so re-providing a changed
85/// `WindowMetrics` does not itself mark anything dirty or wake a frame. A new
86/// value becomes visible only on the next rebuild, which the resize or inset
87/// change that produced it already drives; do not write a shell that assumes
88/// a `provide_context` write triggers one. A shell wiring this up (see
89/// `docs/SHELLS_ARCHITECTURE.md`) must still re-provide `WindowMetrics` only
90/// on an actual change (mirroring `RenderRoot::set_insets`'s
91/// `PartialEq`-guarded no-op) — the reason is cost at the FFI boundary (a
92/// lock write plus an allocation every frame), not a rebuild storm.
93/// Separately, there is no per-component rebuild skipping in this framework
94/// (a component always re-runs `build` on any rebuild it does take part in),
95/// which is affordable only because builds are cheap by construction.
96#[derive(Clone, Copy, Debug, PartialEq)]
97pub struct WindowMetrics {
98 /// The window's logical (density-independent) size.
99 pub size: Size,
100 /// The device-pixel scale factor (logical → physical px multiplier).
101 pub scale: f64,
102 /// The orientation derived from `size` — see the type's doc for why this
103 /// is computed, never platform-sourced.
104 pub orientation: Orientation,
105 /// A copy of the window's current insets — see the type's doc for why
106 /// this does not replace the standalone `WindowInsets` context.
107 pub insets: WindowInsets,
108}
109
110impl WindowMetrics {
111 /// Construct a [`WindowMetrics`] from its transported fields, deriving
112 /// [`orientation`](Self::orientation) from `size` rather than accepting it
113 /// as an input — see the type's doc for why orientation is never
114 /// platform-sourced.
115 pub fn new(size: Size, scale: f64, insets: WindowInsets) -> Self {
116 Self {
117 size,
118 scale,
119 orientation: Orientation::from_size(size),
120 insets,
121 }
122 }
123}
124
125/// A window's derived portrait/landscape orientation.
126///
127/// Always computed from a [`WindowMetrics::size`] via [`Orientation::from_size`]
128/// — see [`WindowMetrics`]'s doc for why no platform callback carries this as
129/// an enum directly.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum Orientation {
132 /// `size.height >= size.width`, including the exact-square case.
133 Portrait,
134 /// `size.height < size.width`.
135 Landscape,
136}
137
138impl Orientation {
139 /// Derives orientation from a logical window size: portrait when
140 /// `height >= width` (an exact square reads as portrait), landscape
141 /// otherwise.
142 pub fn from_size(size: Size) -> Self {
143 if size.height >= size.width {
144 Orientation::Portrait
145 } else {
146 Orientation::Landscape
147 }
148 }
149}
150
151/// How many [`InputEvent::Housekeeping`] flush passes one
152/// [`RenderRoot::rebuild`] will run before deferring the rest to the next frame.
153///
154/// A flushed pop-result callback may itself push or pop, queueing another
155/// callback — so the flush/re-diff cycle has to be allowed to iterate, but it
156/// must never be allowed to spin: a pair of callbacks that push each other would
157/// otherwise hang the frame. Three passes covers every shape observed in
158/// practice (a result that navigates once, and that page's own result), while
159/// keeping the worst case at four build-closure runs per frame — the closure is
160/// cheap by construction (see [`RenderRoot::rebuild`]).
161///
162/// Past the cap the mark stays raised and one more frame is requested, so the
163/// remaining work lands next frame instead of being lost.
164const MAX_PENDING_RESULT_FLUSH_PASSES: usize = 3;
165
166/// Change-guarded write of the shell-facing IME surface: replaces `slot` and
167/// bumps `generation` **only** when the value actually moves ([`ImeState`] is
168/// `PartialEq`).
169///
170/// A free function over the two fields rather than a `&mut self` method, so the
171/// change guard has exactly one implementation whatever borrows its caller
172/// happens to hold; [`RenderRoot::store_ime_state`] is the `&mut self` form, and
173/// is what every current caller goes through.
174///
175/// The change guard is load-bearing, not an optimisation: the paint pass
176/// re-publishes the focused widget's IME surface every frame, so an
177/// unconditional bump would make the shell's `focus_or_ime_changed` edge fire on
178/// every vsync for the whole life of a focus session — the level-input behavior
179/// the generation exists to replace.
180fn store_ime_state_in(slot: &mut Option<ImeState>, generation: &mut u64, next: Option<ImeState>) {
181 if *slot != next {
182 *slot = next;
183 *generation = generation.wrapping_add(1);
184 }
185}
186
187/// Release the whole focus/IME session: drop `focus_active` **and** the
188/// shell-facing surface together, bumping `generation` **exactly once** if
189/// either actually moved.
190///
191/// The paired form of [`RenderRoot::set_focus_active`]`(false)` +
192/// [`RenderRoot::store_ime_state`]`(None)`, and the single primitive every
193/// release site goes through — the blur-on-outside-tap `Down`, an explicit
194/// [`EventCtx::release_focus`](crate::event::EventCtx::release_focus), a widget
195/// publishing an *inactive* surface (see [`RenderRoot::paint`]), and the
196/// generic-unmount orphan drain in [`RenderRoot::rebuild`]. Keeping them on one
197/// primitive is what makes "a session ends" mean the same thing everywhere,
198/// rather than four hand-assembled pairs that can drift apart.
199///
200/// **One release is one edge.** The two field writers bump on each field's own
201/// change, so calling them in sequence would move
202/// [`focus_ime_generation`](RenderRoot::focus_ime_generation) *twice* for the
203/// ordinary release (focus `true`→`false` and surface `Some`→`None`). A shell
204/// only ever compares the value, so two bumps and one bump raise the same single
205/// `focus_or_ime_changed` edge — but a counter that moves once per observable
206/// transition is the contract the field doc states, and is what the release
207/// tests pin. The change guard itself is unchanged: an already-released root
208/// writes the same values back and moves nothing.
209///
210/// **A release ends the session's identity too.** On an actual move it advances
211/// `focus_epoch` and republishes it, which strands every recorded focus link at
212/// once — the chain the session ran through, and a floated surface's link that
213/// no container's blur sweep can reach. That is why the release has to own the
214/// epoch rather than leave it to the caller: a session cleared without moving
215/// its identity leaves links behind that still name it.
216///
217/// A free function over the fields (not a `&mut self` method) for the same
218/// reason [`store_ime_state_in`] is — one implementation of the contract,
219/// whatever borrows the caller holds. [`RenderRoot::release_focus_session`] is
220/// the method form, and is what every current caller goes through.
221fn release_focus_session_in(
222 focus_active: &mut bool,
223 ime_slot: &mut Option<ImeState>,
224 generation: &mut u64,
225 focus_epoch: &mut u64,
226 root_identity: u64,
227) {
228 let moved = *focus_active || ime_slot.is_some();
229 *focus_active = false;
230 *ime_slot = None;
231 if moved {
232 *generation = generation.wrapping_add(1);
233 // A session that ends strands every link recorded against it, wherever
234 // in (or off) the tree it sits — the one clearing sweep no container can
235 // be asked to run. Only on an actual release: a `Down` on already-blurred
236 // chrome is the commonest event there is and must move nothing.
237 *focus_epoch = advance_focus_epoch(*focus_epoch);
238 crate::widget::set_live_focus_session(root_identity, *focus_epoch, *focus_epoch);
239 }
240}
241
242/// The allocator behind [`RenderRoot::root_identity`], handing every root a
243/// value no other root shares.
244///
245/// Starts at `1` so `0` stays available as "no root" (a pod that has never held a
246/// claim, and the at-rest published hover link — see
247/// `crate::event::set_live_hover_link`).
248///
249/// A module-level static rather than an associated const/`static` inside the
250/// generic `impl`: the latter is monomorphized per `<State, V>` pair, which would
251/// hand two roots of different concrete types the same identity — exactly the
252/// collision this counter exists to remove. `Relaxed` is enough because the value
253/// is only ever compared for equality, never used to order anything.
254static NEXT_ROOT_IDENTITY: AtomicU64 = AtomicU64::new(1);
255
256/// The next focus epoch after `epoch`, skipping `0`.
257///
258/// `0` is reserved twice over — it is a never-claimed
259/// [`ChildPod`](crate::widget::ChildPod)'s stamp, and it is the epoch half of
260/// the `(0, 0)` pair a dispatch driven with no root at all compares against — so
261/// a root that wrapped onto it would hand every unclaimed pod in the tree a live
262/// link at once.
263fn advance_focus_epoch(epoch: u64) -> u64 {
264 match epoch.wrapping_add(1) {
265 0 => 1,
266 next => next,
267 }
268}
269
270/// What [`RenderRoot`]'s overlay pre-pass decided about one incoming event.
271///
272/// The pre-pass runs before anything else [`RenderRoot::event`] does, and has
273/// exactly two answers: the event belonged to a floated surface (or was swallowed
274/// by a modal light-dismiss) and the main tree must not see it, or it did not and
275/// today's dispatch continues. Both arms carry an [`EventOutcome`], because even
276/// the "continue" answer may already have produced one — an
277/// [`OutsideTap::Notify`]`{ consume: false }` surface is told about the press
278/// *and* lets it through, and the redraw that notification asked for must not be
279/// dropped on the floor when the main dispatch's own outcome replaces it.
280enum OverlayRoute {
281 /// The overlay layer consumed the event; return this outcome unchanged.
282 Consumed(EventOutcome),
283 /// The event continues into today's dispatch; merge this outcome into
284 /// whatever that produces.
285 Continue(EventOutcome),
286}
287
288/// Owns the retained tree and drives the rebuild/layout/paint passes for a
289/// single-root application.
290///
291/// Generic over the application `State` and the concrete root view type `V`
292/// returned by the build closure.
293pub struct RenderRoot<State: 'static, V: View<State>> {
294 tree: WidgetTree,
295 root_id: Option<WidgetId>,
296 /// The previous view, retained to diff against on the next rebuild.
297 prev_view: Option<V>,
298 /// Monotonic widget-id counter, borrowed by each `BuildCtx`.
299 next_id: u64,
300 window_size: Size,
301 /// The **claimant** of the pointer capture in flight, if any: the contact
302 /// whose `Down` requested capture. Set on that `Down`, cleared only by the
303 /// claimant's own `Up`/`Cancel` — another contact's release never touches
304 /// it (rule (c) of [`InputEvent::PointerContact`]'s multi-contact contract).
305 /// Root-level mirror of the per-container `active` path bookkeeping, which
306 /// is keyed on the same claimant (see [`crate::widget::ChildPod::set_active`]).
307 capture_claimant: Option<PointerId>,
308 /// Whether the live capture's captor opted into the gesture's other
309 /// contacts ([`EventCtx::capture_contacts`]) on the `Down` it captured
310 /// with. Meaningless — and kept `false` — while nothing is captured.
311 /// Cleared early when a container takes the gesture over from that captor
312 /// ([`EventCtx::release_captured_child`]).
313 capture_contacts: bool,
314 /// Whether the live opt-in was made by the root widget itself rather than
315 /// by a pod below it — in which case a non-claimant contact is handed to the
316 /// root widget directly instead of walking the active path (see
317 /// [`crate::widget::ChildPod::event_child`]). Same lifetime as
318 /// `capture_contacts`.
319 contacts_captor_is_root: bool,
320 /// Whether some widget in the tree currently holds focus. Root-level mirror of
321 /// the per-container `focused` path bookkeeping (the focus analog of
322 /// `capture_claimant`): set when a dispatch requested focus, cleared by a
323 /// session release — a blur-on-outside-tap `Down`, an explicit focus release,
324 /// a widget publishing an inactive IME surface, or the generic-unmount orphan
325 /// drain in [`RenderRoot::rebuild`] (see [`release_focus_session_in`]).
326 focus_active: bool,
327 /// Which branch the live focus/IME session belongs to: `None` for the main
328 /// tree, `Some(key)` for the floated surface whose pod holds the recorded
329 /// focus path. The identity `focus_active` deliberately does not carry — one
330 /// bool cannot say *whose* session it is, and a surface's chain and the main
331 /// tree's are not siblings any container's blur sweep can reach across.
332 ///
333 /// **Resolved from links that can be shown to be live.** The only
334 /// authoritative view of a pod's recorded link the root ever gets is the pod
335 /// itself, which it holds for exactly the length of
336 /// [`RenderRoot::paint_overlays`] — so that pass writes this, from
337 /// [`crate::widget::ChildPod::holds_live_focus`], and the event pass only ever
338 /// *clears* it (a hit-tested claim is the main tree's by construction, and a
339 /// release ends the session outright). An overlay-pass focus request cannot
340 /// be attributed at the root: the bubble is a bare flag, and a field in the
341 /// main tree re-claiming its own session through a floated toolbar raises
342 /// exactly the same one as an editable inside the surface claiming it for the
343 /// first time.
344 ///
345 /// The liveness half is what makes the record worth keeping. A pod's raw
346 /// `focused` flag survives the session moving away from it — nothing visits
347 /// an abandoned branch to clear one — so a record resolved from the flag
348 /// alone latched on the first surface that ever took focus and never let go.
349 /// Resolved from the stamp instead, it answers a question the code can
350 /// falsify, and it names nobody the moment the session leaves every surface.
351 ///
352 /// **It no longer gates the tree's paint seed**, which is the other half of
353 /// the same correction: seeding is per-link, against `focus_epoch`, so a
354 /// branch proves its own claim rather than the root vouching for it from one
355 /// frame behind. What is left here is *provenance* — whether an overlay
356 /// dispatch's IME publish is the session owner's — plus the cross-surface
357 /// retirement in [`RenderRoot::event`], both of which genuinely need a name
358 /// rather than a per-link answer.
359 focus_surface: Option<OverlayKey>,
360 /// The identity of the live focus session — what a
361 /// [`ChildPod`](crate::widget::ChildPod) stamps beside its recorded focus
362 /// link, and the only thing that tells a link on the session the root has
363 /// now from one a moved session left behind.
364 ///
365 /// The focus analog of `hover_epoch`, with one difference that follows from
366 /// focus having no per-pass rhythm: this advances **around a dispatch**
367 /// rather than at the end of one. [`RenderRoot::event`] moves it forward
368 /// before it dispatches and puts it back afterwards unless the dispatch
369 /// actually recorded a claim — so a claim is stamped with a value nothing
370 /// older carries, and a pass that moved no focus leaves every standing link
371 /// exactly as it found it. Published to the pods through
372 /// [`crate::widget::set_live_focus_session`], which is where a container
373 /// deciding routing, and a pod's own destructor, read it.
374 ///
375 /// Starts at `1`, not `0`: a freshly built pod's stamp is `0`, and `(0, 0)`
376 /// is what a dispatch driven with no root at all sees, so a real root must
377 /// never publish that pair.
378 ///
379 /// Wrapping is deliberate and harmless — the value is only ever compared for
380 /// equality, never ordered — but it skips `0` on the way round (see
381 /// [`advance_focus_epoch`]).
382 focus_epoch: u64,
383 /// Whether the last completed hover pass left some widget in the tree holding
384 /// the hover link. Root-level mirror of the per-pod hover stamp (the hover
385 /// analog of `focus_active`), seeded into every event/paint pass so nothing
386 /// below can read as hovered while the root says nothing is.
387 hover_active: bool,
388 /// The live hover epoch: the identity of the most recent completed hover pass.
389 ///
390 /// Advanced by exactly one per hover pass — an **uncaptured**
391 /// [`PointerPhase::Move`] (which may record a claim), or the `Down`/`Up`/
392 /// `Cancel` that ends a hover outright (which may not) — and by nothing else,
393 /// so a scroll, key, IME, or housekeeping pass leaves a live hover standing.
394 /// A [`crate::widget::ChildPod`]'s recorded stamp counts as hovered only while
395 /// it equals this, which is what strands the previous claimant's path with no
396 /// container having to clear it (see the [`crate::event`] module docs).
397 ///
398 /// Starts at `1`, not `0`: a freshly built pod's stamp is `0`, and starting the
399 /// epoch past it means a never-claimed pod cannot match the live epoch by
400 /// accident before the first hover pass ever runs.
401 hover_epoch: u64,
402 /// This root's process-unique identity, assigned once at construction from
403 /// [`NEXT_ROOT_IDENTITY`] and never reused.
404 ///
405 /// It exists for exactly one comparison: the hover-orphan channel
406 /// (`crate::event`'s `mark_hover_orphaned`/`take_hover_orphaned`) is a
407 /// thread-local a *destructor* writes, so a second root driving passes on the
408 /// same thread can otherwise see a mark that is none of its business.
409 /// `hover_epoch` cannot tell them apart — every root's counter starts at `1`
410 /// and advances per hover pass, so two roots hold colliding integers as a rule
411 /// rather than as a fluke. Publishing and draining `(identity, epoch)` is what
412 /// keeps one root's unmounting claimant from ending another's live hover.
413 root_identity: u64,
414 /// The cursor the last cursor pass resolved — hover's sibling channel, and
415 /// the value a desktop shell reads through [`RenderRoot::cursor`].
416 ///
417 /// Deliberately **not** derived from `hover_active`: that mirror is
418 /// identity-free (it knows *that* something is hovered, not which widget or
419 /// what shape it wants), so a request travels its own pass-scoped slot
420 /// ([`EventCtx::set_cursor`]) and is resolved here.
421 ///
422 /// Re-resolved on every pointer [`PointerPhase::Move`], captured or not:
423 /// whatever the pass requested, or [`CursorIcon::Default`] when it requested
424 /// nothing. Every other pass leaves it standing — see [`RenderRoot::event`]
425 /// for why a `Down`/`Up` must not reset it. There is no generation counter
426 /// beside it: the shell compares the value it last applied (see
427 /// [`RenderRoot::cursor`]).
428 cursor: CursorIcon,
429 /// The text the tree last asked the shell to put on the host clipboard, or
430 /// `None` once drained — the cursor's write-only sibling, resolved from the
431 /// same kind of per-pass slot ([`EventCtx::write_clipboard`]) by the same
432 /// [`RequestPass`] bracket.
433 ///
434 /// **One-shot, unlike [`cursor`](RenderRoot::cursor).** A cursor is a *level*
435 /// (a standing shape a shell re-applies when it differs); a clipboard write
436 /// is an *edge* (a thing to do once), so the accessor
437 /// [`RenderRoot::take_clipboard_write`] drains it and a shell that forgets to
438 /// call it merely delays the write rather than repeating it.
439 ///
440 /// A pass that writes replaces whatever stood here undrained — the newest
441 /// copy is the one the user meant, and the shell is expected to drain after
442 /// every dispatch — while a pass that writes nothing leaves it alone rather
443 /// than silently discarding a write nobody has taken yet.
444 pending_clipboard_write: Option<String>,
445 /// Whether the tree has asked the shell to read the host clipboard back to it
446 /// ([`EventCtx::request_paste`]), until drained by
447 /// [`RenderRoot::take_paste_request`].
448 ///
449 /// The data-free twin of [`pending_clipboard_write`](RenderRoot::pending_clipboard_write),
450 /// and one-shot for the same reason. Raised by any pass in which a widget
451 /// asked and lowered only by the drain, so a shell that skips a drain answers
452 /// late rather than losing the paste.
453 pending_paste_request: bool,
454 /// The IME surface the focused widget last published (via
455 /// [`EventCtx::publish_ime_state`]), surfaced to the shell by
456 /// [`RenderRoot::ime_state`]. Persists across rebuilds/events until refreshed
457 /// by a new publish or dropped by a release (a blur, a focus release, an
458 /// inactive publish, or a generic-unmount orphan drain — see
459 /// [`release_focus_session_in`]).
460 ///
461 /// Only ever `None` or an **active** surface: an inactive publish is a
462 /// release, never a stored value (see [`RenderRoot::ime_state`]).
463 ime_state: Option<ImeState>,
464 /// A monotonically-increasing generation bumped on every **actual** change
465 /// of `focus_active` or `ime_state` — the focus/IME session's edge signal,
466 /// read by a shell through [`RenderRoot::focus_ime_generation`].
467 ///
468 /// The mobile frame gate turns this into an *edge* input
469 /// (`FrameInputs::focus_or_ime_changed`): a shell caches the last value it
470 /// saw and runs a frame when it moves. A *level* input ("something holds
471 /// focus") forced a frame every vsync for as long as a field stayed focused,
472 /// which made caret pacing unreachable — measured at 62–120 fps on a static
473 /// screen whose only live input was focus (Xiaomi 12).
474 ///
475 /// Same-value writes deliberately do **not** bump it (see
476 /// [`RenderRoot::set_focus_active`]/[`RenderRoot::store_ime_state`]): the
477 /// paint pass republishes the focused widget's IME surface on *every* frame,
478 /// so bumping on write rather than on change would re-create exactly the
479 /// per-vsync forcing this edge exists to remove.
480 focus_ime_gen: u64,
481 /// The [`PlatformViewFrame`]s the tree published during the most recent
482 /// [`RenderRoot::paint`], surfaced to the shell via
483 /// [`RenderRoot::platform_view_frames`]. Unlike `ime_state` above, this is
484 /// REPLACED wholesale every pass (never merged with the previous one), so
485 /// a pass that publishes none yields an empty `Vec` — a culled/removed
486 /// slot from the prior frame does not linger as a stale frame. Core stays
487 /// dumb here: the shell's differ owns absent-means-hide/dispose semantics.
488 platform_view_frames: Vec<PlatformViewFrame>,
489 /// The z-shield rects the tree reported during the most recent
490 /// [`RenderRoot::paint`] (via [`crate::widget::PaintCtx::report_input_shield`]),
491 /// surfaced to the shell via [`RenderRoot::input_shields`].
492 ///
493 /// Exactly the `platform_view_frames` discipline above — REPLACED wholesale
494 /// every pass, so a pass whose shields stopped painting reports none. Core
495 /// stays dumb: it never associates a shield with a slot, that is the
496 /// shell-side differ's job.
497 input_shields: Vec<Rect>,
498 /// Dirtiness accumulated since the last [`RenderRoot::take_change_flags`] —
499 /// merged from each rebuild so a shell can decide, in one place, whether a
500 /// frame needs layout/paint at all.
501 pending: ChangeFlags,
502 /// The app's active theme, stored type-erased so `frust-core` needs no
503 /// `frust-theme` dependency (the concrete `Theme` is boxed by the shell —
504 /// see [`RenderRoot::set_theme`]). Lent as `Option<&dyn Any>` into each
505 /// [`LayoutCtx`]/[`PaintCtx`]; `None` until a shell sets one (a supported
506 /// state — bare-core tests and pre-theme apps run without a theme).
507 theme: Option<Box<dyn Any>>,
508 /// The window's insets ([`WindowInsets`]), delivered by the shell via
509 /// [`RenderRoot::set_insets`] and threaded into every subsequent
510 /// layout/paint pass (recovered by widgets through
511 /// [`crate::widget::LayoutCtx::window_insets`]/
512 /// [`crate::widget::PaintCtx::window_insets`]). Unlike the theme this is a
513 /// concrete core-owned type (only `f64` scalars), stored by value — no
514 /// `Box<dyn Any>` erasure needed. Defaults to the zero inset until a shell
515 /// pushes one (a supported state — bare-core tests and pre-insets apps).
516 insets: WindowInsets,
517 /// The shell's running count of frames the render thread has actually
518 /// presented, threaded into every subsequent paint pass and recovered by
519 /// widgets through [`crate::widget::PaintCtx::presented_frames`]. A plain
520 /// `u64` core stores by value (like the insets). `None` until a shell pushes
521 /// one via [`RenderRoot::set_presented_frames`] — a supported state
522 /// (bare-core tests and pre-wiring shells run without it), so widgets can
523 /// fall back to a paint-cadence measure. Unlike the theme/insets this is a
524 /// pure observation: [`RenderRoot::set_presented_frames`] deliberately marks
525 /// NO [`ChangeFlags`] and bumps NO semantics generation (see its doc), so a
526 /// ticking presented count never forces a relayout or feeds the mobile frame
527 /// gate.
528 presented_frames: Option<u64>,
529 /// Whether the shell created a translucent (alpha-channel, "Mode B") GPU
530 /// surface, threaded into every subsequent paint pass and recovered by
531 /// widgets through [`crate::widget::PaintCtx::is_translucent`]. A plain
532 /// `bool` core stores by value (like the insets); `false` (opaque, "Mode A")
533 /// until a shell pushes one via [`RenderRoot::set_surface_translucent`] — the
534 /// supported default for every desktop app and bare-core test. The
535 /// platform-view hole-punch is the sole reader: a slot clears its rect only
536 /// on a translucent surface (see `frust-widgets`' `PlatformViewWidget`).
537 surface_translucent: bool,
538 /// The persistent, never-reused per-pod semantics base-id allocator's next
539 /// value. Seeded at `2` (ids `0`/`1` reserved: `0` keeps
540 /// `NonZeroU64` valid, `1` is the [`ROOT_NODE_ID`] window node), advanced as
541 /// [`ChildPod`](crate::widget::ChildPod)s are assigned bases on their first
542 /// semantics visit, and carried across passes so a pod that first appears on a
543 /// later frame never collides with an already-assigned one. A `Cell` because
544 /// [`RenderRoot::semantics`] runs behind `&self`.
545 semantics_alloc: Cell<u64>,
546 /// The root widget's stable semantics base id (the root pod is arena-backed,
547 /// not a [`ChildPod`](crate::widget::ChildPod), so it caches its base here
548 /// rather than in a pod). Lazily assigned on the first semantics pass.
549 root_semantics_id: Cell<Option<NonZeroU64>>,
550 /// A monotonically-increasing generation bumped whenever a rebuild or theme
551 /// swap could have changed the semantics tree, so a shell can cheaply skip
552 /// re-pulling + re-pushing an unchanged accessibility tree (the semantics
553 /// dirty gate — see [`RenderRoot::semantics_if_changed`]). v1 recompute is
554 /// acceptable; this is the seam a shell gates on.
555 semantics_gen: u64,
556 /// Set by [`RenderRoot::rebuild`] when the deferred-callback flush owes the
557 /// shell a frame, and folded into the next [`RenderRoot::paint`]'s
558 /// [`PaintOutcome::needs_frame`] (then cleared). Two raisers, both in the
559 /// flush loop: hitting [`MAX_PENDING_RESULT_FLUSH_PASSES`] with work still
560 /// owed, and a dispatched [`InputEvent::Housekeeping`] whose
561 /// [`EventOutcome::needs_redraw`] came back set.
562 ///
563 /// The frame-request half of the deferral: `pending |= PAINT` already tells
564 /// the mobile frame gate to run its next tick, but the desktop loop is
565 /// dirty-driven (`ControlFlow::Wait`) and schedules off `needs_frame`, so the
566 /// deferral has to surface there too — otherwise the remaining flush would
567 /// wait for whatever input happens to arrive next, which is the exact
568 /// failure this whole mechanism exists to remove.
569 deferred_frame: bool,
570 /// The routing half of the overlay entries the **last** [`RenderRoot::paint`]
571 /// registered, in paint order (`Floating` band then `Tooltip`, registration
572 /// order within each) — what [`RenderRoot::event`]'s overlay pre-pass
573 /// hit-tests before the main tree ever sees a pointer.
574 ///
575 /// Replaced wholesale every paint, exactly like `platform_view_frames`: an
576 /// owner keeps a surface routable by registering it again each frame, so a
577 /// surface whose owner stopped registering (or was unmounted) stops taking
578 /// input after the next paint with nothing to unregister.
579 ///
580 /// Carries **no pod handle** by construction (see
581 /// [`OverlayHit`](crate::overlay::OverlayHit)): the owner owns the pod, and a
582 /// root holding a clone of it between passes would both outlive the owner and
583 /// invite a borrow held across a pass boundary.
584 ///
585 /// One frame of lag is inherent and intended: input is routed against where
586 /// the surfaces were painted, which is the only place the user could have
587 /// seen them.
588 overlay_hits: Vec<OverlayHit>,
589 /// The selection-toolbar request the focused field published during the most
590 /// recent [`RenderRoot::paint`], surfaced to the shell through
591 /// [`RenderRoot::selection_toolbar`] for the platform edit-menu route.
592 ///
593 /// Resolved per paint pass: a pass in which nothing published clears it, which
594 /// is what puts the menu away when a selection collapses. A session release
595 /// clears it too (see [`RenderRoot::release_focus_session`]), so the menu can
596 /// never outlive the focus the selection belonged to — the event pass's blur
597 /// lands a whole frame before the paint that would otherwise notice.
598 selection_toolbar: Option<SelectionToolbarRequest>,
599 /// A monotonically-increasing generation bumped on every **actual** change of
600 /// `selection_toolbar` — the same change-guarded edge signal
601 /// `focus_ime_gen` is, and for the same reason: the publishing field
602 /// re-publishes an unchanged request every single frame its selection stands,
603 /// so bumping on write rather than on change would ask the shell to re-present
604 /// the platform menu on every vsync.
605 ///
606 /// Kept beside the value rather than inside it so the counter survives a
607 /// clear: a shell diffs the generation to notice the menu went *away* just as
608 /// much as to notice it appeared.
609 selection_toolbar_gen: u64,
610 _state: core::marker::PhantomData<fn(&mut State)>,
611}
612
613impl<State: 'static, V: View<State>> RenderRoot<State, V> {
614 /// Create an empty render root with no widget yet built.
615 pub fn new() -> Self {
616 Self {
617 tree: WidgetTree::new(),
618 root_id: None,
619 prev_view: None,
620 next_id: 0,
621 window_size: Size::ZERO,
622 capture_claimant: None,
623 capture_contacts: false,
624 contacts_captor_is_root: false,
625 focus_active: false,
626 focus_surface: None,
627 // Past a fresh pod's `0` stamp, and past the `(0, 0)` a rootless
628 // dispatch sees — see the field doc.
629 focus_epoch: 1,
630 hover_active: false,
631 // Past a fresh pod's `0` stamp — see the field doc.
632 hover_epoch: 1,
633 root_identity: NEXT_ROOT_IDENTITY.fetch_add(1, Ordering::Relaxed),
634 cursor: CursorIcon::Default,
635 pending_clipboard_write: None,
636 pending_paste_request: false,
637 ime_state: None,
638 focus_ime_gen: 0,
639 platform_view_frames: Vec::new(),
640 input_shields: Vec::new(),
641 pending: ChangeFlags::NONE,
642 theme: None,
643 insets: WindowInsets::default(),
644 presented_frames: None,
645 surface_translucent: false,
646 // Ids 0 and 1 are reserved (see the field doc); pods start at 2.
647 semantics_alloc: Cell::new(2),
648 root_semantics_id: Cell::new(None),
649 semantics_gen: 0,
650 deferred_frame: false,
651 overlay_hits: Vec::new(),
652 selection_toolbar: None,
653 selection_toolbar_gen: 0,
654 _state: core::marker::PhantomData,
655 }
656 }
657
658 /// Store the app's active theme, threaded into every subsequent
659 /// layout/paint pass as `Option<&dyn Any>` and recovered by widgets via
660 /// [`crate::widget::PaintCtx::theme_as`]/[`crate::widget::LayoutCtx::theme_as`].
661 ///
662 /// The theme is boxed **type-erased** (`Box<dyn Any>`) so this crate stays
663 /// independent of `frust-theme`; the shell boxes the concrete `Theme`
664 /// (and re-boxes it on a live appearance change, e.g. dark-mode toggle).
665 /// Calling again replaces the stored theme.
666 ///
667 /// Marks `LAYOUT | PAINT` pending (drained by
668 /// [`RenderRoot::take_change_flags`]): a theme swap can change baked-in
669 /// paint state a widget resolves at layout time (e.g. `Text`'s themed
670 /// glyph color, cached into its `TextLayout` — see
671 /// `frust-widgets::text`), so a shell that later gates layout/paint on
672 /// this seam must still see a bare `set_theme` as dirty even though no
673 /// view changed.
674 pub fn set_theme(&mut self, theme: Box<dyn Any>) {
675 self.theme = Some(theme);
676 self.pending |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
677 // A theme swap can change semantics-visible state (e.g. a relabelled or
678 // re-bounded node once layout re-runs); treat it as semantics-dirty too.
679 self.semantics_gen = self.semantics_gen.wrapping_add(1);
680 }
681
682 /// Store the window's insets ([`WindowInsets`]), threaded into every
683 /// subsequent layout/paint pass and recovered by widgets via
684 /// [`crate::widget::LayoutCtx::window_insets`]/
685 /// [`crate::widget::PaintCtx::window_insets`].
686 ///
687 /// Mirrors [`RenderRoot::set_theme`]'s dirty-tracking contract: a change
688 /// marks `LAYOUT | PAINT` pending (drained by
689 /// [`RenderRoot::take_change_flags`]) so a shell gating layout/paint on that
690 /// seam still relayouts when the insets move — a `SafeArea` widget resolves
691 /// its inset at layout time, so the mobile layout-skip gate must see a bare
692 /// `set_insets` as dirty even though no view changed (the same reasoning as
693 /// the theme swap — see `docs/ARCHITECTURE.md`'s Theme delivery and Frame
694 /// gate). A change also bumps the semantics generation, since a moved inset
695 /// shifts laid-out node bounds.
696 ///
697 /// No-op guarded by [`WindowInsets`]'s `PartialEq`: pushing the current
698 /// value marks nothing dirty, so a shell that polls the platform insets
699 /// every frame and forwards unconditionally never forces a needless
700 /// relayout. (A shell may also skip the call itself by comparing first —
701 /// this is the same guard, held on the core side.)
702 pub fn set_insets(&mut self, insets: WindowInsets) {
703 if self.insets == insets {
704 return;
705 }
706 self.insets = insets;
707 self.pending |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
708 // A moved inset shifts laid-out node bounds once layout re-runs; treat
709 // it as semantics-dirty too (mirrors `set_theme`).
710 self.semantics_gen = self.semantics_gen.wrapping_add(1);
711 }
712
713 /// The window's insets currently threaded into the layout/paint passes.
714 pub fn insets(&self) -> WindowInsets {
715 self.insets
716 }
717
718 /// Store the shell's running count of frames the render thread has actually
719 /// presented, threaded into every subsequent paint pass and recovered by
720 /// widgets via [`crate::widget::PaintCtx::presented_frames`]. A shell loads
721 /// the atomic its render side increments (once per presented frame) and
722 /// pushes it here once per UI frame, before `paint`.
723 ///
724 /// **Deliberately dirties nothing.** Unlike [`RenderRoot::set_theme`] and
725 /// [`RenderRoot::set_insets`] — which mark `LAYOUT | PAINT` pending because a
726 /// widget bakes their value in at layout time — this setter marks NO
727 /// [`ChangeFlags`] and bumps NO semantics generation. The presented count is
728 /// a paint-only *observation* a widget reads live every paint (never baked at
729 /// layout), so treating it as dirty would be wrong twice over: it would force
730 /// a needless relayout, and — critically — on the mobile shells a
731 /// monotonically ticking counter would keep the frame gate's pending-flags
732 /// input perpetually true, so the menu would never idle (the 32s-idle
733 /// behavior must survive). Keeping this setter dirt-free
734 /// is exactly what keeps the frame gate unaware of it (see
735 /// `docs/ARCHITECTURE.md`'s Frame gate).
736 pub fn set_presented_frames(&mut self, presented: u64) {
737 self.presented_frames = Some(presented);
738 }
739
740 /// The presented-frame count currently threaded into the paint pass, or
741 /// `None` if no shell has pushed one.
742 pub fn presented_frames(&self) -> Option<u64> {
743 self.presented_frames
744 }
745
746 /// Store whether the shell's GPU surface is translucent (alpha-channel,
747 /// "Mode B"), threaded into every subsequent paint pass and recovered by
748 /// widgets through [`crate::widget::PaintCtx::is_translucent`]. A shell
749 /// pushes the surface's **resolved** translucency here — what the GPU
750 /// backend reports after the surface is installed, not what the app
751 /// requested via `frust-shell-common::surface_mode`'s latch: a translucency
752 /// request the platform refuses must degrade to the opaque contract, or
753 /// every `platform_view` slot punches a hole in an opaque swapchain
754 /// (black rectangles). Every desktop app leaves the default `false`
755 /// (opaque, "Mode A").
756 ///
757 /// Marks `PAINT` pending on an actual change (`PartialEq`-guarded, mirroring
758 /// [`RenderRoot::set_insets`]'s no-op guard): translucency is read purely at
759 /// paint time (the hole-punch runs in `paint`, never baked at layout), so a
760 /// flip must repaint but need not relayout. A flip is rare but **real**: a
761 /// surface (re)install can resolve differently from the previous one, and
762 /// both mobile shells re-push this every frame (the no-op-if-unchanged
763 /// guard is what makes that free).
764 pub fn set_surface_translucent(&mut self, translucent: bool) {
765 if self.surface_translucent == translucent {
766 return;
767 }
768 self.surface_translucent = translucent;
769 self.pending |= ChangeFlags::PAINT;
770 }
771
772 /// Whether the shell's GPU surface is currently marked translucent.
773 pub fn is_surface_translucent(&self) -> bool {
774 self.surface_translucent
775 }
776
777 /// Whether a captured pointer gesture is currently in flight.
778 pub fn is_pointer_captured(&self) -> bool {
779 self.capture_claimant.is_some()
780 }
781
782 /// The contact that claimed the pointer capture in flight — the only one
783 /// whose `Up`/`Cancel` can end it — or `None` while nothing is captured.
784 pub fn pointer_capture_claimant(&self) -> Option<PointerId> {
785 self.capture_claimant
786 }
787
788 /// Whether the capture in flight routes the gesture's **other** contacts to
789 /// its captor — the captor opted in with [`EventCtx::capture_contacts`] on
790 /// the `Down` it captured with, and no container has since taken the
791 /// gesture over from it ([`EventCtx::release_captured_child`]). `false`
792 /// while nothing is captured.
793 pub fn pointer_capture_contacts(&self) -> bool {
794 self.capture_contacts
795 }
796
797 /// Whether some widget in the tree currently holds keyboard/IME focus.
798 pub fn is_focus_active(&self) -> bool {
799 self.focus_active
800 }
801
802 /// Whether some widget in the tree currently holds the hover link — i.e.
803 /// whether the last hover pass (an uncaptured [`PointerPhase::Move`]) left the
804 /// pointer over a widget that claimed it.
805 ///
806 /// The hover analog of [`RenderRoot::is_focus_active`], and a level accessor
807 /// like it: hover is not a session (nothing has to be released), so there is no
808 /// generation counterpart. `false` for any app whose widgets never call
809 /// [`EventCtx::claim_hover`](crate::event::EventCtx::claim_hover). A touch app
810 /// can still see it go `true` transiently: nothing distinguishes a touch
811 /// contact from a mouse here, so an uncaptured touch drag over a
812 /// non-capturing claimant is an ordinary hover pass — ended by the `Up` at
813 /// lift (see `docs/LIMITATIONS.md`'s `hover-window-leave-standing`).
814 ///
815 /// A [`RenderRoot::rebuild`] that removes the claimant ends the link too, so
816 /// this never reports a hover held by a widget that no longer exists — the
817 /// hover counterpart of the unmount focus release (see that method).
818 pub fn is_hover_active(&self) -> bool {
819 self.hover_active
820 }
821
822 /// The cursor the tree last asked the host to show — what a desktop shell
823 /// pushes to its window (`frust-shell-desktop` maps it onto winit's own
824 /// cursor icons).
825 ///
826 /// A **level** accessor like [`RenderRoot::is_hover_active`], not an edge one:
827 /// the value re-resolves on every pointer [`PointerPhase::Move`] and stands
828 /// unchanged through every other pass, so a shell caches what it last applied
829 /// and calls the platform only when this differs. There is deliberately no
830 /// generation counter — a cursor is a *value*, not a session, and an unmoved
831 /// cursor is indistinguishable from one re-resolved to the same shape.
832 ///
833 /// [`CursorIcon::Default`] before the first `Move`, and after any `Move` in
834 /// which no widget called
835 /// [`EventCtx::set_cursor`](crate::event::EventCtx::set_cursor) — so any app
836 /// whose widgets never request a cursor reads `Default` forever, and the mobile
837 /// shells never read this at all regardless of what resolves here.
838 ///
839 /// **Residual:** a widget that is torn down (or moves out from under a
840 /// stationary pointer) while its request stands leaves the last shape in
841 /// place until the next `Move` re-resolves it — the same self-correction
842 /// window hover has, and for the same reason: nothing re-resolves without
843 /// pointer motion.
844 pub fn cursor(&self) -> CursorIcon {
845 self.cursor
846 }
847
848 /// Take (and clear) the text the tree asked the shell to put on the host
849 /// clipboard — the drain a shell performs immediately after every
850 /// [`RenderRoot::event`], beside [`cursor()`](RenderRoot::cursor) and
851 /// [`ime_state()`](RenderRoot::ime_state).
852 ///
853 /// `Some` exactly when some widget called
854 /// [`EventCtx::write_clipboard`](crate::event::EventCtx::write_clipboard)
855 /// during a pass since the last drain (answering a
856 /// [`EditCommand::Copy`](crate::event::EditCommand::Copy)/[`Cut`](crate::event::EditCommand::Cut),
857 /// or a chord the widget decoded itself). The shell hands the text to its host
858 /// clipboard — winit's `arboard` on desktop, `ClipboardManager` on Android,
859 /// `UIPasteboard` on iOS — and does nothing at all on `None`.
860 ///
861 /// **Destructive**, unlike [`cursor()`](RenderRoot::cursor): a clipboard write
862 /// is an edge, not a standing level, so a caller that drains and drops the
863 /// result loses that write. Draining twice after one pass yields `None` the
864 /// second time.
865 ///
866 /// A widget that never copies leaves this `None` forever, so a shell with no
867 /// clipboard (the mobile shells before their own clipboard work lands) may
868 /// call it and discard the result, or not call it at all.
869 pub fn take_clipboard_write(&mut self) -> Option<String> {
870 self.pending_clipboard_write.take()
871 }
872
873 /// Take (and clear) whether the tree asked the shell to read the host
874 /// clipboard back to it — drained beside
875 /// [`take_clipboard_write`](RenderRoot::take_clipboard_write) after every
876 /// [`RenderRoot::event`].
877 ///
878 /// `true` exactly when some widget called
879 /// [`EventCtx::request_paste`](crate::event::EventCtx::request_paste) during a
880 /// pass since the last drain. The shell answers by reading its host clipboard
881 /// and dispatching
882 /// [`InputEvent::EditCommand`]`(`[`EditCommand::Paste`](crate::event::EditCommand::Paste)`(text))`
883 /// — a *new* dispatch, because the read may be asynchronous and the pass that
884 /// asked is over. That answer carries no identity of its own and is
885 /// focus-routed to whoever holds focus when it lands: a release in between
886 /// drops it harmlessly, but a focus *move* in between lands it in the new
887 /// field rather than the one that asked. A synchronous read has no such
888 /// window; an asynchronous one snapshots
889 /// [`focus_epoch`](RenderRoot::focus_epoch) at this drain and discards an
890 /// answer whose epoch no longer matches — not
891 /// [`focus_ime_generation`](RenderRoot::focus_ime_generation), which also
892 /// moves within a single session.
893 ///
894 /// **Destructive**, for [`take_clipboard_write`](RenderRoot::take_clipboard_write)'s
895 /// reason. A pass may both write and request (a cut that immediately re-reads,
896 /// or a widget answering two chords) — the two drains are independent.
897 pub fn take_paste_request(&mut self) -> bool {
898 std::mem::take(&mut self.pending_paste_request)
899 }
900
901 /// The IME surface the focused widget published, for the shell to drive the
902 /// platform input method (winit `set_ime_cursor_area`, Android
903 /// `updateSelection`, iOS `inputDelegate`). `None` when nothing is focused or
904 /// the focused widget publishes no IME surface.
905 ///
906 /// Written by the focused widget through [`EventCtx::publish_ime_state`] during
907 /// the event pass and refreshed on every event; it survives a rebuild (so the
908 /// shell can query it between frames) and is cleared when focus is lost.
909 ///
910 /// # `None` is the only "no session" form — an inactive surface is never stored
911 ///
912 /// A widget publishing `ImeState { active: false, .. }` is ending the session,
913 /// not describing it, so both publish paths turn that into a full release
914 /// (see [`RenderRoot::paint`]) and this returns `None` rather than
915 /// `Some(inactive)`. A shell therefore never has to distinguish the two, and
916 /// `is_some()` means "a live IME session" with no second check.
917 ///
918 /// **The platform still sees the keyboard-hide.** All three shells already
919 /// map `None` onto the inactive form on the way out, so the observable wire
920 /// behavior is unchanged: `frust-shell-android`'s `ime_state_to_json` returns
921 /// `ImeJsonState::default()` (`active:false`, empty text, `-1` indices, null
922 /// caret, `"normal"`) and `frust-shell-ios`' returns the byte-identical
923 /// `ime_state_json(false, "", -1, -1, -1, -1, None, "normal")` — exactly what
924 /// the navigator's own cleared surface serialised to before. Kotlin's
925 /// `pollImeAfterDispatch` and Swift's `syncImeFocus` both branch on `active`
926 /// alone (an inactive surface's text/caret/content-type are ignored), and the
927 /// desktop shell's `sync_ime` reads `is_some_and(|s| s.active)`. Dropping the
928 /// inactive surface's payload also stops a disabled *secret* field's text
929 /// riding to the platform after its session ended.
930 pub fn ime_state(&self) -> Option<ImeState> {
931 self.ime_state.clone()
932 }
933
934 /// The focus/IME session generation — bumped on every **actual** change of
935 /// [`is_focus_active`](RenderRoot::is_focus_active) or
936 /// [`ime_state`](RenderRoot::ime_state), and on nothing else.
937 ///
938 /// The *edge* counterpart of those two level accessors, for a shell that
939 /// needs "did the focus/IME session move since I last looked?" rather than
940 /// "is something focused?". A shell caches the value it last saw and
941 /// compares (mirroring [`semantics_generation`](RenderRoot::semantics_generation)'s
942 /// cheap dirty gate) — that comparison is the mobile frame gate's
943 /// `FrameInputs::focus_or_ime_changed` input.
944 ///
945 /// A same-value write never moves it: re-publishing an identical IME
946 /// surface (which the paint pass does on every frame a field stays focused)
947 /// or re-blurring an already-blurred root is not an edge. Wrapping is
948 /// deliberate and harmless — a comparison, never an ordering.
949 ///
950 /// # Not the session's identity
951 ///
952 /// This counts *changes to the published surface*, not *sessions*, and the
953 /// two come apart in both directions — see
954 /// [`focus_epoch`](RenderRoot::focus_epoch), which is what to reach for when
955 /// the question is "is this still the same focus session?". Answering that
956 /// one from this counter is wrong whenever focus moves between two fields
957 /// without the published value changing.
958 pub fn focus_ime_generation(&self) -> u64 {
959 self.focus_ime_gen
960 }
961
962 /// The live focus session's **identity** — advanced once per honoured focus
963 /// claim and once per session release, and by nothing else.
964 ///
965 /// The neighbour of [`focus_ime_generation`](RenderRoot::focus_ime_generation)
966 /// and easy to mistake for it, so: that one counts *changes to the published
967 /// surface* (the focus flag, or the [`ImeState`] value), this one counts
968 /// *sessions*. They come apart in both directions, which is why both exist:
969 ///
970 /// * Focus moving from one field to another moves this one and can leave
971 /// that one completely still. Claiming focus while some field already
972 /// holds it writes `true` over `true`, and the surface the new field
973 /// publishes may compare equal to the old field's ([`ImeState`] is
974 /// `{active, editing, caret, content_type}` and names no widget) — or may
975 /// not be published at all, since a widget is free to take focus and
976 /// publish nothing, which leaves the previous field's surface standing.
977 /// * An edit landing, a caret moving, or the field being repositioned under
978 /// the user moves that one and leaves this one still: the session is the
979 /// same session throughout.
980 ///
981 /// So a caller binding an asynchronous answer to the session that asked for
982 /// it wants this one; a caller asking "must I run a frame, or re-sync the
983 /// platform IME?" wants that one.
984 ///
985 /// **Never `0`.** The counter is built at `1` and steps *past* `0` on wrap,
986 /// because `0` is a never-claimed [`ChildPod`](crate::widget::ChildPod)'s
987 /// stamp and a root publishing it would hand every unclaimed pod in the tree
988 /// a live link. A caller is therefore free to use `0` as its own "no root /
989 /// no answer" sentinel with no risk of colliding with a live value. Wrapping
990 /// is otherwise deliberate and harmless: the value is compared for equality,
991 /// never ordered.
992 pub fn focus_epoch(&self) -> u64 {
993 self.focus_epoch
994 }
995
996 /// Set the root's focus flag, bumping [`RenderRoot::focus_ime_generation`]
997 /// only when the value actually moves.
998 ///
999 /// One of the two writers of `focus_active` outside construction (the other
1000 /// is [`release_focus_session_in`], which clears it together with the IME
1001 /// surface as one edge): every focus/blur arm of [`RenderRoot::event`] goes
1002 /// through one of them, so the edge generation cannot drift from the state it
1003 /// describes. In practice this one only ever *sets* focus — a clear is always
1004 /// a session release.
1005 fn set_focus_active(&mut self, active: bool) {
1006 if self.focus_active != active {
1007 self.focus_active = active;
1008 self.focus_ime_gen = self.focus_ime_gen.wrapping_add(1);
1009 }
1010 }
1011
1012 /// Store (or clear) the shell-facing IME surface, bumping
1013 /// [`RenderRoot::focus_ime_generation`] only when the stored value actually
1014 /// moves — the `&mut self` form of [`store_ime_state_in`], for the event
1015 /// pass (the paint pass holds disjoint field borrows and calls that
1016 /// function directly).
1017 fn store_ime_state(&mut self, ime: Option<ImeState>) {
1018 store_ime_state_in(&mut self.ime_state, &mut self.focus_ime_gen, ime);
1019 }
1020
1021 /// End the focus/IME session: clear `focus_active` and drop the shell-facing
1022 /// surface together, moving [`RenderRoot::focus_ime_generation`] exactly once
1023 /// if either was set. The `&mut self` form of [`release_focus_session_in`]
1024 /// (whose doc carries the full contract), for the event and rebuild passes;
1025 /// the paint pass holds disjoint field borrows and calls that function
1026 /// directly.
1027 ///
1028 /// Idempotent: releasing an already-released root writes the same values
1029 /// back and fires no edge.
1030 fn release_focus_session(&mut self) {
1031 release_focus_session_in(
1032 &mut self.focus_active,
1033 &mut self.ime_state,
1034 &mut self.focus_ime_gen,
1035 &mut self.focus_epoch,
1036 self.root_identity,
1037 );
1038 // No session, no owner. A paint pass that releases re-resolves the owner
1039 // from the pods before it ends anyway (see
1040 // `RenderRoot::resolve_session_surface`), so this write is the event and
1041 // rebuild passes' own.
1042 self.focus_surface = None;
1043 // A selection toolbar describes the *focused* field's selection, so the
1044 // session ending is the toolbar ending — and it must end on the event
1045 // pass that blurred, not a frame later when the next paint happens to
1046 // publish nothing. Change-guarded like every other edge here: releasing
1047 // an already-toolbarless root moves no generation.
1048 if self.selection_toolbar.take().is_some() {
1049 self.selection_toolbar_gen = self.selection_toolbar_gen.wrapping_add(1);
1050 }
1051 }
1052
1053 /// Publish this root's live focus session so the pods can compare their own
1054 /// stamps against it — the focus counterpart of the `(root, epoch)` pair
1055 /// `crate::event::set_live_hover_link` publishes for hover.
1056 ///
1057 /// Called at the head of every pass, not only when the session moves: the
1058 /// channel mirrors one root, so a second root driving passes on the same
1059 /// thread would otherwise leave this one's pods comparing against a session
1060 /// that is none of their business. Re-publishing an unchanged triple costs a
1061 /// `Cell` write and notifies nothing.
1062 ///
1063 /// Publishes the session "at rest" — the live epoch and the epoch a claim
1064 /// would take are the same value. [`RenderRoot::event`] publishes the two
1065 /// apart for the length of its dispatch; see that method.
1066 fn publish_focus_session(&self) {
1067 crate::widget::set_live_focus_session(
1068 self.root_identity,
1069 self.focus_epoch,
1070 self.focus_epoch,
1071 );
1072 }
1073
1074 /// End the standing hover link outright, outside any hover pass: advance the
1075 /// epoch (which strands every stamp in the tree at once, so no container has
1076 /// to be told) and clear the mirror.
1077 ///
1078 /// Hover's analog of [`RenderRoot::release_focus_session`], and idempotent in
1079 /// the same way — ending a hover nothing holds writes the same mirror back and
1080 /// costs one epoch. There is no generation counter to move: hover is not a
1081 /// session a shell mirrors (see [`RenderRoot::is_hover_active`]).
1082 ///
1083 /// The one caller is [`RenderRoot::rebuild`]'s severed-claimant drain; a hover
1084 /// pass ends its own link inline, where it also decides the *new* one.
1085 fn end_hover_link(&mut self) {
1086 self.hover_epoch = self.hover_epoch.wrapping_add(1);
1087 self.hover_active = false;
1088 crate::event::set_live_hover_link(self.root_identity, 0);
1089 }
1090
1091 /// The [`PlatformViewFrame`]s published during the most recent
1092 /// [`RenderRoot::paint`], in paint order.
1093 ///
1094 /// Replaced wholesale every pass (see the `platform_view_frames` field
1095 /// doc), so a pass with no publishers yields an empty slice — a shell
1096 /// never sees a stale frame for a slot that stopped painting.
1097 pub fn platform_view_frames(&self) -> &[PlatformViewFrame] {
1098 &self.platform_view_frames
1099 }
1100
1101 /// The z-shield rects reported during the most recent [`RenderRoot::paint`]
1102 /// (see [`crate::widget::PaintCtx::report_input_shield`]), in paint order.
1103 ///
1104 /// Replaced wholesale every pass, exactly like
1105 /// [`RenderRoot::platform_view_frames`] — a shell feeds both into the same
1106 /// differ ingest call, and the differ intersects these against each
1107 /// interactive slot's rect.
1108 pub fn input_shields(&self) -> &[Rect] {
1109 &self.input_shields
1110 }
1111
1112 /// Drain the slot ids whose `platform_view` widgets were torn down since the
1113 /// last call (`View::teardown` ran on them — see
1114 /// [`crate::widget::report_retired_slot`]).
1115 ///
1116 /// The prompt-teardown channel: a shell calls this once per frame, right
1117 /// after its rebuild, and retires each id in its platform-view differ
1118 /// (`PlatformViewState::retire`) so a disposed slot's native view goes away
1119 /// immediately instead of waiting out the differ's missing-streak
1120 /// heuristic. Draining is destructive, mirroring
1121 /// [`RenderRoot::take_change_flags`]: an id is reported exactly once, so a
1122 /// shell that drains and drops the result loses the prompt path (the
1123 /// missing-streak backstop still covers it).
1124 ///
1125 /// A merely *culled* slot (scrolled offscreen, a parent skipping paint)
1126 /// never appears here — culling doesn't run `teardown` — which is what
1127 /// keeps the camera keep-alive contract intact.
1128 pub fn take_retired_platform_views(&mut self) -> Vec<u64> {
1129 crate::widget::take_retired_slots()
1130 }
1131
1132 /// Take (and clear) the dirtiness accumulated since the last call.
1133 ///
1134 /// A shell can consult this to skip the layout/paint passes when nothing has
1135 /// changed and no redraw was requested (a desktop optimisation; the mobile
1136 /// continuous-loop shells may ignore it and repaint every tick). Each
1137 /// [`RenderRoot::rebuild`] merges its result here; this drains it.
1138 pub fn take_change_flags(&mut self) -> ChangeFlags {
1139 let flags = self.pending;
1140 self.pending = ChangeFlags::NONE;
1141 flags
1142 }
1143
1144 /// Non-draining peek at the dirtiness accumulated since the last
1145 /// [`RenderRoot::take_change_flags`] — `true` when any `LAYOUT`/`PAINT`
1146 /// bit is pending, without clearing it.
1147 ///
1148 /// Complements [`take_change_flags`](RenderRoot::take_change_flags) for a
1149 /// shell frame gate: the gate reads this as one of its
1150 /// "should this frame run" inputs *before* deciding, so a frame it chooses
1151 /// to skip leaves `pending` intact for the next non-skipped frame to drain
1152 /// and act on. Draining stays the job of `take_change_flags`, called only
1153 /// on a frame that actually runs its layout/paint passes. No behavioral
1154 /// change to rebuild/layout/paint.
1155 pub fn has_pending_change_flags(&self) -> bool {
1156 !self.pending.is_empty()
1157 }
1158
1159 /// The root widget id, once built.
1160 pub fn root_id(&self) -> Option<WidgetId> {
1161 self.root_id
1162 }
1163
1164 /// Shared access to the retained tree (for the shell / tests).
1165 pub fn tree(&self) -> &WidgetTree {
1166 &self.tree
1167 }
1168
1169 /// A read-only, pre-order snapshot of the retained tree for tooling: per
1170 /// node an id, its parent and children, the concrete widget's type name, an
1171 /// optional debug label, and its absolute border box in logical px.
1172 ///
1173 /// Computed on demand in O(nodes) and takes `&self` — no per-frame
1174 /// bookkeeping, no mutation, and nothing here participates in
1175 /// build/layout/paint. Bounds reflect the **last layout pass**, so call it
1176 /// after one (before the first, every rect is zero-sized).
1177 ///
1178 /// Scope: the walk covers the [`WidgetTree`] arena *and* the
1179 /// [`ChildPod`](crate::widget::ChildPod)s containers own, reached through
1180 /// [`Widget::visit_children`](crate::widget::Widget::visit_children) — so it
1181 /// is the real retained hierarchy, not just the arena (which holds little
1182 /// more than the root pod). A container that leaves that seam defaulted
1183 /// reads as a leaf.
1184 pub fn inspect(&self) -> Vec<InspectNode> {
1185 self.tree.inspect()
1186 }
1187
1188 /// Run the build closure, then build (first call) or rebuild (subsequent calls)
1189 /// the root widget, returning what changed.
1190 ///
1191 /// the build closure is expected to be cheap and re-entrant: it is
1192 /// re-run in full every rebuild.
1193 ///
1194 /// # Deferred-callback flush
1195 ///
1196 /// The view diff itself is state-free (`rebuild_view` below takes no
1197 /// `State`), so a widget applying a structural op there — the navigator
1198 /// draining its queued `push`/`pop` is the shipped case — cannot run an app
1199 /// callback that needs `&mut State`. It instead queues the callback and calls
1200 /// [`mark_pending_result_flush`](crate::event::mark_pending_result_flush);
1201 /// this method drains that flag and dispatches an
1202 /// [`InputEvent::Housekeeping`] broadcast through the ordinary
1203 /// [`event`](RenderRoot::event) plumbing, where `state` *is* in scope. This
1204 /// is the only unconditional per-frame pass that holds `&mut State`, which is
1205 /// why the dispatch lives here and not in a shell (flushing on the next
1206 /// real input meant waiting seconds for a touch, or forever when the next
1207 /// touch went to chrome outside the navigator).
1208 ///
1209 /// A flushed callback mutates `State`, so the view built before it ran is
1210 /// stale — the build closure + `rebuild_view` cycle therefore re-runs after
1211 /// each flush, and the same frame shows the result. Results can queue further
1212 /// nav ops, so the loop is **bounded**; past the cap the flag is left standing
1213 /// and one more frame is requested rather than spinning (see
1214 /// `MAX_PENDING_RESULT_FLUSH_PASSES`, this module's private cap constant).
1215 ///
1216 /// The broadcast's [`EventOutcome`] is propagated, not discarded: a
1217 /// `needs_redraw` coming back from the dispatch folds into this rebuild's
1218 /// [`ChangeFlags::PAINT`] and the deferred frame request, so a callback
1219 /// whose only effect is [`EventCtx::request_redraw`]
1220 /// — invisible to the re-diff, since no view-visible state changed — still
1221 /// wakes both the mobile frame gate and the desktop `Wait` loop.
1222 pub fn rebuild(
1223 &mut self,
1224 build: &mut impl FnMut(&mut State) -> V,
1225 state: &mut State,
1226 ) -> ChangeFlags {
1227 // Republish this root's focus session before the diff runs: a pod
1228 // severed by it compares its own stamp against the channel from its
1229 // destructor, and the channel mirrors one root at a time.
1230 self.publish_focus_session();
1231 let view = build(state);
1232 let mut flags = self.rebuild_view(view);
1233
1234 // Deferred-callback convergence loop (see the method doc). Each pass:
1235 // drain the flag, run the queued callbacks against real state, then
1236 // re-diff so this frame reflects them.
1237 let mut passes = 0usize;
1238 while crate::event::take_pending_result_flush() {
1239 if passes >= MAX_PENDING_RESULT_FLUSH_PASSES {
1240 // Cap reached. Put the flag back — the work is still owed — and
1241 // ask for one more frame instead of spinning inside this one.
1242 // `pending |= PAINT` is what the mobile frame gate reads
1243 // (`has_pending_change_flags`); `deferred_frame` is what surfaces
1244 // on the next `paint` as `needs_frame`, which is how the desktop
1245 // `ControlFlow::Wait` loop learns to wake.
1246 crate::event::mark_pending_result_flush();
1247 flags |= ChangeFlags::PAINT;
1248 self.deferred_frame = true;
1249 break;
1250 }
1251 // The dispatch's own outcome is load-bearing, not noise: a flushed
1252 // callback whose *only* effect is `EventCtx::request_redraw` (no
1253 // signal write, no state the next build-closure run reads) leaves the
1254 // re-diff below reporting `ChangeFlags::NONE`, so nothing else in
1255 // this method would ever mark the frame dirty and the requested
1256 // redraw would be dropped on the floor. Fold it into exactly the
1257 // wake the cap branch above raises: `PAINT` reaches `self.pending`,
1258 // which is what the mobile frame gate reads
1259 // (`has_pending_change_flags`), and `deferred_frame` surfaces on the
1260 // next `paint` as `needs_frame`, which is how the desktop
1261 // `ControlFlow::Wait` loop learns to schedule a frame. Both
1262 // Housekeeping producers need it (a navigator pop-result callback
1263 // and `frust-widgets`' gesture long-press latch), and without it a
1264 // redraw-only effect waits for whatever input happens to arrive
1265 // next — exactly the failure this mechanism exists to remove.
1266 //
1267 // Non-empty flags also bump the semantics generation below, which
1268 // is correct: the callback just mutated real `State` through a live
1269 // `EventCtx`, so the accessibility tree may genuinely have changed,
1270 // and every other paint-class path here bumps it the same way (a
1271 // spurious bump costs one recompute of an unchanged tree, a missed
1272 // one strands a stale tree).
1273 let outcome = self.event(state, &InputEvent::Housekeeping);
1274 if outcome.needs_redraw {
1275 flags |= ChangeFlags::PAINT;
1276 self.deferred_frame = true;
1277 }
1278 let view = build(state);
1279 flags |= self.rebuild_view(view);
1280 passes += 1;
1281 }
1282
1283 // Generic-unmount focus release. A reconciler that tears down (or
1284 // type-swaps, or clears the `focused` flag of) a child pod holding the
1285 // recorded focus path *on the live focus chain* has severed that path,
1286 // but runs over a `BuildCtx` with no `RenderRoot` in scope — so it raises
1287 // `mark_focus_orphaned` and this drain performs the release the
1288 // reconciler could not. "On the live chain" is what `rebuild_view`'s seed
1289 // buys: a mark means a live session lost its owner, never that some stale
1290 // flag deep in an already-blurred branch went away (see
1291 // `mark_focus_orphaned`). Without it the root's mirror stays standing over
1292 // a widget that no longer exists: `is_focus_active()` keeps reporting
1293 // true and `ime_state()` keeps handing the shell a surface for a dead
1294 // field, self-correcting only on the next event pass — which never
1295 // arrives on a screen the user has stopped touching (the pop-into-idle
1296 // case this whole seam exists for).
1297 //
1298 // Drained *after* the flush loop so one release covers every pass: a
1299 // flushed callback that navigates re-diffs, and either diff may orphan
1300 // the focus. `Housekeeping` claims no focus of its own (its root arm is
1301 // inert), so nothing the loop dispatched can be undone here.
1302 //
1303 // The release marks no `ChangeFlags` of its own: the structural change
1304 // that severed the path already flagged `LAYOUT | PAINT`, and the
1305 // generation bump is what wakes the mobile frame gate's
1306 // `focus_or_ime_changed` edge for the one repaint the release needs.
1307 if crate::event::take_focus_orphaned() {
1308 self.release_focus_session();
1309 }
1310
1311 // Generic-unmount hover release, the same shape one channel over: a diff
1312 // that dropped the `ChildPod` holding the live hover link has severed a
1313 // path the epoch mechanism cannot strand, because stranding needs a hover
1314 // pass and the dead claimant will never see another one. Without this the
1315 // mirror stands over a widget that no longer exists — `is_hover_active()`
1316 // reporting a link nothing holds — and every surviving ancestor of the
1317 // claimant keeps painting hover chrome off its own still-matching stamp
1318 // until some later `Move` re-derives, which never comes on a pointer the
1319 // user has stopped moving.
1320 //
1321 // The mark is raised by the pod's destructor rather than by the
1322 // reconcilers (the stamp has no setter for a container to cooperate
1323 // through — see `mark_hover_orphaned`), which is what makes this cover
1324 // every removal route, including hand-rolled containers outside this
1325 // workspace. That reach is also why the mark is qualified by
1326 // `root_identity`: a destructor fires whenever a pod happens to die, so
1327 // an unqualified mark could be a second root's on this thread. Drained
1328 // after the flush loop for the focus release's reason: any pass of the
1329 // loop may re-diff, and one end covers them all.
1330 //
1331 // Unlike that release this one flags `PAINT` of its own. The reconciler
1332 // that dropped the claimant usually reported `LAYOUT | PAINT` already,
1333 // but "usually" is not a contract this drain can rest on: the destructor
1334 // route deliberately covers containers outside this workspace (that is
1335 // its whole reason for existing), and one of those can drop a pod while
1336 // reporting whatever flags it likes. Ending a hover always changes what
1337 // paints, so the correction states its own need for the frame it rides
1338 // on — idempotent where the reconciler already said so.
1339 if crate::event::take_hover_orphaned(self.root_identity) && self.hover_active {
1340 self.end_hover_link();
1341 flags |= ChangeFlags::PAINT;
1342 }
1343
1344 self.pending |= flags;
1345 // A rebuild that changed layout/paint could have changed the semantics
1346 // tree (added/removed/relabelled nodes); bump the dirty gate a shell polls
1347 // via `semantics_if_changed`.
1348 if !flags.is_empty() {
1349 self.semantics_gen = self.semantics_gen.wrapping_add(1);
1350 }
1351 flags
1352 }
1353
1354 /// The rebuild body, split out so [`RenderRoot::rebuild`] can accumulate the
1355 /// result into [`RenderRoot::pending`] in one place.
1356 ///
1357 /// # Seeding the diff's focus chain
1358 ///
1359 /// The root is where the effective focus chain ([`BuildCtx::has_focus`])
1360 /// starts: the root widget sits in no `ChildPod`, so its "link above" is the
1361 /// root's own session mirror. A reconciler deep in the diff ANDs its pod's
1362 /// `focused` flag onto this seed and marks an orphan only if the whole chain
1363 /// holds — which is why the seed is "is there a session to lose" rather than
1364 /// `focus_active` alone: an active surface parked without the flag is still a
1365 /// live session `release_focus_session` would move. With neither set there is
1366 /// nothing to release, so the seed is `false` and the diff marks nothing.
1367 fn rebuild_view(&mut self, view: V) -> ChangeFlags {
1368 // Read before the `&mut self.next_id` borrow below (disjoint fields, but
1369 // spelled out for the reader).
1370 let session_live = self.focus_active || self.ime_state.is_some();
1371 match (self.root_id, self.prev_view.take()) {
1372 // Reconcile against the previous view of the same type.
1373 (Some(root_id), Some(prev)) => {
1374 let mut ctx = BuildCtx::new(&mut self.next_id);
1375 ctx.set_has_focus(session_live);
1376 let flags = {
1377 let pod = self
1378 .tree
1379 .pod_mut(root_id)
1380 .expect("root pod present when root_id is set");
1381 let element = pod
1382 .widget_mut()
1383 .downcast_mut::<V::Element>()
1384 .expect("root widget type matches its originating view");
1385 view.rebuild(&prev, element, &mut ctx)
1386 };
1387 if let Some(pod) = self.tree.pod_mut(root_id) {
1388 pod.merge_flags(flags);
1389 }
1390 self.prev_view = Some(view);
1391 flags
1392 }
1393 // First build: materialise the widget and insert it as the root.
1394 _ => {
1395 let mut ctx = BuildCtx::new(&mut self.next_id);
1396 // A first build tears nothing down, so the seed is moot — set it
1397 // anyway so the rule is "the root always seeds the chain", with no
1398 // arm exempt.
1399 ctx.set_has_focus(session_live);
1400 let id = ctx.alloc_id();
1401 let element = view.build(&mut ctx);
1402 // `new_typed` boxes the element exactly like `new` would, and
1403 // additionally records `V::Element`'s type name for
1404 // introspection — the concrete type is only nameable here.
1405 let pod = WidgetPod::new_typed(id, element);
1406 let root_id = self.tree.insert_root(pod);
1407 self.root_id = Some(root_id);
1408 self.prev_view = Some(view);
1409 ChangeFlags::LAYOUT | ChangeFlags::PAINT
1410 }
1411 }
1412 }
1413
1414 /// Lay out the root widget against `window_size` and record its geometry.
1415 ///
1416 /// The root receives loose constraints (zero up to the window size) and is
1417 /// placed at the origin. Returns the size the root chose. No text context is
1418 /// threaded in (use [`RenderRoot::layout_with_text`] when the tree contains
1419 /// text widgets); the stored theme, if any, is still threaded down.
1420 pub fn layout(&mut self, window_size: Size) -> Size {
1421 self.layout_inner(window_size, None)
1422 }
1423
1424 /// Lay out the root widget, threading a shared text-shaping context down to
1425 /// text widgets.
1426 ///
1427 /// `text_ctx` is the shell-owned `frust_text::TextContext`, passed
1428 /// type-erased so this crate needs no `frust-text` dependency. Text
1429 /// widgets recover it via [`crate::widget::LayoutCtx::text_context`]. The
1430 /// stored theme, if any, is threaded down alongside it.
1431 pub fn layout_with_text(&mut self, window_size: Size, text_ctx: &mut dyn Any) -> Size {
1432 self.layout_inner(window_size, Some(text_ctx))
1433 }
1434
1435 /// Shared layout body: hands the root loose window constraints, lends the
1436 /// optional text context and the stored theme into a [`LayoutCtx`], and
1437 /// records the size the root returns.
1438 fn layout_inner(&mut self, window_size: Size, text_ctx: Option<&mut dyn Any>) -> Size {
1439 self.window_size = window_size;
1440 let Some(root_id) = self.root_id else {
1441 return Size::ZERO;
1442 };
1443 let bc = BoxConstraints::loose(window_size);
1444 // Disjoint field borrows: the theme (immut) and the tree (mut) are
1445 // different fields of `self`, so both borrows coexist through the layout.
1446 let theme = self.theme.as_deref();
1447 // Copied out before the `&mut self.tree` borrow below (a disjoint,
1448 // `Copy` field read).
1449 let insets = self.insets;
1450 let Some(pod) = self.tree.pod_mut(root_id) else {
1451 return Size::ZERO;
1452 };
1453 let mut ctx = LayoutCtx::with_resources(text_ctx, theme);
1454 // Thread the window insets down; one layout context reaches the whole
1455 // tree, so the global insets are set once here (see `crate::insets`).
1456 ctx.set_window_insets(insets);
1457 // Thread the window's own size down the same way — global and
1458 // origin-independent like the insets. A widget floating an overlay pod
1459 // lays it out against this rather than against its own constraints (see
1460 // `LayoutCtx::window_size`).
1461 ctx.set_window_size(window_size);
1462 let size = pod.widget_mut().layout(&mut ctx, &bc);
1463 pod.set_layout(Point::ZERO, size);
1464 size
1465 }
1466
1467 /// Paint the root widget into `scene`, returning whether the tree wants
1468 /// another frame to continue an animation.
1469 ///
1470 /// A widget whose paint advances animation state (e.g. a scroll fling) signals
1471 /// [`PaintCtx::request_frame`]; that flag bubbles up through the container
1472 /// [`ChildPod`](crate::widget::ChildPod)s and out here as
1473 /// [`PaintOutcome::needs_frame`], which the shell honors by scheduling the next
1474 /// frame (desktop `window.request_redraw()`; the mobile continuous loops
1475 /// already do so). Mirrors how [`RenderRoot::event`] surfaces `needs_redraw`.
1476 ///
1477 /// A widget whose animation changes its *layout* signals
1478 /// [`PaintCtx::request_layout`] instead (or as well); that bubbles up the same
1479 /// way and is folded here into the render root's pending [`ChangeFlags`]
1480 /// (`LAYOUT`), so the *next* frame's
1481 /// [`take_change_flags`](RenderRoot::take_change_flags)`().needs_layout()`
1482 /// reports it and the mobile intra-frame layout skip relayouts while the
1483 /// animation is in flight. It is also surfaced on the returned
1484 /// [`PaintOutcome::needs_layout`].
1485 ///
1486 /// `frame_time` is the shell's shared monotonic clock for this frame
1487 /// (time enters `frust-core` from the shell, never `Instant::now()` here). It
1488 /// is seeded onto the root [`PaintCtx`] and threaded unchanged to every child
1489 /// ([`crate::widget::ChildPod::paint_child`]), so an animating widget advances
1490 /// against one consistent timestamp — see [`PaintCtx::frame_time`].
1491 ///
1492 /// # The overlay post-pass
1493 ///
1494 /// Painting the main tree is only the first half. Widgets registering a
1495 /// floated surface during that walk ([`PaintCtx::register_overlay`]) are
1496 /// drained here and painted **after** it, in band order — which is the only
1497 /// way a popover, menu or tooltip escapes its owner's paint order and every
1498 /// ancestor's clip. Their routing rects are retained (see
1499 /// `RenderRoot::overlay_hits`) for the next event pass to hit-test first, and
1500 /// their paint outcomes merge into this pass's own, so an animating overlay
1501 /// keeps the frames coming exactly like an animating widget in the tree.
1502 pub fn paint(&mut self, scene: &mut dyn PaintScene, frame_time: FrameTime) -> PaintOutcome {
1503 // Open the two paint-pass channels for the whole pass. Entering CLEARS
1504 // each slot, which is what makes "the registry is empty at the start of
1505 // every paint" true by construction rather than by everyone remembering
1506 // to unregister; `Drop` hands an enclosing pass its own back.
1507 let overlay_pass = OverlayPaintPass::enter();
1508 let toolbar_pass = SelectionToolbarPass::enter();
1509 // Republish this root's focus session before anything reads it: the
1510 // channel mirrors one root, and the pods about to be visited must
1511 // compare their stamps against *this* root's session.
1512 self.publish_focus_session();
1513
1514 let (mut outcome, main_tree_ime) = self.paint_main_tree(scene, frame_time);
1515
1516 // Drain what the main tree registered and paint it above everything.
1517 // Sorting is stable, so the band decides and registration order breaks
1518 // ties within a band (see `crate::overlay::sort_into_paint_order`).
1519 let mut entries = overlay_pass.take();
1520 sort_into_paint_order(&mut entries);
1521 // Retain the routing half — never the pods — for the next event pass.
1522 self.overlay_hits = entries.iter().map(OverlayHit::of).collect();
1523 let (holder, holder_ime) = self.paint_overlays(&entries, scene, frame_time, &mut outcome);
1524 // Release the owners' pod clones before the pass ends: the root holds no
1525 // overlay pod at rest, so a pod's lifetime stays exactly its owner's.
1526 drop(entries);
1527
1528 // Both halves have now spoken, so the one question neither of them can
1529 // answer alone — whose surface the shell is configured with — is settled
1530 // in one place, with both answers in hand.
1531 self.resolve_session_surface(main_tree_ime, holder, holder_ime);
1532
1533 // Resolve the selection-toolbar publish last, so a field that published
1534 // while painting *inside* a floated pod (a text input hosted in a
1535 // popover) is resolved by the same rule as one in the main tree.
1536 self.resolve_selection_toolbar(toolbar_pass.take());
1537
1538 outcome
1539 }
1540
1541 /// Settle which branch's IME surface the shell is configured with, from the
1542 /// two halves of the paint pass: what the main tree published, and what the
1543 /// floated surface holding the live focus link published (`holder`/
1544 /// `holder_ime`, both resolved from the pods' own stamps — see
1545 /// [`RenderRoot::paint_overlays`]).
1546 ///
1547 /// # Why it is decided here and not where the publish happens
1548 ///
1549 /// A publish is a claim about *the* session, and the session has exactly one
1550 /// owner. The main tree paints first, so at the moment it publishes, nothing
1551 /// yet knows whether a floated surface is about to prove that the session is
1552 /// no longer the tree's. Storing it there and correcting later is what
1553 /// produced a frame of lag with a secure field's surface standing in it —
1554 /// and the record the correction had to be driven from was one nothing could
1555 /// falsify. Deferring the decision by the width of one pass removes both.
1556 ///
1557 /// # The rules
1558 ///
1559 /// * the owner's own publish wins, whether that owner is a surface or the
1560 /// tree; a publish from anywhere else was already dropped by the half that
1561 /// collected it;
1562 /// * an **inactive** publish from the owner is the session ending, not a
1563 /// value — the same reading every other release site gives it;
1564 /// * an owner that published **nothing** leaves a standing surface standing,
1565 /// *unless* ownership moved this pass: then what stands belongs to the
1566 /// branch that just lost the session, and goes with it rather than
1567 /// remaining as the shell's idea of a live one. This is the rule that stops
1568 /// a popover taking focus from leaving the platform keyboard configured for
1569 /// the secure field underneath it — in both directions, since the session
1570 /// coming back to the tree moves ownership just as much as it leaving.
1571 fn resolve_session_surface(
1572 &mut self,
1573 main_tree_ime: Option<ImeState>,
1574 holder: Option<OverlayKey>,
1575 holder_ime: Option<ImeState>,
1576 ) {
1577 let published = if holder.is_some() {
1578 holder_ime
1579 } else {
1580 main_tree_ime
1581 };
1582 let ownership_moved = holder != self.focus_surface;
1583 match published {
1584 Some(ime) if ime.active => self.store_ime_state(Some(ime)),
1585 Some(_) => self.release_focus_session(),
1586 None if ownership_moved => self.store_ime_state(None),
1587 None => {}
1588 }
1589 // Resolved from the links themselves, never from the focus request that
1590 // opened the session — see the field's doc.
1591 self.focus_surface = if self.focus_active { holder } else { None };
1592 }
1593
1594 /// Paint the main widget tree — everything [`RenderRoot::paint`] does before
1595 /// the floated overlay pods get their turn.
1596 ///
1597 /// Split out from [`RenderRoot::paint`] purely for borrow scoping: this body
1598 /// holds `&mut self.tree` (beside disjoint borrows of the theme) for its whole
1599 /// length, while painting an overlay pod needs the root's fields again to
1600 /// merge outcomes and extend the per-pass channels. Nothing about the pass
1601 /// itself changed when it moved here.
1602 ///
1603 /// Reports, beside the outcome, the IME surface the tree published this pass
1604 /// (if any) — handed back rather than stored, because whether the tree still
1605 /// owns the session is not knowable until the floated pods have been visited
1606 /// (see [`RenderRoot::resolve_session_surface`]).
1607 fn paint_main_tree(
1608 &mut self,
1609 scene: &mut dyn PaintScene,
1610 frame_time: FrameTime,
1611 ) -> (PaintOutcome, Option<ImeState>) {
1612 let Some(root_id) = self.root_id else {
1613 return (PaintOutcome::default(), None);
1614 };
1615 // Disjoint field borrows: the theme (immut) vs the tree (mut).
1616 let theme = self.theme.as_deref();
1617 // Copied out before the `&mut self.tree` borrow (a disjoint `Copy` read).
1618 let insets = self.insets;
1619 // Same disjoint `Copy` read: the presented-frame count threaded to widgets.
1620 let presented_frames = self.presented_frames;
1621 // Same disjoint `Copy` read: the surface-translucency flag the
1622 // platform-view hole-punch reads (see `PaintCtx::is_translucent`).
1623 let surface_translucent = self.surface_translucent;
1624 if let Some(pod) = self.tree.pod_mut(root_id) {
1625 let mut ctx = PaintCtx::new(pod.origin(), pod.size());
1626 // Seed the shared shell clock so the whole paint pass sees one time.
1627 ctx.set_frame_time(frame_time);
1628 // Lend the stored theme (type-erased) into the paint pass; widgets
1629 // recover it via `PaintCtx::theme_as`.
1630 ctx.set_theme(theme);
1631 // Thread the window insets down (global — see `crate::insets`).
1632 ctx.set_window_insets(insets);
1633 // Thread the shell's presented-frame count down (global; a widget
1634 // measuring FPS differences it — see `PaintCtx::presented_frames`).
1635 ctx.set_presented_frames(presented_frames);
1636 // Thread the surface-translucency flag down (global; the
1637 // platform-view hole-punch gates its rect-clear on it — see
1638 // `PaintCtx::is_translucent`).
1639 ctx.set_translucent(surface_translucent);
1640 // Seed the root widget's paint-time focus from the session mirror so
1641 // a leaf-root editable observes its own focus, and thread the live
1642 // session's identity down beside it: deeper focus is resolved
1643 // per-pod by `ChildPod::paint_child`, which counts a recorded link
1644 // only while its stamp names this session.
1645 //
1646 // The mirror alone, deliberately. Narrowing the seed by *which
1647 // branch* the root believes owns the session was the previous shape,
1648 // and it de-seeded the whole tree off a record no pass could
1649 // falsify: a link a floated pod recorded is reached by no container's
1650 // blur sweep, so once one existed the tree never got seeded again.
1651 // The epoch answers the same question where it can actually be
1652 // answered — at each link, against the session that link was
1653 // recorded for.
1654 //
1655 // The arena root has no recorded link of its own (it is a
1656 // `WidgetPod`, not a `ChildPod`, and carries no stamp), so a
1657 // *leaf-root* editable is still seeded from the bare mirror. Every
1658 // real tree puts a container there, and the first `ChildPod` below it
1659 // composes the stamp back in.
1660 ctx.set_has_focus(self.focus_active);
1661 ctx.set_focus_epoch(self.focus_epoch);
1662 // Thread the hover mirror + live epoch the same way: the root widget's
1663 // own hover comes from the mirror (a leaf root can claim hover itself),
1664 // and deeper links are resolved per-pod by `ChildPod::paint_child`
1665 // against this epoch.
1666 ctx.set_hovered(self.hover_active);
1667 ctx.set_hover_epoch(self.hover_epoch);
1668 pod.widget_mut().paint(&mut ctx, scene);
1669 pod.clear_flags();
1670 // A focused editable republishes its IME surface during paint (which
1671 // runs after every rebuild), so a controlled change applied by the
1672 // rebuild — e.g. a submit clearing the field — refreshes the
1673 // shell-facing `ime_state` that the event pass alone would leave
1674 // stale.
1675 //
1676 // Collected, not stored: what the tree published is only the
1677 // session's if the session is still the tree's, and the pods that
1678 // could say otherwise have not been visited yet.
1679 // `resolve_session_surface` decides once both halves have spoken,
1680 // and routes the store through the change guard so an *unchanged*
1681 // republish — the overwhelmingly common case, a focused field
1682 // re-publishing the same surface frame after frame — moves no
1683 // generation and therefore fires no `focus_or_ime_changed` edge at
1684 // the shell.
1685 //
1686 // An **inactive** publish is not a surface refresh at all: it is the
1687 // publishing widget saying "this session is over" — the navigator's
1688 // post-pop `cleared_ime_state`, `PatternSwitcher`'s equivalent, and
1689 // a `TextInput` turned disabled/read-only under a live focus are the
1690 // three shipped producers, and every one of them is an unmount or a
1691 // de-focus. Storing it as `Some(inactive)` and leaving `focus_active`
1692 // standing is what leaked the session after a pop: the shells' gate
1693 // saw `ime_state().is_some()`, `is_focus_active()` kept lying, and the
1694 // next real focus interaction started from a corrupt baseline. So the
1695 // resolver takes the *full* release instead — the same one a
1696 // blur-on-outside-tap `Down` performs, firing exactly one edge.
1697 //
1698 // The `self.focus_active` guard stays and is applied here, at the
1699 // point of collection: a publish arriving when no session is live
1700 // describes nothing, so a widget whose pod focus was just cleared by
1701 // a container-routed blur (but whose internal flag lags one frame)
1702 // can never resurrect the `ime_state` that blur dropped — even
1703 // before it observes the blur via `PaintCtx::has_focus`.
1704 //
1705 // Provenance below that guard is the chain's own job now: a link
1706 // reads focused only while its stamp names the live session, so a
1707 // branch the session has left publishes nothing to collect.
1708 let main_tree_ime = if self.focus_active {
1709 ctx.take_ime_state()
1710 } else {
1711 None
1712 };
1713 // Replace (never merge) the whole platform-view collection with
1714 // whatever this pass published — unlike `ime_state` above there is
1715 // no single "the" published instance to guard behind a focus
1716 // check, and a pass that publishes none must clear out every
1717 // stale frame from the previous one (see the field's doc comment).
1718 self.platform_view_frames = ctx.take_platform_views();
1719 // Same replace-per-pass discipline for the z-shield channel: a pass
1720 // whose shields stopped painting reports none, so a stale shield can
1721 // never keep stealing input from an interactive slot (see
1722 // `PaintCtx::report_input_shield`).
1723 self.input_shields = ctx.take_input_shields();
1724 // Fold a bubbled `request_layout` into `pending` so the *next* frame
1725 // relayouts. `pending` survives to the next frame and feeds both the
1726 // frame gate (`has_pending_change_flags`) and the Android layout-skip
1727 // (`take_change_flags().needs_layout()`), so no shell change is needed
1728 // on any platform. Deliberately opt-in: `request_frame` alone never
1729 // sets LAYOUT, keeping paint-only animations layout-free.
1730 let needs_layout = ctx.needs_layout();
1731 if needs_layout {
1732 self.pending |= ChangeFlags::LAYOUT;
1733 }
1734 // A rebuild that ran out of flush passes owes one more frame; surface
1735 // it here (and clear it) so a dirty-driven shell schedules the frame
1736 // that finishes the flush — see the `deferred_frame` field doc.
1737 let deferred_frame = std::mem::take(&mut self.deferred_frame);
1738 let outcome = PaintOutcome {
1739 needs_frame: ctx.needs_frame() || deferred_frame,
1740 needs_layout,
1741 // Aggregate tick class: paced-only iff a frame was requested and
1742 // every request was CosmeticLoop-class. The mobile frame gate
1743 // may throttle such a frame; any Transition request
1744 // (including the LAYOUT-implying `request_layout` above) leaves
1745 // this false so the frame runs every vsync.
1746 needs_frame_paced_only: ctx.needs_frame_paced_only(),
1747 // ...and, when it IS paceable, how fast it asked to be re-run:
1748 // the MIN over every paced request this pass (`Duration::ZERO`
1749 // / `None` meaning the theme's own cosmetic rate). The gate
1750 // resolves it against the live theme's cap — see
1751 // `PaintCtx::request_frame_paced_at`.
1752 paced_interval: ctx.paced_interval(),
1753 };
1754 (outcome, main_tree_ime)
1755 } else {
1756 (PaintOutcome::default(), None)
1757 }
1758 }
1759
1760 /// Paint the pods registered during this pass, above the main tree, and fold
1761 /// each one's paint outcome back into `outcome`.
1762 ///
1763 /// `entries` arrives in paint order (`Floating` band first, then `Tooltip`,
1764 /// registration order within each). Each pod is painted through a
1765 /// [`PaintCtx`] whose absolute origin is its own registered
1766 /// [`window_rect`](crate::overlay::OverlayEntry::window_rect) — not its
1767 /// owner's origin, which is the whole point of floating — carrying the same
1768 /// clock, theme, insets, presented count, translucency and focus/hover
1769 /// seeding the root pod's context carries, so a widget inside a pod cannot
1770 /// tell it is not in the tree.
1771 ///
1772 /// The pod borrow is taken per entry and released before the next: the root
1773 /// must never hold one across a pass boundary, nor across another entry's
1774 /// paint (two entries may belong to the same owner).
1775 ///
1776 /// It is also the one pass that can act on a floated pod's focus link at
1777 /// all, because it is the one pass holding the pods. Two things follow, and
1778 /// both happen here:
1779 ///
1780 /// * it **retires** a link the live session has already stranded
1781 /// ([`ChildPod::retire_stale_focus_link`](crate::widget::ChildPod::retire_stale_focus_link)),
1782 /// so the raw flag consumers that cannot consult an epoch — an owner's own
1783 /// `is_focused()` read, a hand-written container's routing — stop seeing a
1784 /// record of a session that has moved on;
1785 /// * it **resolves** which surface, if any, holds a link on the live session,
1786 /// and collects that one surface's IME publish. Every other pod's publish
1787 /// is dropped where it is taken: paint descends into every pod
1788 /// unconditionally and the bubble up `paint_child` carries a published
1789 /// surface whatever the publisher's link says, so without this the last pod
1790 /// painted would decide what the shell is configured with — a surface
1791 /// belonging to a field it has nothing to do with, secure-text
1792 /// configuration and all.
1793 ///
1794 /// Returns `(holder, holder's publish)` for
1795 /// [`RenderRoot::resolve_session_surface`] to settle against the main tree's.
1796 /// **Two holders resolve to none:** a second live link is a contradiction the
1797 /// mechanism is supposed to make impossible (one claim, one chain, one
1798 /// stamp), and answering it by picking the topmost would let a surface speak
1799 /// for a session on evidence that has already failed. Reporting no holder
1800 /// instead means neither surface's publish is carried out of this pass —
1801 /// the resolver then reads the pass exactly as it reads one where nothing is
1802 /// floated at all.
1803 fn paint_overlays(
1804 &mut self,
1805 entries: &[OverlayEntry],
1806 scene: &mut dyn PaintScene,
1807 frame_time: FrameTime,
1808 outcome: &mut PaintOutcome,
1809 ) -> (Option<OverlayKey>, Option<ImeState>) {
1810 if entries.is_empty() {
1811 // Nothing is floated, so nothing floated owns the session.
1812 return (None, None);
1813 }
1814 // The same disjoint field borrows the main pass takes, for the same
1815 // reason: the theme is lent immutably into each context while other
1816 // fields of `self` are written.
1817 let theme = self.theme.as_deref();
1818 let presented_frames = self.presented_frames;
1819 let surface_translucent = self.surface_translucent;
1820 let hover_active = self.hover_active;
1821 let hover_epoch = self.hover_epoch;
1822 let focus_active = self.focus_active;
1823 let focus_epoch = self.focus_epoch;
1824
1825 // The surface whose pod holds a link on the LIVE session, resolved as the
1826 // pods go past, together with the surface that pod published — one pair,
1827 // so the publish can never be attributed to an entry that did not make
1828 // it. A second live holder sets `contested` and the pair is discarded.
1829 let mut holder: Option<(OverlayKey, Option<ImeState>)> = None;
1830 let mut contested = false;
1831
1832 for entry in entries {
1833 let mut ctx = PaintCtx::new(entry.window_rect.origin(), entry.window_rect.size());
1834 ctx.set_frame_time(frame_time);
1835 ctx.set_theme(theme);
1836 ctx.set_window_insets(entry.insets);
1837 ctx.set_presented_frames(presented_frames);
1838 ctx.set_translucent(surface_translucent);
1839 // Seeded from the root's own mirrors exactly as the root pod's
1840 // context is, so a focused editable inside a floated pod observes its
1841 // focus (and a hovered one its hover) through the ordinary
1842 // `ChildPod::paint_child` composition.
1843 //
1844 // The mirror alone, deliberately: the pod's own link is ANDed onto it
1845 // inside `paint_child`, which is the same composition that decides
1846 // the main tree's, so a pod that holds no link reads unfocused
1847 // whatever the mirror says.
1848 ctx.set_has_focus(focus_active);
1849 ctx.set_focus_epoch(focus_epoch);
1850 ctx.set_hovered(hover_active);
1851 ctx.set_hover_epoch(hover_epoch);
1852 // Retired and read before the paint, in the same borrow: nothing in a
1853 // paint pass moves the focus path, and the answer is what decides
1854 // whether this pod may describe the session below.
1855 let pod_holds_focus = {
1856 let mut pod = entry.pod.borrow_mut();
1857 pod.retire_stale_focus_link();
1858 let holds = pod.holds_live_focus();
1859 pod.paint_child(&mut ctx, scene);
1860 holds
1861 };
1862
1863 // Fold this pod's continuation-frame request into the frame's outcome
1864 // on the two lattices the tree's own aggregation uses: `needs_frame`
1865 // ORs, the class is a max-lattice (any unpaced request makes the whole
1866 // frame unpaced), and the paced interval is a MIN-lattice. The
1867 // standing aggregate's class is recovered from the outcome itself —
1868 // `needs_frame && !needs_frame_paced_only` is precisely "something
1869 // unpaced asked" — which also preserves the deferred-flush frame the
1870 // main pass may have folded in.
1871 let stood_unpaced = outcome.needs_frame && !outcome.needs_frame_paced_only;
1872 let entry_unpaced = ctx.needs_frame() && !ctx.needs_frame_paced_only();
1873 outcome.needs_frame |= ctx.needs_frame();
1874 outcome.needs_frame_paced_only =
1875 outcome.needs_frame && !(stood_unpaced || entry_unpaced);
1876 outcome.paced_interval = match (outcome.paced_interval, ctx.paced_interval()) {
1877 (Some(standing), Some(asked)) => Some(standing.min(asked)),
1878 (standing, asked) => standing.or(asked),
1879 };
1880 // A layout-animating widget inside a pod relayouts the next frame the
1881 // same way one in the tree does.
1882 if ctx.needs_layout() {
1883 outcome.needs_layout = true;
1884 self.pending |= ChangeFlags::LAYOUT;
1885 }
1886 // A focused editable inside a pod republishes its IME surface on every
1887 // paint, exactly like one in the tree, so the same rules apply
1888 // verbatim: collect a publish only while a session is actually active
1889 // (never resurrect a surface a blur cleared), and only from the pod
1890 // that holds a link on THAT session — the provenance a child of the
1891 // tree gets for free from its chain. A publish taken from any other
1892 // pod is dropped here, which is why the take is unconditional: the
1893 // per-entry context is about to be discarded either way, and leaving
1894 // a surface in it would only invite a later reader to trust it.
1895 //
1896 // An *inactive* publish is refused on exactly the same terms rather
1897 // than treated as a release: ending a session is a claim about it
1898 // too, and a pod that does not hold it makes neither.
1899 let published = ctx.take_ime_state();
1900 if focus_active && pod_holds_focus {
1901 if holder.is_some() {
1902 // Two pods claiming one session. See the method doc: the
1903 // contradiction is answered by attributing the session to
1904 // nobody, not by ranking the claimants.
1905 contested = true;
1906 } else {
1907 holder = Some((entry.key, published));
1908 }
1909 }
1910 // EXTEND the two replace-per-pass channels rather than replacing them:
1911 // `paint_main_tree` already put this pass's tree-published frames and
1912 // shields there, and a platform-view slot or z-shield that happens to
1913 // paint inside a floated pod must survive beside them (see
1914 // `PaintCtx::publish_platform_view`).
1915 self.platform_view_frames.extend(ctx.take_platform_views());
1916 self.input_shields.extend(ctx.take_input_shields());
1917 }
1918
1919 match holder {
1920 Some((key, published)) if !contested => (Some(key), published),
1921 _ => (None, None),
1922 }
1923 }
1924
1925 /// Resolve this paint pass's selection-toolbar publish into the shell-facing
1926 /// slot, moving [`RenderRoot::selection_toolbar_generation`] only on a
1927 /// **menu edge**.
1928 ///
1929 /// The request carries two shapes of fact, and they are resolved differently
1930 /// (see [`SelectionToolbarRequest`]):
1931 ///
1932 /// * The whole request is stored as a **level**, newest wins. A shell reads
1933 /// [`SelectionToolbarRequest::anchor`] on every tick it has a menu on
1934 /// screen, so the stored anchor has to be the current one, not the one the
1935 /// generation last moved for.
1936 /// * The generation moves on the **menu-significant** part alone:
1937 /// [`SelectionToolbarRequest::present_menu`] and
1938 /// [`SelectionToolbarRequest::actions`], with an absent request reading as
1939 /// "no menu, no verbs" so appearing and disappearing are edges on the same
1940 /// comparison. An anchor that merely moved is deliberately **not** an edge:
1941 /// a focused field recomputes its anchor every painted frame, and a
1942 /// selection dragged wider moves it on every touch sample — bumping there
1943 /// would ask the platform to re-present its menu per sample.
1944 ///
1945 /// The publish is refused outright while no focus session is active, mirroring
1946 /// `paint`'s refusal to let a paint-time publish resurrect a cleared IME
1947 /// surface: the request describes the focused field, so one arriving after the
1948 /// blur describes a field that no longer holds anything.
1949 fn resolve_selection_toolbar(&mut self, published: Option<SelectionToolbarRequest>) {
1950 let next = if self.focus_active { published } else { None };
1951 // "Nothing published" is the same statement as "no menu wanted, no verbs
1952 // enabled" — which is what lets one comparison cover a change between two
1953 // requests, a first appearance, and a clearing alike.
1954 let menu_edge = |request: &Option<SelectionToolbarRequest>| {
1955 request.map_or((false, SelectionToolbarActions::default()), |request| {
1956 (request.present_menu, request.actions)
1957 })
1958 };
1959 if menu_edge(&self.selection_toolbar) != menu_edge(&next) {
1960 self.selection_toolbar_gen = self.selection_toolbar_gen.wrapping_add(1);
1961 }
1962 self.selection_toolbar = next;
1963 }
1964
1965 /// The selection-toolbar request the focused field published during the most
1966 /// recent [`RenderRoot::paint`], or `None` when no field is focused at all.
1967 ///
1968 /// The shell half of the platform edit-menu route
1969 /// ([`SelectionToolbarPolicy::Native`](crate::selection_toolbar::SelectionToolbarPolicy::Native)):
1970 /// a shell reads it beside [`RenderRoot::ime_state`], answers "may I offer
1971 /// this verb?" from [`SelectionToolbarRequest::actions`] whenever the platform
1972 /// asks, and presents the host's own menu at
1973 /// [`SelectionToolbarRequest::anchor`] when
1974 /// [`SelectionToolbarRequest::present_menu`] says so. A **level**, not an edge
1975 /// — re-read it as often as you like; pair it with
1976 /// [`RenderRoot::selection_toolbar_generation`] to notice the changes worth
1977 /// presenting or dismissing for.
1978 ///
1979 /// Present for a focused field with no selection at all, which is not a
1980 /// wasted answer: paste applies to a bare caret, and a platform asking
1981 /// whether it may offer one needs a reply before any bar exists.
1982 ///
1983 /// A field under the framework policy publishes this too (it floats its own
1984 /// toolbar through [`crate::overlay`] as well), so a shell that drives the
1985 /// platform menu must decide on the policy, not on the presence of a request.
1986 pub fn selection_toolbar(&self) -> Option<SelectionToolbarRequest> {
1987 self.selection_toolbar
1988 }
1989
1990 /// A monotonically-increasing generation bumped on every change to the
1991 /// **menu-significant** part of [`RenderRoot::selection_toolbar`] — its
1992 /// [`present_menu`](SelectionToolbarRequest::present_menu) flag and its
1993 /// [`actions`](SelectionToolbarRequest::actions) — including the clearing that
1994 /// a blur produces, so a shell sees the menu going away as an edge too.
1995 ///
1996 /// A moved [`anchor`](SelectionToolbarRequest::anchor) is **not** an edge: it
1997 /// is republished (and recomputed) every painted frame, so a shell re-reads it
1998 /// from [`RenderRoot::selection_toolbar`] rather than waiting for this to move
1999 /// — bumping on it would re-present a menu on every touch sample of a drag
2000 /// that widens a selection.
2001 ///
2002 /// The `focus_ime_generation` contract one channel over: a shell caches the
2003 /// last value it acted on and acts only when it moves, which is what keeps a
2004 /// standing selection — republished every single frame — from asking the
2005 /// platform to re-present its menu on every vsync.
2006 pub fn selection_toolbar_generation(&self) -> u64 {
2007 self.selection_toolbar_gen
2008 }
2009
2010 /// Collect the accessibility tree for the current frame,
2011 /// returning a [`SemanticsUpdate`] a platform adapter (`accesskit_*`)
2012 /// can consume.
2013 ///
2014 /// Pull-based and stateless: the shell calls this when a platform a11y client
2015 /// asks for the tree (or after a change), *never* per frame — this crate owns
2016 /// no scheduling. Must run **after** [`RenderRoot::layout`], since node bounds
2017 /// come from the pods' post-layout geometry.
2018 ///
2019 /// The result is always rooted at a synthetic [`accesskit::Role::Window`]
2020 /// node covering the window, whose children are whatever the root widget
2021 /// contributed. An unbuilt tree yields a bare window node with no children.
2022 pub fn semantics(&self) -> SemanticsUpdate {
2023 let mut ctx = SemanticsCtx::new(self.window_size, self.semantics_alloc.get());
2024 let window = self.window_size;
2025 let root_pod = self.root_id.and_then(|id| self.tree.pod(id));
2026 // The root pod is arena-backed (not a `ChildPod`), so it caches its stable
2027 // base id in `root_semantics_id` rather than in a pod — assigned on first
2028 // pass and reused thereafter, exactly like `ChildPod::semantics_base`.
2029 let root_widget_base = match self.root_semantics_id.get() {
2030 Some(id) => id,
2031 None => {
2032 let id = ctx.alloc_base();
2033 self.root_semantics_id.set(Some(id));
2034 id
2035 }
2036 };
2037 let root_node = ctx.push_container_with_id(
2038 ROOT_NODE_ID,
2039 accesskit::Role::Window,
2040 |node| {
2041 node.set_bounds(accesskit::Rect {
2042 x0: 0.0,
2043 y0: 0.0,
2044 x1: window.width,
2045 y1: window.height,
2046 });
2047 },
2048 |ctx| {
2049 if let Some(pod) = root_pod {
2050 // The root pod sits at its recorded origin (ZERO today) with
2051 // its laid-out size; descend into that geometry and its stable
2052 // id scope, mirroring `ChildPod::semantics_child`.
2053 ctx.descend_into_pod(
2054 root_widget_base,
2055 pod.origin().to_vec2(),
2056 pod.size(),
2057 |ctx| {
2058 pod.widget().semantics(ctx);
2059 },
2060 );
2061 }
2062 },
2063 );
2064 // Persist the allocator's high-water mark so the next pass keeps handing
2065 // out fresh, never-reused bases to pods that first appear later.
2066 self.semantics_alloc.set(ctx.next_base());
2067 ctx.finish(root_node)
2068 }
2069
2070 /// The current semantics generation — bumped by every rebuild/theme swap that
2071 /// could have changed the accessibility tree (the semantics dirty gate).
2072 ///
2073 /// A shell records the value it last pushed and compares; see
2074 /// [`RenderRoot::semantics_if_changed`].
2075 pub fn semantics_generation(&self) -> u64 {
2076 self.semantics_gen
2077 }
2078
2079 /// Pull a fresh [`SemanticsUpdate`] **only if** the semantics tree may have
2080 /// changed since generation `last_seen`.
2081 ///
2082 /// Returns `None` when nothing relevant changed, letting a shell skip both the
2083 /// tree walk and the platform `accesskit_*` push. Call it post-layout (bounds
2084 /// must be valid). A shell threads its stored generation in and, on `Some`,
2085 /// updates it from [`RenderRoot::semantics_generation`]. v1 pushes the whole
2086 /// tree when it does recompute (stable ids make that valid); finer-grained
2087 /// diffing is a later optimization.
2088 pub fn semantics_if_changed(&self, last_seen: u64) -> Option<SemanticsUpdate> {
2089 (self.semantics_gen != last_seen).then(|| self.semantics())
2090 }
2091
2092 /// Deliver an input event to the widget tree, returning what happened.
2093 ///
2094 /// Builds a root [`EventCtx`] over the (type-erased) `state`, dispatches to
2095 /// the root widget — which routes the event down through its container
2096 /// children — and folds the result into an [`EventOutcome`]. The outcome's
2097 /// `needs_redraw` is set whenever a widget consumed the event or explicitly
2098 /// requested a redraw; the shell turns that into a `window.request_redraw()`.
2099 ///
2100 /// Root capture bookkeeping mirrors the per-container `active`-child model: a
2101 /// `Down` whose dispatch requested capture marks a gesture in flight and
2102 /// latches the contact that sent it as the gesture's claimant; the
2103 /// claimant's `Up` and `Cancel` release it (never a window-leave, and never
2104 /// another contact's release).
2105 ///
2106 /// Pointer contacts are gated **first**, before anything else runs: an
2107 /// [`InputEvent::PointerContact`] is unwrapped into the plain
2108 /// [`InputEvent::Pointer`] every widget matches on, with
2109 /// [`EventCtx::pointer_id`](crate::event::EventCtx::pointer_id) reporting its
2110 /// id, and a contact the multi-contact contract does not route — an
2111 /// additional contact with nothing captured, or one the captor did not opt
2112 /// into — is dropped here with an empty outcome (see that variant's
2113 /// *Multi-contact contract*). A bare `InputEvent::Pointer` is the mouse.
2114 ///
2115 /// Root hover bookkeeping is the third recorded path, and the one this pass
2116 /// *derives* rather than merely mirrors: an **uncaptured** `Move` opens a hover
2117 /// pass (widgets on the hit-tested path may claim it — see
2118 /// [`EventCtx::claim_hover`](crate::event::EventCtx::claim_hover)), a
2119 /// `Down`/`Up`/`Cancel` ends whatever hover stood, and every other event leaves
2120 /// it alone. There is nothing to release and no generation to bump: the epoch
2121 /// advance strands the previous claimant's path by itself, and the outcome's
2122 /// `needs_redraw` carries the one repaint **no widget can ask for** — a hover
2123 /// that ended with nothing taking it. A hover that *begins* or *moves from one
2124 /// claimant to another* is repainted by the new claimant's own change-gated
2125 /// `request_redraw`, which is why keeping an internal hover flag is part of the
2126 /// consumer contract rather than an optimization (see `claim_hover`).
2127 /// **No shell change is required for hover** — the desktop shell already
2128 /// dispatches a `Move` on every cursor move.
2129 ///
2130 /// The cursor is hover's sibling channel and the fourth thing this pass
2131 /// resolves: any pointer `Move` (captured included) re-resolves
2132 /// [`RenderRoot::cursor`] from the pass's last
2133 /// [`EventCtx::set_cursor`](crate::event::EventCtx::set_cursor), defaulting to
2134 /// [`CursorIcon::Default`] when nothing asked. It deliberately does **not**
2135 /// fold into the outcome's `needs_redraw`: applying a cursor is a platform
2136 /// call a desktop shell makes straight after this pass returns, with no frame
2137 /// involved, and folding it in would repaint the tree on every hover move.
2138 ///
2139 /// The **clipboard channel** rides the same bracket and is the fifth thing
2140 /// this pass resolves: whatever the dispatch asked for through
2141 /// [`EventCtx::write_clipboard`](crate::event::EventCtx::write_clipboard) and
2142 /// [`EventCtx::request_paste`](crate::event::EventCtx::request_paste) lands in
2143 /// [`RenderRoot::take_clipboard_write`] / [`RenderRoot::take_paste_request`],
2144 /// which a shell drains immediately after this returns, beside
2145 /// [`RenderRoot::cursor`] and [`RenderRoot::ime_state`]. Unlike the cursor,
2146 /// both commit on **every** pass rather than on a pointer `Move` alone — a
2147 /// copy can be answered from a key chord, a context-menu tap, or an
2148 /// [`InputEvent::EditCommand`] — and both are one-shot drains rather than
2149 /// standing levels. Like the cursor, neither folds into `needs_redraw`:
2150 /// talking to the host clipboard paints nothing (a `Cut` that mutates the
2151 /// document asks for its own redraw, for the mutation).
2152 ///
2153 /// # Reentrancy
2154 ///
2155 /// This pass **never rebuilds or repaints**. Event handlers mutate `state`
2156 /// synchronously through the context; the shell is expected to run a single
2157 /// [`RenderRoot::rebuild`] (then layout/paint) *after* the event pass returns,
2158 /// driven by the outcome. Rebuilding re-entrantly here would invalidate the
2159 /// widget references the dispatch still holds and turn the event→state→view
2160 /// feedback into recursion.
2161 ///
2162 /// [`RenderRoot::rebuild`] calls this itself with
2163 /// [`InputEvent::Housekeeping`] to flush deferred state-bearing callbacks.
2164 /// That is *sequential*, not re-entrant — the dispatch fully returns before
2165 /// the next diff starts — so the rule above is intact.
2166 pub fn event(&mut self, state: &mut State, event: &InputEvent) -> EventOutcome {
2167 let Some(root_id) = self.root_id else {
2168 return EventOutcome::default();
2169 };
2170
2171 // The multi-contact gate (`InputEvent::PointerContact`'s contract). A
2172 // contact is unwrapped into the plain `Pointer` every widget matches on,
2173 // so everything below sees one pointer shape; a bare `Pointer` is the
2174 // mouse. Any other event dispatches under the contact the enclosing pass
2175 // carries (the overlay pre-pass re-enters this method with a broadcast
2176 // that must keep its gesture's id), or the mouse at the top level.
2177 let unwrapped;
2178 let (event, pointer_id) = match event {
2179 InputEvent::PointerContact {
2180 pointer_id,
2181 event: pointer,
2182 } => {
2183 unwrapped = InputEvent::Pointer(*pointer);
2184 (&unwrapped, *pointer_id)
2185 }
2186 InputEvent::Pointer(_) => (event, PointerId::MOUSE),
2187 _ => (event, crate::event::current_pointer_id()),
2188 };
2189 let secondary = if matches!(event, InputEvent::Pointer(_)) {
2190 match self.contact_route(pointer_id) {
2191 Some(secondary) => secondary,
2192 // Rule (b), or rule (c) for a captor that did not opt in: the
2193 // contact reaches nothing and moves no root state.
2194 None => return EventOutcome::default(),
2195 }
2196 } else {
2197 false
2198 };
2199 // Published for the whole dispatch: `EventCtx::new` seeds
2200 // `pointer_id()` from it (so the id survives a component boundary),
2201 // `capture_contacts` records its opt-in into it, and `ChildPod::set_active`
2202 // reads its `secondary` mark to keep every container's active link on
2203 // the claimant. Restored on drop, so a nested pass scopes its own.
2204 let contact_pass = ContactPass::enter(pointer_id, secondary);
2205
2206 // The overlay pre-pass runs before every other thing this method does —
2207 // before the hover derivation, before the cursor bracket, before the
2208 // dispatch — because a pointer over a floated surface must reach none of
2209 // them: not the main tree's hit test, not its hover pass, and above all
2210 // not its blur rule. See `route_overlay`.
2211 let carried = match self.route_overlay(state, event) {
2212 OverlayRoute::Consumed(outcome) => return outcome,
2213 OverlayRoute::Continue(outcome) => outcome,
2214 };
2215
2216 // Open a candidate focus session for this dispatch, BEFORE anything is
2217 // routed. A claim recorded below is stamped with this new epoch, which
2218 // nothing older carries — so the branch the session is leaving is
2219 // stranded by arithmetic rather than by a clearing sweep that would have
2220 // to visit it, and a floated pod no container owns is retired on exactly
2221 // the same terms as a field in the tree.
2222 //
2223 // Candidate, not committed: `self.focus_epoch` is left alone and the
2224 // pass settles it below, because whether this dispatch recorded anything
2225 // is only known once it has run. A `Move` over a focused field, a
2226 // housekeeping broadcast, a press inside a surface that claims nothing —
2227 // none of those may disturb a standing link.
2228 //
2229 // Both epochs are published because a claim recorded on the way back up
2230 // has to be observable to the container still unwinding around it (a
2231 // blur sweep asking which child kept focus, a portal noticing its surface
2232 // took the session) while a link recorded *before* this dispatch still
2233 // has to read live. See `crate::widget::set_live_focus_session`.
2234 //
2235 // After `route_overlay`, deliberately: an event that belongs to a floated
2236 // surface is re-dispatched through the front door and opens its own
2237 // candidate session there, and this call returns that outcome untouched.
2238 let live_focus_epoch = self.focus_epoch;
2239 let candidate_focus_epoch = advance_focus_epoch(live_focus_epoch);
2240 crate::widget::set_live_focus_session(
2241 self.root_identity,
2242 live_focus_epoch,
2243 candidate_focus_epoch,
2244 );
2245 let focus_active_before = self.focus_active;
2246
2247 let Some(pod) = self.tree.pod_mut(root_id) else {
2248 // Nothing to dispatch into, but an outside-tap notification may
2249 // already have produced an outcome; returning it rather than the
2250 // default keeps that redraw.
2251 self.publish_focus_session();
2252 return carried;
2253 };
2254
2255 // Hover is derived per **uncaptured** pointer `Move`: that pass, and only
2256 // that pass, may record a claim, so a captured drag can never paint hover
2257 // under the pointer. Every other pointer phase — `Down`, `Up`, `Cancel` —
2258 // is an epoch-advancing pass that *ends* whatever hover stood without
2259 // opening a new one: a press is not a hover, a touch `Down` must not
2260 // inherit one, and a lift is the only signal a touch contact leaving the
2261 // screen ever produces (no further `Move` follows it, so a tint claimed
2262 // during an uncaptured touch drag would otherwise stand indefinitely).
2263 // Ending on `Up` costs a mouse the hover tint between a click's release
2264 // and its next motion — the same standing-until-next-move class as the
2265 // press-then-hold-still gap, and traded deliberately for a touch link that
2266 // cannot outlive the finger (`docs/LIMITATIONS.md`'s
2267 // `hover-window-leave-standing`). A scroll, key, IME, or the housekeeping
2268 // broadcast leaves a live hover exactly as it was.
2269 //
2270 // A non-claimant contact delivered down a live capture (`secondary`) is
2271 // neither: it is not a gesture of its own, so it leaves hover exactly as
2272 // the claimant's gesture had it.
2273 let hover_pass = matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Move)
2274 && self.capture_claimant.is_none();
2275 let hover_ends = !secondary
2276 && matches!(
2277 event,
2278 InputEvent::Pointer(p)
2279 if matches!(
2280 p.phase,
2281 PointerPhase::Down | PointerPhase::Up | PointerPhase::Cancel
2282 )
2283 );
2284 let hover_epoch = self.hover_epoch;
2285 let hover_was_active = self.hover_active;
2286 let root_identity = self.root_identity;
2287
2288 // The cursor pass is hover's pass widened by one case: **any** pointer
2289 // `Move`, captured included, because a captured `Move` routes only to the
2290 // capturing widget and that is exactly how a drag keeps its own cursor
2291 // while the pointer is outside its bounds. Every other pass leaves the
2292 // resolved cursor standing — notably `Down`/`Up`, whose handlers have no
2293 // reason to restate a cursor and whose reset would blink the shape back to
2294 // `Default` for the length of a click.
2295 //
2296 // The slot is cleared here rather than trusted to be empty: a `set_cursor`
2297 // from a dispatch no root drove (a reconciler's synthesized `Cancel`)
2298 // must not leak into this pass's resolution. The clear and the drain below
2299 // are one bracket (`CursorPass`) rather than two bare calls, so a dispatch
2300 // that re-entered this method could not silently eat the enclosing pass's
2301 // request — see that guard.
2302 //
2303 // The claimant's moves only: a second finger moving under a captured
2304 // drag says nothing about the drag's own cursor.
2305 let cursor_pass =
2306 !secondary && matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Move);
2307 // The same bracket carries the clipboard channel (a write and a paste
2308 // request), which differs only in when it commits: every pass, not the
2309 // pointer-move subset, since a copy can be answered from a key chord or an
2310 // `EditCommand` that never moved a pointer. See `RequestPass`.
2311 let request_slot = RequestPass::enter();
2312
2313 // Whether the root widget itself holds the live opt-in (rather than a
2314 // pod below it), and whether it opted in during this dispatch.
2315 let contacts_captor_is_root = self.capture_claimant.is_some()
2316 && self.capture_contacts
2317 && self.contacts_captor_is_root;
2318 let root_opted_in;
2319
2320 let (handled, needs_redraw, captured, hover_claimed, focus_req, focus_rel, ime) = {
2321 let state_any: &mut dyn Any = state;
2322 let mut ctx = EventCtx::new(state_any, pod.origin(), pod.size());
2323 // Seed the root widget's focus flag so a leaf-root editable that holds
2324 // focus can observe `has_focus()`; deeper focus is threaded per-pod.
2325 ctx.set_has_focus(self.focus_active);
2326 // Same for the hover link, plus the live epoch every pod compares its
2327 // stamp against and the eligibility gate that decides whether a claim
2328 // is recordable at all this pass.
2329 ctx.set_hovered(hover_was_active);
2330 ctx.set_hover_epoch(hover_epoch);
2331 ctx.set_hover_eligible(hover_pass);
2332 // Stamped onto whichever pod records a claim, so that pod's
2333 // destructor can tell this root's link from another root's
2334 // identically-numbered epoch (see `root_identity`).
2335 ctx.set_hover_root(root_identity);
2336 // The root widget's own contact frame: attributes a
2337 // `capture_contacts` made by the root widget itself (not by a pod
2338 // below it) to the root, and marks the dispatch as running under the
2339 // live opt-in's holder when the root widget is that holder.
2340 let frame = ContactFrame::enter(contacts_captor_is_root);
2341 let result = if secondary && !contacts_captor_is_root {
2342 // A non-claimant contact walks the recorded active path
2343 // forward-only (see `ChildPod::event_child`): the root widget is
2344 // handed the inert carrier and the real event rides beside it,
2345 // so only the captor that opted in runs its pointer handling. A
2346 // walk that never reached a pod on the path (the root widget
2347 // does not forward broadcasts to its captured child) falls back
2348 // to the ordinary delivery.
2349 let widget = pod.widget_mut();
2350 let carrier = crate::event::secondary_walk_carrier();
2351 let (_, delivered) = crate::event::run_secondary_walk(event.clone(), || {
2352 widget.event(&mut ctx, &carrier)
2353 });
2354 match delivered {
2355 Some(result) => result,
2356 None => crate::event::without_secondary_walk(|| widget.event(&mut ctx, event)),
2357 }
2358 } else {
2359 pod.widget_mut().event(&mut ctx, event)
2360 };
2361 root_opted_in = frame.close().0;
2362 let handled = matches!(result, EventResult::Handled);
2363 (
2364 handled,
2365 ctx.needs_redraw() || handled,
2366 ctx.is_pointer_captured(),
2367 ctx.is_hover_claimed(),
2368 ctx.is_focus_requested(),
2369 ctx.is_focus_released(),
2370 ctx.take_ime_state(),
2371 )
2372 };
2373
2374 // A published IME surface refreshes the stored one (persists past this
2375 // event, survives rebuild) until a blur clears it below. `store_ime_state`
2376 // bumps the focus/IME edge generation only if the surface actually moved
2377 // (a keystroke that changes nothing observable is not an edge).
2378 //
2379 // An **inactive** publish carries the same release intent here as it does
2380 // in `paint` (see that method's take path): a widget that publishes
2381 // `active: false` is ending the session, not describing it, so it takes
2382 // the full release. Reachable from this pass too — every paint-time
2383 // producer of an inactive surface is a container/widget whose `event` arm
2384 // can run first — and the two passes must not disagree about what an
2385 // inactive surface means. No `focus_active` guard is needed (unlike
2386 // `paint`, which must refuse to *resurrect* a cleared surface): releasing
2387 // an already-released root moves nothing and fires no edge.
2388 //
2389 // Ordering: this runs before the focus match below, so a dispatch that
2390 // both published an inactive surface and requested focus still ends up
2391 // focused — the later, more specific claim wins.
2392 //
2393 // The provenance rule `paint_overlays` applies to a paint-time publish,
2394 // mirrored onto this pass — the two must not disagree about who is
2395 // allowed to speak for the session, so a change to either belongs in
2396 // both.
2397 //
2398 // What the root can attribute here is bounded by how the event was
2399 // routed. A hit-tested or focus-routed dispatch reaches a publisher
2400 // through the tree, and the tree's own chain is what gated it (a
2401 // well-behaved editable publishes only while `has_focus`, which is now
2402 // stamp-gated) — so the rule for those is the liveness half alone: a
2403 // session must exist, or be opening in this very dispatch, for a publish
2404 // to describe anything. An `InputEvent::Overlay` is broadcast to the
2405 // whole tree and reaches every floated pod whether or not it holds
2406 // anything, so there the addressed surface must be the recorded owner of
2407 // the session, or be claiming it now — the same "the publisher holds the
2408 // link" question `paint_overlays` answers from the pods themselves.
2409 //
2410 // The *inactive* publish is refused on the same terms and for the same
2411 // reason it is at paint: ending a session is a claim about it too, and a
2412 // branch that owns none makes neither.
2413 let publish_attributable = match event {
2414 InputEvent::Overlay(overlay) => focus_req || self.focus_surface == Some(overlay.key),
2415 _ => self.focus_active || focus_req,
2416 };
2417 match ime {
2418 Some(_) if !publish_attributable => {}
2419 Some(ime) if !ime.active => {
2420 self.release_focus_session();
2421 }
2422 Some(ime) => self.store_ime_state(Some(ime)),
2423 None => {}
2424 }
2425
2426 // Root-level capture path: a captured `Down` opens a gesture; `Up`/`Cancel`
2427 // close it. `Move` leaves the flag untouched so it survives the drag.
2428 //
2429 // Root-level focus path (the capture mirror): a `Down` that requested
2430 // focus opens the focus session; a `Down` that did not is a
2431 // blur-on-outside-tap and closes it (the per-container `focused` flags are
2432 // cleared by the routing helpers). Key/Ime/Scroll only adjust focus if the
2433 // dispatch explicitly requested or released it.
2434 //
2435 // Every arm mutates through `set_focus_active`/`store_ime_state` or the
2436 // paired `release_focus_session`, the change-guarded writers that own the
2437 // focus/IME edge generation: a `Down` on already-blurred chrome (the
2438 // commonest event of all) writes the same values back and must therefore
2439 // NOT fire an edge.
2440 //
2441 // Whether an arm below actually *honoured* a focus claim — the one thing
2442 // that makes the candidate epoch opened above the real one. Deliberately
2443 // not `focus_req` itself: the housekeeping arm ignores a claim by
2444 // contract, and a claim it ignored must leave the standing session (and
2445 // therefore the standing epoch) exactly where it was.
2446 let mut focus_claim_honoured = false;
2447 match event {
2448 // A non-claimant contact delivered down a live capture (rule (c) of
2449 // `InputEvent::PointerContact`'s contract): not a gesture of its own,
2450 // so it opens, moves and releases no capture and never blurs — its
2451 // `Up` must not end the claimant's gesture, and its `Down` is not a
2452 // tap outside anything. An explicit focus request or release from
2453 // its handler is honoured exactly as the scroll arm below honours
2454 // one.
2455 InputEvent::Pointer(_) if secondary => {
2456 if focus_req {
2457 focus_claim_honoured = true;
2458 self.set_focus_active(true);
2459 }
2460 if focus_rel {
2461 self.release_focus_session();
2462 }
2463 }
2464 // Never reached: the gate at the top unwrapped every contact into a
2465 // plain `Pointer`, and routed it by the arms around this one.
2466 InputEvent::PointerContact { .. } => {}
2467 InputEvent::Pointer(pointer) => match pointer.phase {
2468 PointerPhase::Down => {
2469 if captured {
2470 // Rule (a): the contact whose `Down` captured is the
2471 // claimant, and only its release ends the gesture. The
2472 // opt-in is read from the pass rather than the root
2473 // context's bubble so a captor below a component
2474 // boundary (which mirrors capture but not the opt-in)
2475 // is still heard.
2476 self.capture_claimant = Some(pointer_id);
2477 self.capture_contacts = contact_pass.contacts_requested();
2478 self.contacts_captor_is_root = root_opted_in;
2479 }
2480 if focus_req {
2481 focus_claim_honoured = true;
2482 self.set_focus_active(true);
2483 // A hit-tested press reached the claimant through the
2484 // containers, so the session is the main tree's — the one
2485 // attribution the event pass can make without help, and
2486 // what makes the next paint seed the tree's links again
2487 // the moment focus comes back to it (see `focus_surface`).
2488 self.focus_surface = None;
2489 } else {
2490 // Blur: no widget on the tapped path took focus. The
2491 // canonical release — flag and surface drop together, as
2492 // one edge (see `release_focus_session_in`).
2493 self.release_focus_session();
2494 }
2495 }
2496 // Only ever the claimant's (or an uncaptured contact's) release:
2497 // any other contact's was routed to the `secondary` arm above or
2498 // dropped at the gate.
2499 PointerPhase::Up | PointerPhase::Cancel => {
2500 self.capture_claimant = None;
2501 self.capture_contacts = false;
2502 self.contacts_captor_is_root = false;
2503 }
2504 PointerPhase::Move => {}
2505 },
2506 InputEvent::Scroll { .. }
2507 | InputEvent::Scale(_)
2508 | InputEvent::Key(_)
2509 | InputEvent::Ime(_)
2510 | InputEvent::EditCommand(_) => {
2511 if focus_req {
2512 // The candidate epoch opened above becomes this claim's, so
2513 // a focus move driven from the keyboard retires the branch it
2514 // supersedes on exactly the terms a press does — including a
2515 // floated surface's link, which this arm could otherwise
2516 // never reach. `focus_surface` is deliberately left alone:
2517 // which branch a focus-routed claim came from is not
2518 // something the root can attribute, so the next paint
2519 // re-resolves it from the pods' own live links.
2520 focus_claim_honoured = true;
2521 self.set_focus_active(true);
2522 }
2523 if focus_rel {
2524 self.release_focus_session();
2525 }
2526 }
2527 // A broadcast is not user input: it opens no gesture, claims no
2528 // focus, and blurs nothing. Deliberately inert here — a housekeeping
2529 // pass that moved the root's capture/focus bookkeeping would change
2530 // what the *next* real event does, which is exactly what this
2531 // mechanism must not do (see `InputEvent::Housekeeping`).
2532 //
2533 // The one thing a broadcast *can* still move is the IME surface, via
2534 // the publish handling above (which is pass-agnostic by design, and
2535 // was before this arm existed): a widget that publishes while
2536 // flushing has said something about its session either way, and an
2537 // inactive publish releases it. No shipped widget does — `TextInput`
2538 // ignores a broadcast outright, and both cleared-surface publishers
2539 // are paint-time — so this is a contract note, not live behavior.
2540 InputEvent::Housekeeping => {}
2541 // A floated surface's own input: a broadcast at the root, but a real
2542 // user gesture underneath, so it moves *some* of what a hit-tested
2543 // event moves and deliberately none of the rest.
2544 InputEvent::Overlay(overlay) => {
2545 // Honoured: a text field inside a popover may claim focus, and the
2546 // session it opens is an ordinary one.
2547 //
2548 // What it must not do is stack on top of the session it
2549 // supersedes. A hit-tested press has the containers' blur sweep
2550 // under it, which clears every focused child but the one the
2551 // press kept; a press inside a floated surface reaches no
2552 // container's hit test, so nothing retires the chain the claim
2553 // replaces and two branches go on believing they are focused —
2554 // one of them still describing its own IME surface to the shell.
2555 // The root retires it in the two places it has standing to: here,
2556 // when its record already names a *different* surface as the
2557 // owner, and in `paint_overlays`, which resolves that record from
2558 // the pods themselves and stops seeding the main tree's links the
2559 // pass after the session leaves it.
2560 //
2561 // The other repair — running the blur sweep for this event, with
2562 // the claiming surface's owner kept — was not taken: that sweep
2563 // belongs to the containers, which run it over the children they
2564 // hold when a pointer `Down` passes through them. The root has no
2565 // mutable route to a `focused` link below its own pod, so from
2566 // here it is not a sweep at all but a change to how every
2567 // container routes a broadcast.
2568 //
2569 // The record is deliberately not *written* here: an overlay-pass
2570 // focus request cannot be attributed at the root (see
2571 // `focus_surface`) — a field in the tree re-claiming its own
2572 // session through a floated toolbar raises exactly the flag an
2573 // editable inside the surface raises to take it away.
2574 if focus_req {
2575 if self.focus_surface.is_some_and(|owner| owner != overlay.key) {
2576 // A session another surface owns, which *is* attributable:
2577 // retire it whole rather than let the new claim inherit
2578 // the surface the old one published.
2579 self.release_focus_session();
2580 }
2581 focus_claim_honoured = true;
2582 self.set_focus_active(true);
2583 }
2584 // Honoured: an **explicit** `EventCtx::release_focus` from inside
2585 // the surface ends the session it asked to end — the same rule the
2586 // `Scroll`/`Key`/`Ime` arm applies.
2587 if focus_rel {
2588 self.release_focus_session();
2589 }
2590 // NEVER the blur branch. A `Down` that claims no focus blurs the
2591 // tree only when it was hit-tested *in* the tree; a press inside a
2592 // floated surface is the one press that must not, or tapping a
2593 // selection toolbar would drop the very selection the toolbar acts
2594 // on — which is the whole reason these two are routed apart.
2595 //
2596 // Nor does it advance the hover epoch: `hover_pass`/`hover_ends`
2597 // above match `InputEvent::Pointer` only, so a live hover in the
2598 // main tree survives an overlay pass by construction rather than by
2599 // a check here.
2600 //
2601 // A capture requested from inside the surface IS honoured, exactly
2602 // as a hit-tested `Down`'s is: the gesture then owns every
2603 // follow-up, and because a live capture short-circuits the overlay
2604 // pre-pass those follow-ups arrive as ordinary `Pointer` events
2605 // routed by the capture path — which is what lets a drag begun
2606 // inside a surface continue outside it, and what closes it on the
2607 // `Up` through the arm above.
2608 //
2609 // Whatever phase claimed it, not `Down` alone. The pods record
2610 // their own `active` path on any phase, so a capture claimed on a
2611 // `Move` left the surface latched and the root's mirror clear:
2612 // the pre-pass kept hit-testing, the follow-ups kept missing the
2613 // latched surface, and the `Up` that would have released it never
2614 // arrived — leaving the surface free to divert every later
2615 // pointer event its owner is reached by. Mirroring the claim on
2616 // any phase is what routes those follow-ups, and that `Up`, back
2617 // down the capture path to the surface that opened it.
2618 //
2619 // The claimant is the contact this overlay event was routed for —
2620 // the pre-pass re-enters this method under the gesture's own
2621 // contact pass, so `pointer_id` is that gesture's id.
2622 if captured {
2623 self.capture_claimant = Some(pointer_id);
2624 self.capture_contacts = contact_pass.contacts_requested();
2625 self.contacts_captor_is_root = root_opted_in;
2626 }
2627 // ...and released on the phase that ends the gesture, in the
2628 // same door it was claimed through. The claim is mirrored on any
2629 // phase (see above), so the release has to be too: an `Up` or
2630 // `Cancel` that arrives *as an overlay event* — which is what
2631 // happens when the surface was not holding the capture when the
2632 // gesture began, so the pre-pass kept hit-testing it — would
2633 // otherwise leave the root's latch standing on a gesture that is
2634 // over, and a standing latch short-circuits the pre-pass for
2635 // every floated surface until some unrelated pointer release
2636 // happens along.
2637 //
2638 // A gesture whose claim *did* short-circuit the pre-pass ends on
2639 // the ordinary pointer arm above instead, because that is the
2640 // door its follow-ups come in by; the pod's own `active` link is
2641 // released by whichever of the two the release arrived through
2642 // (see `frust-widgets`' `OverlaySlot::forward`).
2643 if let OverlayEventKind::Pointer(pointer) = &overlay.kind
2644 && matches!(pointer.phase, PointerPhase::Up | PointerPhase::Cancel)
2645 && self
2646 .capture_claimant
2647 .is_none_or(|claimant| claimant == pointer_id)
2648 {
2649 self.capture_claimant = None;
2650 self.capture_contacts = false;
2651 self.contacts_captor_is_root = false;
2652 }
2653 }
2654 }
2655
2656 // A takeover during the claimant's own dispatch: a container cancelled
2657 // the widget that opted into the gesture's other contacts and released
2658 // it from the active path (`EventCtx::release_captured_child`). The
2659 // opt-in goes with it — unless the container that took over opted in
2660 // itself afterwards, which the pass records — so the other contacts are
2661 // dropped from here on rather than routed to a captor that is gone. The
2662 // claimant latch is deliberately kept: the gesture is still the
2663 // claimant's, now held by the container that took it over (still on the
2664 // active path, as the released child's ancestor), and only the
2665 // claimant's own `Up`/`Cancel` ends it. Never on a non-claimant
2666 // contact's dispatch, in which no container can clear a recorded link
2667 // (`ChildPod::set_active`), so nothing could have left the path.
2668 if !secondary && self.capture_claimant.is_some() && contact_pass.capture_released() {
2669 self.capture_contacts = contact_pass.contacts_requested();
2670 }
2671
2672 // Settle the session's identity — the one write that decides which
2673 // recorded links survive this dispatch, in one place, from what the
2674 // dispatch actually did.
2675 //
2676 // * A **live session that ended** strands everything: the chain it ran
2677 // through, a floated surface's link, and anything this very pass
2678 // stamped (a claim the same dispatch then released). Past the candidate,
2679 // therefore, not merely onto it — this is the clearing sweep no
2680 // container can be asked to run.
2681 // * A **claim the root honoured** commits the candidate, so the chain it
2682 // stamped is the only one that reads live and every older link is
2683 // stranded by arithmetic.
2684 // * Anything else leaves the session exactly where it was, which is what
2685 // makes a `Move`, a housekeeping broadcast, or a press inside a surface
2686 // that claimed nothing incapable of disturbing a standing link — and
2687 // what strands a claim an arm declined to honour, since the candidate
2688 // it was stamped with is then never published again.
2689 let session_ended = focus_active_before && !self.focus_active;
2690 self.focus_epoch = if session_ended {
2691 advance_focus_epoch(candidate_focus_epoch)
2692 } else if focus_claim_honoured {
2693 candidate_focus_epoch
2694 } else {
2695 live_focus_epoch
2696 };
2697 self.publish_focus_session();
2698
2699 // Close the hover pass: advance the epoch (which strands every stamp this
2700 // pass did not renew, wherever in the tree it sits) and refresh the mirror.
2701 // A claim only counts on a `hover_pass` — an ineligible pass records none
2702 // anyway, but stating it here keeps the mirror true by construction rather
2703 // than by the eligibility gate alone.
2704 if hover_pass || hover_ends {
2705 self.hover_epoch = self.hover_epoch.wrapping_add(1);
2706 self.hover_active = hover_pass && hover_claimed;
2707 // Republish the link a dropping pod checks its stamp against, so a
2708 // rebuild that removes the claimant can report the severance the
2709 // epoch alone cannot strand (see `ChildPod`'s `Drop`). Epoch `0` while
2710 // nothing holds a link, which is what makes a stale stamp's drop —
2711 // the common case — cost one comparison and mark nothing. The
2712 // identity rides with it because epoch integers are per-root and
2713 // collide by construction (see `root_identity`).
2714 crate::event::set_live_hover_link(
2715 self.root_identity,
2716 if self.hover_active {
2717 self.hover_epoch
2718 } else {
2719 0
2720 },
2721 );
2722 }
2723
2724 // Close the cursor pass: the last request of the pass wins, and its
2725 // absence resolves to `Default` — which is what makes the request
2726 // stateless (a widget that stops asking needs no clearing) and what a
2727 // pointer moving off every requesting widget resolves to. Drained
2728 // unconditionally so a request made on a non-cursor pass cannot survive
2729 // into the next one; only a cursor pass commits it.
2730 let requested = request_slot.take();
2731 if cursor_pass {
2732 self.cursor = requested.cursor.unwrap_or_default();
2733 }
2734 // The clipboard half of the same drain, committed on **every** pass: a copy
2735 // is answered from whatever event decoded it, and there is no
2736 // "clipboard pass" the way there is a cursor pass. Both fields are
2737 // one-shot (see their docs) — a write from this pass supersedes one still
2738 // standing undrained, and a request is raised but never lowered here, so a
2739 // shell that skipped a drain answers late rather than losing the paste.
2740 if let Some(text) = requested.clipboard_write {
2741 self.pending_clipboard_write = Some(text);
2742 }
2743 self.pending_paste_request |= requested.paste_request;
2744 // A hover that ended with *nothing* taking it needs one repaint no widget
2745 // can ask for: the pointer moved onto empty chrome (or a press/lift/cancel
2746 // cleared the link), so the old claimant's `event()` was never called and
2747 // the new state has no claimant to speak for it. Every other edge is the
2748 // consumer's own: a hover that *began* or *moved from one claimant to
2749 // another* is repainted by the new claimant's change-gated
2750 // `request_redraw`, and because a repaint is global, that one frame is also
2751 // what lets the widget losing the link drop its overlay from
2752 // `PaintCtx::is_hovered`. This mirror is identity-free, so an A→B handoff
2753 // is `true`→`true` here and manufactures nothing — which is exactly why the
2754 // consumer's internal flag is normative (see `EventCtx::claim_hover`).
2755 let needs_redraw = needs_redraw || (hover_was_active && !self.hover_active);
2756
2757 EventOutcome {
2758 // Merge whatever the overlay pre-pass already produced: a
2759 // pass-through outside-tap notification ran before this dispatch and
2760 // its redraw is owed just as much as the dispatch's own.
2761 handled: handled || carried.handled,
2762 needs_redraw: needs_redraw || carried.needs_redraw,
2763 }
2764 }
2765
2766 /// Decide what one pointer contact does — the root half of
2767 /// [`InputEvent::PointerContact`]'s multi-contact contract.
2768 ///
2769 /// `Some(false)` routes it as the gesture's own pointer (rule (a): slot `0`
2770 /// with nothing captured, hit-tested; or the claimant of a live capture,
2771 /// down the captured path). `Some(true)` routes it as a **non-claimant**
2772 /// contact down a live capture's path, which happens only while the captor's
2773 /// opt-in stands (rule (c)): the dispatch then walks the recorded active
2774 /// path forward-only, so only the captor's own handler sees it (see
2775 /// [`crate::widget::ChildPod::event_child`]). `None` drops it: an additional
2776 /// contact with nothing captured (rule (b)), or one the captor did not opt
2777 /// into — or whose opt-in ended when a container took the gesture over from
2778 /// the captor ([`EventCtx::release_captured_child`]) — (rule (c)).
2779 fn contact_route(&self, pointer_id: PointerId) -> Option<bool> {
2780 match self.capture_claimant {
2781 None if pointer_id.slot == 0 => Some(false),
2782 None => None,
2783 Some(claimant) if claimant == pointer_id => Some(false),
2784 Some(_) if self.capture_contacts => Some(true),
2785 Some(_) => None,
2786 }
2787 }
2788
2789 /// Hit-test the surfaces the last paint floated, **before** the main tree
2790 /// sees an uncaptured pointer, scroll, or scale — the routing half of the
2791 /// overlay portal (see [`crate::overlay`]).
2792 ///
2793 /// # What it does
2794 ///
2795 /// Walks [`RenderRoot::overlay_hits`] topmost-first (the `Tooltip` band before
2796 /// `Floating`, later registration before earlier), skipping
2797 /// [`OverlayInput::Transparent`] entries, and on the first rect containing the
2798 /// event's position re-dispatches it as
2799 /// [`InputEvent::Overlay`] — a broadcast carrying the owner's key and a
2800 /// **window-space** payload — then returns
2801 /// [`OverlayRoute::Consumed`]. The main tree never sees the original event.
2802 ///
2803 /// If nothing was hit and the event is a primary `Down`, every `Interactive`
2804 /// entry registered [`OutsideTap::Notify`] is told, topmost-first, with
2805 /// [`OverlayEventKind::OutsideDown`]; the press is then consumed iff any of
2806 /// them asked to consume it, and otherwise continues into today's dispatch.
2807 ///
2808 /// # What it deliberately does not do
2809 ///
2810 /// A **live capture short-circuits it entirely**: a gesture that has captured
2811 /// the pointer owns every follow-up until it ends, and re-hit-testing a drag
2812 /// that wandered over a floated surface would hand it to the wrong widget
2813 /// mid-gesture. That is also what lets a drag *begun* inside a surface
2814 /// continue outside it — the capture the overlay `Down` opened routes the
2815 /// follow-ups by the ordinary captured path.
2816 ///
2817 /// Housekeeping, `Key`, `Ime`, `EditCommand` and an overlay event already
2818 /// being routed pass straight through: a broadcast and a focus-routed event
2819 /// each reach their target with no hit test, so there is nothing here to
2820 /// redirect.
2821 fn route_overlay(&mut self, state: &mut State, event: &InputEvent) -> OverlayRoute {
2822 if self.capture_claimant.is_some() || self.overlay_hits.is_empty() {
2823 return OverlayRoute::Continue(EventOutcome::default());
2824 }
2825 let (position, kind) = match event {
2826 // A contact is routed for its position and phase exactly like a
2827 // plain pointer (the root's gate has already unwrapped one, so the
2828 // second pattern is for totality); the re-entered dispatch below
2829 // keeps its id through the contact pass it runs under.
2830 InputEvent::Pointer(pointer) | InputEvent::PointerContact { event: pointer, .. } => {
2831 (pointer.position, OverlayEventKind::Pointer(*pointer))
2832 }
2833 InputEvent::Scroll { position, delta } => (
2834 *position,
2835 OverlayEventKind::Scroll {
2836 position: *position,
2837 delta: *delta,
2838 },
2839 ),
2840 InputEvent::Scale(scale) => (
2841 scale.focal,
2842 OverlayEventKind::Scale {
2843 focal: scale.focal,
2844 phase: scale.phase,
2845 scale_delta: scale.scale_delta,
2846 velocity: scale.velocity,
2847 },
2848 ),
2849 InputEvent::Key(_)
2850 | InputEvent::Ime(_)
2851 | InputEvent::EditCommand(_)
2852 | InputEvent::Housekeeping
2853 | InputEvent::Overlay(_) => {
2854 return OverlayRoute::Continue(EventOutcome::default());
2855 }
2856 };
2857
2858 // `overlay_hits` is in paint order, so walking it in reverse walks from
2859 // the surface painted last — the one the user sees on top — downward.
2860 // Cloned because each dispatch below needs `&mut self`; the list is one
2861 // small `Copy` struct per floated surface, and the clone never happens on
2862 // the overwhelmingly common no-overlays path (guarded above).
2863 let hits = self.overlay_hits.clone();
2864 for hit in hits.iter().rev() {
2865 // An `egui`-style transparent surface is painted above the app and
2866 // hit-tested by nothing: the pointer passes straight through to
2867 // whatever the main tree has underneath.
2868 if hit.input == OverlayInput::Transparent {
2869 continue;
2870 }
2871 if hit.contains(position) {
2872 let routed = InputEvent::Overlay(OverlayEvent {
2873 key: hit.key,
2874 kind: kind.clone(),
2875 });
2876 // Re-entering this method is deliberate and shallow: the routed
2877 // event is a broadcast, so it takes the branch above out
2878 // immediately and can never recurse further. Going back through
2879 // the front door is what gives the overlay dispatch the same
2880 // request bracket, focus bookkeeping and outcome folding every
2881 // other event gets, with one arm's worth of difference rather
2882 // than a second copy of the pass.
2883 return OverlayRoute::Consumed(self.event(state, &routed));
2884 }
2885 }
2886
2887 // Nothing floated was hit. Only a **primary** press is a light-dismiss
2888 // signal: a secondary press is a context gesture (see
2889 // `docs/CODE_STANDARDS.md`'s Interaction Semantics), and a `Move`, `Up` or
2890 // scroll outside a surface says nothing about dismissing it.
2891 let dismissing = matches!(
2892 event,
2893 InputEvent::Pointer(pointer) | InputEvent::PointerContact { event: pointer, .. }
2894 if pointer.phase == PointerPhase::Down
2895 && pointer.button == PointerButton::Primary
2896 );
2897 if !dismissing {
2898 return OverlayRoute::Continue(EventOutcome::default());
2899 }
2900
2901 let mut carried = EventOutcome::default();
2902 let mut consumed = false;
2903 for hit in hits.iter().rev() {
2904 // A transparent surface takes no input at all, outside-taps included:
2905 // it is chrome the pointer does not know about.
2906 if hit.input == OverlayInput::Transparent {
2907 continue;
2908 }
2909 let OutsideTap::Notify { consume } = hit.outside_tap else {
2910 continue;
2911 };
2912 let routed = InputEvent::Overlay(OverlayEvent {
2913 key: hit.key,
2914 kind: OverlayEventKind::OutsideDown,
2915 });
2916 let outcome = self.event(state, &routed);
2917 carried.handled |= outcome.handled;
2918 carried.needs_redraw |= outcome.needs_redraw;
2919 // Every notified surface is told before any of them consumes:
2920 // dismissing one menu must not hide the press from a second surface
2921 // that also wanted to close.
2922 consumed |= consume;
2923 }
2924 if consumed {
2925 OverlayRoute::Consumed(carried)
2926 } else {
2927 OverlayRoute::Continue(carried)
2928 }
2929 }
2930
2931 /// Perform a platform accessibility action, returning the same
2932 /// [`EventOutcome`] the synthesized input produced.
2933 ///
2934 /// A platform `accesskit_*` adapter delivers an `ActionRequest(node_id,
2935 /// action)`; a shell forwards it here. v1 routes actions through the **normal
2936 /// event path** by synthesizing pointer events at the target node's absolute
2937 /// bounds center (recovered from a fresh semantics pass — the id → bounds map
2938 /// the pass produces), so *every* fire-on-up-inside widget is operable with
2939 /// **zero** widget-side changes:
2940 ///
2941 /// * [`accesskit::Action::Click`] → a `Down` then an `Up` at the center,
2942 /// activating any button/switch/checkbox exactly as a real tap would.
2943 /// * [`accesskit::Action::Focus`] → a `Down` then a synthetic `Cancel` at
2944 /// the center: the `Down` claims focus for a widget that opts in on `Down`
2945 /// (the recorded-focus contract), and the `Cancel` releases the capture
2946 /// that same `Down` opened without touching the recorded focus path — so
2947 /// the action claims focus without leaving the widget permanently
2948 /// capturing every later pointer event. A widget that does not claim focus
2949 /// on `Down` is unaffected, and `Cancel` never fires an on-press callback.
2950 /// * any other action → ignored (a no-op [`EventOutcome`]); richer actions are
2951 /// deferred.
2952 ///
2953 /// An unknown `node_id` (not in the current tree) is a benign no-op. Requires
2954 /// a prior [`RenderRoot::layout`] so the bounds are valid. Synthetic-pointer
2955 /// activation cannot drive widgets that require a real drag (e.g. a slider) —
2956 /// an accepted v1 limitation.
2957 pub fn perform_accessibility_action(
2958 &mut self,
2959 state: &mut State,
2960 node_id: accesskit::NodeId,
2961 action: accesskit::Action,
2962 ) -> EventOutcome {
2963 // Recover the node's absolute bounds from a fresh semantics pass (the
2964 // id → absolute-bounds map this action routing needs; recomputing keeps
2965 // it in step with the live tree without a stored cache).
2966 let update = self.semantics();
2967 let Some(bounds) = update
2968 .nodes
2969 .iter()
2970 .find(|(id, _)| *id == node_id)
2971 .and_then(|(_, node)| node.bounds())
2972 else {
2973 return EventOutcome::default();
2974 };
2975 let center = Point::new((bounds.x0 + bounds.x1) / 2.0, (bounds.y0 + bounds.y1) / 2.0);
2976 let synth = |phase| {
2977 InputEvent::Pointer(PointerEvent {
2978 phase,
2979 position: center,
2980 button: PointerButton::Primary,
2981 })
2982 };
2983 match action {
2984 accesskit::Action::Click => {
2985 let down = self.event(state, &synth(PointerPhase::Down));
2986 let up = self.event(state, &synth(PointerPhase::Up));
2987 EventOutcome {
2988 handled: down.handled || up.handled,
2989 needs_redraw: down.needs_redraw || up.needs_redraw,
2990 }
2991 }
2992 accesskit::Action::Focus => {
2993 // A `Down` claims focus for a widget that opts in on `Down`; a
2994 // synthetic `Cancel` then releases the capture that `Down` also
2995 // opened (mirroring a real gesture steal), leaving the recorded
2996 // focus path intact — `Cancel` clears both `active` and
2997 // `capture_claimant` while never touching `focused`/`focus_active`.
2998 // Without the `Cancel`, the `Down` alone would leave the widget
2999 // permanently capturing every subsequent pointer event.
3000 let down = self.event(state, &synth(PointerPhase::Down));
3001 let cancel = self.event(state, &synth(PointerPhase::Cancel));
3002 EventOutcome {
3003 handled: down.handled || cancel.handled,
3004 needs_redraw: down.needs_redraw || cancel.needs_redraw,
3005 }
3006 }
3007 // Other actions are not modelled in v1: ignore rather than guess.
3008 _ => EventOutcome::default(),
3009 }
3010 }
3011}
3012
3013impl<State: 'static, V: View<State>> Default for RenderRoot<State, V> {
3014 fn default() -> Self {
3015 Self::new()
3016 }
3017}
3018
3019#[cfg(test)]
3020mod tests {
3021 use super::*;
3022 use crate::event::{ImeContentType, ScrollDelta};
3023 use crate::overlay::OverlayKey;
3024
3025 /// Application state for the tests.
3026 #[derive(Default)]
3027 struct AppState {
3028 label: String,
3029 }
3030
3031 /// The retained widget produced by `MockTextView`: stores the current text
3032 /// and records what it painted.
3033 struct TextWidget {
3034 text: String,
3035 }
3036
3037 impl crate::widget::Widget for TextWidget {
3038 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3039 // A crude intrinsic size: width proportional to text length.
3040 let intrinsic = Size::new(self.text.len() as f64 * 8.0, 16.0);
3041 bc.constrain(intrinsic)
3042 }
3043 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3044 scene.draw_text(ctx.origin(), &self.text);
3045 }
3046 }
3047
3048 /// The task's `MockTextView`: a real `View` impl living in tests.
3049 struct MockTextView {
3050 text: String,
3051 }
3052
3053 impl View<AppState> for MockTextView {
3054 type Element = TextWidget;
3055
3056 fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
3057 TextWidget {
3058 text: self.text.clone(),
3059 }
3060 }
3061
3062 fn rebuild(
3063 &self,
3064 prev: &Self,
3065 element: &mut Self::Element,
3066 _ctx: &mut BuildCtx<'_>,
3067 ) -> ChangeFlags {
3068 if prev.text != self.text {
3069 element.text = self.text.clone();
3070 // Text change: same size model would relayout, but the intrinsic
3071 // width can change, so signal PAINT here and let callers decide.
3072 ChangeFlags::PAINT
3073 } else {
3074 ChangeFlags::NONE
3075 }
3076 }
3077 }
3078
3079 /// A scene recorder for asserting paint output.
3080 #[derive(Default)]
3081 struct RecordingScene {
3082 texts: Vec<(Point, String)>,
3083 }
3084 impl PaintScene for RecordingScene {
3085 fn fill_rect(&mut self, _origin: Point, _size: Size, _color: peniko::Color) {}
3086 fn draw_text(&mut self, origin: Point, text: &str) {
3087 self.texts.push((origin, text.to_string()));
3088 }
3089 fn draw_scene_texture(&mut self, _id: u64, _dest: Rect) {}
3090 }
3091
3092 fn build(state: &mut AppState) -> MockTextView {
3093 MockTextView {
3094 text: state.label.clone(),
3095 }
3096 }
3097
3098 #[test]
3099 fn build_inserts_widget_into_arena() {
3100 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3101 let mut state = AppState {
3102 label: "hello".to_string(),
3103 };
3104 let flags = root.rebuild(&mut build, &mut state);
3105 // First build dirties both passes.
3106 assert!(flags.needs_layout());
3107 assert!(flags.needs_paint());
3108 let id = root.root_id().expect("root built");
3109 let pod = root.tree().pod(id).expect("pod in arena");
3110 assert!(pod.widget().downcast_ref_is::<TextWidget>());
3111 }
3112
3113 #[test]
3114 fn rebuild_changed_data_yields_paint() {
3115 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3116 let mut state = AppState {
3117 label: "a".to_string(),
3118 };
3119 root.rebuild(&mut build, &mut state);
3120 state.label = "b".to_string();
3121 let flags = root.rebuild(&mut build, &mut state);
3122 assert_eq!(flags, ChangeFlags::PAINT);
3123 }
3124
3125 #[test]
3126 fn rebuild_unchanged_data_yields_none() {
3127 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3128 let mut state = AppState {
3129 label: "same".to_string(),
3130 };
3131 root.rebuild(&mut build, &mut state);
3132 let flags = root.rebuild(&mut build, &mut state);
3133 assert_eq!(flags, ChangeFlags::NONE);
3134 }
3135
3136 #[test]
3137 fn layout_stores_size_in_pod() {
3138 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3139 let mut state = AppState {
3140 label: "hi".to_string(),
3141 };
3142 root.rebuild(&mut build, &mut state);
3143 let size = root.layout(Size::new(800.0, 600.0));
3144 // "hi" -> 2 * 8 = 16 wide, 16 tall, within the window.
3145 assert_eq!(size, Size::new(16.0, 16.0));
3146 let id = root.root_id().unwrap();
3147 let pod = root.tree().pod(id).unwrap();
3148 assert_eq!(pod.origin(), Point::ZERO);
3149 assert_eq!(pod.size(), Size::new(16.0, 16.0));
3150 }
3151
3152 #[test]
3153 fn inspect_reports_the_laid_out_root() {
3154 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3155 let mut state = AppState {
3156 label: "hi".to_string(),
3157 };
3158 // Before the first build there is nothing to inspect.
3159 assert!(root.inspect().is_empty());
3160
3161 root.rebuild(&mut build, &mut state);
3162 root.layout(Size::new(800.0, 600.0));
3163
3164 let nodes = root.inspect();
3165 assert_eq!(nodes.len(), 1);
3166 let node = &nodes[0];
3167 assert_eq!(node.id, root.root_id().unwrap());
3168 assert_eq!(node.parent, None);
3169 assert_eq!(node.depth, 0);
3170 assert!(node.children.is_empty());
3171 // The concrete element type is captured, not the erased box.
3172 assert!(node.type_name.ends_with("TextWidget"), "{}", node.type_name);
3173 assert_eq!(node.debug_label, None);
3174 // Bounds match what the layout pass recorded on the pod.
3175 let pod = root.tree().pod(node.id).unwrap();
3176 assert_eq!(
3177 node.bounds,
3178 Rect::from_origin_size(pod.origin(), pod.size())
3179 );
3180 assert_eq!(node.bounds, Rect::new(0.0, 0.0, 16.0, 16.0));
3181 }
3182
3183 #[test]
3184 fn layout_clamps_to_window() {
3185 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3186 let mut state = AppState {
3187 label: "wwwwwwwwwww".to_string(), // 10 chars -> 80 wide intrinsic
3188 };
3189 root.rebuild(&mut build, &mut state);
3190 let size = root.layout(Size::new(40.0, 40.0));
3191 // Intrinsic width 80 is clamped to the 40-wide window.
3192 assert_eq!(size.width, 40.0);
3193 }
3194
3195 #[test]
3196 fn paint_emits_current_text() {
3197 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3198 let mut state = AppState {
3199 label: "one".to_string(),
3200 };
3201 root.rebuild(&mut build, &mut state);
3202 root.layout(Size::new(200.0, 200.0));
3203
3204 let mut scene = RecordingScene::default();
3205 root.paint(&mut scene, FrameTime::ZERO);
3206 assert_eq!(scene.texts, vec![(Point::ZERO, "one".to_string())]);
3207
3208 // Change data, rebuild, repaint -> new text.
3209 state.label = "two".to_string();
3210 root.rebuild(&mut build, &mut state);
3211 let mut scene2 = RecordingScene::default();
3212 root.paint(&mut scene2, FrameTime::ZERO);
3213 assert_eq!(scene2.texts, vec![(Point::ZERO, "two".to_string())]);
3214 }
3215
3216 /// A leaf widget that publishes a fixed [`PlatformViewFrame`] — and reports
3217 /// a z-shield rect over its own bounds — on every paint, unless
3218 /// `should_publish` is false (the widget-level toggle that simulates a slot
3219 /// no longer publishing between two rebuilds). Both channels ride the same
3220 /// toggle so one fixture covers both replace-per-pass contracts.
3221 struct PlatformViewProbeWidget {
3222 slot_id: u64,
3223 should_publish: bool,
3224 }
3225
3226 impl crate::widget::Widget for PlatformViewProbeWidget {
3227 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3228 bc.constrain(Size::new(10.0, 10.0))
3229 }
3230 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3231 if self.should_publish {
3232 ctx.publish_platform_view(PlatformViewFrame {
3233 slot_id: self.slot_id,
3234 view_type: "dev.frust.Probe".to_string(),
3235 params_json: String::new(),
3236 params_generation: 0,
3237 rect: kurbo::Rect::from_origin_size(ctx.origin(), ctx.size()),
3238 clip: None,
3239 visible: true,
3240 interactive: false,
3241 shields: Vec::new(),
3242 });
3243 ctx.report_input_shield(kurbo::Rect::from_origin_size(ctx.origin(), ctx.size()));
3244 }
3245 }
3246 }
3247
3248 /// A root widget owning two independently toggleable [`ChildPod`]s (a
3249 /// minimal two-slot container) so a rebuild can flip either slot's
3250 /// `should_publish` — the fixture the "two slots in one pass" and
3251 /// "empty-pass clears stale frames" tests below need.
3252 struct PlatformViewRootWidget {
3253 a: crate::widget::ChildPod,
3254 b: crate::widget::ChildPod,
3255 }
3256
3257 impl crate::widget::Widget for PlatformViewRootWidget {
3258 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3259 self.a.layout_child(ctx, bc);
3260 self.b.layout_child(ctx, bc);
3261 bc.constrain(Size::new(10.0, 10.0))
3262 }
3263 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3264 self.a.paint_child(ctx, scene);
3265 self.b.paint_child(ctx, scene);
3266 }
3267 }
3268
3269 /// The `View` producing [`PlatformViewRootWidget`], reconciling each
3270 /// slot's `should_publish` flag on rebuild like any controlled widget.
3271 struct PlatformViewRootView {
3272 publish_a: bool,
3273 publish_b: bool,
3274 }
3275
3276 impl View<PvState> for PlatformViewRootView {
3277 type Element = PlatformViewRootWidget;
3278
3279 fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
3280 PlatformViewRootWidget {
3281 a: crate::widget::ChildPod::new(Box::new(PlatformViewProbeWidget {
3282 slot_id: 1,
3283 should_publish: self.publish_a,
3284 })),
3285 b: crate::widget::ChildPod::new(Box::new(PlatformViewProbeWidget {
3286 slot_id: 2,
3287 should_publish: self.publish_b,
3288 })),
3289 }
3290 }
3291
3292 fn rebuild(
3293 &self,
3294 prev: &Self,
3295 element: &mut Self::Element,
3296 _ctx: &mut BuildCtx<'_>,
3297 ) -> ChangeFlags {
3298 if prev.publish_a != self.publish_a || prev.publish_b != self.publish_b {
3299 element
3300 .a
3301 .widget_mut()
3302 .downcast_mut::<PlatformViewProbeWidget>()
3303 .expect("slot a stays a PlatformViewProbeWidget")
3304 .should_publish = self.publish_a;
3305 element
3306 .b
3307 .widget_mut()
3308 .downcast_mut::<PlatformViewProbeWidget>()
3309 .expect("slot b stays a PlatformViewProbeWidget")
3310 .should_publish = self.publish_b;
3311 ChangeFlags::PAINT
3312 } else {
3313 ChangeFlags::NONE
3314 }
3315 }
3316 }
3317
3318 /// App state for the platform-view frame-channel tests.
3319 #[derive(Default)]
3320 struct PvState {
3321 publish_a: bool,
3322 publish_b: bool,
3323 }
3324
3325 fn platform_view_logic(state: &mut PvState) -> PlatformViewRootView {
3326 PlatformViewRootView {
3327 publish_a: state.publish_a,
3328 publish_b: state.publish_b,
3329 }
3330 }
3331
3332 #[test]
3333 fn platform_view_frames_arrive_in_order_and_clear_on_empty_pass() {
3334 let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3335 let mut state = PvState {
3336 publish_a: true,
3337 publish_b: true,
3338 };
3339 root.rebuild(&mut platform_view_logic, &mut state);
3340 root.layout(Size::new(200.0, 200.0));
3341
3342 let mut scene = RecordingScene::default();
3343 root.paint(&mut scene, FrameTime::ZERO);
3344
3345 // Two slots publishing in one pass both arrive, in paint order — the
3346 // regression test for the overwrite hazard (an Option-based `ime_state`
3347 // shape here would leave only the second slot's frame).
3348 let frames = root.platform_view_frames();
3349 assert_eq!(frames.len(), 2);
3350 assert_eq!(frames[0].slot_id, 1);
3351 assert_eq!(frames[1].slot_id, 2);
3352
3353 // Next pass: neither slot publishes (simulates both going away/culled).
3354 // The collection is REPLACED, so the previous pass's frames must not
3355 // survive as stale entries.
3356 state.publish_a = false;
3357 state.publish_b = false;
3358 root.rebuild(&mut platform_view_logic, &mut state);
3359 root.layout(Size::new(200.0, 200.0));
3360 root.paint(&mut scene, FrameTime::ZERO);
3361 assert!(
3362 root.platform_view_frames().is_empty(),
3363 "a paint pass with no publishers must yield an empty slice"
3364 );
3365 }
3366
3367 #[test]
3368 fn input_shields_arrive_in_order_and_clear_on_empty_pass() {
3369 // The shield channel's half of the contract above:
3370 // two shields reported in one pass both survive (the `Vec`
3371 // extend, not an `Option` overwrite), and a pass that reports none
3372 // replaces the collection rather than merging — a stale shield must
3373 // never keep stealing input from an interactive slot.
3374 let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3375 let mut state = PvState {
3376 publish_a: true,
3377 publish_b: true,
3378 };
3379 root.rebuild(&mut platform_view_logic, &mut state);
3380 root.layout(Size::new(200.0, 200.0));
3381
3382 let mut scene = RecordingScene::default();
3383 root.paint(&mut scene, FrameTime::ZERO);
3384 assert_eq!(root.input_shields().len(), 2);
3385 assert_eq!(
3386 root.input_shields()[0],
3387 Rect::from_origin_size(Point::ZERO, Size::new(10.0, 10.0)),
3388 "a shield is reported in absolute paint coordinates"
3389 );
3390
3391 state.publish_a = false;
3392 state.publish_b = false;
3393 root.rebuild(&mut platform_view_logic, &mut state);
3394 root.layout(Size::new(200.0, 200.0));
3395 root.paint(&mut scene, FrameTime::ZERO);
3396 assert!(
3397 root.input_shields().is_empty(),
3398 "a paint pass reporting no shields must yield an empty slice"
3399 );
3400 }
3401
3402 #[test]
3403 fn retired_platform_views_drain_exactly_once() {
3404 // The prompt-teardown channel: a reported slot
3405 // id is handed to the shell once and then gone, mirroring
3406 // `take_change_flags`. Serialized against the other test touching the
3407 // process-wide list (see `RETIRE_TEST_LOCK`).
3408 let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3409 let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3410 let _ = root.take_retired_platform_views(); // clear anything a sibling left
3411
3412 crate::widget::report_retired_slot(7);
3413 crate::widget::report_retired_slot(9);
3414 assert_eq!(root.take_retired_platform_views(), vec![7, 9]);
3415 assert!(
3416 root.take_retired_platform_views().is_empty(),
3417 "draining is destructive — a second drain reports nothing"
3418 );
3419 }
3420
3421 #[test]
3422 fn retired_platform_views_are_capped_dropping_the_oldest() {
3423 // A shell that never drains (desktop: no native compositor) must not
3424 // grow this list forever; past the cap the OLDEST id is dropped and the
3425 // differ's missing-streak backstop covers it.
3426 let _guard = RETIRE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3427 let mut root: RenderRoot<PvState, PlatformViewRootView> = RenderRoot::new();
3428 let _ = root.take_retired_platform_views();
3429
3430 for slot_id in 0..1_000u64 {
3431 crate::widget::report_retired_slot(slot_id);
3432 }
3433 let drained = root.take_retired_platform_views();
3434 assert!(drained.len() <= 256, "the pending list stays bounded");
3435 assert_eq!(
3436 *drained.last().expect("non-empty"),
3437 999,
3438 "the newest report always survives"
3439 );
3440 assert!(
3441 !drained.contains(&0),
3442 "the oldest reports are the ones dropped"
3443 );
3444 }
3445
3446 /// Serializes the two tests that drive the process-wide retire list
3447 /// (`crate::widget::report_retired_slot`), which `cargo test`'s parallel
3448 /// threads would otherwise interleave — the same shape
3449 /// `frust-shell-common::theme_override`'s tests use for its global slot.
3450 static RETIRE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3451
3452 /// A root widget that advances no state but requests a continuation frame on
3453 /// every paint — stands in for an animating widget (e.g. a scroll fling).
3454 struct FrameWidget;
3455 impl crate::widget::Widget for FrameWidget {
3456 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3457 bc.constrain(Size::new(10.0, 10.0))
3458 }
3459 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3460 ctx.request_frame();
3461 }
3462 }
3463
3464 struct FrameView;
3465 impl View<AppState> for FrameView {
3466 type Element = FrameWidget;
3467 fn build(&self, _ctx: &mut BuildCtx<'_>) -> FrameWidget {
3468 FrameWidget
3469 }
3470 fn rebuild(
3471 &self,
3472 _prev: &Self,
3473 _element: &mut FrameWidget,
3474 _ctx: &mut BuildCtx<'_>,
3475 ) -> ChangeFlags {
3476 ChangeFlags::NONE
3477 }
3478 }
3479
3480 #[test]
3481 fn paint_reports_needs_frame_from_animating_root() {
3482 // A still root reports no continuation frame.
3483 let mut still: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3484 let mut state = AppState {
3485 label: "x".to_string(),
3486 };
3487 still.rebuild(&mut build, &mut state);
3488 still.layout(Size::new(100.0, 100.0));
3489 let mut scene = RecordingScene::default();
3490 assert!(!still.paint(&mut scene, FrameTime::ZERO).needs_frame);
3491
3492 // An animating root bubbles request_frame out as PaintOutcome::needs_frame.
3493 let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
3494 anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
3495 anim.layout(Size::new(100.0, 100.0));
3496 let mut scene2 = RecordingScene::default();
3497 assert!(anim.paint(&mut scene2, FrameTime::ZERO).needs_frame);
3498 }
3499
3500 /// A root widget whose animation changes its layout: it requests a layout
3501 /// re-run on every paint — stands in for an expanding accordion.
3502 struct LayoutFrameWidget;
3503 impl crate::widget::Widget for LayoutFrameWidget {
3504 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3505 bc.constrain(Size::new(10.0, 10.0))
3506 }
3507 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3508 ctx.request_layout();
3509 }
3510 }
3511
3512 struct LayoutFrameView;
3513 impl View<AppState> for LayoutFrameView {
3514 type Element = LayoutFrameWidget;
3515 fn build(&self, _ctx: &mut BuildCtx<'_>) -> LayoutFrameWidget {
3516 LayoutFrameWidget
3517 }
3518 fn rebuild(
3519 &self,
3520 _prev: &Self,
3521 _element: &mut LayoutFrameWidget,
3522 _ctx: &mut BuildCtx<'_>,
3523 ) -> ChangeFlags {
3524 ChangeFlags::NONE
3525 }
3526 }
3527
3528 #[test]
3529 fn paint_folds_request_layout_into_pending_change_flags() {
3530 let mut state = AppState {
3531 label: "x".to_string(),
3532 };
3533
3534 // A root calling `request_layout` in paint surfaces it on the outcome AND
3535 // folds LAYOUT into `pending`, so the NEXT frame's `take_change_flags`
3536 // reports `needs_layout()`.
3537 let mut anim: RenderRoot<AppState, LayoutFrameView> = RenderRoot::new();
3538 anim.rebuild(&mut |_s: &mut AppState| LayoutFrameView, &mut state);
3539 anim.layout(Size::new(100.0, 100.0));
3540 // Drain any rebuild/layout dirtiness so we observe only paint's fold.
3541 let _ = anim.take_change_flags();
3542 let mut scene = RecordingScene::default();
3543 let outcome = anim.paint(&mut scene, FrameTime::ZERO);
3544 assert!(outcome.needs_layout, "outcome reports needs_layout");
3545 // `request_layout` implies `request_frame`, so the animation still runs.
3546 assert!(outcome.needs_frame, "request_layout implies needs_frame");
3547 assert!(
3548 anim.has_pending_change_flags(),
3549 "the fold survives to the next frame"
3550 );
3551 assert!(
3552 anim.take_change_flags().needs_layout(),
3553 "next frame's take_change_flags reports needs_layout"
3554 );
3555 }
3556
3557 #[test]
3558 fn paint_request_frame_only_does_not_fold_layout() {
3559 let mut state = AppState {
3560 label: "x".to_string(),
3561 };
3562
3563 // A paint-only animation (request_frame, no request_layout) must NOT fold
3564 // LAYOUT — the mobile layout-skip win depends on this staying opt-in.
3565 let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
3566 anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
3567 anim.layout(Size::new(100.0, 100.0));
3568 let _ = anim.take_change_flags();
3569 let mut scene = RecordingScene::default();
3570 let outcome = anim.paint(&mut scene, FrameTime::ZERO);
3571 assert!(outcome.needs_frame);
3572 assert!(
3573 !outcome.needs_layout,
3574 "request_frame alone: no needs_layout"
3575 );
3576 assert!(
3577 !anim.has_pending_change_flags(),
3578 "request_frame alone must not fold LAYOUT into pending"
3579 );
3580 }
3581
3582 /// A root whose paint requests a *pacable* cosmetic-loop frame — stands in
3583 /// for a skeleton shimmer whose cadence the mobile frame gate may throttle.
3584 struct PacedFrameWidget;
3585 impl crate::widget::Widget for PacedFrameWidget {
3586 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3587 bc.constrain(Size::new(10.0, 10.0))
3588 }
3589 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3590 ctx.request_frame_paced();
3591 }
3592 }
3593
3594 struct PacedFrameView;
3595 impl View<AppState> for PacedFrameView {
3596 type Element = PacedFrameWidget;
3597 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PacedFrameWidget {
3598 PacedFrameWidget
3599 }
3600 fn rebuild(
3601 &self,
3602 _prev: &Self,
3603 _element: &mut PacedFrameWidget,
3604 _ctx: &mut BuildCtx<'_>,
3605 ) -> ChangeFlags {
3606 ChangeFlags::NONE
3607 }
3608 }
3609
3610 #[test]
3611 fn paint_surfaces_paced_only_tick_class_on_outcome() {
3612 let mut state = AppState {
3613 label: "x".to_string(),
3614 };
3615
3616 // A paced-only root surfaces `needs_frame_paced_only` on the outcome so
3617 // the mobile frame gate may throttle its cadence.
3618 let mut paced: RenderRoot<AppState, PacedFrameView> = RenderRoot::new();
3619 paced.rebuild(&mut |_s: &mut AppState| PacedFrameView, &mut state);
3620 paced.layout(Size::new(100.0, 100.0));
3621 let mut scene = RecordingScene::default();
3622 let outcome = paced.paint(&mut scene, FrameTime::ZERO);
3623 assert!(outcome.needs_frame);
3624 assert!(
3625 outcome.needs_frame_paced_only,
3626 "a purely-cosmetic frame surfaces as paced-only"
3627 );
3628 assert_eq!(
3629 outcome.paced_interval,
3630 Some(std::time::Duration::ZERO),
3631 "a bare `request_frame_paced` names no interval (the theme's own rate)"
3632 );
3633
3634 // A Transition-class (`request_frame`) root is never paced-only, keeping
3635 // today's every-vsync behavior for existing callers.
3636 let mut anim: RenderRoot<AppState, FrameView> = RenderRoot::new();
3637 anim.rebuild(&mut |_s: &mut AppState| FrameView, &mut state);
3638 anim.layout(Size::new(100.0, 100.0));
3639 let mut scene2 = RecordingScene::default();
3640 let outcome2 = anim.paint(&mut scene2, FrameTime::ZERO);
3641 assert!(outcome2.needs_frame);
3642 assert!(
3643 !outcome2.needs_frame_paced_only,
3644 "request_frame stays unpaced (Transition)"
3645 );
3646 assert_eq!(
3647 outcome2.paced_interval, None,
3648 "an unpaced frame names no paced interval"
3649 );
3650 }
3651
3652 /// A root widget whose decorative loop names its own slow cadence — the
3653 /// `request_frame_paced_at` counterpart of [`PacedFrameWidget`].
3654 struct SlowPacedFrameWidget;
3655 impl crate::widget::Widget for SlowPacedFrameWidget {
3656 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3657 bc.constrain(Size::new(10.0, 10.0))
3658 }
3659 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3660 ctx.request_frame_paced_at(std::time::Duration::from_millis(500));
3661 }
3662 }
3663 struct SlowPacedFrameView;
3664 impl View<AppState> for SlowPacedFrameView {
3665 type Element = SlowPacedFrameWidget;
3666 fn build(&self, _ctx: &mut BuildCtx<'_>) -> SlowPacedFrameWidget {
3667 SlowPacedFrameWidget
3668 }
3669 fn rebuild(
3670 &self,
3671 _prev: &Self,
3672 _element: &mut SlowPacedFrameWidget,
3673 _ctx: &mut BuildCtx<'_>,
3674 ) -> ChangeFlags {
3675 ChangeFlags::NONE
3676 }
3677 }
3678
3679 #[test]
3680 fn paint_surfaces_the_requested_paced_interval_on_outcome() {
3681 // The end-to-end core half of the per-request pacing seam: a widget's
3682 // `request_frame_paced_at` reaches the shell on `PaintOutcome`, which is
3683 // what the mobile gate latches into `FramePacing`.
3684 let mut state = AppState {
3685 label: "x".to_string(),
3686 };
3687 let mut root: RenderRoot<AppState, SlowPacedFrameView> = RenderRoot::new();
3688 root.rebuild(&mut |_s: &mut AppState| SlowPacedFrameView, &mut state);
3689 root.layout(Size::new(100.0, 100.0));
3690 let mut scene = RecordingScene::default();
3691 let outcome = root.paint(&mut scene, FrameTime::ZERO);
3692 assert!(outcome.needs_frame_paced_only);
3693 assert_eq!(
3694 outcome.paced_interval,
3695 Some(std::time::Duration::from_millis(500))
3696 );
3697 }
3698
3699 /// A root widget that records the `frame_time` its paint observed, so a test
3700 /// can prove the shell-injected clock reaches `PaintCtx::frame_time()`.
3701 struct ClockWidget {
3702 seen: std::rc::Rc<std::cell::Cell<Option<FrameTime>>>,
3703 }
3704 impl crate::widget::Widget for ClockWidget {
3705 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3706 bc.constrain(Size::new(10.0, 10.0))
3707 }
3708 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3709 self.seen.set(Some(ctx.frame_time()));
3710 }
3711 }
3712
3713 struct ClockView {
3714 seen: std::rc::Rc<std::cell::Cell<Option<FrameTime>>>,
3715 }
3716 impl View<AppState> for ClockView {
3717 type Element = ClockWidget;
3718 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ClockWidget {
3719 ClockWidget {
3720 seen: self.seen.clone(),
3721 }
3722 }
3723 fn rebuild(
3724 &self,
3725 _prev: &Self,
3726 _element: &mut ClockWidget,
3727 _ctx: &mut BuildCtx<'_>,
3728 ) -> ChangeFlags {
3729 ChangeFlags::NONE
3730 }
3731 }
3732
3733 #[test]
3734 fn paint_threads_injected_frame_time_to_widget() {
3735 let seen = std::rc::Rc::new(std::cell::Cell::new(None));
3736 let mut root: RenderRoot<AppState, ClockView> = RenderRoot::new();
3737 let mut state = AppState::default();
3738 let seen_for_view = seen.clone();
3739 root.rebuild(
3740 &mut move |_s: &mut AppState| ClockView {
3741 seen: seen_for_view.clone(),
3742 },
3743 &mut state,
3744 );
3745 root.layout(Size::new(100.0, 100.0));
3746
3747 // Two paints with distinct injected times: the widget observes each one,
3748 // proving the clock is shell-fed (not read from an ambient `Instant`).
3749 let mut scene = RecordingScene::default();
3750 root.paint(&mut scene, FrameTime::from_nanos(1_000));
3751 assert_eq!(seen.get(), Some(FrameTime::from_nanos(1_000)));
3752 root.paint(&mut scene, FrameTime::from_nanos(17_000));
3753 assert_eq!(seen.get(), Some(FrameTime::from_nanos(17_000)));
3754 }
3755
3756 // --- Theme threading: a dummy theme recovered during paint/layout. ---
3757
3758 /// A dummy theme type standing in for `frust_theme::Theme` — `frust-core`
3759 /// never names the real one, so this proves the type-erased slot works for
3760 /// any `'static` type.
3761 #[derive(Debug, Clone, PartialEq)]
3762 struct TestTheme {
3763 accent: u32,
3764 }
3765
3766 /// A root widget recording the theme accent it recovered during paint (and
3767 /// during layout), or `None` when no theme was threaded in.
3768 struct ThemeWidget {
3769 seen_paint: std::rc::Rc<std::cell::Cell<Option<u32>>>,
3770 seen_layout: std::rc::Rc<std::cell::Cell<Option<u32>>>,
3771 }
3772 impl crate::widget::Widget for ThemeWidget {
3773 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3774 self.seen_layout
3775 .set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
3776 bc.constrain(Size::new(10.0, 10.0))
3777 }
3778 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3779 self.seen_paint
3780 .set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
3781 }
3782 }
3783
3784 struct ThemeView {
3785 seen_paint: std::rc::Rc<std::cell::Cell<Option<u32>>>,
3786 seen_layout: std::rc::Rc<std::cell::Cell<Option<u32>>>,
3787 }
3788 impl View<AppState> for ThemeView {
3789 type Element = ThemeWidget;
3790 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ThemeWidget {
3791 ThemeWidget {
3792 seen_paint: self.seen_paint.clone(),
3793 seen_layout: self.seen_layout.clone(),
3794 }
3795 }
3796 fn rebuild(
3797 &self,
3798 _prev: &Self,
3799 _element: &mut ThemeWidget,
3800 _ctx: &mut BuildCtx<'_>,
3801 ) -> ChangeFlags {
3802 ChangeFlags::NONE
3803 }
3804 }
3805
3806 fn drive_theme_root(theme: Option<TestTheme>) -> (Option<u32>, Option<u32>) {
3807 let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
3808 let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
3809 let mut root: RenderRoot<AppState, ThemeView> = RenderRoot::new();
3810 if let Some(theme) = theme {
3811 root.set_theme(Box::new(theme));
3812 }
3813 let mut state = AppState::default();
3814 let sp = seen_paint.clone();
3815 let sl = seen_layout.clone();
3816 root.rebuild(
3817 &mut move |_s: &mut AppState| ThemeView {
3818 seen_paint: sp.clone(),
3819 seen_layout: sl.clone(),
3820 },
3821 &mut state,
3822 );
3823 root.layout(Size::new(100.0, 100.0));
3824 let mut scene = RecordingScene::default();
3825 root.paint(&mut scene, FrameTime::ZERO);
3826 (seen_layout.get(), seen_paint.get())
3827 }
3828
3829 #[test]
3830 fn set_theme_threads_into_layout_and_paint() {
3831 let (layout, paint) = drive_theme_root(Some(TestTheme { accent: 5 }));
3832 assert_eq!(layout, Some(5));
3833 assert_eq!(paint, Some(5));
3834 }
3835
3836 #[test]
3837 fn no_theme_yields_none_in_layout_and_paint() {
3838 let (layout, paint) = drive_theme_root(None);
3839 assert_eq!(layout, None);
3840 assert_eq!(paint, None);
3841 }
3842
3843 #[test]
3844 fn set_theme_replaces_the_previous_theme() {
3845 // A second `set_theme` (a live dark-mode flip on desktop) wins on the
3846 // next paint.
3847 let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
3848 let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
3849 let mut root: RenderRoot<AppState, ThemeView> = RenderRoot::new();
3850 root.set_theme(Box::new(TestTheme { accent: 1 }));
3851 let mut state = AppState::default();
3852 let sp = seen_paint.clone();
3853 let sl = seen_layout.clone();
3854 root.rebuild(
3855 &mut move |_s: &mut AppState| ThemeView {
3856 seen_paint: sp.clone(),
3857 seen_layout: sl.clone(),
3858 },
3859 &mut state,
3860 );
3861 root.layout(Size::new(100.0, 100.0));
3862 let mut scene = RecordingScene::default();
3863 root.paint(&mut scene, FrameTime::ZERO);
3864 assert_eq!(seen_paint.get(), Some(1));
3865
3866 // Flip the theme, repaint — the new accent is observed.
3867 root.set_theme(Box::new(TestTheme { accent: 2 }));
3868 root.layout(Size::new(100.0, 100.0));
3869 root.paint(&mut scene, FrameTime::ZERO);
3870 assert_eq!(seen_paint.get(), Some(2));
3871 }
3872
3873 // --- Event-pass fixtures: a widget that mutates state on pointer-down. ---
3874
3875 #[derive(Default)]
3876 struct ClickState {
3877 clicks: u32,
3878 }
3879
3880 struct ButtonWidget;
3881 impl crate::widget::Widget for ButtonWidget {
3882 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3883 bc.constrain(Size::new(40.0, 20.0))
3884 }
3885 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3886 fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
3887 if let InputEvent::Pointer(p) = event {
3888 match p.phase {
3889 PointerPhase::Down => {
3890 ctx.state_mut::<ClickState>().clicks += 1;
3891 ctx.request_redraw();
3892 ctx.capture_pointer();
3893 return EventResult::Handled;
3894 }
3895 PointerPhase::Up | PointerPhase::Cancel => return EventResult::Handled,
3896 PointerPhase::Move => {}
3897 }
3898 }
3899 EventResult::Ignored
3900 }
3901 }
3902
3903 struct ButtonView;
3904 impl View<ClickState> for ButtonView {
3905 type Element = ButtonWidget;
3906 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ButtonWidget {
3907 ButtonWidget
3908 }
3909 fn rebuild(
3910 &self,
3911 _prev: &Self,
3912 _element: &mut ButtonWidget,
3913 _ctx: &mut BuildCtx<'_>,
3914 ) -> ChangeFlags {
3915 ChangeFlags::NONE
3916 }
3917 }
3918
3919 fn button_logic(_state: &mut ClickState) -> ButtonView {
3920 ButtonView
3921 }
3922
3923 fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
3924 InputEvent::Pointer(crate::event::PointerEvent {
3925 phase,
3926 position: Point::new(x, y),
3927 button: crate::event::PointerButton::Primary,
3928 })
3929 }
3930
3931 #[test]
3932 fn event_reaches_root_widget_and_mutates_state() {
3933 let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
3934 let mut state = ClickState::default();
3935 root.rebuild(&mut button_logic, &mut state);
3936 root.layout(Size::new(200.0, 200.0));
3937
3938 let outcome = root.event(&mut state, &pointer(PointerPhase::Down, 5.0, 5.0));
3939 assert!(outcome.handled);
3940 assert!(outcome.needs_redraw);
3941 assert_eq!(state.clicks, 1);
3942 // A captured Down opens the root gesture.
3943 assert!(root.is_pointer_captured());
3944 }
3945
3946 #[test]
3947 fn event_before_build_is_a_benign_no_op() {
3948 let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
3949 let mut state = ClickState::default();
3950 let outcome = root.event(&mut state, &pointer(PointerPhase::Down, 1.0, 1.0));
3951 assert_eq!(outcome, EventOutcome::default());
3952 assert_eq!(state.clicks, 0);
3953 }
3954
3955 #[test]
3956 fn capture_releases_on_pointer_up() {
3957 let mut root: RenderRoot<ClickState, ButtonView> = RenderRoot::new();
3958 let mut state = ClickState::default();
3959 root.rebuild(&mut button_logic, &mut state);
3960 root.layout(Size::new(200.0, 200.0));
3961
3962 root.event(&mut state, &pointer(PointerPhase::Down, 5.0, 5.0));
3963 assert!(root.is_pointer_captured());
3964 root.event(&mut state, &pointer(PointerPhase::Up, 5.0, 5.0));
3965 assert!(!root.is_pointer_captured());
3966 }
3967
3968 #[test]
3969 fn take_change_flags_drains_accumulated_dirtiness() {
3970 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3971 let mut state = AppState {
3972 label: "x".to_string(),
3973 };
3974 root.rebuild(&mut build, &mut state);
3975 // First build accumulated LAYOUT|PAINT.
3976 let flags = root.take_change_flags();
3977 assert!(flags.needs_layout());
3978 // Draining leaves it empty until the next rebuild.
3979 assert!(root.take_change_flags().is_empty());
3980 }
3981
3982 #[test]
3983 fn has_pending_change_flags_peeks_without_draining() {
3984 // The frame-gate peek: observe pending dirtiness without
3985 // clearing it, so a skipped frame preserves the flags for the next
3986 // frame that actually runs.
3987 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
3988 assert!(
3989 !root.has_pending_change_flags(),
3990 "a fresh root has nothing pending"
3991 );
3992 let mut state = AppState {
3993 label: "x".to_string(),
3994 };
3995 root.rebuild(&mut build, &mut state);
3996 // First build accumulated LAYOUT|PAINT — the peek sees it...
3997 assert!(root.has_pending_change_flags());
3998 // ...and repeated peeks do NOT drain it.
3999 assert!(root.has_pending_change_flags());
4000 // Only `take_change_flags` drains.
4001 assert!(!root.take_change_flags().is_empty());
4002 assert!(!root.has_pending_change_flags());
4003 }
4004
4005 #[test]
4006 fn set_theme_marks_layout_and_paint_pending() {
4007 // `set_theme` alone (no rebuild) must dirty layout/paint so a shell
4008 // gating on `take_change_flags` doesn't skip re-resolving theme-baked
4009 // widget state (e.g. Text's themed glyph color) on a bare theme swap.
4010 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4011 root.set_theme(Box::new(TestTheme { accent: 1 }));
4012 let flags = root.take_change_flags();
4013 assert!(flags.needs_layout());
4014 assert!(flags.needs_paint());
4015
4016 // Draining clears it until the next `set_theme`/rebuild.
4017 assert!(root.take_change_flags().is_empty());
4018 root.set_theme(Box::new(TestTheme { accent: 2 }));
4019 let flags = root.take_change_flags();
4020 assert!(flags.needs_layout());
4021 assert!(flags.needs_paint());
4022 }
4023
4024 // --- Window insets: pushed value reaches layout/paint contexts. ---
4025
4026 use crate::insets::{EdgeInsets, WindowInsets};
4027
4028 /// A root widget recording the `WindowInsets` it observed during layout and
4029 /// paint, proving the shell-pushed value threads through both contexts.
4030 struct InsetsWidget {
4031 seen_layout: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4032 seen_paint: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4033 }
4034 impl crate::widget::Widget for InsetsWidget {
4035 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4036 self.seen_layout.set(Some(ctx.window_insets()));
4037 bc.constrain(Size::new(10.0, 10.0))
4038 }
4039 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4040 self.seen_paint.set(Some(ctx.window_insets()));
4041 }
4042 }
4043
4044 struct InsetsView {
4045 seen_layout: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4046 seen_paint: std::rc::Rc<std::cell::Cell<Option<WindowInsets>>>,
4047 }
4048 impl View<AppState> for InsetsView {
4049 type Element = InsetsWidget;
4050 fn build(&self, _ctx: &mut BuildCtx<'_>) -> InsetsWidget {
4051 InsetsWidget {
4052 seen_layout: self.seen_layout.clone(),
4053 seen_paint: self.seen_paint.clone(),
4054 }
4055 }
4056 fn rebuild(
4057 &self,
4058 _prev: &Self,
4059 _element: &mut InsetsWidget,
4060 _ctx: &mut BuildCtx<'_>,
4061 ) -> ChangeFlags {
4062 ChangeFlags::NONE
4063 }
4064 }
4065
4066 fn drive_insets_root(
4067 insets: Option<WindowInsets>,
4068 ) -> (Option<WindowInsets>, Option<WindowInsets>) {
4069 let seen_layout = std::rc::Rc::new(std::cell::Cell::new(None));
4070 let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
4071 let mut root: RenderRoot<AppState, InsetsView> = RenderRoot::new();
4072 if let Some(insets) = insets {
4073 root.set_insets(insets);
4074 }
4075 let mut state = AppState::default();
4076 let sl = seen_layout.clone();
4077 let sp = seen_paint.clone();
4078 root.rebuild(
4079 &mut move |_s: &mut AppState| InsetsView {
4080 seen_layout: sl.clone(),
4081 seen_paint: sp.clone(),
4082 },
4083 &mut state,
4084 );
4085 root.layout(Size::new(100.0, 100.0));
4086 let mut scene = RecordingScene::default();
4087 root.paint(&mut scene, FrameTime::ZERO);
4088 (seen_layout.get(), seen_paint.get())
4089 }
4090
4091 #[test]
4092 fn set_insets_threads_into_layout_and_paint() {
4093 let insets = WindowInsets::new(
4094 EdgeInsets::new(0.0, 24.0, 0.0, 34.0),
4095 EdgeInsets::new(0.0, 0.0, 0.0, 0.0),
4096 );
4097 let (layout, paint) = drive_insets_root(Some(insets));
4098 assert_eq!(layout, Some(insets));
4099 assert_eq!(paint, Some(insets));
4100 }
4101
4102 #[test]
4103 fn no_insets_yields_zero_in_layout_and_paint() {
4104 let (layout, paint) = drive_insets_root(None);
4105 assert_eq!(layout, Some(WindowInsets::default()));
4106 assert_eq!(paint, Some(WindowInsets::default()));
4107 }
4108
4109 #[test]
4110 fn set_insets_marks_layout_and_paint_pending() {
4111 // Mirrors `set_theme_marks_layout_and_paint_pending`: a bare inset push
4112 // (no rebuild) must dirty layout/paint so a shell gating on
4113 // `take_change_flags` relayouts a `SafeArea` when the insets move.
4114 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4115 root.set_insets(WindowInsets::new(
4116 EdgeInsets::new(0.0, 24.0, 0.0, 0.0),
4117 EdgeInsets::ZERO,
4118 ));
4119 let flags = root.take_change_flags();
4120 assert!(flags.needs_layout());
4121 assert!(flags.needs_paint());
4122 // Drained until the next change.
4123 assert!(root.take_change_flags().is_empty());
4124 }
4125
4126 #[test]
4127 fn set_insets_no_op_when_unchanged_marks_nothing() {
4128 // The `PartialEq` no-op guard: re-pushing the current insets dirties
4129 // nothing, so a shell that forwards the platform insets every frame
4130 // never forces a needless relayout.
4131 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4132 let insets = WindowInsets::new(EdgeInsets::new(0.0, 24.0, 0.0, 34.0), EdgeInsets::ZERO);
4133 root.set_insets(insets);
4134 assert!(!root.take_change_flags().is_empty());
4135 // Same value again: no dirtiness.
4136 root.set_insets(insets);
4137 assert!(root.take_change_flags().is_empty());
4138 // A different value dirties again.
4139 root.set_insets(WindowInsets::default());
4140 assert!(!root.take_change_flags().is_empty());
4141 }
4142
4143 #[test]
4144 fn set_insets_round_trips_corner_insets() {
4145 use crate::insets::{CornerInset, CornerInsets};
4146 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4147 let corners = CornerInsets::new(
4148 CornerInset::ZERO,
4149 CornerInset::new(72.0, 24.0),
4150 CornerInset::ZERO,
4151 CornerInset::ZERO,
4152 );
4153 let insets = WindowInsets::default().with_corner_insets(corners);
4154 root.set_insets(insets);
4155 let flags = root.take_change_flags();
4156 assert!(flags.needs_layout());
4157 assert!(flags.needs_paint());
4158 assert_eq!(root.insets().corner_insets, corners);
4159 // Identical re-push marks nothing.
4160 root.set_insets(insets);
4161 assert!(root.take_change_flags().is_empty());
4162 // A push differing only in corners dirties again.
4163 let moved = insets.with_corner_insets(CornerInsets::new(
4164 CornerInset::new(72.0, 24.0),
4165 CornerInset::ZERO,
4166 CornerInset::ZERO,
4167 CornerInset::ZERO,
4168 ));
4169 root.set_insets(moved);
4170 let flags = root.take_change_flags();
4171 assert!(flags.needs_layout());
4172 assert!(flags.needs_paint());
4173 }
4174
4175 // --- Presented-frame count: pushed value reaches the paint context, unset
4176 // yields `None`, and — unlike theme/insets — the setter dirties nothing. ---
4177
4178 /// A root widget recording the `presented_frames` count it observed during
4179 /// paint, proving the shell-pushed value threads through `PaintCtx`.
4180 struct PresentedWidget {
4181 seen_paint: std::rc::Rc<std::cell::Cell<Option<Option<u64>>>>,
4182 }
4183 impl crate::widget::Widget for PresentedWidget {
4184 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4185 bc.constrain(Size::new(10.0, 10.0))
4186 }
4187 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4188 self.seen_paint.set(Some(ctx.presented_frames()));
4189 }
4190 }
4191
4192 struct PresentedView {
4193 seen_paint: std::rc::Rc<std::cell::Cell<Option<Option<u64>>>>,
4194 }
4195 impl View<AppState> for PresentedView {
4196 type Element = PresentedWidget;
4197 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PresentedWidget {
4198 PresentedWidget {
4199 seen_paint: self.seen_paint.clone(),
4200 }
4201 }
4202 fn rebuild(
4203 &self,
4204 _prev: &Self,
4205 _element: &mut PresentedWidget,
4206 _ctx: &mut BuildCtx<'_>,
4207 ) -> ChangeFlags {
4208 ChangeFlags::NONE
4209 }
4210 }
4211
4212 fn drive_presented_root(presented: Option<u64>) -> Option<u64> {
4213 let seen_paint = std::rc::Rc::new(std::cell::Cell::new(None));
4214 let mut root: RenderRoot<AppState, PresentedView> = RenderRoot::new();
4215 if let Some(presented) = presented {
4216 root.set_presented_frames(presented);
4217 }
4218 let mut state = AppState::default();
4219 let sp = seen_paint.clone();
4220 root.rebuild(
4221 &mut move |_s: &mut AppState| PresentedView {
4222 seen_paint: sp.clone(),
4223 },
4224 &mut state,
4225 );
4226 root.layout(Size::new(100.0, 100.0));
4227 let mut scene = RecordingScene::default();
4228 root.paint(&mut scene, FrameTime::ZERO);
4229 // Unwrap the "did paint run" outer Option; the inner is what the widget saw.
4230 seen_paint.get().expect("paint ran")
4231 }
4232
4233 #[test]
4234 fn set_presented_frames_threads_into_paint() {
4235 assert_eq!(drive_presented_root(Some(12)), Some(12));
4236 }
4237
4238 #[test]
4239 fn unset_presented_frames_yields_none_in_paint() {
4240 assert_eq!(drive_presented_root(None), None);
4241 }
4242
4243 #[test]
4244 fn set_presented_frames_marks_no_change_flags() {
4245 // Unlike `set_theme`/`set_insets`, a presented-count push is a paint-only
4246 // observation — it must dirty NOTHING, so a monotonically ticking counter
4247 // never forces a relayout or (on mobile) keeps the frame gate perpetually
4248 // "Run" (the menu-idle behavior depends on this).
4249 let mut root: RenderRoot<AppState, MockTextView> = RenderRoot::new();
4250 let gen_before = root.semantics_generation();
4251 root.set_presented_frames(1);
4252 assert!(root.take_change_flags().is_empty());
4253 assert!(!root.has_pending_change_flags());
4254 // A second, changed push still dirties nothing.
4255 root.set_presented_frames(2);
4256 assert!(root.take_change_flags().is_empty());
4257 // And bumps no semantics generation (mirrors the no-dirty contract).
4258 assert_eq!(root.semantics_generation(), gen_before);
4259 }
4260
4261 // Small test helper: does the boxed widget downcast to `W`?
4262 trait DowncastRefIs {
4263 fn downcast_ref_is<W: crate::widget::Widget>(&self) -> bool;
4264 }
4265 impl DowncastRefIs for dyn crate::widget::Widget {
4266 fn downcast_ref_is<W: crate::widget::Widget>(&self) -> bool {
4267 (self as &dyn std::any::Any).is::<W>()
4268 }
4269 }
4270
4271 // --- Focus / IME surface fixtures: a root editable that focuses + publishes
4272 // an IME surface on a `Down` in its left half, and blurs (no focus) on a
4273 // `Down` in its right half. ---
4274
4275 use std::cell::RefCell;
4276 use std::rc::Rc;
4277
4278 use crate::event::{EditingState, ImeState};
4279
4280 struct ImeWidget;
4281 impl crate::widget::Widget for ImeWidget {
4282 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4283 bc.constrain(Size::new(100.0, 100.0))
4284 }
4285 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4286 fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4287 if let InputEvent::Pointer(p) = event {
4288 if p.phase == PointerPhase::Down && p.position.x < 50.0 {
4289 ctx.request_focus();
4290 ctx.publish_ime_state(ImeState {
4291 active: true,
4292 editing: EditingState {
4293 text: "abc".to_string(),
4294 selection_base: 3,
4295 selection_extent: 3,
4296 composing_base: -1,
4297 composing_extent: -1,
4298 },
4299 caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
4300 content_type: Default::default(),
4301 suppress_soft_keyboard: false,
4302 });
4303 return EventResult::Handled;
4304 }
4305 if p.phase == PointerPhase::Down {
4306 // Right-half tap: a blur (no focus request).
4307 return EventResult::Handled;
4308 }
4309 }
4310 EventResult::Ignored
4311 }
4312 }
4313
4314 struct ImeView;
4315 impl View<ClickState> for ImeView {
4316 type Element = ImeWidget;
4317 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeWidget {
4318 ImeWidget
4319 }
4320 fn rebuild(
4321 &self,
4322 _prev: &Self,
4323 _element: &mut ImeWidget,
4324 _ctx: &mut BuildCtx<'_>,
4325 ) -> ChangeFlags {
4326 ChangeFlags::NONE
4327 }
4328 }
4329
4330 fn ime_logic(_state: &mut ClickState) -> ImeView {
4331 ImeView
4332 }
4333
4334 #[test]
4335 fn focus_and_ime_state_surface_and_clear_on_blur() {
4336 let mut root: RenderRoot<ClickState, ImeView> = RenderRoot::new();
4337 let mut state = ClickState::default();
4338 root.rebuild(&mut ime_logic, &mut state);
4339 root.layout(Size::new(100.0, 100.0));
4340
4341 // No focus / no IME surface initially.
4342 assert!(!root.is_focus_active());
4343 assert!(root.ime_state().is_none());
4344
4345 // A left-half Down focuses the widget and publishes an IME surface.
4346 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4347 assert!(root.is_focus_active());
4348 let ime = root
4349 .ime_state()
4350 .expect("focused widget published an IME surface");
4351 assert!(ime.active);
4352 assert_eq!(ime.editing.text, "abc");
4353
4354 // The published surface survives a rebuild (shell can query it between
4355 // frames).
4356 root.rebuild(&mut ime_logic, &mut state);
4357 assert!(root.ime_state().is_some());
4358
4359 // A right-half Down is a blur: focus and the IME surface both clear.
4360 root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
4361 assert!(!root.is_focus_active());
4362 assert!(root.ime_state().is_none());
4363 }
4364
4365 // --- Session release: the focus session must die with its owner -----------
4366 //
4367 // Two routes end a session without any user input reaching the root:
4368 //
4369 // (a) a widget publishes an INACTIVE IME surface (the navigator's post-pop
4370 // `cleared_ime_state`, `PatternSwitcher`'s equivalent, a `TextInput`
4371 // turned disabled under a live focus), and
4372 // (b) a reconciler tears the focused pod out of the tree (any generic
4373 // unmount — `frust-widgets`' `cancel_active_children`/`teardown_child`),
4374 // which raises `mark_focus_orphaned` because it has no `RenderRoot` to
4375 // reach from a `BuildCtx` pass.
4376 //
4377 // Both must perform the SAME full release a blur does. Leaving either half
4378 // standing — `focus_active` true, or `ime_state` parked at `Some(inactive)` —
4379 // is what stranded a popped screen: `is_focus_active()` kept lying, the
4380 // shell's IME poll kept seeing a surface, and the next real focus
4381 // interaction started from a corrupt baseline.
4382
4383 /// The navigator's cleared surface, spelled out here so the fixture below
4384 /// publishes exactly the shape `nav::navigator::cleared_ime_state` does
4385 /// (`frust-core` cannot name it — `frust-widgets` sits above this crate).
4386 fn cleared_surface() -> ImeState {
4387 ImeState {
4388 active: false,
4389 editing: EditingState {
4390 text: String::new(),
4391 selection_base: -1,
4392 selection_extent: -1,
4393 composing_base: -1,
4394 composing_extent: -1,
4395 },
4396 caret: None,
4397 content_type: Default::default(),
4398 suppress_soft_keyboard: false,
4399 }
4400 }
4401
4402 /// A focused editable that publishes its active surface from paint (like a
4403 /// real field), and — once `clear` is raised — publishes the *inactive*
4404 /// surface from paint instead: the container-after-a-pop shape.
4405 struct PopImeWidget {
4406 clear: Rc<Cell<bool>>,
4407 }
4408 impl crate::widget::Widget for PopImeWidget {
4409 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4410 bc.constrain(Size::new(100.0, 100.0))
4411 }
4412 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4413 if self.clear.get() {
4414 ctx.publish_ime_state(cleared_surface());
4415 } else if ctx.has_focus() {
4416 ctx.publish_ime_state(PaintImeWidget::surface("abc"));
4417 }
4418 }
4419 fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4420 match event {
4421 InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
4422 ctx.request_focus();
4423 ctx.publish_ime_state(PaintImeWidget::surface("abc"));
4424 EventResult::Handled
4425 }
4426 _ => EventResult::Ignored,
4427 }
4428 }
4429 }
4430
4431 struct PopImeView {
4432 clear: Rc<Cell<bool>>,
4433 }
4434 impl View<ClickState> for PopImeView {
4435 type Element = PopImeWidget;
4436 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PopImeWidget {
4437 PopImeWidget {
4438 clear: self.clear.clone(),
4439 }
4440 }
4441 fn rebuild(
4442 &self,
4443 _prev: &Self,
4444 _element: &mut PopImeWidget,
4445 _ctx: &mut BuildCtx<'_>,
4446 ) -> ChangeFlags {
4447 ChangeFlags::NONE
4448 }
4449 }
4450
4451 #[test]
4452 fn inactive_paint_publish_releases_the_whole_session() {
4453 // (a) The pop shape. Before this, the paint take stored `Some(inactive)`
4454 // and never touched `focus_active`, so the session outlived the page.
4455 let clear = Rc::new(Cell::new(false));
4456 let mut build = {
4457 let clear = clear.clone();
4458 move |_state: &mut ClickState| PopImeView {
4459 clear: clear.clone(),
4460 }
4461 };
4462 let mut root: RenderRoot<ClickState, PopImeView> = RenderRoot::new();
4463 let mut state = ClickState::default();
4464 root.rebuild(&mut build, &mut state);
4465 root.layout(Size::new(100.0, 100.0));
4466
4467 let mut scene = RecordingScene::default();
4468 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4469 root.paint(&mut scene, FrameTime::ZERO);
4470 assert!(root.is_focus_active());
4471 assert!(root.ime_state().is_some_and(|s| s.active));
4472 let focused = root.focus_ime_generation();
4473
4474 // The "pop": the next paint publishes the cleared surface.
4475 clear.set(true);
4476 root.paint(&mut scene, FrameTime::ZERO);
4477 assert!(
4478 !root.is_focus_active(),
4479 "an inactive publish ends the session, not just the surface"
4480 );
4481 assert_eq!(
4482 root.ime_state(),
4483 None,
4484 "the surface is dropped, never parked at Some(inactive)"
4485 );
4486 assert_eq!(
4487 root.focus_ime_generation(),
4488 focused.wrapping_add(1),
4489 "one release is exactly one edge"
4490 );
4491
4492 // The widget keeps publishing the cleared surface every frame (a real
4493 // one-shot flag would not, but an idle screen must survive the worst
4494 // case): the paint take's `focus_active` guard makes each a no-op, so
4495 // the released session neither resurrects nor spins the edge.
4496 let released = root.focus_ime_generation();
4497 for _ in 0..30 {
4498 root.paint(&mut scene, FrameTime::ZERO);
4499 }
4500 assert!(!root.is_focus_active());
4501 assert!(root.ime_state().is_none());
4502 assert_eq!(
4503 root.focus_ime_generation(),
4504 released,
4505 "an inactive publish against an already-released root is inert"
4506 );
4507 }
4508
4509 /// A view whose rebuild raises the generic-unmount orphan mark on demand —
4510 /// standing in for `frust-widgets`' reconcilers, which clear a focused
4511 /// `ChildPod` mid-diff and raise exactly this flag (this crate has no
4512 /// multi-child container of its own to diff).
4513 struct UnmountView {
4514 orphan: Rc<Cell<bool>>,
4515 }
4516 impl View<ClickState> for UnmountView {
4517 type Element = ImeWidget;
4518 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeWidget {
4519 ImeWidget
4520 }
4521 fn rebuild(
4522 &self,
4523 _prev: &Self,
4524 _element: &mut ImeWidget,
4525 _ctx: &mut BuildCtx<'_>,
4526 ) -> ChangeFlags {
4527 if self.orphan.get() {
4528 crate::event::mark_focus_orphaned();
4529 return ChangeFlags::LAYOUT | ChangeFlags::PAINT;
4530 }
4531 ChangeFlags::NONE
4532 }
4533 }
4534
4535 #[test]
4536 fn generic_unmount_orphan_releases_the_whole_session() {
4537 // (b) The child-list-diff shape: no publish, no event — the focused
4538 // widget simply stops existing. Nothing self-corrects this on an idle
4539 // screen, which is why the reconciler's mark is drained here.
4540 let _ = crate::event::take_focus_orphaned();
4541 let orphan = Rc::new(Cell::new(false));
4542 let mut build = {
4543 let orphan = orphan.clone();
4544 move |_state: &mut ClickState| UnmountView {
4545 orphan: orphan.clone(),
4546 }
4547 };
4548 let mut root: RenderRoot<ClickState, UnmountView> = RenderRoot::new();
4549 let mut state = ClickState::default();
4550 root.rebuild(&mut build, &mut state);
4551 root.layout(Size::new(100.0, 100.0));
4552
4553 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4554 assert!(root.is_focus_active());
4555 assert!(root.ime_state().is_some());
4556 let focused = root.focus_ime_generation();
4557
4558 // The unmount rebuild.
4559 orphan.set(true);
4560 root.rebuild(&mut build, &mut state);
4561 assert!(
4562 !root.is_focus_active(),
4563 "the root's focus mirror does not outlive the widget it mirrors"
4564 );
4565 assert!(root.ime_state().is_none());
4566 assert_eq!(
4567 root.focus_ime_generation(),
4568 focused.wrapping_add(1),
4569 "one orphaned focus path is exactly one edge"
4570 );
4571
4572 // The mark was drained, so an ordinary rebuild afterwards is inert...
4573 let released = root.focus_ime_generation();
4574 orphan.set(false);
4575 root.rebuild(&mut build, &mut state);
4576 assert_eq!(root.focus_ime_generation(), released);
4577
4578 // ...and re-marking against an already-released root fires no edge
4579 // either (a stale `focused` flag torn down later must not spin it).
4580 orphan.set(true);
4581 root.rebuild(&mut build, &mut state);
4582 assert_eq!(root.focus_ime_generation(), released);
4583 assert!(!root.is_focus_active());
4584 }
4585
4586 // --- The focus/IME EDGE generation ---------------------------------------
4587 //
4588 // `focus_ime_generation` is the shell-facing edge behind the mobile frame
4589 // gate's `FrameInputs::focus_or_ime_changed`: a shell caches the value and
4590 // runs a frame when it moves. Two properties make that safe, and both are
4591 // pinned below: EVERY real transition moves it (or a focus change strands
4592 // unpainted), and NO same-value write moves it (or a focused screen forces
4593 // a frame every vsync — the level-input behavior this replaced, measured at
4594 // 62–120 fps on a static focused screen).
4595
4596 /// A widget that focuses on `Down`, releases focus on any `Key`, and — the
4597 /// point of the fixture — re-publishes an IME surface from its **paint**
4598 /// pass on every frame, reading the text from a shared cell so a test can
4599 /// make a republish genuinely change (or genuinely not).
4600 struct PaintImeWidget {
4601 published: Rc<RefCell<String>>,
4602 }
4603 impl PaintImeWidget {
4604 fn surface(text: &str) -> ImeState {
4605 ImeState {
4606 active: true,
4607 editing: EditingState {
4608 text: text.to_string(),
4609 selection_base: 0,
4610 selection_extent: 0,
4611 composing_base: -1,
4612 composing_extent: -1,
4613 },
4614 caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
4615 content_type: Default::default(),
4616 suppress_soft_keyboard: false,
4617 }
4618 }
4619 }
4620 impl crate::widget::Widget for PaintImeWidget {
4621 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4622 bc.constrain(Size::new(100.0, 100.0))
4623 }
4624 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4625 ctx.publish_ime_state(Self::surface(&self.published.borrow()));
4626 }
4627 fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
4628 match event {
4629 InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
4630 ctx.request_focus();
4631 EventResult::Handled
4632 }
4633 InputEvent::Key(_) => {
4634 ctx.release_focus();
4635 EventResult::Handled
4636 }
4637 _ => EventResult::Ignored,
4638 }
4639 }
4640 }
4641
4642 struct PaintImeView {
4643 published: Rc<RefCell<String>>,
4644 }
4645 impl View<ClickState> for PaintImeView {
4646 type Element = PaintImeWidget;
4647 fn build(&self, _ctx: &mut BuildCtx<'_>) -> PaintImeWidget {
4648 PaintImeWidget {
4649 published: self.published.clone(),
4650 }
4651 }
4652 fn rebuild(
4653 &self,
4654 _prev: &Self,
4655 _element: &mut PaintImeWidget,
4656 _ctx: &mut BuildCtx<'_>,
4657 ) -> ChangeFlags {
4658 ChangeFlags::NONE
4659 }
4660 }
4661
4662 fn key_event() -> InputEvent {
4663 InputEvent::Key(crate::event::KeyEvent {
4664 key: crate::event::Key::Named(crate::event::NamedKey::Enter),
4665 modifiers: crate::event::Modifiers::default(),
4666 repeat: false,
4667 })
4668 }
4669
4670 #[test]
4671 fn focus_ime_generation_moves_on_every_pointer_transition_only() {
4672 let mut root: RenderRoot<ClickState, ImeView> = RenderRoot::new();
4673 let mut state = ClickState::default();
4674 root.rebuild(&mut ime_logic, &mut state);
4675 root.layout(Size::new(100.0, 100.0));
4676
4677 // Idle: a rebuild/layout touches neither focus nor the IME surface.
4678 let idle = root.focus_ime_generation();
4679 root.rebuild(&mut ime_logic, &mut state);
4680 assert_eq!(
4681 root.focus_ime_generation(),
4682 idle,
4683 "a rebuild is not an edge"
4684 );
4685
4686 // Focus gained + IME surface published (one transition for a shell,
4687 // however many field writes it took).
4688 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4689 let focused = root.focus_ime_generation();
4690 assert_ne!(
4691 focused, idle,
4692 "focus + IME publish must move the generation"
4693 );
4694
4695 // The SAME tap again, on the already-focused widget publishing the
4696 // identical surface: no state moved, so no edge. This is the case that
4697 // decides whether a live text field forces a frame per vsync.
4698 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4699 assert_eq!(
4700 root.focus_ime_generation(),
4701 focused,
4702 "a same-value focus/IME write must not spin the edge"
4703 );
4704
4705 // Blur: focus cleared and the surface dropped — a real transition.
4706 root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
4707 let blurred = root.focus_ime_generation();
4708 assert_ne!(blurred, focused, "a blur must move the generation");
4709
4710 // Blur while already blurred (a tap on inert chrome — the commonest
4711 // event there is) writes `false`/`None` back over `false`/`None`.
4712 root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 20.0));
4713 assert_eq!(
4714 root.focus_ime_generation(),
4715 blurred,
4716 "blurring an already-blurred root must not move the generation"
4717 );
4718 }
4719
4720 #[test]
4721 fn focus_ime_generation_ignores_an_unchanged_paint_republish() {
4722 // The paint pass re-publishes the focused widget's IME surface on EVERY
4723 // frame (that is how a rebuild-applied controlled change refreshes the
4724 // shell-facing state). If that unconditional write moved the
4725 // generation, the frame gate's edge would fire every single frame for
4726 // the whole life of a focus session — exactly the per-vsync forcing the
4727 // edge exists to remove.
4728 let published = Rc::new(RefCell::new("abc".to_string()));
4729 let mut build = {
4730 let published = published.clone();
4731 move |_state: &mut ClickState| PaintImeView {
4732 published: published.clone(),
4733 }
4734 };
4735 let mut root: RenderRoot<ClickState, PaintImeView> = RenderRoot::new();
4736 let mut state = ClickState::default();
4737 root.rebuild(&mut build, &mut state);
4738 root.layout(Size::new(100.0, 100.0));
4739
4740 // Focus the field, then let it paint: the first paint publishes.
4741 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4742 let mut scene = RecordingScene::default();
4743 root.paint(&mut scene, FrameTime::ZERO);
4744 let steady = root.focus_ime_generation();
4745 assert_eq!(
4746 root.ime_state(),
4747 Some(PaintImeWidget::surface("abc")),
4748 "the paint pass published the focused widget's surface"
4749 );
4750
4751 // 120 further frames of the same focused, unchanged field: the caret
4752 // blinks, nothing else moves. Not one edge.
4753 for _ in 0..120 {
4754 root.paint(&mut scene, FrameTime::ZERO);
4755 }
4756 assert_eq!(
4757 root.focus_ime_generation(),
4758 steady,
4759 "an unchanged paint republish must never move the generation"
4760 );
4761
4762 // A real change (the app applied a controlled edit) publishes a
4763 // different surface: exactly one edge, then quiet again.
4764 *published.borrow_mut() = "abcd".to_string();
4765 root.paint(&mut scene, FrameTime::ZERO);
4766 let edited = root.focus_ime_generation();
4767 assert_ne!(edited, steady, "a changed republish IS an edge");
4768 for _ in 0..10 {
4769 root.paint(&mut scene, FrameTime::ZERO);
4770 }
4771 assert_eq!(
4772 root.focus_ime_generation(),
4773 edited,
4774 "the session goes quiet again at the new value"
4775 );
4776 }
4777
4778 #[test]
4779 fn focus_ime_generation_moves_on_a_focus_release_only_once() {
4780 // The Key/Ime/Scroll arm of the root focus path: a dispatch that
4781 // RELEASES focus clears both the flag and the published surface.
4782 let published = Rc::new(RefCell::new("abc".to_string()));
4783 let mut build = {
4784 let published = published.clone();
4785 move |_state: &mut ClickState| PaintImeView {
4786 published: published.clone(),
4787 }
4788 };
4789 let mut root: RenderRoot<ClickState, PaintImeView> = RenderRoot::new();
4790 let mut state = ClickState::default();
4791 root.rebuild(&mut build, &mut state);
4792 root.layout(Size::new(100.0, 100.0));
4793 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
4794 let mut scene = RecordingScene::default();
4795 root.paint(&mut scene, FrameTime::ZERO);
4796 let focused = root.focus_ime_generation();
4797 assert!(root.is_focus_active());
4798
4799 // A key that releases focus: one edge.
4800 root.event(&mut state, &key_event());
4801 let released = root.focus_ime_generation();
4802 assert!(!root.is_focus_active());
4803 assert!(root.ime_state().is_none());
4804 assert_ne!(
4805 released, focused,
4806 "a focus release must move the generation"
4807 );
4808
4809 // A second release against an already-released root: no edge. (The
4810 // paint pass republishes nothing now — the paint-take arm only accepts
4811 // a publish while focus is active.)
4812 root.event(&mut state, &key_event());
4813 root.paint(&mut scene, FrameTime::ZERO);
4814 assert_eq!(
4815 root.focus_ime_generation(),
4816 released,
4817 "releasing an already-released focus must not move the generation"
4818 );
4819 }
4820
4821 // --- The focus session's IDENTITY, beside the edge generation ------------
4822 //
4823 // `focus_epoch` answers a question `focus_ime_generation` cannot: "is this
4824 // still the session that asked?". A caller that binds a slow, asynchronous
4825 // answer to the field that asked for it needs an identity, and a counter
4826 // over the published surface's *value* is not one — two fields publish
4827 // equal surfaces, and a field is free to take focus and publish nothing at
4828 // all.
4829 //
4830 // Each test below asserts what BOTH counters did at the same moment. The
4831 // `focus_ime_generation` assertions are the point rather than decoration:
4832 // they are what states, in a form the compiler checks, that the edge
4833 // generation stands still exactly where the identity moves.
4834
4835 /// The text both fields publish from a press, so neither can be told from
4836 /// the other by the published value alone.
4837 const SHARED_FIELD_TEXT: &str = "shared";
4838
4839 /// One field of the two-field fixture.
4840 ///
4841 /// Takes the focus session on any press inside itself; publishes an IME
4842 /// surface on that press only when built to; and treats an `Ime` event as an
4843 /// edit — the text changes and the surface is republished, but nothing
4844 /// re-claims a session the field already holds.
4845 ///
4846 /// `publishes: false` is not a contrivance: the baseline text input claims
4847 /// focus and republishes nothing for a press that lands inside text it
4848 /// already had selected (such a press moves no caret and collapses no
4849 /// selection), and any app-authored focusable that publishes no IME surface
4850 /// of its own behaves the same way.
4851 ///
4852 /// A press publishes the *shared* surface, so the two fields are
4853 /// indistinguishable to anything reading the published value — that is the
4854 /// case under test. An edit publishes the field's own `name` instead, which
4855 /// is how a test proves which field a focus-routed event actually reached.
4856 struct SessionField {
4857 name: &'static str,
4858 publishes: bool,
4859 }
4860
4861 impl SessionField {
4862 /// The surface a field publishes. Deliberately carries nothing that
4863 /// tells one field from another: `ImeState` is
4864 /// `{active, editing, caret, content_type}` and names no widget, so two
4865 /// fields holding the same text and caret publish equal values — which
4866 /// is the ordinary shape of two empty fields, or two overlapping ones
4867 /// mid-transition.
4868 fn surface(text: &str) -> ImeState {
4869 ImeState {
4870 active: true,
4871 editing: EditingState {
4872 text: text.to_string(),
4873 selection_base: 0,
4874 selection_extent: 0,
4875 composing_base: -1,
4876 composing_extent: -1,
4877 },
4878 caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
4879 content_type: Default::default(),
4880 suppress_soft_keyboard: false,
4881 }
4882 }
4883 }
4884
4885 impl crate::widget::Widget for SessionField {
4886 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4887 bc.constrain(Size::new(100.0, 20.0))
4888 }
4889 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4890 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4891 match event {
4892 InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
4893 ctx.request_focus();
4894 if self.publishes {
4895 ctx.publish_ime_state(Self::surface(SHARED_FIELD_TEXT));
4896 }
4897 EventResult::Handled
4898 }
4899 InputEvent::Ime(_) => {
4900 ctx.publish_ime_state(Self::surface(self.name));
4901 EventResult::Handled
4902 }
4903 _ => EventResult::Ignored,
4904 }
4905 }
4906 }
4907
4908 /// The fixture's root: two stacked fields, with a pointer event hit-tested
4909 /// to the one under it and a focus-routed event forwarded down the recorded
4910 /// focus path without a hit test — the routing every real container does.
4911 struct TwoFields {
4912 top: crate::widget::ChildPod,
4913 bottom: crate::widget::ChildPod,
4914 }
4915
4916 impl crate::widget::Widget for TwoFields {
4917 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4918 self.top.layout_child(ctx, bc);
4919 self.top.set_origin(Point::new(0.0, 0.0));
4920 self.bottom.layout_child(ctx, bc);
4921 self.bottom.set_origin(Point::new(0.0, 50.0));
4922 bc.max()
4923 }
4924 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4925 self.top.paint_child(ctx, scene);
4926 self.bottom.paint_child(ctx, scene);
4927 }
4928 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4929 if matches!(event, InputEvent::Pointer(_)) {
4930 let pos = event.position();
4931 if self.top.contains(pos) {
4932 return self.top.event_child(ctx, event);
4933 }
4934 if self.bottom.contains(pos) {
4935 return self.bottom.event_child(ctx, event);
4936 }
4937 return EventResult::Ignored;
4938 }
4939 if self.top.holds_live_focus() {
4940 return self.top.event_child(ctx, event);
4941 }
4942 if self.bottom.holds_live_focus() {
4943 return self.bottom.event_child(ctx, event);
4944 }
4945 EventResult::Ignored
4946 }
4947 fn semantics(&self, ctx: &mut SemanticsCtx) {
4948 self.top.semantics_child(ctx);
4949 self.bottom.semantics_child(ctx);
4950 }
4951 }
4952
4953 struct TwoFieldsView {
4954 bottom_publishes: bool,
4955 }
4956
4957 impl View<ClickState> for TwoFieldsView {
4958 type Element = TwoFields;
4959 fn build(&self, _ctx: &mut BuildCtx<'_>) -> TwoFields {
4960 TwoFields {
4961 top: crate::widget::ChildPod::new(Box::new(SessionField {
4962 name: "top",
4963 publishes: true,
4964 })),
4965 bottom: crate::widget::ChildPod::new(Box::new(SessionField {
4966 name: "bottom",
4967 publishes: self.bottom_publishes,
4968 })),
4969 }
4970 }
4971 fn rebuild(
4972 &self,
4973 _prev: &Self,
4974 _element: &mut TwoFields,
4975 _ctx: &mut BuildCtx<'_>,
4976 ) -> ChangeFlags {
4977 ChangeFlags::NONE
4978 }
4979 }
4980
4981 /// An edit pushed down the focus path by the platform IME: it claims no
4982 /// focus, so only the field already holding the session sees it — which is
4983 /// what makes the surface it republishes name that field.
4984 fn edit_event() -> InputEvent {
4985 InputEvent::Ime(crate::event::ImeEvent::ApplyEditingState(
4986 SessionField::surface(SHARED_FIELD_TEXT).editing,
4987 ))
4988 }
4989
4990 /// Mount the two-field fixture and press the top field, returning the root
4991 /// with a live session on it.
4992 fn two_fields_focused(
4993 bottom_publishes: bool,
4994 ) -> (RenderRoot<ClickState, TwoFieldsView>, ClickState) {
4995 let mut root: RenderRoot<ClickState, TwoFieldsView> = RenderRoot::new();
4996 let mut state = ClickState::default();
4997 root.rebuild(&mut |_| TwoFieldsView { bottom_publishes }, &mut state);
4998 root.layout(Size::new(200.0, 200.0));
4999 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
5000 assert!(root.is_focus_active(), "the top field opened a session");
5001 (root, state)
5002 }
5003
5004 #[test]
5005 fn focus_epoch_moves_when_focus_crosses_two_fields_publishing_alike() {
5006 let (mut root, mut state) = two_fields_focused(true);
5007 let first_session = root.focus_epoch();
5008 let steady_edge = root.focus_ime_generation();
5009 assert_eq!(
5010 root.ime_state(),
5011 Some(SessionField::surface(SHARED_FIELD_TEXT))
5012 );
5013
5014 // Press the bottom field. Focus really does move — and nothing
5015 // observable about the published surface moves with it: claiming while
5016 // some field is already focused writes `true` over `true`, and the
5017 // surface the second field publishes compares equal to the first's.
5018 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
5019 assert!(root.is_focus_active());
5020 assert_eq!(
5021 root.focus_ime_generation(),
5022 steady_edge,
5023 "the edge generation is blind to this move, which is why a caller \
5024 asking 'is this the same session?' must not be built on it"
5025 );
5026 assert_ne!(
5027 root.focus_epoch(),
5028 first_session,
5029 "the session identity must move when focus crosses to another field"
5030 );
5031
5032 // Not merely "some counter moved": the focus PATH is the bottom
5033 // field's now, which a focus-routed event proves by reaching it.
5034 root.event(&mut state, &edit_event());
5035 assert_eq!(
5036 root.ime_state(),
5037 Some(SessionField::surface("bottom")),
5038 "the second field is the one holding the session"
5039 );
5040 }
5041
5042 #[test]
5043 fn focus_epoch_moves_when_the_field_taking_focus_publishes_nothing() {
5044 let (mut root, mut state) = two_fields_focused(false);
5045 let first_session = root.focus_epoch();
5046 let steady_edge = root.focus_ime_generation();
5047
5048 // Press the bottom field, which takes the session and publishes no
5049 // surface of its own. A publish-nothing dispatch leaves the standing
5050 // surface standing, so what the shell still sees describes the field
5051 // the user just left — for as long as this session lasts, not merely
5052 // until the next frame.
5053 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
5054 assert!(root.is_focus_active());
5055 assert_eq!(
5056 root.ime_state(),
5057 Some(SessionField::surface(SHARED_FIELD_TEXT)),
5058 "the field that lost focus is still the one the surface describes"
5059 );
5060 assert_eq!(
5061 root.focus_ime_generation(),
5062 steady_edge,
5063 "no published value moved, so the edge generation cannot have"
5064 );
5065 assert_ne!(
5066 root.focus_epoch(),
5067 first_session,
5068 "an honoured claim moves the session identity whether or not the \
5069 claimant publishes anything"
5070 );
5071
5072 // And again, the move is a real one: the focus path now ends at the
5073 // field that published nothing.
5074 root.event(&mut state, &edit_event());
5075 assert_eq!(
5076 root.ime_state(),
5077 Some(SessionField::surface("bottom")),
5078 "the second field is the one holding the session"
5079 );
5080 }
5081
5082 #[test]
5083 fn focus_epoch_ignores_an_edit_inside_one_session() {
5084 // The converse direction, and the reason the two counters are kept
5085 // apart rather than merged: a caller holding an identity can let a
5086 // harmless edit ride, where a caller comparing the published value has
5087 // to treat every keystroke as a reason to give up.
5088 let (mut root, mut state) = two_fields_focused(true);
5089 let session = root.focus_epoch();
5090 let before_edit = root.focus_ime_generation();
5091
5092 root.event(&mut state, &edit_event());
5093 assert_eq!(
5094 root.ime_state(),
5095 Some(SessionField::surface("top")),
5096 "the focused field applied the edit and republished"
5097 );
5098 assert_ne!(
5099 root.focus_ime_generation(),
5100 before_edit,
5101 "a changed surface IS an edge"
5102 );
5103 assert_eq!(
5104 root.focus_epoch(),
5105 session,
5106 "an edit does not end or restart the session it lands in"
5107 );
5108
5109 // A release ends the identity too, so a stale one can never come back
5110 // round to matching by standing still.
5111 root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 90.0));
5112 assert!(!root.is_focus_active());
5113 assert_ne!(
5114 root.focus_epoch(),
5115 session,
5116 "a release retires the session's identity"
5117 );
5118 }
5119
5120 // --- Semantics: stable ids + accessibility action routing ---
5121 //
5122 // Fixtures: an accessibility-visible button (fire-on-up-inside, contributes a
5123 // `Role::Button` node) and a checkbox variant (`Role::CheckBox`), plus a
5124 // labelled leaf used to prove id stability survives a pod relocation.
5125
5126 use accesskit::{Action, NodeId, Role};
5127
5128 /// A fire-on-up-inside button that also contributes a semantics node — the
5129 /// end-to-end target for `perform_accessibility_action(Click)`.
5130 struct A11yButtonWidget;
5131 impl crate::widget::Widget for A11yButtonWidget {
5132 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5133 bc.constrain(Size::new(40.0, 20.0))
5134 }
5135 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5136 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5137 if let InputEvent::Pointer(p) = event {
5138 match p.phase {
5139 PointerPhase::Down => {
5140 ctx.capture_pointer();
5141 return EventResult::Handled;
5142 }
5143 PointerPhase::Up => {
5144 let size = ctx.size();
5145 let inside = p.position.x >= 0.0
5146 && p.position.y >= 0.0
5147 && p.position.x <= size.width
5148 && p.position.y <= size.height;
5149 if inside {
5150 ctx.state_mut::<ClickState>().clicks += 1;
5151 ctx.request_redraw();
5152 }
5153 return EventResult::Handled;
5154 }
5155 _ => {}
5156 }
5157 }
5158 EventResult::Ignored
5159 }
5160 fn semantics(&self, ctx: &mut SemanticsCtx) {
5161 ctx.push_node(Role::Button, |n| n.set_label("Go"));
5162 }
5163 }
5164
5165 struct A11yButtonView;
5166 impl View<ClickState> for A11yButtonView {
5167 type Element = A11yButtonWidget;
5168 fn build(&self, _ctx: &mut BuildCtx<'_>) -> A11yButtonWidget {
5169 A11yButtonWidget
5170 }
5171 fn rebuild(
5172 &self,
5173 _prev: &Self,
5174 _el: &mut A11yButtonWidget,
5175 _ctx: &mut BuildCtx<'_>,
5176 ) -> ChangeFlags {
5177 ChangeFlags::NONE
5178 }
5179 }
5180
5181 fn a11y_button_logic(_state: &mut ClickState) -> A11yButtonView {
5182 A11yButtonView
5183 }
5184
5185 fn button_node_id(update: &SemanticsUpdate, role: Role) -> NodeId {
5186 update
5187 .nodes
5188 .iter()
5189 .find(|(_, n)| n.role() == role)
5190 .map(|(id, _)| *id)
5191 .unwrap_or_else(|| panic!("a {role:?} node is present"))
5192 }
5193
5194 #[test]
5195 fn semantics_ids_are_stable_across_frames() {
5196 let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5197 let mut state = ClickState::default();
5198 root.rebuild(&mut a11y_button_logic, &mut state);
5199 root.layout(Size::new(200.0, 200.0));
5200
5201 let first = button_node_id(&root.semantics(), Role::Button);
5202 // Re-run rebuild+layout+semantics several times: the button keeps its id.
5203 for _ in 0..3 {
5204 root.rebuild(&mut a11y_button_logic, &mut state);
5205 root.layout(Size::new(200.0, 200.0));
5206 assert_eq!(
5207 button_node_id(&root.semantics(), Role::Button),
5208 first,
5209 "the same widget must keep its NodeId across frames"
5210 );
5211 }
5212 // The window root is the reserved constant id.
5213 assert_eq!(root.semantics().root, ROOT_NODE_ID);
5214 }
5215
5216 #[test]
5217 fn semantics_ids_survive_a_pod_relocation() {
5218 // A keyed reorder relocates the whole `ChildPod` (preserving its cached
5219 // semantics id); simulate that here by swapping two pods in place and
5220 // asserting each labelled node keeps its id despite changing position.
5221 struct LabeledLeaf {
5222 label: &'static str,
5223 }
5224 impl crate::widget::Widget for LabeledLeaf {
5225 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5226 bc.constrain(Size::new(10.0, 10.0))
5227 }
5228 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5229 fn semantics(&self, ctx: &mut SemanticsCtx) {
5230 let label = self.label;
5231 ctx.push_node(Role::Label, |n| n.set_label(label));
5232 }
5233 }
5234
5235 let mut pods = vec![
5236 crate::widget::ChildPod::new(Box::new(LabeledLeaf { label: "A" })),
5237 crate::widget::ChildPod::new(Box::new(LabeledLeaf { label: "B" })),
5238 ];
5239
5240 // Collect (label -> id) for a given pod order. `SemanticsCtx` is
5241 // crate-private, so this drives the pods directly — the same allocation
5242 // path `RenderRoot::semantics` uses.
5243 let collect = |pods: &[crate::widget::ChildPod]| {
5244 let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), 2);
5245 for pod in pods {
5246 pod.semantics_child(&mut ctx);
5247 }
5248 let update = ctx.finish(ROOT_NODE_ID);
5249 update
5250 .nodes
5251 .iter()
5252 .filter(|(id, _)| *id != ROOT_NODE_ID)
5253 .map(|(id, n)| (n.label().unwrap().to_string(), *id))
5254 .collect::<Vec<_>>()
5255 };
5256
5257 let before = collect(&pods);
5258 // Relocate: swap the pods (the pods themselves, with their cached ids,
5259 // move — mirroring the keyed reconciler's `take`-and-reorder).
5260 pods.swap(0, 1);
5261 let after = collect(&pods);
5262
5263 for (label, id) in &before {
5264 let relocated = after.iter().find(|(l, _)| l == label).unwrap().1;
5265 assert_eq!(
5266 *id, relocated,
5267 "widget {label:?} must keep its NodeId across the reorder"
5268 );
5269 }
5270 // And the reorder actually changed positions (A now second).
5271 assert_eq!(after[0].0, "B");
5272 assert_eq!(after[1].0, "A");
5273 }
5274
5275 #[test]
5276 fn semantics_full_update_assembles_window_and_child() {
5277 let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5278 let mut state = ClickState::default();
5279 root.rebuild(&mut a11y_button_logic, &mut state);
5280 root.layout(Size::new(200.0, 200.0));
5281
5282 let update = root.semantics();
5283 // Window root + the button.
5284 assert_eq!(update.nodes.len(), 2);
5285 assert_eq!(update.root, ROOT_NODE_ID);
5286 let root_node = update
5287 .nodes
5288 .iter()
5289 .find(|(id, _)| *id == update.root)
5290 .unwrap();
5291 assert_eq!(root_node.1.role(), Role::Window);
5292 let button = button_node_id(&update, Role::Button);
5293 assert_eq!(
5294 root_node.1.children(),
5295 &[button],
5296 "the button attaches under the window root"
5297 );
5298 // Nothing focused → the adapter-facing focus id defaults to the root.
5299 assert!(update.focus.is_none());
5300 assert_eq!(update.focus_id(), ROOT_NODE_ID);
5301 }
5302
5303 #[test]
5304 fn perform_click_action_activates_a_button() {
5305 let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5306 let mut state = ClickState::default();
5307 root.rebuild(&mut a11y_button_logic, &mut state);
5308 root.layout(Size::new(200.0, 200.0));
5309
5310 let button = button_node_id(&root.semantics(), Role::Button);
5311 let outcome = root.perform_accessibility_action(&mut state, button, Action::Click);
5312 assert!(outcome.handled, "the synthesized Down+Up was handled");
5313 assert!(outcome.needs_redraw);
5314 assert_eq!(state.clicks, 1, "Click synthesized a real up-inside tap");
5315
5316 // An unknown node id is a benign no-op.
5317 let outcome = root.perform_accessibility_action(&mut state, NodeId(999_999), Action::Click);
5318 assert_eq!(outcome, EventOutcome::default());
5319 assert_eq!(state.clicks, 1);
5320
5321 // An unmodelled action is ignored.
5322 let outcome = root.perform_accessibility_action(&mut state, button, Action::ScrollDown);
5323 assert_eq!(outcome, EventOutcome::default());
5324 assert_eq!(state.clicks, 1);
5325 }
5326
5327 #[test]
5328 fn perform_click_action_toggles_a_checkbox() {
5329 // A checkbox-shaped widget (`Role::CheckBox`) reached through the same
5330 // synthetic-pointer path — proving Click drives any fire-on-up-inside
5331 // control, not just buttons.
5332 struct CheckboxWidget;
5333 impl crate::widget::Widget for CheckboxWidget {
5334 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5335 bc.constrain(Size::new(24.0, 24.0))
5336 }
5337 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5338 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5339 if let InputEvent::Pointer(p) = event {
5340 match p.phase {
5341 PointerPhase::Down => {
5342 ctx.capture_pointer();
5343 return EventResult::Handled;
5344 }
5345 PointerPhase::Up => {
5346 let size = ctx.size();
5347 if p.position.x >= 0.0
5348 && p.position.y >= 0.0
5349 && p.position.x <= size.width
5350 && p.position.y <= size.height
5351 {
5352 ctx.state_mut::<ClickState>().clicks += 1;
5353 }
5354 return EventResult::Handled;
5355 }
5356 _ => {}
5357 }
5358 }
5359 EventResult::Ignored
5360 }
5361 fn semantics(&self, ctx: &mut SemanticsCtx) {
5362 ctx.push_node(Role::CheckBox, |n| n.set_label("Agree"));
5363 }
5364 }
5365 struct CheckboxView;
5366 impl View<ClickState> for CheckboxView {
5367 type Element = CheckboxWidget;
5368 fn build(&self, _ctx: &mut BuildCtx<'_>) -> CheckboxWidget {
5369 CheckboxWidget
5370 }
5371 fn rebuild(
5372 &self,
5373 _p: &Self,
5374 _e: &mut CheckboxWidget,
5375 _c: &mut BuildCtx<'_>,
5376 ) -> ChangeFlags {
5377 ChangeFlags::NONE
5378 }
5379 }
5380
5381 let mut root: RenderRoot<ClickState, CheckboxView> = RenderRoot::new();
5382 let mut state = ClickState::default();
5383 root.rebuild(&mut |_| CheckboxView, &mut state);
5384 root.layout(Size::new(200.0, 200.0));
5385
5386 let cb = button_node_id(&root.semantics(), Role::CheckBox);
5387 root.perform_accessibility_action(&mut state, cb, Action::Click);
5388 assert_eq!(state.clicks, 1, "Click toggled the checkbox once");
5389 }
5390
5391 #[test]
5392 fn perform_focus_action_claims_focus_and_clears_capture() {
5393 // Two focus-claiming, fire-on-up-inside buttons in a container. A11y
5394 // `Focus` on B must claim focus for B *and* release the capture the
5395 // synthesized `Down` opened — the CRITICAL leak this regresses: without
5396 // the trailing `Cancel`, B stayed captured and swallowed every later
5397 // pointer event, so a tap on A never reached A.
5398
5399 #[derive(Default)]
5400 struct FocusState {
5401 a_press: u32,
5402 b_press: u32,
5403 b_move: u32,
5404 }
5405
5406 #[derive(Clone, Copy)]
5407 enum Btn {
5408 A,
5409 B,
5410 }
5411
5412 /// A button that opts into both recorded paths (capture + focus) on
5413 /// `Down` and fires its press only on `Up`-inside — never on `Cancel`.
5414 struct FocusButton {
5415 id: Btn,
5416 }
5417 impl crate::widget::Widget for FocusButton {
5418 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5419 bc.constrain(Size::new(40.0, 20.0))
5420 }
5421 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5422 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5423 let InputEvent::Pointer(p) = event else {
5424 return EventResult::Ignored;
5425 };
5426 match p.phase {
5427 PointerPhase::Down => {
5428 ctx.capture_pointer();
5429 ctx.request_focus();
5430 EventResult::Handled
5431 }
5432 PointerPhase::Move => {
5433 if let Btn::B = self.id {
5434 ctx.state_mut::<FocusState>().b_move += 1;
5435 }
5436 EventResult::Handled
5437 }
5438 PointerPhase::Up => {
5439 let size = ctx.size();
5440 let inside = p.position.x >= 0.0
5441 && p.position.y >= 0.0
5442 && p.position.x <= size.width
5443 && p.position.y <= size.height;
5444 if inside {
5445 match self.id {
5446 Btn::A => ctx.state_mut::<FocusState>().a_press += 1,
5447 Btn::B => ctx.state_mut::<FocusState>().b_press += 1,
5448 }
5449 }
5450 EventResult::Handled
5451 }
5452 // A `Cancel` clears without firing on_press and never touches
5453 // state — the contract the Focus action's trailing Cancel rides.
5454 PointerPhase::Cancel => EventResult::Handled,
5455 }
5456 }
5457 fn semantics(&self, ctx: &mut SemanticsCtx) {
5458 let label = match self.id {
5459 Btn::A => "A",
5460 Btn::B => "B",
5461 };
5462 ctx.push_node(Role::Button, |n| n.set_label(label));
5463 }
5464 }
5465
5466 /// A minimal two-child container mirroring `frust-widgets`'
5467 /// `route_event`: a captured gesture goes straight to the active child
5468 /// (auto-released on `Up`/`Cancel`), otherwise the event is hit-tested to
5469 /// the child under it.
5470 struct TwoButtons {
5471 a: crate::widget::ChildPod,
5472 b: crate::widget::ChildPod,
5473 }
5474 impl crate::widget::Widget for TwoButtons {
5475 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5476 self.a.layout_child(ctx, bc);
5477 self.a.set_origin(Point::new(0.0, 0.0));
5478 self.b.layout_child(ctx, bc);
5479 self.b.set_origin(Point::new(0.0, 30.0));
5480 bc.max()
5481 }
5482 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
5483 self.a.paint_child(ctx, scene);
5484 self.b.paint_child(ctx, scene);
5485 }
5486 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5487 let releases = matches!(
5488 event,
5489 InputEvent::Pointer(p)
5490 if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
5491 );
5492 // Capture fast-path: a recorded active child receives every event
5493 // until it releases on Up/Cancel, bypassing the hit test entirely.
5494 if self.a.is_active() {
5495 let r = self.a.event_child(ctx, event);
5496 if releases {
5497 self.a.set_active(false);
5498 }
5499 return r;
5500 }
5501 if self.b.is_active() {
5502 let r = self.b.event_child(ctx, event);
5503 if releases {
5504 self.b.set_active(false);
5505 }
5506 return r;
5507 }
5508 // Fresh event: route to the child under the point.
5509 let pos = event.position();
5510 if self.a.contains(pos) {
5511 return self.a.event_child(ctx, event);
5512 }
5513 if self.b.contains(pos) {
5514 return self.b.event_child(ctx, event);
5515 }
5516 EventResult::Ignored
5517 }
5518 fn semantics(&self, ctx: &mut SemanticsCtx) {
5519 self.a.semantics_child(ctx);
5520 self.b.semantics_child(ctx);
5521 }
5522 }
5523
5524 struct TwoButtonsView;
5525 impl View<FocusState> for TwoButtonsView {
5526 type Element = TwoButtons;
5527 fn build(&self, _ctx: &mut BuildCtx<'_>) -> TwoButtons {
5528 TwoButtons {
5529 a: crate::widget::ChildPod::new(Box::new(FocusButton { id: Btn::A })),
5530 b: crate::widget::ChildPod::new(Box::new(FocusButton { id: Btn::B })),
5531 }
5532 }
5533 fn rebuild(
5534 &self,
5535 _p: &Self,
5536 _e: &mut TwoButtons,
5537 _c: &mut BuildCtx<'_>,
5538 ) -> ChangeFlags {
5539 ChangeFlags::NONE
5540 }
5541 }
5542
5543 let mut root: RenderRoot<FocusState, TwoButtonsView> = RenderRoot::new();
5544 let mut state = FocusState::default();
5545 root.rebuild(&mut |_| TwoButtonsView, &mut state);
5546 root.layout(Size::new(200.0, 200.0));
5547
5548 // B's semantics node (label "B") is the a11y Focus target.
5549 let b_id = root
5550 .semantics()
5551 .nodes
5552 .iter()
5553 .find(|(_, n)| n.label().is_some_and(|l| l == "B"))
5554 .map(|(id, _)| *id)
5555 .expect("button B contributes a semantics node");
5556
5557 // A11y `Focus` on B: claims the focus session, fires no on_press, and —
5558 // crucially — leaves nothing captured (the Down+Cancel shape).
5559 root.perform_accessibility_action(&mut state, b_id, Action::Focus);
5560 assert!(root.is_focus_active(), "Focus opened the focus session");
5561 assert!(
5562 !root.is_pointer_captured(),
5563 "the trailing Cancel released the capture the Focus Down opened"
5564 );
5565 assert_eq!(
5566 state.b_press, 0,
5567 "Focus (Down+Cancel) must not fire B's on_press"
5568 );
5569
5570 // B is not stuck-captured: a `Move` outside both buttons is ignored. Were
5571 // B still captured, the capture fast-path would route this to B regardless
5572 // of position (b_move would tick).
5573 let outside = InputEvent::Pointer(PointerEvent {
5574 phase: PointerPhase::Move,
5575 position: Point::new(100.0, 100.0),
5576 button: PointerButton::Primary,
5577 });
5578 root.event(&mut state, &outside);
5579 assert_eq!(state.b_move, 0, "no leaked capture: B saw no stray Move");
5580
5581 // A real Down+Up on A activates A exactly once and never reaches B.
5582 let at_a = |phase| {
5583 InputEvent::Pointer(PointerEvent {
5584 phase,
5585 position: Point::new(20.0, 10.0),
5586 button: PointerButton::Primary,
5587 })
5588 };
5589 root.event(&mut state, &at_a(PointerPhase::Down));
5590 root.event(&mut state, &at_a(PointerPhase::Up));
5591 assert_eq!(state.a_press, 1, "A fired once from its own tap");
5592 assert_eq!(
5593 state.b_press, 0,
5594 "B never fired — its capture never leaked onto A's tap"
5595 );
5596 }
5597
5598 #[test]
5599 fn semantics_if_changed_gates_on_generation() {
5600 let mut root: RenderRoot<ClickState, A11yButtonView> = RenderRoot::new();
5601 let mut state = ClickState::default();
5602 // First build bumps the generation from 0.
5603 root.rebuild(&mut a11y_button_logic, &mut state);
5604 root.layout(Size::new(200.0, 200.0));
5605
5606 let generation = root.semantics_generation();
5607 assert!(generation > 0);
5608 // A shell that already pushed `generation` sees no change.
5609 assert!(root.semantics_if_changed(generation).is_none());
5610 // A stale generation triggers a fresh pull.
5611 assert!(root.semantics_if_changed(generation - 1).is_some());
5612
5613 // A theme swap marks the tree semantics-dirty.
5614 root.set_theme(Box::new(0u32));
5615 assert!(root.semantics_generation() > generation);
5616 assert!(root.semantics_if_changed(generation).is_some());
5617 }
5618
5619 #[test]
5620 fn orientation_from_size_is_portrait_when_taller_than_wide() {
5621 assert_eq!(
5622 Orientation::from_size(Size::new(400.0, 800.0)),
5623 Orientation::Portrait
5624 );
5625 }
5626
5627 #[test]
5628 fn orientation_from_size_is_landscape_when_wider_than_tall() {
5629 assert_eq!(
5630 Orientation::from_size(Size::new(800.0, 400.0)),
5631 Orientation::Landscape
5632 );
5633 }
5634
5635 #[test]
5636 fn orientation_from_size_square_reads_as_portrait() {
5637 // Height >= width is the derivation rule (see `Orientation::from_size`'s
5638 // doc); an exact square satisfies `>=` and must not panic/ambiguously
5639 // resolve, so this is pinned explicitly rather than left implicit.
5640 assert_eq!(
5641 Orientation::from_size(Size::new(500.0, 500.0)),
5642 Orientation::Portrait
5643 );
5644 }
5645
5646 #[test]
5647 fn window_metrics_new_derives_orientation_from_size() {
5648 let insets = WindowInsets::default();
5649 let portrait = WindowMetrics::new(Size::new(390.0, 844.0), 3.0, insets);
5650 assert_eq!(portrait.orientation, Orientation::Portrait);
5651 assert_eq!(portrait.size, Size::new(390.0, 844.0));
5652 assert_eq!(portrait.scale, 3.0);
5653 assert_eq!(portrait.insets, insets);
5654
5655 let landscape = WindowMetrics::new(Size::new(844.0, 390.0), 3.0, insets);
5656 assert_eq!(landscape.orientation, Orientation::Landscape);
5657
5658 let square = WindowMetrics::new(Size::new(500.0, 500.0), 2.0, insets);
5659 assert_eq!(square.orientation, Orientation::Portrait);
5660 }
5661
5662 // --- Deferred state-bearing callbacks: the `InputEvent::Housekeeping` flush
5663 // `RenderRoot::rebuild` dispatches. ---
5664
5665 /// App state for the flush tests.
5666 #[derive(Default)]
5667 struct FlushState {
5668 /// How many deferred callbacks have run.
5669 flushes: u32,
5670 /// How many more times a running callback re-queues itself — the knob the
5671 /// chained/capped tests turn.
5672 chain_left: u32,
5673 /// The `flushes` value each build-closure run observed, in order. This is
5674 /// what proves the rebuild re-runs the build closure *after* a flush rather than
5675 /// shipping the now-stale pre-flush view.
5676 observed: Vec<u32>,
5677 }
5678
5679 /// The navigator's deferred-callback shape reduced to one leaf: a shared
5680 /// `Rc<Cell<u32>>` op queue (the `NavigatorController` analog) is drained
5681 /// during the state-free [`View::rebuild`], which can therefore only *queue*
5682 /// the callback and raise the flush mark; [`crate::widget::Widget::event`]
5683 /// runs it when the broadcast arrives, where `&mut State` finally exists.
5684 struct FlushView {
5685 ops: std::rc::Rc<Cell<u32>>,
5686 }
5687
5688 struct FlushWidget {
5689 ops: std::rc::Rc<Cell<u32>>,
5690 queued: u32,
5691 }
5692
5693 impl FlushWidget {
5694 fn drain_ops(&mut self) {
5695 let ops = self.ops.replace(0);
5696 if ops > 0 {
5697 self.queued += ops;
5698 crate::event::mark_pending_result_flush();
5699 }
5700 }
5701 }
5702
5703 impl View<FlushState> for FlushView {
5704 type Element = FlushWidget;
5705 fn build(&self, _ctx: &mut BuildCtx<'_>) -> FlushWidget {
5706 let mut widget = FlushWidget {
5707 ops: self.ops.clone(),
5708 queued: 0,
5709 };
5710 widget.drain_ops();
5711 widget
5712 }
5713 fn rebuild(
5714 &self,
5715 _prev: &Self,
5716 element: &mut FlushWidget,
5717 _ctx: &mut BuildCtx<'_>,
5718 ) -> ChangeFlags {
5719 element.ops = self.ops.clone();
5720 element.drain_ops();
5721 ChangeFlags::NONE
5722 }
5723 }
5724
5725 impl crate::widget::Widget for FlushWidget {
5726 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5727 bc.constrain(Size::new(10.0, 10.0))
5728 }
5729 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5730 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5731 if !event.is_broadcast() || self.queued == 0 {
5732 return EventResult::Ignored;
5733 }
5734 let queued = std::mem::take(&mut self.queued);
5735 let state = ctx.state_mut::<FlushState>();
5736 for _ in 0..queued {
5737 state.flushes += 1;
5738 if state.chain_left > 0 {
5739 state.chain_left -= 1;
5740 self.ops.set(self.ops.get() + 1);
5741 }
5742 }
5743 EventResult::Ignored
5744 }
5745 }
5746
5747 fn flush_logic(ops: std::rc::Rc<Cell<u32>>) -> impl FnMut(&mut FlushState) -> FlushView {
5748 move |state: &mut FlushState| {
5749 state.observed.push(state.flushes);
5750 FlushView { ops: ops.clone() }
5751 }
5752 }
5753
5754 #[test]
5755 fn a_queued_callback_flushes_and_re_diffs_inside_one_rebuild() {
5756 let ops = std::rc::Rc::new(Cell::new(0u32));
5757 let mut root: RenderRoot<FlushState, FlushView> = RenderRoot::new();
5758 let mut app = flush_logic(ops.clone());
5759 let mut state = FlushState::default();
5760
5761 root.rebuild(&mut app, &mut state);
5762 assert_eq!(state.flushes, 0);
5763 assert_eq!(
5764 state.observed,
5765 vec![0],
5766 "nothing queued ⇒ exactly one build run, no broadcast"
5767 );
5768
5769 // Queue one op — the `NavigatorController::pop_with_result` analog.
5770 state.observed.clear();
5771 ops.set(1);
5772 root.rebuild(&mut app, &mut state);
5773 assert_eq!(
5774 state.flushes, 1,
5775 "the queued callback ran inside this rebuild — no event was dispatched \
5776 by anyone but the rebuild itself"
5777 );
5778 assert_eq!(
5779 state.observed,
5780 vec![0, 1],
5781 "the build closure re-ran after the flush and saw the post-callback state, so \
5782 the view this frame ships is not the stale pre-flush one"
5783 );
5784 assert!(
5785 !crate::event::take_pending_result_flush(),
5786 "the mark was consumed; nothing is owed to a later frame"
5787 );
5788 }
5789
5790 #[test]
5791 fn a_runaway_callback_chain_is_capped_and_deferred_to_the_next_frame() {
5792 let ops = std::rc::Rc::new(Cell::new(0u32));
5793 let mut root: RenderRoot<FlushState, FlushView> = RenderRoot::new();
5794 let mut app = flush_logic(ops.clone());
5795 // Far more chaining than the cap allows: unbounded, this rebuild would
5796 // never return. Reaching the assertions below at all is the no-spin proof.
5797 let mut state = FlushState {
5798 chain_left: 100,
5799 ..Default::default()
5800 };
5801 root.rebuild(&mut app, &mut state);
5802
5803 ops.set(1);
5804 root.rebuild(&mut app, &mut state);
5805 assert_eq!(
5806 state.flushes, MAX_PENDING_RESULT_FLUSH_PASSES as u32,
5807 "exactly the cap's worth of flush passes, then stop"
5808 );
5809
5810 // The remainder is owed, not lost: the mark still stands and the next
5811 // paint asks for the follow-up frame that will finish it.
5812 root.layout(Size::new(50.0, 50.0));
5813 let mut scene = RecordingScene::default();
5814 let outcome = root.paint(&mut scene, FrameTime::ZERO);
5815 assert!(
5816 outcome.needs_frame,
5817 "hitting the cap requests one more frame, so a dirty-driven shell \
5818 wakes instead of waiting for input"
5819 );
5820 assert!(
5821 !outcome.needs_frame_paced_only,
5822 "a deferred flush is not a cosmetic loop — the mobile frame gate must \
5823 not throttle it"
5824 );
5825
5826 // That next frame picks up exactly where the capped one left off.
5827 root.rebuild(&mut app, &mut state);
5828 assert_eq!(
5829 state.flushes,
5830 2 * MAX_PENDING_RESULT_FLUSH_PASSES as u32,
5831 "the deferred remainder resumed on the following frame"
5832 );
5833
5834 // Leave this thread's flag clean for anything else in the binary.
5835 let _ = crate::event::take_pending_result_flush();
5836 }
5837
5838 /// The redraw-only flush shape: a widget that queues a callback exactly like
5839 /// [`FlushView`] above, but whose broadcast handler touches **no** state at
5840 /// all — it only calls [`EventCtx::request_redraw`]. `frust-widgets`' gesture
5841 /// long-press latch is the shipped instance (its `on_long_press` consumer may
5842 /// mutate nothing the view diff can see), and the widget's own
5843 /// `ctx.request_redraw()` after firing is then the whole wake signal.
5844 struct RedrawOnlyView {
5845 ops: std::rc::Rc<Cell<u32>>,
5846 }
5847
5848 struct RedrawOnlyWidget {
5849 ops: std::rc::Rc<Cell<u32>>,
5850 queued: bool,
5851 }
5852
5853 impl RedrawOnlyWidget {
5854 fn drain_ops(&mut self) {
5855 if self.ops.replace(0) > 0 {
5856 self.queued = true;
5857 crate::event::mark_pending_result_flush();
5858 }
5859 }
5860 }
5861
5862 impl View<()> for RedrawOnlyView {
5863 type Element = RedrawOnlyWidget;
5864 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RedrawOnlyWidget {
5865 let mut widget = RedrawOnlyWidget {
5866 ops: self.ops.clone(),
5867 queued: false,
5868 };
5869 widget.drain_ops();
5870 widget
5871 }
5872 fn rebuild(
5873 &self,
5874 _prev: &Self,
5875 element: &mut RedrawOnlyWidget,
5876 _ctx: &mut BuildCtx<'_>,
5877 ) -> ChangeFlags {
5878 element.ops = self.ops.clone();
5879 element.drain_ops();
5880 // The whole point: the re-diff after the flush reports nothing, so
5881 // the dispatch's own outcome is the only wake signal there is.
5882 ChangeFlags::NONE
5883 }
5884 }
5885
5886 impl crate::widget::Widget for RedrawOnlyWidget {
5887 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5888 bc.constrain(Size::new(10.0, 10.0))
5889 }
5890 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5891 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
5892 if event.is_broadcast() && std::mem::take(&mut self.queued) {
5893 // No `state_mut`, no signal, no view-visible change — a repaint
5894 // request and nothing else.
5895 ctx.request_redraw();
5896 }
5897 EventResult::Ignored
5898 }
5899 }
5900
5901 #[test]
5902 fn a_redraw_only_flushed_callback_wakes_both_loop_styles() {
5903 let ops = std::rc::Rc::new(Cell::new(0u32));
5904 let mut root: RenderRoot<(), RedrawOnlyView> = RenderRoot::new();
5905 let mut app = |_state: &mut ()| RedrawOnlyView { ops: ops.clone() };
5906 let mut state = ();
5907
5908 // Settle the first build so the assertions below observe only the flush.
5909 root.rebuild(&mut app, &mut state);
5910 root.layout(Size::new(50.0, 50.0));
5911 let mut scene = RecordingScene::default();
5912 let settled = root.paint(&mut scene, FrameTime::ZERO);
5913 assert!(!settled.needs_frame, "nothing queued ⇒ the tree is at rest");
5914 let _ = root.take_change_flags();
5915
5916 // Queue the redraw-only callback (the gesture long-press latch analog: a
5917 // prior pass marks, this rebuild flushes).
5918 ops.set(1);
5919 let flags = root.rebuild(&mut app, &mut state);
5920 assert!(
5921 flags.needs_paint(),
5922 "the broadcast's `needs_redraw` folds into the rebuild's flags even \
5923 though the re-diff saw no view change"
5924 );
5925 assert!(
5926 root.has_pending_change_flags(),
5927 "PAINT reached `pending`, which is the input the mobile frame gate \
5928 reads to decide the next tick runs at all"
5929 );
5930
5931 let outcome = root.paint(&mut scene, FrameTime::ZERO);
5932 assert!(
5933 outcome.needs_frame,
5934 "the same wake surfaces as `needs_frame`, which is how the desktop \
5935 `ControlFlow::Wait` loop schedules a frame with no input pending"
5936 );
5937 assert!(
5938 !outcome.needs_frame_paced_only,
5939 "a flushed callback's repaint is not a cosmetic loop — the mobile \
5940 frame gate must not throttle it"
5941 );
5942 assert!(
5943 !crate::event::take_pending_result_flush(),
5944 "the mark was consumed; nothing is owed to a later frame"
5945 );
5946
5947 // And it settles: the next frame asks for nothing, so neither loop spins.
5948 let _ = root.take_change_flags();
5949 root.rebuild(&mut app, &mut state);
5950 let settled = root.paint(&mut scene, FrameTime::ZERO);
5951 assert!(
5952 !settled.needs_frame,
5953 "one wake, not a perpetual one — the flush is over"
5954 );
5955 assert!(!root.has_pending_change_flags());
5956 }
5957
5958 // --- Hover: the claim pipeline -------------------------------------------
5959 //
5960 // Hover has no Enter/Leave phase to lean on (adding one to `PointerPhase`
5961 // would break every out-of-tree exhaustive match). It is instead an opt-in
5962 // claim a widget makes from its uncaptured `Move` arm, recorded as an epoch
5963 // stamp down the pod chain — so the fixture below is deliberately shaped like
5964 // a real container: two hit-tested children, a capture fast-path, and paint
5965 // recording what `PaintCtx::is_hovered` reported.
5966
5967 /// Which of the fixture's two leaves an assertion is about.
5968 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
5969 enum Leaf {
5970 Top,
5971 Bottom,
5972 }
5973
5974 /// What one hover leaf observed, shared out of the widget tree.
5975 #[derive(Default)]
5976 struct HoverProbe {
5977 /// `PaintCtx::is_hovered()` as of the last paint.
5978 painted_hovered: Cell<bool>,
5979 /// `EventCtx::is_hovered()` as of the last event dispatch that reached it.
5980 event_hovered: Cell<bool>,
5981 }
5982
5983 /// A leaf that claims hover on any `Move` landing inside its own bounds — the
5984 /// canonical opt-in shape — and optionally captures the pointer on `Down` (the
5985 /// drag fixture: a captured pointer must never create hover).
5986 ///
5987 /// With `latches` set it follows the whole consumer contract: the same hit test
5988 /// updates an internal flag, `request_redraw` is gated on that flag changing,
5989 /// and `paint` self-corrects the flag from the authoritative
5990 /// `PaintCtx::is_hovered`. Clearing `latches` is a deliberate negative control —
5991 /// a claimant that keeps no flag — used to pin which frames the pipeline itself
5992 /// does and does not manufacture.
5993 struct HoverLeaf {
5994 captures: bool,
5995 latches: bool,
5996 hovered: bool,
5997 probe: Rc<HoverProbe>,
5998 }
5999
6000 impl crate::widget::Widget for HoverLeaf {
6001 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6002 bc.constrain(Size::new(100.0, 30.0))
6003 }
6004 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
6005 self.probe.painted_hovered.set(ctx.is_hovered());
6006 if self.latches {
6007 // The self-correction half of the contract: authoritative here,
6008 // whatever the event arm last recorded.
6009 self.hovered = ctx.is_hovered();
6010 }
6011 }
6012 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6013 self.probe.event_hovered.set(ctx.is_hovered());
6014 let InputEvent::Pointer(p) = event else {
6015 return EventResult::Ignored;
6016 };
6017 match p.phase {
6018 PointerPhase::Move => {
6019 let size = ctx.size();
6020 let inside = p.position.x >= 0.0
6021 && p.position.y >= 0.0
6022 && p.position.x < size.width
6023 && p.position.y < size.height;
6024 if inside {
6025 ctx.claim_hover();
6026 }
6027 if self.latches && self.hovered != inside {
6028 self.hovered = inside;
6029 ctx.request_redraw();
6030 }
6031 // Deliberately `Ignored`: a hovering widget does not consume a
6032 // move it merely watched (the shipped `ListItem` shape).
6033 EventResult::Ignored
6034 }
6035 PointerPhase::Down => {
6036 if self.captures {
6037 ctx.capture_pointer();
6038 }
6039 EventResult::Handled
6040 }
6041 _ => EventResult::Ignored,
6042 }
6043 }
6044 }
6045
6046 /// Whether the container claims hover for itself, and when relative to routing
6047 /// the move into its child — the ordering the claim contract binds a container
6048 /// to.
6049 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
6050 enum GroupClaim {
6051 /// Never claims — the transparent container, hovered only via the path.
6052 Never,
6053 /// Claims *after* routing: the contract-following container, whose claim is
6054 /// a fallback the child's claim beats.
6055 AfterRouting,
6056 /// Claims *before* routing: the documented anti-pattern, kept as a
6057 /// negative control.
6058 BeforeRouting,
6059 }
6060
6061 /// A container wrapping one hover leaf, recording what its **own**
6062 /// `PaintCtx::is_hovered`/`EventCtx::is_hovered` reported — the
6063 /// ancestor-on-the-claim-path case, which the two sibling leaves alone cannot
6064 /// show. With `claims` set it also wants hover chrome of its own, claiming
6065 /// either side of the route to exercise the ordering rule.
6066 ///
6067 /// The child is an `Option` so a rebuild can *remove* it — the unmount case,
6068 /// where the claimant stops existing between hover passes.
6069 struct HoverGroup {
6070 probe: Rc<HoverProbe>,
6071 claims: GroupClaim,
6072 child: Option<crate::widget::ChildPod>,
6073 }
6074
6075 impl HoverGroup {
6076 /// Whether this event is an uncaptured-move-shaped pass landing inside the
6077 /// container's own bounds — the same local hit test a leaf claims on.
6078 fn claims_on(&self, ctx: &EventCtx, event: &InputEvent) -> bool {
6079 let InputEvent::Pointer(p) = event else {
6080 return false;
6081 };
6082 let size = ctx.size();
6083 matches!(p.phase, PointerPhase::Move)
6084 && p.position.x >= 0.0
6085 && p.position.y >= 0.0
6086 && p.position.x < size.width
6087 && p.position.y < size.height
6088 }
6089 }
6090
6091 impl crate::widget::Widget for HoverGroup {
6092 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6093 match &mut self.child {
6094 Some(child) => {
6095 let size = child.layout_child(ctx, bc);
6096 child.set_origin(Point::ZERO);
6097 size
6098 }
6099 // The same box with nothing in it, so removing the claimant
6100 // changes what is under the pointer without moving the container.
6101 None => bc.constrain(Size::new(100.0, 30.0)),
6102 }
6103 }
6104 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
6105 self.probe.painted_hovered.set(ctx.is_hovered());
6106 if let Some(child) = &mut self.child {
6107 child.paint_child(ctx, scene);
6108 }
6109 }
6110 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6111 self.probe.event_hovered.set(ctx.is_hovered());
6112 let claims = self.claims != GroupClaim::Never && self.claims_on(ctx, event);
6113 if claims && self.claims == GroupClaim::BeforeRouting {
6114 ctx.claim_hover();
6115 }
6116 let result = match &mut self.child {
6117 Some(child) => child.event_child(ctx, event),
6118 None => EventResult::Ignored,
6119 };
6120 if claims && self.claims == GroupClaim::AfterRouting {
6121 ctx.claim_hover();
6122 }
6123 result
6124 }
6125 }
6126
6127 /// Two stacked hover leaves with a hit-tested route and a capture fast-path —
6128 /// the minimum container that can show a claim moving between siblings.
6129 struct HoverPair {
6130 top: crate::widget::ChildPod,
6131 bottom: crate::widget::ChildPod,
6132 }
6133
6134 impl crate::widget::Widget for HoverPair {
6135 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6136 self.top.layout_child(ctx, bc);
6137 self.top.set_origin(Point::ZERO);
6138 self.bottom.layout_child(ctx, bc);
6139 self.bottom.set_origin(Point::new(0.0, 30.0));
6140 bc.max()
6141 }
6142 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
6143 self.top.paint_child(ctx, scene);
6144 self.bottom.paint_child(ctx, scene);
6145 }
6146 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6147 let releases = matches!(
6148 event,
6149 InputEvent::Pointer(p)
6150 if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
6151 );
6152 for pod in [&mut self.top, &mut self.bottom] {
6153 if pod.is_active() {
6154 let r = pod.event_child(ctx, event);
6155 if releases {
6156 pod.set_active(false);
6157 }
6158 return r;
6159 }
6160 }
6161 let pos = event.position();
6162 for pod in [&mut self.top, &mut self.bottom] {
6163 if pod.contains(pos) {
6164 return pod.event_child(ctx, event);
6165 }
6166 }
6167 EventResult::Ignored
6168 }
6169 }
6170
6171 /// How one `HoverHarness` is shaped.
6172 #[derive(Clone, Copy)]
6173 struct HoverFixture {
6174 /// Leaves capture the pointer on `Down` (the drag case).
6175 captures: bool,
6176 /// Leaves follow the consumer contract (latched flag + change-gated redraw
6177 /// + paint-time self-correction).
6178 latches: bool,
6179 /// Wrap the top leaf in a [`HoverGroup`], so the claim path has an
6180 /// ancestor pod between the claimant and the root.
6181 nested: bool,
6182 /// Whether (and when) that container claims hover for itself.
6183 group_claims: GroupClaim,
6184 /// Rebuild the container without its child: the unmount case, where the
6185 /// pod holding the hover link is dropped by the view diff.
6186 drop_claimant: bool,
6187 /// Report [`ChangeFlags::NONE`] from that removal — the hand-rolled
6188 /// container outside this workspace, which drops a pod while reporting
6189 /// whatever it likes. The in-tree reconcilers report `LAYOUT | PAINT`,
6190 /// which is what the release used to lean on instead of flagging its own.
6191 silent_reconciler: bool,
6192 }
6193
6194 struct HoverPairView {
6195 top: Rc<HoverProbe>,
6196 bottom: Rc<HoverProbe>,
6197 group: Rc<HoverProbe>,
6198 fixture: HoverFixture,
6199 }
6200
6201 impl HoverPairView {
6202 fn leaf(&self, probe: &Rc<HoverProbe>) -> Box<dyn crate::widget::Widget> {
6203 Box::new(HoverLeaf {
6204 captures: self.fixture.captures,
6205 latches: self.fixture.latches,
6206 hovered: false,
6207 probe: probe.clone(),
6208 })
6209 }
6210 }
6211
6212 impl View<()> for HoverPairView {
6213 type Element = HoverPair;
6214 fn build(&self, _ctx: &mut BuildCtx<'_>) -> HoverPair {
6215 let top: Box<dyn crate::widget::Widget> = if self.fixture.nested {
6216 Box::new(HoverGroup {
6217 probe: self.group.clone(),
6218 claims: self.fixture.group_claims,
6219 child: Some(crate::widget::ChildPod::new(self.leaf(&self.top))),
6220 })
6221 } else {
6222 self.leaf(&self.top)
6223 };
6224 HoverPair {
6225 top: crate::widget::ChildPod::new(top),
6226 bottom: crate::widget::ChildPod::new(self.leaf(&self.bottom)),
6227 }
6228 }
6229 fn rebuild(&self, p: &Self, e: &mut HoverPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
6230 // The only structural op this fixture performs: drop the nested
6231 // container's child pod, the way a real reconciler drops a truncated
6232 // or conditionally-removed child.
6233 let removes_child = self.fixture.drop_claimant && !p.fixture.drop_claimant;
6234 if !removes_child {
6235 return ChangeFlags::NONE;
6236 }
6237 let group = e
6238 .top
6239 .widget_mut()
6240 .downcast_mut::<HoverGroup>()
6241 .expect("the drop-claimant fixture is the nested one");
6242 group.child = None;
6243 if self.fixture.silent_reconciler {
6244 ChangeFlags::NONE
6245 } else {
6246 ChangeFlags::LAYOUT | ChangeFlags::PAINT
6247 }
6248 }
6249 }
6250
6251 /// A `RenderRoot` over the hover fixture, plus its probes.
6252 struct HoverHarness {
6253 root: RenderRoot<(), HoverPairView>,
6254 top: Rc<HoverProbe>,
6255 bottom: Rc<HoverProbe>,
6256 group: Rc<HoverProbe>,
6257 state: (),
6258 /// The shape the next rebuild re-states, so
6259 /// [`HoverHarness::rebuild_without_claimant`] can flip one flag without
6260 /// restating the rest.
6261 fixture: HoverFixture,
6262 }
6263
6264 impl HoverHarness {
6265 /// The contract-following fixture: two flat leaves that latch their own
6266 /// hover flag, optionally capturing on `Down`.
6267 fn new(captures: bool) -> Self {
6268 Self::build(HoverFixture {
6269 captures,
6270 latches: true,
6271 nested: false,
6272 group_claims: GroupClaim::Never,
6273 drop_claimant: false,
6274 silent_reconciler: false,
6275 })
6276 }
6277
6278 /// The negative control: leaves that claim hover but keep no flag of their
6279 /// own, so only frames the pipeline manufactures show up.
6280 fn without_consumer_flag() -> Self {
6281 Self::build(HoverFixture {
6282 captures: false,
6283 latches: false,
6284 nested: false,
6285 group_claims: GroupClaim::Never,
6286 drop_claimant: false,
6287 silent_reconciler: false,
6288 })
6289 }
6290
6291 /// The top leaf wrapped in a container pod, for the path-semantics case.
6292 fn nested() -> Self {
6293 Self::build(HoverFixture {
6294 captures: false,
6295 latches: true,
6296 nested: true,
6297 group_claims: GroupClaim::Never,
6298 drop_claimant: false,
6299 silent_reconciler: false,
6300 })
6301 }
6302
6303 /// The same nesting, with the container claiming hover for itself the way
6304 /// the contract requires: after routing the move into its child.
6305 fn nested_group_claiming(claims: GroupClaim) -> Self {
6306 Self::build(HoverFixture {
6307 captures: false,
6308 latches: true,
6309 nested: true,
6310 group_claims: claims,
6311 drop_claimant: false,
6312 silent_reconciler: false,
6313 })
6314 }
6315
6316 /// The same nesting, with a container that drops its child while
6317 /// reporting no flags — the hand-rolled container outside this workspace
6318 /// the destructor route exists to cover.
6319 fn nested_group_with_silent_reconciler() -> Self {
6320 Self::build(HoverFixture {
6321 captures: false,
6322 latches: true,
6323 nested: true,
6324 group_claims: GroupClaim::AfterRouting,
6325 drop_claimant: false,
6326 silent_reconciler: true,
6327 })
6328 }
6329
6330 fn build(fixture: HoverFixture) -> Self {
6331 let top = Rc::new(HoverProbe::default());
6332 let bottom = Rc::new(HoverProbe::default());
6333 let group = Rc::new(HoverProbe::default());
6334 let mut root: RenderRoot<(), HoverPairView> = RenderRoot::new();
6335 let mut state = ();
6336 let (t, b, g) = (top.clone(), bottom.clone(), group.clone());
6337 root.rebuild(
6338 &mut move |_: &mut ()| HoverPairView {
6339 top: t.clone(),
6340 bottom: b.clone(),
6341 group: g.clone(),
6342 fixture,
6343 },
6344 &mut state,
6345 );
6346 root.layout(Size::new(100.0, 60.0));
6347 HoverHarness {
6348 root,
6349 top,
6350 bottom,
6351 group,
6352 state,
6353 fixture,
6354 }
6355 }
6356
6357 /// Rebuild with the nested container's child removed — the claimant
6358 /// unmounting between hover passes — and re-lay out, returning what the
6359 /// diff reported.
6360 fn rebuild_without_claimant(&mut self) -> ChangeFlags {
6361 self.fixture.drop_claimant = true;
6362 self.rebuild_current()
6363 }
6364
6365 /// Rebuild restating the shape already on screen: nothing of this root's
6366 /// own is severed, so any hover end it performs came from elsewhere.
6367 fn rebuild_unchanged(&mut self) -> ChangeFlags {
6368 self.rebuild_current()
6369 }
6370
6371 /// Re-run the diff against the fixture as it currently stands, then
6372 /// re-lay out, returning what the diff reported.
6373 fn rebuild_current(&mut self) -> ChangeFlags {
6374 let fixture = self.fixture;
6375 let (t, b, g) = (self.top.clone(), self.bottom.clone(), self.group.clone());
6376 let flags = self.root.rebuild(
6377 &mut move |_: &mut ()| HoverPairView {
6378 top: t.clone(),
6379 bottom: b.clone(),
6380 group: g.clone(),
6381 fixture,
6382 },
6383 &mut self.state,
6384 );
6385 self.root.layout(Size::new(100.0, 60.0));
6386 flags
6387 }
6388
6389 /// Dispatch a pointer event at `(x, y)` in window space.
6390 fn dispatch(&mut self, phase: PointerPhase, x: f64, y: f64) -> EventOutcome {
6391 let event = InputEvent::Pointer(PointerEvent {
6392 phase,
6393 position: Point::new(x, y),
6394 button: PointerButton::Primary,
6395 });
6396 self.root.event(&mut self.state, &event)
6397 }
6398
6399 /// Move the pointer over the given leaf's middle.
6400 fn move_over(&mut self, leaf: Leaf) -> EventOutcome {
6401 match leaf {
6402 Leaf::Top => self.dispatch(PointerPhase::Move, 50.0, 15.0),
6403 Leaf::Bottom => self.dispatch(PointerPhase::Move, 50.0, 45.0),
6404 }
6405 }
6406
6407 /// Paint the tree, refreshing both probes' recorded hover state.
6408 fn paint(&mut self) {
6409 let mut scene = RecordingScene::default();
6410 self.root.paint(&mut scene, FrameTime::ZERO);
6411 }
6412
6413 /// `(top, bottom)` hover as the last paint reported it.
6414 fn painted(&mut self) -> (bool, bool) {
6415 self.paint();
6416 (
6417 self.top.painted_hovered.get(),
6418 self.bottom.painted_hovered.get(),
6419 )
6420 }
6421 }
6422
6423 #[test]
6424 fn an_uncaptured_move_claims_hover_and_paint_reports_it() {
6425 let mut h = HoverHarness::new(false);
6426 assert!(!h.root.is_hover_active(), "nothing is hovered at rest");
6427 assert_eq!(h.painted(), (false, false));
6428
6429 let outcome = h.move_over(Leaf::Top);
6430 assert!(h.root.is_hover_active(), "the claim reached the root");
6431 assert!(
6432 !outcome.handled,
6433 "a hovering widget need not consume the move"
6434 );
6435 assert_eq!(
6436 h.painted(),
6437 (true, false),
6438 "the claimant reads as hovered, its sibling does not"
6439 );
6440
6441 // A second move within the same leaf keeps the link (the claim is
6442 // re-recorded every pass) without re-reporting a change.
6443 h.dispatch(PointerPhase::Move, 60.0, 20.0);
6444 assert_eq!(h.painted(), (true, false));
6445 }
6446
6447 #[test]
6448 fn a_second_widgets_claim_clears_the_first_and_asks_for_a_repaint() {
6449 let mut h = HoverHarness::new(false);
6450 h.move_over(Leaf::Top);
6451 assert_eq!(h.painted(), (true, false));
6452
6453 // The pointer moves onto the sibling. The container never has to clear
6454 // anything: the epoch advance strands the top pod's stamp.
6455 h.move_over(Leaf::Bottom);
6456 assert!(h.root.is_hover_active());
6457 assert_eq!(
6458 h.painted(),
6459 (false, true),
6460 "the previous claimant lost its link when the new one recorded"
6461 );
6462
6463 // Both widgets need a repaint, and a repaint is global — one request
6464 // covers them. The *losing* side is what the root itself must guarantee:
6465 // moving onto a leaf that claims nothing still repaints.
6466 let outcome = h.dispatch(PointerPhase::Move, 50.0, 200.0);
6467 assert!(
6468 !h.root.is_hover_active(),
6469 "a move claiming nothing ends the hover"
6470 );
6471 assert!(
6472 outcome.needs_redraw,
6473 "the widget that lost hover cannot ask for the repaint itself"
6474 );
6475 assert_eq!(h.painted(), (false, false));
6476
6477 // ...and the same move repeated is not a change any more.
6478 let settled = h.dispatch(PointerPhase::Move, 50.0, 200.0);
6479 assert!(
6480 !settled.needs_redraw,
6481 "an already-hoverless move requests nothing"
6482 );
6483 }
6484
6485 #[test]
6486 fn a_captured_move_cannot_claim_hover() {
6487 let mut h = HoverHarness::new(true);
6488 // Press the top leaf: it captures, and the `Down` itself ends any hover.
6489 h.dispatch(PointerPhase::Down, 50.0, 15.0);
6490 assert!(h.root.is_pointer_captured());
6491 assert!(!h.root.is_hover_active());
6492
6493 // Drag: every one of these moves routes to the captured leaf, whose `Move`
6494 // arm hit-tests inside and calls `claim_hover()` — and must record nothing.
6495 h.dispatch(PointerPhase::Move, 50.0, 16.0);
6496 assert!(
6497 !h.root.is_hover_active(),
6498 "a captured pointer never creates hover"
6499 );
6500 assert_eq!(h.painted(), (false, false));
6501
6502 // Dragging outside the leaf keeps routing to it (capture), still no hover.
6503 h.dispatch(PointerPhase::Move, 50.0, 45.0);
6504 assert!(!h.root.is_hover_active());
6505 assert_eq!(h.painted(), (false, false));
6506
6507 // Release, then a fresh uncaptured move: hover is claimable again.
6508 h.dispatch(PointerPhase::Up, 50.0, 15.0);
6509 h.move_over(Leaf::Top);
6510 assert!(h.root.is_hover_active());
6511 assert_eq!(h.painted(), (true, false));
6512 }
6513
6514 #[test]
6515 fn a_down_up_or_cancel_ends_the_hover() {
6516 // Every pointer phase other than an uncaptured `Move` ends the link. `Up`
6517 // is in here for touch: a lifted finger sends no further move, so a tint
6518 // claimed during an uncaptured touch drag would otherwise stand for good.
6519 for ending in [PointerPhase::Down, PointerPhase::Up, PointerPhase::Cancel] {
6520 let mut h = HoverHarness::new(false);
6521 h.move_over(Leaf::Top);
6522 assert!(h.root.is_hover_active());
6523
6524 let outcome = h.dispatch(ending, 50.0, 15.0);
6525 assert!(
6526 !h.root.is_hover_active(),
6527 "{ending:?} ends the hover link outright"
6528 );
6529 assert!(
6530 outcome.needs_redraw,
6531 "{ending:?} that dropped a hover asks for the repaint"
6532 );
6533 assert_eq!(h.painted(), (false, false));
6534 }
6535 }
6536
6537 #[test]
6538 fn a_non_pointer_pass_leaves_a_live_hover_standing() {
6539 let mut h = HoverHarness::new(false);
6540 h.move_over(Leaf::Top);
6541 assert_eq!(h.painted(), (true, false));
6542
6543 // Neither a scroll, a key, nor the housekeeping broadcast is a hover pass:
6544 // the pointer has not moved, so the link must survive them untouched.
6545 h.root.event(
6546 &mut h.state,
6547 &InputEvent::Scroll {
6548 position: Point::new(50.0, 15.0),
6549 delta: crate::event::ScrollDelta::Lines(0.0, 1.0),
6550 },
6551 );
6552 assert!(h.root.is_hover_active());
6553 h.root.event(&mut h.state, &InputEvent::Housekeeping);
6554 assert!(h.root.is_hover_active());
6555 assert_eq!(h.painted(), (true, false));
6556 }
6557
6558 #[test]
6559 fn a_container_on_the_claim_path_reads_hovered_and_a_sibling_does_not() {
6560 // The recorded thing is a path, so hover is `:hover`-shaped: the claimant
6561 // and every ancestor enclosing it read hovered, nothing off the path does.
6562 let mut h = HoverHarness::nested();
6563 h.move_over(Leaf::Top);
6564 h.paint();
6565 assert!(h.top.painted_hovered.get(), "the claimant itself");
6566 assert!(
6567 h.group.painted_hovered.get(),
6568 "the container enclosing the claimant is on the path too"
6569 );
6570 assert!(
6571 !h.bottom.painted_hovered.get(),
6572 "a sibling leaf is off the path"
6573 );
6574
6575 // Move onto the sibling: the container goes unhovered with its child, and
6576 // both reads agree about it on the next pass.
6577 h.move_over(Leaf::Bottom);
6578 h.paint();
6579 assert!(!h.group.painted_hovered.get());
6580 assert!(!h.top.painted_hovered.get());
6581 assert!(h.bottom.painted_hovered.get());
6582 h.move_over(Leaf::Top);
6583 assert!(
6584 !h.group.event_hovered.get(),
6585 "the event read reports the previous pass, like the leaf's"
6586 );
6587 h.move_over(Leaf::Top);
6588 assert!(
6589 h.group.event_hovered.get(),
6590 "the container observes the link its child holds"
6591 );
6592 }
6593
6594 #[test]
6595 fn an_ancestor_claiming_after_routing_loses_to_its_child_and_still_reads_hovered() {
6596 // A container that wants hover chrome of its own claims after routing the
6597 // move into its child. Only one claim per pass is recorded and the first one
6598 // recorded wins, so the child's claim is the one that lands; the container's
6599 // own late call is a silent no-op, and it reads hovered through the stamped
6600 // path anyway — which is what makes this ordering correct in every case.
6601 let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6602 let gain = h.move_over(Leaf::Top);
6603 h.paint();
6604 assert!(
6605 h.top.painted_hovered.get(),
6606 "the child under the pointer holds the link"
6607 );
6608 assert!(
6609 h.group.painted_hovered.get(),
6610 "the container is on that path, so it reads hovered too"
6611 );
6612 assert!(
6613 !h.bottom.painted_hovered.get(),
6614 "a sibling leaf is off the path"
6615 );
6616 assert!(gain.needs_redraw, "hover gain repaints");
6617
6618 // The child's latched flag now agrees with the authoritative paint read, so
6619 // wandering on within the same widget settles instead of repainting.
6620 let settled = h.dispatch(PointerPhase::Move, 60.0, 20.0);
6621 assert!(!settled.needs_redraw, "an unchanged flag asks for nothing");
6622 h.paint();
6623 assert!(h.top.painted_hovered.get());
6624 assert!(h.group.painted_hovered.get());
6625 }
6626
6627 #[test]
6628 fn an_ancestor_claiming_before_routing_starves_its_subtree() {
6629 // The negative control for the ordering rule above, pinning the trap it
6630 // exists to prevent: a container that claims *before* forwarding is recorded
6631 // first, which closes the pass to every descendant. The child under the
6632 // pointer can never read hovered, so its hover chrome never appears — and
6633 // because its latched flag is corrected back to `false` at paint time, it
6634 // flips and asks for a frame again on every single move.
6635 let mut h = HoverHarness::nested_group_claiming(GroupClaim::BeforeRouting);
6636 h.move_over(Leaf::Top);
6637 h.paint();
6638 assert!(
6639 !h.top.painted_hovered.get(),
6640 "the ancestor's earlier claim made its child ineligible"
6641 );
6642 assert!(
6643 h.group.painted_hovered.get(),
6644 "the outermost claimant is the one holding the link here"
6645 );
6646 assert!(!h.bottom.painted_hovered.get());
6647
6648 // Repaint-per-move: the flag never converges, because the event arm and the
6649 // authoritative paint read permanently disagree.
6650 let again = h.dispatch(PointerPhase::Move, 60.0, 20.0);
6651 assert!(
6652 again.needs_redraw,
6653 "the starved child re-flips its flag on every move"
6654 );
6655 h.paint();
6656 assert!(!h.top.painted_hovered.get(), "and still paints no chrome");
6657 }
6658
6659 #[test]
6660 fn hover_gain_is_repainted_by_the_consumers_own_flag() {
6661 let mut h = HoverHarness::new(false);
6662 // Entering a widget: the root manufactures nothing here (its mirror went
6663 // `false` → `true`, and it cannot know which widget cares), so the frame
6664 // comes from the claimant's own change-gated request.
6665 let gain = h.move_over(Leaf::Top);
6666 assert!(gain.needs_redraw, "hover gain repaints");
6667 assert_eq!(h.painted(), (true, false));
6668
6669 // Wandering within the same widget claims again but changes nothing, so it
6670 // must not repaint per event.
6671 let settled = h.dispatch(PointerPhase::Move, 60.0, 20.0);
6672 assert!(!settled.needs_redraw, "an unchanged flag asks for nothing");
6673
6674 // A handoff repaints both sides at once: the arriving leaf's flag changed
6675 // (it asks), and because a repaint is global that same frame is what lets
6676 // the departing leaf drop its chrome from the authoritative paint read.
6677 let handoff = h.move_over(Leaf::Bottom);
6678 assert!(handoff.needs_redraw, "a claimant handoff repaints");
6679 assert_eq!(
6680 h.painted(),
6681 (false, true),
6682 "one frame settles both the loss and the gain"
6683 );
6684 }
6685
6686 #[test]
6687 fn a_claimant_without_its_own_flag_gets_only_the_loss_frame() {
6688 // The negative control for the contract above: leaves that claim hover but
6689 // keep no flag of their own. Gain and handoff are invisible to the root
6690 // (its hover mirror is identity-free — `false` → `true` and `true` →
6691 // `true`), so nothing repaints for them, which is exactly why the
6692 // consumer's latched flag is normative rather than an optimization.
6693 let mut h = HoverHarness::without_consumer_flag();
6694 let gain = h.move_over(Leaf::Top);
6695 assert!(h.root.is_hover_active());
6696 assert!(!gain.needs_redraw, "no widget asked, and the root cannot");
6697
6698 let handoff = h.move_over(Leaf::Bottom);
6699 assert!(h.root.is_hover_active());
6700 assert!(!handoff.needs_redraw, "a handoff is `true` → `true` here");
6701
6702 // Loss is the one edge the root does cover, since no widget can see it.
6703 let loss = h.dispatch(PointerPhase::Move, 50.0, 200.0);
6704 assert!(!h.root.is_hover_active());
6705 assert!(loss.needs_redraw, "the root manufactures the loss frame");
6706 }
6707
6708 #[test]
6709 fn a_rebuild_that_removes_the_claimant_ends_the_hover() {
6710 // The one severance the epoch cannot strand: the claimant is dropped by a
6711 // view diff, so it will never see the `Move` that would have re-derived
6712 // the link. Its pod reports the drop and `rebuild` ends the hover.
6713 let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6714 h.move_over(Leaf::Top);
6715 h.paint();
6716 assert!(h.root.is_hover_active());
6717 assert!(h.top.painted_hovered.get(), "the claimant holds the link");
6718 assert!(
6719 h.group.painted_hovered.get(),
6720 "its container is on the path"
6721 );
6722
6723 let flags = h.rebuild_without_claimant();
6724 assert!(
6725 !h.root.is_hover_active(),
6726 "the mirror cannot outlive the widget it described"
6727 );
6728 assert!(
6729 flags.contains(ChangeFlags::PAINT),
6730 "the correction states its own need for a frame, whatever the \
6731 reconciler that dropped the claimant reported"
6732 );
6733
6734 // The survivor is the ancestor that was on the claim path: its own stamp
6735 // still names the epoch the claim recorded, so nothing but the epoch
6736 // advance keeps it from painting hover chrome for a child that is gone.
6737 h.paint();
6738 assert!(
6739 !h.group.painted_hovered.get(),
6740 "the surviving ancestor lost the link with its child"
6741 );
6742 assert!(
6743 !h.bottom.painted_hovered.get(),
6744 "and the sibling never had it"
6745 );
6746
6747 // And the pipeline is not wedged: the next move over the same spot claims
6748 // cleanly, now for the container itself. (No frame is manufactured for
6749 // that gain — this container keeps no latched flag of its own, which is
6750 // the documented consumer-side half of the contract, not a pipeline job.)
6751 h.move_over(Leaf::Top);
6752 assert!(h.root.is_hover_active(), "the next move re-claims");
6753 h.paint();
6754 assert!(
6755 h.group.painted_hovered.get(),
6756 "the container is the claimant now"
6757 );
6758 }
6759
6760 #[test]
6761 fn a_silent_reconcilers_removal_still_carries_its_own_repaint() {
6762 // The reason the release flags `PAINT` itself rather than trusting the
6763 // diff to have reported one. The destructor route deliberately reaches
6764 // containers this workspace never sees, and such a container can drop the
6765 // claimant while reporting nothing — leaving the hover correctly ended but
6766 // the frame that shows it unrequested, on a pointer the user has stopped
6767 // moving.
6768 let mut h = HoverHarness::nested_group_with_silent_reconciler();
6769 h.move_over(Leaf::Top);
6770 assert!(h.root.is_hover_active());
6771
6772 let flags = h.rebuild_without_claimant();
6773 assert!(!h.root.is_hover_active(), "the link ends either way");
6774 assert_eq!(
6775 flags,
6776 ChangeFlags::PAINT,
6777 "and the frame it needs comes from the release, not from the diff"
6778 );
6779 }
6780
6781 #[test]
6782 fn a_rebuild_that_keeps_the_claimant_leaves_the_hover_standing() {
6783 // The negative control for the release above, and the reason the mark is
6784 // gated on the *live* epoch rather than on "some pod with a stamp died":
6785 // an ordinary rebuild — including one that drops pods carrying stale
6786 // stamps — must not touch a link the pointer still rests on.
6787 let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6788 // Hover the top leaf, then hand the link to the sibling. The top pod chain
6789 // keeps its (now stale) stamp, which is what the next rebuild drops.
6790 h.move_over(Leaf::Top);
6791 h.move_over(Leaf::Bottom);
6792 assert!(h.root.is_hover_active());
6793
6794 h.rebuild_without_claimant();
6795 assert!(
6796 h.root.is_hover_active(),
6797 "dropping a stale stamp is not a severance"
6798 );
6799 h.paint();
6800 assert!(
6801 h.bottom.painted_hovered.get(),
6802 "the widget actually under the pointer keeps its chrome"
6803 );
6804 }
6805
6806 #[test]
6807 fn another_roots_dying_claimant_cannot_end_this_roots_hover() {
6808 // Two roots on one thread. Each has run exactly one hover pass, so their
6809 // epoch counters hold the identical integer — the collision the root
6810 // identity exists to break. Without it, the first root's pods dying (its
6811 // window closing, a page tearing down) raise a mark the second root's
6812 // next rebuild drains, ending a hover the pointer is still resting on.
6813 let mut first = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6814 let mut second = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
6815 first.move_over(Leaf::Top);
6816 second.move_over(Leaf::Top);
6817 assert_eq!(
6818 first.root.hover_epoch, second.root.hover_epoch,
6819 "the two roots' epochs collide, which is the whole premise"
6820 );
6821 assert!(first.root.is_hover_active());
6822 assert!(second.root.is_hover_active());
6823
6824 // Drop the first root outright: every pod it owns runs the destructor
6825 // that reports a severed hover link, including the claimant's.
6826 drop(first);
6827
6828 second.rebuild_unchanged();
6829 assert!(
6830 second.root.is_hover_active(),
6831 "a mark another root raised is none of this root's business"
6832 );
6833 second.paint();
6834 assert!(
6835 second.top.painted_hovered.get(),
6836 "the widget under the pointer keeps its chrome"
6837 );
6838 }
6839
6840 #[test]
6841 fn event_ctx_hover_reports_the_previous_pass_not_this_ones_claim() {
6842 let mut h = HoverHarness::new(false);
6843 // First move over the top leaf: it was not hovered when its handler ran.
6844 h.move_over(Leaf::Top);
6845 assert!(
6846 !h.top.event_hovered.get(),
6847 "a fresh claim does not retroactively flip `is_hovered`"
6848 );
6849 // Second move over the same leaf: now it observes the link it holds.
6850 h.move_over(Leaf::Top);
6851 assert!(
6852 h.top.event_hovered.get(),
6853 "the link recorded last pass is visible to this pass's handler"
6854 );
6855 }
6856
6857 // --- Cursor: the per-pass request channel ---------------------------------
6858 //
6859 // The cursor is hover's sibling and is deliberately not derived from it (the
6860 // root's hover mirror is identity-free), so it gets its own fixture: two
6861 // leaves that ask for different shapes, a container that can speak before or
6862 // after routing (the last-writer case), and a capture fast-path (the drag
6863 // case, where the shape must survive the pointer leaving the widget).
6864
6865 /// How the cursor fixture's container speaks relative to its children.
6866 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
6867 enum ContainerCursor {
6868 /// Says nothing at all — the ordinary container.
6869 Silent,
6870 /// Asks *before* routing, so whatever the child asks for comes later.
6871 BeforeRouting(CursorIcon),
6872 /// Asks *after* routing, deliberately overriding its child.
6873 AfterRouting(CursorIcon),
6874 }
6875
6876 /// A leaf that asks for one cursor while the pointer is over it and another
6877 /// while it holds the capture — the two shapes a real draggable control wants.
6878 struct CursorLeaf {
6879 hover_icon: Option<CursorIcon>,
6880 drag_icon: Option<CursorIcon>,
6881 captures: bool,
6882 captured: bool,
6883 }
6884
6885 impl crate::widget::Widget for CursorLeaf {
6886 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6887 bc.constrain(Size::new(100.0, 30.0))
6888 }
6889 // A cursor is resolved entirely in the event pass — this fixture never
6890 // paints, unlike the hover one (whose authoritative read is at paint time).
6891 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
6892 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6893 let InputEvent::Pointer(p) = event else {
6894 return EventResult::Ignored;
6895 };
6896 match p.phase {
6897 PointerPhase::Move => {
6898 if self.captured {
6899 // The captured pass belongs to this widget wherever the
6900 // pointer has gone — re-asking here is what keeps the drag
6901 // shape alive outside its own bounds.
6902 if let Some(icon) = self.drag_icon {
6903 ctx.set_cursor(icon);
6904 }
6905 return EventResult::Handled;
6906 }
6907 let size = ctx.size();
6908 let inside = p.position.x >= 0.0
6909 && p.position.y >= 0.0
6910 && p.position.x < size.width
6911 && p.position.y < size.height;
6912 if inside && let Some(icon) = self.hover_icon {
6913 ctx.set_cursor(icon);
6914 }
6915 EventResult::Ignored
6916 }
6917 PointerPhase::Down => {
6918 if self.captures {
6919 ctx.capture_pointer();
6920 self.captured = true;
6921 }
6922 EventResult::Handled
6923 }
6924 PointerPhase::Up | PointerPhase::Cancel => {
6925 self.captured = false;
6926 EventResult::Ignored
6927 }
6928 }
6929 }
6930 }
6931
6932 /// Two stacked cursor leaves routed exactly like [`HoverPair`], plus the
6933 /// container's own optional request.
6934 struct CursorPair {
6935 top: crate::widget::ChildPod,
6936 bottom: crate::widget::ChildPod,
6937 own: ContainerCursor,
6938 }
6939
6940 impl crate::widget::Widget for CursorPair {
6941 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6942 self.top.layout_child(ctx, bc);
6943 self.top.set_origin(Point::ZERO);
6944 self.bottom.layout_child(ctx, bc);
6945 self.bottom.set_origin(Point::new(0.0, 30.0));
6946 bc.max()
6947 }
6948 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
6949 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6950 if let ContainerCursor::BeforeRouting(icon) = self.own {
6951 ctx.set_cursor(icon);
6952 }
6953 let result = self.route(ctx, event);
6954 if let ContainerCursor::AfterRouting(icon) = self.own {
6955 ctx.set_cursor(icon);
6956 }
6957 result
6958 }
6959 }
6960
6961 impl CursorPair {
6962 /// The capture-first, then hit-test routing every real container does.
6963 fn route(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6964 let releases = matches!(
6965 event,
6966 InputEvent::Pointer(p)
6967 if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
6968 );
6969 for pod in [&mut self.top, &mut self.bottom] {
6970 if pod.is_active() {
6971 let r = pod.event_child(ctx, event);
6972 if releases {
6973 pod.set_active(false);
6974 }
6975 return r;
6976 }
6977 }
6978 let pos = event.position();
6979 for pod in [&mut self.top, &mut self.bottom] {
6980 if pod.contains(pos) {
6981 return pod.event_child(ctx, event);
6982 }
6983 }
6984 EventResult::Ignored
6985 }
6986 }
6987
6988 #[derive(Clone, Copy)]
6989 struct CursorPairView {
6990 own: ContainerCursor,
6991 captures: bool,
6992 }
6993
6994 impl View<()> for CursorPairView {
6995 type Element = CursorPair;
6996 fn build(&self, _ctx: &mut BuildCtx<'_>) -> CursorPair {
6997 CursorPair {
6998 top: crate::widget::ChildPod::new(Box::new(CursorLeaf {
6999 hover_icon: Some(CursorIcon::Pointer),
7000 drag_icon: Some(CursorIcon::Grabbing),
7001 captures: self.captures,
7002 captured: false,
7003 })),
7004 bottom: crate::widget::ChildPod::new(Box::new(CursorLeaf {
7005 hover_icon: Some(CursorIcon::Text),
7006 drag_icon: None,
7007 captures: false,
7008 captured: false,
7009 })),
7010 own: self.own,
7011 }
7012 }
7013 fn rebuild(&self, _p: &Self, _e: &mut CursorPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
7014 ChangeFlags::NONE
7015 }
7016 }
7017
7018 /// A `RenderRoot` over the cursor fixture.
7019 struct CursorHarness {
7020 root: RenderRoot<(), CursorPairView>,
7021 state: (),
7022 }
7023
7024 impl CursorHarness {
7025 fn new(own: ContainerCursor, captures: bool) -> Self {
7026 let mut root: RenderRoot<(), CursorPairView> = RenderRoot::new();
7027 let mut state = ();
7028 root.rebuild(
7029 &mut move |_: &mut ()| CursorPairView { own, captures },
7030 &mut state,
7031 );
7032 root.layout(Size::new(100.0, 60.0));
7033 CursorHarness { root, state }
7034 }
7035
7036 fn dispatch(&mut self, phase: PointerPhase, x: f64, y: f64) -> EventOutcome {
7037 let event = InputEvent::Pointer(PointerEvent {
7038 phase,
7039 position: Point::new(x, y),
7040 button: PointerButton::Primary,
7041 });
7042 self.root.event(&mut self.state, &event)
7043 }
7044
7045 fn move_over(&mut self, leaf: Leaf) {
7046 match leaf {
7047 Leaf::Top => self.dispatch(PointerPhase::Move, 50.0, 15.0),
7048 Leaf::Bottom => self.dispatch(PointerPhase::Move, 50.0, 45.0),
7049 };
7050 }
7051
7052 fn cursor(&self) -> CursorIcon {
7053 self.root.cursor()
7054 }
7055 }
7056
7057 #[test]
7058 fn a_move_resolves_the_requested_cursor_and_absence_resolves_default() {
7059 let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7060 assert_eq!(
7061 h.cursor(),
7062 CursorIcon::Default,
7063 "nothing has asked for anything yet"
7064 );
7065
7066 h.move_over(Leaf::Top);
7067 assert_eq!(
7068 h.cursor(),
7069 CursorIcon::Pointer,
7070 "the request reached the root"
7071 );
7072
7073 // Moving onto the sibling re-resolves to *its* shape with nothing cleared:
7074 // the pass simply has a different last writer.
7075 h.move_over(Leaf::Bottom);
7076 assert_eq!(h.cursor(), CursorIcon::Text);
7077
7078 // Moving off both: the next pass has no writer at all, and absence is the
7079 // default rather than a stale value — the whole point of a stateless
7080 // request.
7081 h.dispatch(PointerPhase::Move, 50.0, 200.0);
7082 assert_eq!(
7083 h.cursor(),
7084 CursorIcon::Default,
7085 "a widget that stops asking falls back with nothing to clear"
7086 );
7087 }
7088
7089 #[test]
7090 fn a_cursor_change_does_not_ask_for_a_repaint() {
7091 // Applying a cursor is a platform call the shell makes after the pass, with
7092 // no frame involved; folding it into `needs_redraw` would repaint the whole
7093 // tree on every hover move.
7094 let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7095 let outcome = h.dispatch(PointerPhase::Move, 50.0, 15.0);
7096 assert_eq!(h.cursor(), CursorIcon::Pointer);
7097 assert!(
7098 !outcome.needs_redraw,
7099 "a cursor request alone never schedules a frame"
7100 );
7101 }
7102
7103 #[test]
7104 fn the_last_writer_on_the_routed_path_wins() {
7105 // A container that asks before routing loses to its child: the child's
7106 // handler runs later in the same pass, which is what makes a specific
7107 // control override the generic surface behind it.
7108 let mut h =
7109 CursorHarness::new(ContainerCursor::BeforeRouting(CursorIcon::ColResize), false);
7110 h.move_over(Leaf::Top);
7111 assert_eq!(
7112 h.cursor(),
7113 CursorIcon::Pointer,
7114 "the innermost widget the route reached spoke last"
7115 );
7116
7117 // ...and the container's own request still resolves where no child asks
7118 // (moving off both leaves leaves the container as the only writer).
7119 h.dispatch(PointerPhase::Move, 50.0, 200.0);
7120 assert_eq!(h.cursor(), CursorIcon::ColResize);
7121
7122 // The deliberate override is the mirror case: asking *after* routing beats
7123 // the child.
7124 let mut h =
7125 CursorHarness::new(ContainerCursor::AfterRouting(CursorIcon::NotAllowed), false);
7126 h.move_over(Leaf::Top);
7127 assert_eq!(
7128 h.cursor(),
7129 CursorIcon::NotAllowed,
7130 "a container overriding its children asks after routing"
7131 );
7132 }
7133
7134 #[test]
7135 fn only_a_pointer_move_re_resolves_the_cursor() {
7136 let mut h = CursorHarness::new(ContainerCursor::Silent, false);
7137 h.move_over(Leaf::Top);
7138 assert_eq!(h.cursor(), CursorIcon::Pointer);
7139
7140 // A press/release says nothing about the cursor, and must not blink it back
7141 // to `Default` for the duration of a click.
7142 h.dispatch(PointerPhase::Down, 50.0, 15.0);
7143 assert_eq!(h.cursor(), CursorIcon::Pointer, "a Down leaves it standing");
7144 h.dispatch(PointerPhase::Up, 50.0, 15.0);
7145 assert_eq!(h.cursor(), CursorIcon::Pointer, "an Up leaves it standing");
7146
7147 // Neither does a scroll, a key, or the housekeeping broadcast.
7148 h.root.event(
7149 &mut h.state,
7150 &InputEvent::Scroll {
7151 position: Point::new(50.0, 15.0),
7152 delta: crate::event::ScrollDelta::Lines(0.0, 1.0),
7153 },
7154 );
7155 assert_eq!(h.cursor(), CursorIcon::Pointer);
7156 h.root.event(&mut h.state, &InputEvent::Housekeeping);
7157 assert_eq!(h.cursor(), CursorIcon::Pointer);
7158 }
7159
7160 #[test]
7161 fn a_captured_drag_keeps_the_capturing_widgets_cursor() {
7162 let mut h = CursorHarness::new(ContainerCursor::Silent, true);
7163 h.move_over(Leaf::Top);
7164 assert_eq!(h.cursor(), CursorIcon::Pointer);
7165
7166 // Press the top leaf: it captures. The `Down` itself resolves nothing.
7167 h.dispatch(PointerPhase::Down, 50.0, 15.0);
7168 assert!(h.root.is_pointer_captured());
7169 assert_eq!(h.cursor(), CursorIcon::Pointer);
7170
7171 // Drag inside, then well outside its own bounds and over the sibling: every
7172 // one of these moves routes to the captured leaf alone, so its drag shape is
7173 // the only request in the pass — the sibling's `Text` never gets a say.
7174 h.dispatch(PointerPhase::Move, 50.0, 16.0);
7175 assert_eq!(h.cursor(), CursorIcon::Grabbing);
7176 h.dispatch(PointerPhase::Move, 50.0, 45.0);
7177 assert_eq!(
7178 h.cursor(),
7179 CursorIcon::Grabbing,
7180 "a captured drag keeps its own cursor outside its bounds"
7181 );
7182 h.dispatch(PointerPhase::Move, 50.0, 500.0);
7183 assert_eq!(h.cursor(), CursorIcon::Grabbing);
7184
7185 // Release, then a fresh uncaptured move: the ordinary hit-tested resolution
7186 // is back, and the drag shape is gone with nothing cleared.
7187 h.dispatch(PointerPhase::Up, 50.0, 45.0);
7188 h.move_over(Leaf::Bottom);
7189 assert_eq!(h.cursor(), CursorIcon::Text);
7190 }
7191
7192 use crate::event::{EditCommand, Key, KeyEvent, Modifiers, NamedKey};
7193
7194 // --- Clipboard: the per-pass write / paste-request channels ---------------
7195 //
7196 // The cursor's two siblings, resolved by the same bracket but committed on
7197 // every pass rather than on a pointer `Move` alone, and drained one-shot
7198 // rather than read as a standing level. The fixture is the cursor pair's
7199 // shape with focus in place of hit testing, because a clipboard verb is
7200 // focus-routed: two stacked leaves, a container that can speak before or
7201 // after routing (the last-writer case) or ask for a paste of its own (the
7202 // two-channels-in-one-pass case).
7203
7204 /// How the clipboard fixture's container speaks relative to its children.
7205 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7206 enum ContainerClipboard {
7207 /// Says nothing at all — the ordinary container.
7208 Silent,
7209 /// Writes *before* routing, so whatever the child writes comes later.
7210 WritesBeforeRouting(&'static str),
7211 /// Writes *after* routing, deliberately overriding its child.
7212 WritesAfterRouting(&'static str),
7213 /// Asks for a paste before routing — a toolbar refreshing whether its
7214 /// paste button should be enabled, which is how a write and a request
7215 /// legitimately ride one pass.
7216 AsksForPaste,
7217 }
7218
7219 /// A focusable editable stand-in: claims focus on a `Down`, answers a
7220 /// copy/cut by writing its "selection", and asks for the clipboard when it
7221 /// sees the hardware [`NamedKey::Paste`] key it decoded itself.
7222 struct ClipboardLeaf {
7223 /// What this leaf would copy.
7224 text: &'static str,
7225 /// Every [`EditCommand`] this leaf was handed, in order — the proof of
7226 /// who the focus routing actually reached.
7227 seen: Rc<RefCell<Vec<EditCommand>>>,
7228 }
7229
7230 impl crate::widget::Widget for ClipboardLeaf {
7231 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7232 bc.constrain(Size::new(100.0, 30.0))
7233 }
7234 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7235 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7236 match event {
7237 InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
7238 ctx.request_focus();
7239 EventResult::Handled
7240 }
7241 InputEvent::EditCommand(cmd) => {
7242 self.seen.borrow_mut().push(cmd.clone());
7243 match cmd {
7244 // The widget owns the selection, so it is the only thing
7245 // that can say what "copy" means.
7246 EditCommand::Copy | EditCommand::Cut => {
7247 ctx.write_clipboard(self.text.to_string());
7248 }
7249 // A paste arrives with its text already read by the shell,
7250 // and select-all touches no clipboard at all.
7251 EditCommand::Paste(_) | EditCommand::SelectAll => {}
7252 }
7253 EventResult::Handled
7254 }
7255 // The hardware clipboard key, decoded by the widget rather than by
7256 // the shell: it carries no text, so the widget asks for some.
7257 InputEvent::Key(k) if k.key == Key::Named(NamedKey::Paste) => {
7258 ctx.request_paste();
7259 EventResult::Handled
7260 }
7261 _ => EventResult::Ignored,
7262 }
7263 }
7264 }
7265
7266 /// Two stacked clipboard leaves, routed by focus for a focus-routed event and
7267 /// by hit test for a pointer one, plus the container's own optional request.
7268 struct ClipboardPair {
7269 top: crate::widget::ChildPod,
7270 bottom: crate::widget::ChildPod,
7271 own: ContainerClipboard,
7272 }
7273
7274 impl crate::widget::Widget for ClipboardPair {
7275 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7276 self.top.layout_child(ctx, bc);
7277 self.top.set_origin(Point::ZERO);
7278 self.bottom.layout_child(ctx, bc);
7279 self.bottom.set_origin(Point::new(0.0, 30.0));
7280 bc.max()
7281 }
7282 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7283 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7284 match self.own {
7285 ContainerClipboard::WritesBeforeRouting(text) => {
7286 ctx.write_clipboard(text.to_string())
7287 }
7288 ContainerClipboard::AsksForPaste => ctx.request_paste(),
7289 _ => {}
7290 }
7291 let result = self.route(ctx, event);
7292 if let ContainerClipboard::WritesAfterRouting(text) = self.own {
7293 ctx.write_clipboard(text.to_string());
7294 }
7295 result
7296 }
7297 }
7298
7299 impl ClipboardPair {
7300 /// Focus routing for a focus-routed event, hit testing for the rest —
7301 /// the two branches every real container has.
7302 fn route(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7303 if event.is_focus_routed() {
7304 for pod in [&mut self.top, &mut self.bottom] {
7305 if pod.is_focused() {
7306 return pod.event_child(ctx, event);
7307 }
7308 }
7309 return EventResult::Ignored;
7310 }
7311 let pos = event.position();
7312 let blurs = matches!(
7313 event,
7314 InputEvent::Pointer(p) if p.phase == PointerPhase::Down
7315 );
7316 let mut result = EventResult::Ignored;
7317 let mut hit = false;
7318 for pod in [&mut self.top, &mut self.bottom] {
7319 if !hit && pod.contains(pos) {
7320 hit = true;
7321 result = pod.event_child(ctx, event);
7322 } else if blurs {
7323 // A `Down` that lands elsewhere blurs the chain, exactly as
7324 // `frust-widgets`' routers do.
7325 pod.set_focused(false);
7326 }
7327 }
7328 result
7329 }
7330 }
7331
7332 /// The fixture's view. It carries the two `seen` logs rather than letting the
7333 /// harness reach into the built tree for them: `Widget` has no downcast, and
7334 /// a shared handle is the same way the hover fixture's probe is watched.
7335 #[derive(Clone)]
7336 struct ClipboardPairView {
7337 own: ContainerClipboard,
7338 top_seen: Rc<RefCell<Vec<EditCommand>>>,
7339 bottom_seen: Rc<RefCell<Vec<EditCommand>>>,
7340 }
7341
7342 impl View<()> for ClipboardPairView {
7343 type Element = ClipboardPair;
7344 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ClipboardPair {
7345 ClipboardPair {
7346 top: crate::widget::ChildPod::new(Box::new(ClipboardLeaf {
7347 text: "top selection",
7348 seen: self.top_seen.clone(),
7349 })),
7350 bottom: crate::widget::ChildPod::new(Box::new(ClipboardLeaf {
7351 text: "bottom selection",
7352 seen: self.bottom_seen.clone(),
7353 })),
7354 own: self.own,
7355 }
7356 }
7357 fn rebuild(&self, _p: &Self, _e: &mut ClipboardPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
7358 ChangeFlags::NONE
7359 }
7360 }
7361
7362 /// A `RenderRoot` over the clipboard fixture, plus the two leaves' `seen`
7363 /// logs (cloned out of the built widgets, which the root owns).
7364 struct ClipboardHarness {
7365 root: RenderRoot<(), ClipboardPairView>,
7366 state: (),
7367 top_seen: Rc<RefCell<Vec<EditCommand>>>,
7368 bottom_seen: Rc<RefCell<Vec<EditCommand>>>,
7369 }
7370
7371 impl ClipboardHarness {
7372 fn new(own: ContainerClipboard) -> Self {
7373 let mut root: RenderRoot<(), ClipboardPairView> = RenderRoot::new();
7374 let mut state = ();
7375 let view = ClipboardPairView {
7376 own,
7377 top_seen: Rc::new(RefCell::new(Vec::new())),
7378 bottom_seen: Rc::new(RefCell::new(Vec::new())),
7379 };
7380 let (top_seen, bottom_seen) = (view.top_seen.clone(), view.bottom_seen.clone());
7381 root.rebuild(&mut move |_: &mut ()| view.clone(), &mut state);
7382 root.layout(Size::new(100.0, 60.0));
7383 ClipboardHarness {
7384 root,
7385 state,
7386 top_seen,
7387 bottom_seen,
7388 }
7389 }
7390
7391 /// Focus a leaf the way a user does: a `Down` inside its bounds.
7392 fn focus(&mut self, leaf: Leaf) {
7393 let y = match leaf {
7394 Leaf::Top => 15.0,
7395 Leaf::Bottom => 45.0,
7396 };
7397 self.root
7398 .event(&mut self.state, &pointer(PointerPhase::Down, 50.0, y));
7399 }
7400
7401 fn dispatch(&mut self, event: &InputEvent) -> EventOutcome {
7402 self.root.event(&mut self.state, event)
7403 }
7404 }
7405
7406 #[test]
7407 fn a_copy_reaches_the_focused_leaf_alone_and_its_text_drains_once() {
7408 let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7409 h.focus(Leaf::Top);
7410 assert!(
7411 h.root.take_clipboard_write().is_none(),
7412 "a focusing tap writes nothing"
7413 );
7414
7415 h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7416 assert_eq!(
7417 h.top_seen.borrow().as_slice(),
7418 &[EditCommand::Copy],
7419 "the focused leaf received the command"
7420 );
7421 assert!(
7422 h.bottom_seen.borrow().is_empty(),
7423 "and the unfocused sibling never saw it — focus routing, not hit testing"
7424 );
7425 assert_eq!(
7426 h.root.take_clipboard_write().as_deref(),
7427 Some("top selection")
7428 );
7429 assert_eq!(
7430 h.root.take_clipboard_write(),
7431 None,
7432 "the drain is one-shot: a clipboard write is an edge, not a level"
7433 );
7434 }
7435
7436 #[test]
7437 fn a_copy_follows_the_focus_when_it_moves() {
7438 let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7439 h.focus(Leaf::Top);
7440 h.focus(Leaf::Bottom);
7441 h.dispatch(&InputEvent::EditCommand(EditCommand::Cut));
7442 assert!(
7443 h.top_seen.borrow().is_empty(),
7444 "the blurred leaf is out of the routed path"
7445 );
7446 assert_eq!(h.bottom_seen.borrow().as_slice(), &[EditCommand::Cut]);
7447 assert_eq!(
7448 h.root.take_clipboard_write().as_deref(),
7449 Some("bottom selection")
7450 );
7451 }
7452
7453 #[test]
7454 fn a_paste_request_drains_once_and_its_answer_is_an_ordinary_dispatch() {
7455 let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7456 h.focus(Leaf::Top);
7457 assert!(
7458 !h.root.take_paste_request(),
7459 "a focusing tap asks for nothing"
7460 );
7461
7462 // The widget decoded the hardware Paste key itself and asked the shell.
7463 h.dispatch(&InputEvent::Key(KeyEvent {
7464 key: Key::Named(NamedKey::Paste),
7465 modifiers: Modifiers::default(),
7466 repeat: false,
7467 }));
7468 assert!(h.root.take_paste_request());
7469 assert!(
7470 !h.root.take_paste_request(),
7471 "the flag is one-shot, so a shell answers a request once"
7472 );
7473
7474 // The shell's answer is a new, focus-routed dispatch carrying the text.
7475 h.dispatch(&InputEvent::EditCommand(EditCommand::Paste(
7476 "from the host".to_string(),
7477 )));
7478 assert_eq!(
7479 h.top_seen.borrow().as_slice(),
7480 &[EditCommand::Paste("from the host".to_string())]
7481 );
7482 assert!(
7483 !h.root.take_paste_request(),
7484 "answering a request does not raise a new one"
7485 );
7486 assert!(
7487 h.root.take_clipboard_write().is_none(),
7488 "and a paste writes nothing back to the host clipboard"
7489 );
7490 }
7491
7492 #[test]
7493 fn a_paste_answered_after_a_blur_reaches_nobody() {
7494 let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7495 h.focus(Leaf::Top);
7496 h.dispatch(&InputEvent::Key(KeyEvent {
7497 key: Key::Named(NamedKey::Paste),
7498 modifiers: Modifiers::default(),
7499 repeat: false,
7500 }));
7501 assert!(h.root.take_paste_request());
7502
7503 // Focus is released while the shell's clipboard read is in flight: a
7504 // `Down` on empty chrome past both leaves blurs the chain.
7505 h.dispatch(&pointer(PointerPhase::Down, 50.0, 100.0));
7506 assert!(!h.root.is_focus_active(), "the tap blurred the field");
7507
7508 // The shell answers anyway — it never has to track who asked, because the
7509 // answer is focus-routed and simply reaches no widget.
7510 h.dispatch(&InputEvent::EditCommand(EditCommand::Paste(
7511 "from the host".to_string(),
7512 )));
7513 assert!(h.top_seen.borrow().is_empty());
7514 assert!(h.bottom_seen.borrow().is_empty());
7515 }
7516
7517 #[test]
7518 fn the_last_clipboard_write_of_a_pass_wins_at_the_root() {
7519 // A container that writes BEFORE routing yields to its child, exactly as
7520 // `set_cursor` does: the innermost widget the route reaches speaks last.
7521 let mut h = ClipboardHarness::new(ContainerClipboard::WritesBeforeRouting("container"));
7522 h.focus(Leaf::Top);
7523 h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7524 assert_eq!(
7525 h.root.take_clipboard_write().as_deref(),
7526 Some("top selection")
7527 );
7528
7529 // A container that writes AFTER routing deliberately overrides it.
7530 let mut h = ClipboardHarness::new(ContainerClipboard::WritesAfterRouting("container"));
7531 h.focus(Leaf::Top);
7532 h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7533 assert_eq!(h.root.take_clipboard_write().as_deref(), Some("container"));
7534 }
7535
7536 #[test]
7537 fn a_write_and_a_paste_request_ride_one_pass_independently() {
7538 let mut h = ClipboardHarness::new(ContainerClipboard::AsksForPaste);
7539 h.focus(Leaf::Top);
7540 // The focusing tap already carried the container's ask; drain it so the
7541 // assertion below is about the copy pass alone.
7542 assert!(h.root.take_paste_request());
7543
7544 h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7545 assert_eq!(
7546 h.root.take_clipboard_write().as_deref(),
7547 Some("top selection"),
7548 "the leaf's write landed"
7549 );
7550 assert!(
7551 h.root.take_paste_request(),
7552 "and the container's request landed in the same pass"
7553 );
7554 }
7555
7556 #[test]
7557 fn a_pass_that_asks_for_neither_leaves_both_empty() {
7558 let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7559 h.focus(Leaf::Top);
7560 // A select-all is a real clipboard verb that touches no clipboard, and a
7561 // pointer move touches nothing at all.
7562 h.dispatch(&InputEvent::EditCommand(EditCommand::SelectAll));
7563 h.dispatch(&pointer(PointerPhase::Move, 50.0, 15.0));
7564 assert_eq!(h.top_seen.borrow().as_slice(), &[EditCommand::SelectAll]);
7565 assert_eq!(h.root.take_clipboard_write(), None);
7566 assert!(!h.root.take_paste_request());
7567 }
7568
7569 #[test]
7570 fn an_edit_command_moves_focus_only_when_the_dispatch_asks() {
7571 // The bookkeeping arm `EditCommand` shares with `Key`/`Ime`: unlike a
7572 // `Down`, it neither claims nor blurs by itself.
7573 let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
7574 h.focus(Leaf::Top);
7575 assert!(h.root.is_focus_active());
7576 let gen_before = h.root.focus_ime_generation();
7577 h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
7578 assert!(
7579 h.root.is_focus_active(),
7580 "a clipboard verb leaves the focus session exactly as it found it"
7581 );
7582 assert_eq!(h.root.focus_ime_generation(), gen_before);
7583 }
7584
7585 // ---------------------------------------------------------------------
7586 // Overlay portal: an owner that floats a pod, a sibling painted after it,
7587 // and a root that routes like any hand-written container.
7588 // ---------------------------------------------------------------------
7589
7590 /// What the owner does when a `Down` arrives inside one of its surfaces.
7591 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7592 enum OwnerReaction {
7593 /// Nothing — the commonest shape (a menu item acts on `Up`).
7594 Nothing,
7595 /// Capture the pointer, the drag-from-inside-a-surface shape.
7596 Capture,
7597 /// Claim focus, the text-field-inside-a-popover shape.
7598 Focus,
7599 }
7600
7601 /// One floated surface the fixture's owner registers.
7602 #[derive(Clone, Debug, PartialEq)]
7603 struct SurfaceSpec {
7604 key: OverlayKey,
7605 label: &'static str,
7606 rect: Rect,
7607 band: crate::overlay::OverlayBand,
7608 input: OverlayInput,
7609 outside_tap: OutsideTap,
7610 /// Whether the owner registers it at all this frame — the fixture for
7611 /// "stop registering and the surface stops existing".
7612 register: bool,
7613 on_down: OwnerReaction,
7614 /// The same, for a `Move` inside the surface — a drag threshold latching
7615 /// a capture mid-gesture, the shape that claims one on no `Down` at all.
7616 on_move: OwnerReaction,
7617 /// The same again, for an `Up` — the phase on which a capture claim has
7618 /// nothing left to own, and the one that reaches the surface as an
7619 /// overlay event precisely because no capture was standing to divert it.
7620 on_up: OwnerReaction,
7621 /// Whether the leaf INSIDE the pod claims focus on a `Down` of its own —
7622 /// the editable-in-a-popover shape, distinct from `on_down`'s claim,
7623 /// which the owner makes on the surface's behalf.
7624 pod_claims_focus: bool,
7625 /// Whether that leaf republishes an active IME surface on every paint.
7626 /// Stated unconditionally, so the fixture can publish from a pod holding
7627 /// no focus link at all — the provenance case.
7628 pod_publishes_ime: bool,
7629 /// The same publish, from the leaf's `event` handler instead of its
7630 /// paint — the event-route half of the provenance case, and equally
7631 /// ungated.
7632 pod_publishes_ime_on_event: bool,
7633 }
7634
7635 impl SurfaceSpec {
7636 fn floating(label: &'static str, rect: Rect) -> Self {
7637 Self {
7638 key: OverlayKey::next(),
7639 label,
7640 rect,
7641 band: crate::overlay::OverlayBand::Floating,
7642 input: OverlayInput::Interactive,
7643 outside_tap: OutsideTap::Ignore,
7644 register: true,
7645 on_down: OwnerReaction::Nothing,
7646 on_move: OwnerReaction::Nothing,
7647 on_up: OwnerReaction::Nothing,
7648 pod_claims_focus: false,
7649 pod_publishes_ime: false,
7650 pod_publishes_ime_on_event: false,
7651 }
7652 }
7653 }
7654
7655 /// Everything the overlay fixture recorded, shared between the widgets and
7656 /// the test.
7657 #[derive(Default)]
7658 struct OverlayLog {
7659 /// Events the owner received, in the order they arrived.
7660 owner: Vec<InputEvent>,
7661 /// `(surface label, event)` for everything a floated pod's content saw —
7662 /// positions here are the pod's own local space.
7663 pod: Vec<(&'static str, InputEvent)>,
7664 /// Events the main-tree sibling received.
7665 sibling: Vec<InputEvent>,
7666 /// What the sibling read from `EventCtx::has_focus` on each event.
7667 sibling_focus: Vec<bool>,
7668 /// What the pod's leaf read from `PaintCtx::has_focus` on each paint.
7669 pod_paint_focus: Vec<bool>,
7670 /// The same read for the main-tree sibling. The two together answer
7671 /// "which branches believe they are focused".
7672 sibling_paint_focus: Vec<bool>,
7673 /// The window size the owner observed in its own (child) layout.
7674 child_window_size: Option<Size>,
7675 }
7676
7677 type Log = std::rc::Rc<std::cell::RefCell<OverlayLog>>;
7678
7679 /// The leaf inside a floated pod: paints its label at the pod's absolute
7680 /// origin and records what reaches it.
7681 ///
7682 /// Optionally an editable — it claims focus on a `Down` of its own and
7683 /// republishes an IME surface on every paint. The publish is deliberately
7684 /// ungated: a pod describing a session it holds no focus link for is
7685 /// precisely what the root has to answer for.
7686 struct OverlayContentWidget {
7687 label: &'static str,
7688 log: Log,
7689 claims_focus: bool,
7690 publishes_ime: bool,
7691 publishes_ime_on_event: bool,
7692 }
7693
7694 impl OverlayContentWidget {
7695 /// What such a pod publishes: an ordinary field's surface, carrying no
7696 /// secure-text configuration of its own.
7697 fn surface() -> ImeState {
7698 ImeState {
7699 active: true,
7700 content_type: ImeContentType::Normal,
7701 ..Default::default()
7702 }
7703 }
7704 }
7705
7706 impl crate::widget::Widget for OverlayContentWidget {
7707 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7708 bc.constrain(Size::new(80.0, 30.0))
7709 }
7710 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
7711 scene.draw_text(ctx.origin(), self.label);
7712 self.log.borrow_mut().pod_paint_focus.push(ctx.has_focus());
7713 if self.publishes_ime {
7714 ctx.publish_ime_state(Self::surface());
7715 }
7716 }
7717 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7718 self.log.borrow_mut().pod.push((self.label, event.clone()));
7719 if self.publishes_ime_on_event {
7720 ctx.publish_ime_state(Self::surface());
7721 }
7722 if self.claims_focus
7723 && let InputEvent::Pointer(pointer) = event
7724 && pointer.phase == PointerPhase::Down
7725 {
7726 ctx.request_focus();
7727 }
7728 EventResult::Handled
7729 }
7730 }
7731
7732 /// One live surface: its spec plus the pod the owner keeps.
7733 struct OwnedSurface {
7734 spec: SurfaceSpec,
7735 pod: crate::overlay::OverlayPod,
7736 }
7737
7738 /// The overlay owner: hosts the pods, registers them from `paint`, and
7739 /// forwards the broadcasts the root routes back to it into the right pod.
7740 struct OverlayOwnerWidget {
7741 surfaces: Vec<OwnedSurface>,
7742 log: Log,
7743 }
7744
7745 impl OverlayOwnerWidget {
7746 fn new(specs: &[SurfaceSpec], log: Log) -> Self {
7747 let mut owner = Self {
7748 surfaces: Vec::new(),
7749 log,
7750 };
7751 owner.sync(specs);
7752 owner
7753 }
7754
7755 /// Reconcile the live surfaces against `specs`, keeping each pod alive
7756 /// across a rebuild (a real owner keeps its popover's widget state).
7757 fn sync(&mut self, specs: &[SurfaceSpec]) {
7758 self.surfaces
7759 .retain(|s| specs.iter().any(|n| n.key == s.spec.key));
7760 for spec in specs {
7761 match self.surfaces.iter_mut().find(|s| s.spec.key == spec.key) {
7762 Some(live) => live.spec = spec.clone(),
7763 None => {
7764 let log = std::rc::Rc::clone(&self.log);
7765 self.surfaces.push(OwnedSurface {
7766 spec: spec.clone(),
7767 pod: std::rc::Rc::new(std::cell::RefCell::new(
7768 crate::widget::ChildPod::new(Box::new(OverlayContentWidget {
7769 label: spec.label,
7770 log,
7771 claims_focus: spec.pod_claims_focus,
7772 publishes_ime: spec.pod_publishes_ime,
7773 publishes_ime_on_event: spec.pod_publishes_ime_on_event,
7774 })),
7775 )),
7776 });
7777 }
7778 }
7779 }
7780 }
7781 }
7782
7783 impl crate::widget::Widget for OverlayOwnerWidget {
7784 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7785 // The window size reaches a CHILD's layout unchanged — this widget is
7786 // a `ChildPod` of the fixture's root.
7787 self.log.borrow_mut().child_window_size = Some(ctx.window_size());
7788 // A floated pod is laid out against the WINDOW, never against the
7789 // owner's own constraints: it escapes the owner's box entirely.
7790 let window = BoxConstraints::loose(ctx.window_size());
7791 for surface in &mut self.surfaces {
7792 surface.pod.borrow_mut().layout_child(ctx, &window);
7793 }
7794 bc.constrain(Size::new(40.0, 20.0))
7795 }
7796
7797 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
7798 scene.draw_text(ctx.origin(), "owner");
7799 for surface in &self.surfaces {
7800 if !surface.spec.register {
7801 continue;
7802 }
7803 // Registered, never painted here: the root paints it last.
7804 ctx.register_overlay(OverlayEntry {
7805 key: surface.spec.key,
7806 band: surface.spec.band,
7807 input: surface.spec.input,
7808 outside_tap: surface.spec.outside_tap,
7809 window_rect: surface.spec.rect,
7810 pod: std::rc::Rc::clone(&surface.pod),
7811 insets: ctx.window_insets(),
7812 });
7813 }
7814 }
7815
7816 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7817 let InputEvent::Overlay(overlay) = event else {
7818 self.log.borrow_mut().owner.push(event.clone());
7819 return EventResult::Ignored;
7820 };
7821 let Some(surface) = self.surfaces.iter_mut().find(|s| s.spec.key == overlay.key) else {
7822 // Another owner's surface: the broadcast reached us, and we
7823 // ignore it. This fall-through is the whole addressing rule.
7824 return EventResult::Ignored;
7825 };
7826 self.log.borrow_mut().owner.push(event.clone());
7827 match &overlay.kind {
7828 OverlayEventKind::Pointer(pointer) => {
7829 let reaction = match pointer.phase {
7830 PointerPhase::Down => Some(surface.spec.on_down),
7831 PointerPhase::Move => Some(surface.spec.on_move),
7832 PointerPhase::Up => Some(surface.spec.on_up),
7833 _ => None,
7834 };
7835 match reaction {
7836 Some(OwnerReaction::Capture) => ctx.capture_pointer(),
7837 Some(OwnerReaction::Focus) => ctx.request_focus(),
7838 Some(OwnerReaction::Nothing) | None => {}
7839 }
7840 // Window space → the pod's own space is one subtraction: the
7841 // registered rect's origin.
7842 let local = InputEvent::Pointer(PointerEvent {
7843 position: pointer.position - surface.spec.rect.origin().to_vec2(),
7844 ..*pointer
7845 });
7846 surface.pod.borrow_mut().event_child(ctx, &local);
7847 }
7848 OverlayEventKind::Scroll { position, delta } => {
7849 let local = InputEvent::Scroll {
7850 position: *position - surface.spec.rect.origin().to_vec2(),
7851 delta: *delta,
7852 };
7853 surface.pod.borrow_mut().event_child(ctx, &local);
7854 }
7855 OverlayEventKind::Scale {
7856 focal,
7857 phase,
7858 scale_delta,
7859 velocity,
7860 } => {
7861 let local = InputEvent::Scale(crate::event::ScaleEvent {
7862 phase: *phase,
7863 scale_delta: *scale_delta,
7864 focal: *focal - surface.spec.rect.origin().to_vec2(),
7865 velocity: *velocity,
7866 });
7867 surface.pod.borrow_mut().event_child(ctx, &local);
7868 }
7869 OverlayEventKind::OutsideDown => {}
7870 }
7871 // A broadcast is never consumed, whatever the pod returned.
7872 EventResult::Ignored
7873 }
7874 }
7875
7876 /// The main-tree sibling: painted AFTER the owner, claims focus on a `Down`
7877 /// inside it, and records what it sees.
7878 ///
7879 /// It stands in for a secure text field: the surface it publishes carries
7880 /// [`ImeContentType::Password`], and it republishes that surface on every
7881 /// paint for as long as the pass seeds it focused — the shipped field's
7882 /// republish-and-self-correct shape, in miniature.
7883 struct SiblingLeafWidget {
7884 log: Log,
7885 /// The selection-toolbar request to publish each paint, if any.
7886 publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
7887 }
7888
7889 impl SiblingLeafWidget {
7890 /// The secure surface this field describes its session with.
7891 fn surface() -> ImeState {
7892 ImeState {
7893 active: true,
7894 content_type: ImeContentType::Password,
7895 ..Default::default()
7896 }
7897 }
7898 }
7899
7900 impl crate::widget::Widget for SiblingLeafWidget {
7901 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7902 bc.constrain(Size::new(60.0, 40.0))
7903 }
7904 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
7905 scene.draw_text(ctx.origin(), "sibling");
7906 self.log
7907 .borrow_mut()
7908 .sibling_paint_focus
7909 .push(ctx.has_focus());
7910 if ctx.has_focus() {
7911 ctx.publish_ime_state(Self::surface());
7912 }
7913 if let Some(request) = self.publish {
7914 ctx.publish_selection_toolbar(request);
7915 }
7916 }
7917 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7918 {
7919 let mut log = self.log.borrow_mut();
7920 log.sibling.push(event.clone());
7921 log.sibling_focus.push(ctx.has_focus());
7922 }
7923 match event {
7924 InputEvent::Pointer(pointer) if pointer.phase == PointerPhase::Down => {
7925 ctx.request_focus();
7926 ctx.publish_ime_state(Self::surface());
7927 EventResult::Handled
7928 }
7929 // A scroll is hit-tested like a press but travels the root's
7930 // keyboard-class focus arm (`Scroll | Key | Ime | EditCommand`),
7931 // so claiming focus here is how the fixture expresses a focus
7932 // move that is not a pointer `Down`.
7933 InputEvent::Scroll { .. } => {
7934 ctx.request_focus();
7935 ctx.publish_ime_state(Self::surface());
7936 EventResult::Handled
7937 }
7938 _ => EventResult::Ignored,
7939 }
7940 }
7941 }
7942
7943 /// The fixture's root: a hand-written two-child container routing exactly
7944 /// like `frust-widgets`' helpers — broadcast first, then capture, then focus,
7945 /// then a topmost-first hit test.
7946 struct OverlayRootWidget {
7947 owner: crate::widget::ChildPod,
7948 sibling: crate::widget::ChildPod,
7949 }
7950
7951 impl crate::widget::Widget for OverlayRootWidget {
7952 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7953 self.owner.layout_child(ctx, bc);
7954 self.owner.set_origin(Point::new(0.0, 0.0));
7955 self.sibling.layout_child(ctx, bc);
7956 self.sibling.set_origin(Point::new(0.0, 100.0));
7957 bc.constrain(Size::new(200.0, 200.0))
7958 }
7959
7960 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
7961 // The owner paints FIRST, the sibling after it — so an overlay pod
7962 // landing after both proves the root's post-pass really is last.
7963 self.owner.paint_child(ctx, scene);
7964 self.sibling.paint_child(ctx, scene);
7965 }
7966
7967 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7968 if event.is_broadcast() {
7969 self.owner.event_child(ctx, event);
7970 self.sibling.event_child(ctx, event);
7971 return EventResult::Ignored;
7972 }
7973 let ends = matches!(
7974 event,
7975 InputEvent::Pointer(p)
7976 if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
7977 );
7978 for (pod, _) in [(&mut self.owner, 0), (&mut self.sibling, 1)] {
7979 if pod.is_active() {
7980 let result = pod.event_child(ctx, event);
7981 if ends {
7982 pod.set_active(false);
7983 }
7984 return result;
7985 }
7986 }
7987 if event.is_focus_routed() {
7988 // `holds_live_focus`, not `is_focused` — the same read
7989 // `frust-widgets`' `route_event` makes, and the reason this
7990 // fixture can stand in for it: both children can carry a
7991 // recorded link at once, and only one of them can carry the live
7992 // session's.
7993 if self.sibling.holds_live_focus() {
7994 return self.sibling.event_child(ctx, event);
7995 }
7996 if self.owner.holds_live_focus() {
7997 return self.owner.event_child(ctx, event);
7998 }
7999 return EventResult::Ignored;
8000 }
8001 // Topmost-first: the sibling paints last, so it hit-tests first.
8002 let position = event.position();
8003 if self.sibling.contains(position) {
8004 return self.sibling.event_child(ctx, event);
8005 }
8006 if self.owner.contains(position) {
8007 return self.owner.event_child(ctx, event);
8008 }
8009 EventResult::Ignored
8010 }
8011 }
8012
8013 /// The view producing the fixture, reconciling the surface specs in place.
8014 struct OverlayRootView {
8015 specs: Vec<SurfaceSpec>,
8016 publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
8017 log: Log,
8018 }
8019
8020 impl View<OverlayState> for OverlayRootView {
8021 type Element = OverlayRootWidget;
8022
8023 fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
8024 OverlayRootWidget {
8025 owner: crate::widget::ChildPod::new(Box::new(OverlayOwnerWidget::new(
8026 &self.specs,
8027 std::rc::Rc::clone(&self.log),
8028 ))),
8029 sibling: crate::widget::ChildPod::new(Box::new(SiblingLeafWidget {
8030 log: std::rc::Rc::clone(&self.log),
8031 publish: self.publish,
8032 })),
8033 }
8034 }
8035
8036 fn rebuild(
8037 &self,
8038 prev: &Self,
8039 element: &mut Self::Element,
8040 _ctx: &mut BuildCtx<'_>,
8041 ) -> ChangeFlags {
8042 element
8043 .owner
8044 .widget_mut()
8045 .downcast_mut::<OverlayOwnerWidget>()
8046 .expect("the owner keeps its type")
8047 .sync(&self.specs);
8048 element
8049 .sibling
8050 .widget_mut()
8051 .downcast_mut::<SiblingLeafWidget>()
8052 .expect("the sibling keeps its type")
8053 .publish = self.publish;
8054 if prev.specs != self.specs || prev.publish != self.publish {
8055 ChangeFlags::PAINT
8056 } else {
8057 ChangeFlags::NONE
8058 }
8059 }
8060 }
8061
8062 /// App state for the overlay fixture: the surface specs the next rebuild
8063 /// applies, plus the shared log.
8064 struct OverlayState {
8065 specs: Vec<SurfaceSpec>,
8066 publish: Option<crate::selection_toolbar::SelectionToolbarRequest>,
8067 log: Log,
8068 }
8069
8070 fn overlay_logic(state: &mut OverlayState) -> OverlayRootView {
8071 OverlayRootView {
8072 specs: state.specs.clone(),
8073 publish: state.publish,
8074 log: std::rc::Rc::clone(&state.log),
8075 }
8076 }
8077
8078 /// Drives the overlay fixture the way a shell does: rebuild, layout, paint,
8079 /// then dispatch.
8080 struct OverlayHarness {
8081 root: RenderRoot<OverlayState, OverlayRootView>,
8082 state: OverlayState,
8083 log: Log,
8084 }
8085
8086 impl OverlayHarness {
8087 fn new(specs: Vec<SurfaceSpec>) -> Self {
8088 let log: Log = std::rc::Rc::new(std::cell::RefCell::new(OverlayLog::default()));
8089 let mut harness = Self {
8090 root: RenderRoot::new(),
8091 state: OverlayState {
8092 specs,
8093 publish: None,
8094 log: std::rc::Rc::clone(&log),
8095 },
8096 log,
8097 };
8098 harness.frame();
8099 harness
8100 }
8101
8102 /// One full frame: rebuild, layout at a 200x200 window, paint.
8103 fn frame(&mut self) -> RecordingScene {
8104 self.root.rebuild(&mut overlay_logic, &mut self.state);
8105 self.root.layout(Size::new(200.0, 200.0));
8106 let mut scene = RecordingScene::default();
8107 self.root.paint(&mut scene, FrameTime::ZERO);
8108 scene
8109 }
8110
8111 fn dispatch(&mut self, event: &InputEvent) -> EventOutcome {
8112 self.root.event(&mut self.state, event)
8113 }
8114
8115 fn down(&mut self, x: f64, y: f64) -> EventOutcome {
8116 self.dispatch(&pointer(PointerPhase::Down, x, y))
8117 }
8118
8119 fn clear_log(&mut self) {
8120 let mut log = self.log.borrow_mut();
8121 log.owner.clear();
8122 log.pod.clear();
8123 log.sibling.clear();
8124 log.sibling_focus.clear();
8125 log.pod_paint_focus.clear();
8126 log.sibling_paint_focus.clear();
8127 }
8128
8129 /// What the pod's leaf and the main-tree sibling each read from
8130 /// `PaintCtx::has_focus` on the most recent frame — "which branches
8131 /// believe they are focused", read from the branches themselves.
8132 fn branch_focus(&self) -> (bool, bool) {
8133 let log = self.log.borrow();
8134 (
8135 *log.pod_paint_focus
8136 .last()
8137 .expect("the pod painted at least once"),
8138 *log.sibling_paint_focus
8139 .last()
8140 .expect("the sibling painted at least once"),
8141 )
8142 }
8143
8144 /// The overlay events the owner received, as `(key, kind)`.
8145 fn owner_overlays(&self) -> Vec<(OverlayKey, OverlayEventKind)> {
8146 self.log
8147 .borrow()
8148 .owner
8149 .iter()
8150 .filter_map(|event| match event {
8151 InputEvent::Overlay(o) => Some((o.key, o.kind.clone())),
8152 _ => None,
8153 })
8154 .collect()
8155 }
8156
8157 /// Pointer events the sibling saw — the "did the main tree get it?" read.
8158 fn sibling_pointers(&self) -> Vec<PointerEvent> {
8159 self.log
8160 .borrow()
8161 .sibling
8162 .iter()
8163 .filter_map(|event| match event {
8164 InputEvent::Pointer(p) => Some(*p),
8165 _ => None,
8166 })
8167 .collect()
8168 }
8169 }
8170
8171 /// A rect well clear of the sibling (which sits at y >= 100).
8172 fn floating_rect() -> Rect {
8173 Rect::new(120.0, 10.0, 200.0, 60.0)
8174 }
8175
8176 #[test]
8177 fn a_registered_pod_paints_after_a_later_sibling_at_its_window_rect() {
8178 let spec = SurfaceSpec::floating("popover", floating_rect());
8179 let mut h = OverlayHarness::new(vec![spec.clone()]);
8180 let scene = h.frame();
8181
8182 assert_eq!(
8183 scene
8184 .texts
8185 .iter()
8186 .map(|(_, text)| text.as_str())
8187 .collect::<Vec<_>>(),
8188 vec!["owner", "sibling", "popover"],
8189 "the floated pod paints after the owner AND after the sibling painted \
8190 later than the owner — escaping paint order is the whole point"
8191 );
8192 assert_eq!(
8193 scene.texts[2].0,
8194 floating_rect().origin(),
8195 "and it paints at its registered window rect, not at its owner's origin"
8196 );
8197 assert_eq!(h.root.overlay_hits.len(), 1);
8198 assert_eq!(h.root.overlay_hits[0].key, spec.key);
8199 assert_eq!(h.root.overlay_hits[0].window_rect, floating_rect());
8200 }
8201
8202 #[test]
8203 fn two_bands_paint_floating_then_tooltip_whatever_the_registration_order() {
8204 // Registered tooltip-first, so registration order and band order
8205 // disagree: the band must win.
8206 let mut tooltip = SurfaceSpec::floating("tooltip", Rect::new(0.0, 0.0, 40.0, 20.0));
8207 tooltip.band = crate::overlay::OverlayBand::Tooltip;
8208 tooltip.input = OverlayInput::Transparent;
8209 let floating = SurfaceSpec::floating("floating", floating_rect());
8210 let mut h = OverlayHarness::new(vec![tooltip, floating]);
8211 let scene = h.frame();
8212
8213 assert_eq!(
8214 scene
8215 .texts
8216 .iter()
8217 .map(|(_, text)| text.as_str())
8218 .collect::<Vec<_>>(),
8219 vec!["owner", "sibling", "floating", "tooltip"],
8220 "Floating paints below Tooltip regardless of who registered first"
8221 );
8222 }
8223
8224 #[test]
8225 fn the_registry_is_empty_at_the_start_of_every_paint() {
8226 let spec = SurfaceSpec::floating("popover", floating_rect());
8227 let mut h = OverlayHarness::new(vec![spec.clone()]);
8228 h.frame();
8229 assert_eq!(h.root.overlay_hits.len(), 1);
8230
8231 // Paint again with nothing changed: the entry is re-registered, not
8232 // accumulated — an owner registering every frame must not grow the table.
8233 let scene = h.frame();
8234 assert_eq!(h.root.overlay_hits.len(), 1);
8235 assert_eq!(scene.texts.len(), 3);
8236
8237 // The owner stops registering (its popover closed). There is nothing to
8238 // unregister: the next paint simply does not see it.
8239 h.state.specs[0].register = false;
8240 let scene = h.frame();
8241 assert!(
8242 h.root.overlay_hits.is_empty(),
8243 "a surface nobody registers stops existing after the next paint"
8244 );
8245 assert_eq!(
8246 scene
8247 .texts
8248 .iter()
8249 .map(|(_, text)| text.as_str())
8250 .collect::<Vec<_>>(),
8251 vec!["owner", "sibling"],
8252 "and stops painting"
8253 );
8254
8255 // A `Down` inside where it used to be now reaches the main tree.
8256 h.clear_log();
8257 h.down(150.0, 30.0);
8258 assert!(h.owner_overlays().is_empty());
8259 }
8260
8261 #[test]
8262 fn the_window_size_reaches_a_childs_layout() {
8263 let h = OverlayHarness::new(vec![SurfaceSpec::floating("popover", floating_rect())]);
8264 assert_eq!(
8265 h.log.borrow().child_window_size,
8266 Some(Size::new(200.0, 200.0)),
8267 "a child lays out knowing the window, which is what an overlay pod is \
8268 sized against"
8269 );
8270 }
8271
8272 #[test]
8273 fn a_down_inside_a_floating_surface_reaches_only_its_owner_and_never_blurs() {
8274 let spec = SurfaceSpec::floating("popover", floating_rect());
8275 let mut h = OverlayHarness::new(vec![spec.clone()]);
8276
8277 // A field elsewhere in the main tree takes focus first.
8278 h.down(30.0, 120.0);
8279 assert!(
8280 h.root.is_focus_active(),
8281 "the sibling holds a focus session"
8282 );
8283 let ime_before = h.root.ime_state();
8284 assert!(ime_before.is_some());
8285 let focus_gen_before = h.root.focus_ime_generation();
8286 h.clear_log();
8287
8288 // Now press inside the floated surface.
8289 h.down(150.0, 30.0);
8290
8291 assert_eq!(
8292 h.owner_overlays(),
8293 vec![(
8294 spec.key,
8295 OverlayEventKind::Pointer(PointerEvent {
8296 phase: PointerPhase::Down,
8297 position: Point::new(150.0, 30.0),
8298 button: PointerButton::Primary,
8299 })
8300 )],
8301 "the owner is reached by key, with a WINDOW-space payload"
8302 );
8303 assert_eq!(
8304 h.log.borrow().pod,
8305 vec![(
8306 "popover",
8307 InputEvent::Pointer(PointerEvent {
8308 phase: PointerPhase::Down,
8309 // 150-120, 30-10: the owner's one subtraction, the rect origin.
8310 position: Point::new(30.0, 20.0),
8311 button: PointerButton::Primary,
8312 })
8313 )],
8314 "and the owner forwards it into the pod in the pod's own space"
8315 );
8316 assert!(
8317 h.sibling_pointers().is_empty(),
8318 "the main tree never saw the press"
8319 );
8320 assert!(
8321 h.root.is_focus_active(),
8322 "and the press did NOT blur the field the surface belongs to"
8323 );
8324 assert_eq!(
8325 h.root.ime_state(),
8326 ime_before,
8327 "nor disturb its IME surface"
8328 );
8329 assert_eq!(
8330 h.root.focus_ime_generation(),
8331 focus_gen_before,
8332 "so no focus/IME edge fires at the shell either"
8333 );
8334 assert_eq!(
8335 h.log.borrow().sibling_focus.last(),
8336 Some(&true),
8337 "the focused leaf still reads as focused when the broadcast reaches it"
8338 );
8339 }
8340
8341 #[test]
8342 fn a_down_inside_a_transparent_surface_reaches_the_main_tree_normally() {
8343 // The transparent surface covers the sibling exactly.
8344 let mut spec = SurfaceSpec::floating("tooltip", Rect::new(0.0, 100.0, 60.0, 140.0));
8345 spec.input = OverlayInput::Transparent;
8346 spec.outside_tap = OutsideTap::Notify { consume: true };
8347 let mut h = OverlayHarness::new(vec![spec]);
8348 h.frame();
8349 h.clear_log();
8350
8351 h.down(30.0, 120.0);
8352
8353 assert!(
8354 h.owner_overlays().is_empty(),
8355 "a transparent surface is never hit-tested — not even for OutsideDown"
8356 );
8357 assert_eq!(
8358 h.sibling_pointers().len(),
8359 1,
8360 "the pointer passed straight through to the widget underneath"
8361 );
8362 assert!(h.root.is_focus_active(), "which claimed focus as usual");
8363 }
8364
8365 #[test]
8366 fn a_scroll_inside_a_surface_routes_to_its_owner_in_window_space() {
8367 let spec = SurfaceSpec::floating("popover", floating_rect());
8368 let mut h = OverlayHarness::new(vec![spec.clone()]);
8369 h.clear_log();
8370
8371 h.dispatch(&InputEvent::Scroll {
8372 position: Point::new(150.0, 30.0),
8373 delta: ScrollDelta::Lines(0.0, 3.0),
8374 });
8375
8376 assert_eq!(
8377 h.owner_overlays(),
8378 vec![(
8379 spec.key,
8380 OverlayEventKind::Scroll {
8381 position: Point::new(150.0, 30.0),
8382 delta: ScrollDelta::Lines(0.0, 3.0),
8383 }
8384 )]
8385 );
8386 assert_eq!(
8387 h.log.borrow().pod,
8388 vec![(
8389 "popover",
8390 InputEvent::Scroll {
8391 position: Point::new(30.0, 20.0),
8392 delta: ScrollDelta::Lines(0.0, 3.0),
8393 }
8394 )]
8395 );
8396 }
8397
8398 #[test]
8399 fn a_capture_from_inside_a_surface_routes_the_next_move_by_the_capture_path() {
8400 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8401 spec.on_down = OwnerReaction::Capture;
8402 let mut h = OverlayHarness::new(vec![spec.clone()]);
8403 h.clear_log();
8404
8405 h.down(150.0, 30.0);
8406 assert!(
8407 h.root.is_pointer_captured(),
8408 "a capture bubbled from an overlay Down opens a gesture exactly like a \
8409 hit-tested one"
8410 );
8411 h.clear_log();
8412
8413 // Still INSIDE the surface's own rect — which is the case that
8414 // discriminates: the pre-pass would happily hit-test this one and route it
8415 // as another broadcast, and it must not, because the gesture is captured.
8416 h.dispatch(&pointer(PointerPhase::Move, 160.0, 45.0));
8417 assert!(
8418 h.owner_overlays().is_empty(),
8419 "a live capture short-circuits the overlay pre-pass even inside the \
8420 surface's own rect"
8421 );
8422 assert_eq!(
8423 h.log.borrow().owner,
8424 vec![InputEvent::Pointer(PointerEvent {
8425 phase: PointerPhase::Move,
8426 position: Point::new(160.0, 45.0),
8427 button: PointerButton::Primary,
8428 })],
8429 "the move reaches the owner by the ordinary captured path instead"
8430 );
8431 h.clear_log();
8432
8433 // And the drag may wander far outside the surface — over the sibling, in
8434 // fact — without the sibling ever hearing about it.
8435 h.dispatch(&pointer(PointerPhase::Move, 30.0, 120.0));
8436
8437 assert!(h.owner_overlays().is_empty());
8438 assert_eq!(
8439 h.log.borrow().owner,
8440 vec![InputEvent::Pointer(PointerEvent {
8441 phase: PointerPhase::Move,
8442 position: Point::new(30.0, 120.0),
8443 button: PointerButton::Primary,
8444 })],
8445 "which is what lets a drag begun inside a floated surface continue \
8446 outside it"
8447 );
8448 assert!(
8449 h.sibling_pointers().is_empty(),
8450 "and never reaches the widget it passed over"
8451 );
8452
8453 // The `Up` closes the gesture through the ordinary pointer arm.
8454 h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
8455 assert!(!h.root.is_pointer_captured());
8456 }
8457
8458 /// A capture claimed on a phase other than `Down` is a capture all the
8459 /// same. The pods record their own active path on any phase, so a root that
8460 /// mirrored the `Down` alone left the surface latched with no `Up` able to
8461 /// reach it — and a latched surface diverts every later pointer event its
8462 /// owner is reached by.
8463 #[test]
8464 fn a_capture_claimed_on_a_move_inside_a_surface_reaches_the_root() {
8465 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8466 spec.on_move = OwnerReaction::Capture;
8467 let mut h = OverlayHarness::new(vec![spec]);
8468
8469 h.dispatch(&pointer(PointerPhase::Move, 150.0, 30.0));
8470 assert!(
8471 h.root.is_pointer_captured(),
8472 "the root mirrors a claim the surface made mid-gesture"
8473 );
8474
8475 // Which is what ends it: the mirrored capture short-circuits the overlay
8476 // pre-pass, so the `Up` routes down the capture path — outside the
8477 // surface's own rect, where a hit test would never have delivered it.
8478 h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
8479 assert!(
8480 !h.root.is_pointer_captured(),
8481 "and the gesture closes on the ordinary pointer arm"
8482 );
8483 }
8484
8485 /// The release side of the root's overlay capture mirror. The claim is
8486 /// mirrored on any phase, so the release has to be bounded on any phase too:
8487 /// an `Up` reaches a surface as an overlay event exactly when no capture was
8488 /// standing to divert it, and a claim made there has no gesture left to own.
8489 /// A latch left standing short-circuits the overlay pre-pass, which stops
8490 /// every floated surface taking input until some unrelated pointer release
8491 /// happens along.
8492 #[test]
8493 fn a_capture_claimed_on_an_overlay_up_does_not_outlive_the_gesture() {
8494 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8495 spec.on_up = OwnerReaction::Capture;
8496 let mut h = OverlayHarness::new(vec![spec]);
8497
8498 h.down(150.0, 30.0);
8499 h.dispatch(&pointer(PointerPhase::Up, 150.0, 30.0));
8500
8501 assert!(
8502 !h.root.is_pointer_captured(),
8503 "the phase that ends a gesture releases the mirror it just set"
8504 );
8505
8506 // Which is what keeps the surfaces routable: a press inside one still
8507 // reaches its owner rather than being diverted down a capture path.
8508 h.clear_log();
8509 h.down(150.0, 30.0);
8510 assert_eq!(
8511 h.owner_overlays().len(),
8512 1,
8513 "the overlay pre-pass is still hit-testing"
8514 );
8515 }
8516
8517 #[test]
8518 fn a_focus_request_from_inside_a_surface_opens_a_session() {
8519 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8520 spec.on_down = OwnerReaction::Focus;
8521 let mut h = OverlayHarness::new(vec![spec]);
8522 assert!(!h.root.is_focus_active());
8523
8524 h.down(150.0, 30.0);
8525
8526 assert!(
8527 h.root.is_focus_active(),
8528 "a text field inside a popover may claim focus — the overlay arm \
8529 honours the request even though it refuses the blur"
8530 );
8531 }
8532
8533 /// The case the test above cannot express: the claim arrives while ANOTHER
8534 /// branch already holds the session. Honouring it without retiring what it
8535 /// supersedes leaves two branches believing they are focused — and leaves
8536 /// the root describing, to the shell, a field that no longer owns anything.
8537 #[test]
8538 fn a_pod_focus_claim_retires_the_branch_it_supersedes() {
8539 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8540 spec.pod_claims_focus = true;
8541 let mut h = OverlayHarness::new(vec![spec]);
8542
8543 // A secure field in the main tree takes the session first.
8544 h.down(30.0, 120.0);
8545 h.frame();
8546 assert_eq!(
8547 h.branch_focus(),
8548 (false, true),
8549 "only the field's own branch reads focused while it holds the session"
8550 );
8551 assert_eq!(
8552 h.root.ime_state().map(|ime| ime.content_type),
8553 Some(ImeContentType::Password),
8554 "and the surface the shell configures its keyboard from is the \
8555 secure one that field published"
8556 );
8557
8558 // Now the editable inside the floated surface claims focus.
8559 h.down(150.0, 30.0);
8560 assert!(h.root.is_focus_active(), "the claim is honoured");
8561
8562 // The claim stamped its own chain with a session identity nothing older
8563 // carries, so the field's link stops counting on the very next pass —
8564 // no convergence frame, and nothing had to visit the branch the session
8565 // left in order to clear it.
8566 h.frame();
8567 assert_eq!(
8568 h.branch_focus(),
8569 (true, false),
8570 "exactly one branch believes it is focused: the one that claimed it"
8571 );
8572 assert_eq!(
8573 h.root.ime_state(),
8574 None,
8575 "and the surface that field published for the session it just lost \
8576 does NOT stand: the branch that took the session described none of \
8577 its own, so the shell is left configuring nothing rather than a \
8578 secure field nobody is in"
8579 );
8580
8581 h.frame();
8582 assert_eq!(
8583 h.branch_focus(),
8584 (true, false),
8585 "which is a settled state, not a frame of transition"
8586 );
8587 assert!(
8588 h.root.is_focus_active(),
8589 "and the session the pod opened is still the live one"
8590 );
8591 }
8592
8593 /// The return direction, which the test above never exercises: the session
8594 /// goes to a floated surface and the user then presses the field in the main
8595 /// tree again.
8596 ///
8597 /// A press outside every floated rect is hit-tested through the containers
8598 /// like any other, so the claim it produces is the main tree's by
8599 /// construction — and the surface's own recorded link has to stop counting
8600 /// the moment that claim lands, or the tree is de-seeded for the rest of the
8601 /// pod's life and the secure surface the field publishes is thrown away
8602 /// every frame.
8603 #[test]
8604 fn a_hit_tested_claim_takes_the_session_back_from_a_floated_surface() {
8605 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8606 spec.pod_claims_focus = true;
8607 let mut h = OverlayHarness::new(vec![spec]);
8608
8609 // The field takes the session, then the editable inside the surface
8610 // takes it away.
8611 h.down(30.0, 120.0);
8612 h.frame();
8613 h.down(150.0, 30.0);
8614 h.frame();
8615 assert_eq!(
8616 h.branch_focus(),
8617 (true, false),
8618 "the surface holds the session and the field's link no longer counts"
8619 );
8620
8621 // The user presses the field again, outside `floating_rect()`, so the
8622 // press routes through the containers exactly as the first one did.
8623 h.down(30.0, 120.0);
8624 h.frame();
8625
8626 assert_eq!(
8627 h.branch_focus(),
8628 (false, true),
8629 "the session is the field's again and the surface's link is retired"
8630 );
8631 assert_eq!(
8632 h.root.ime_state().map(|ime| ime.content_type),
8633 Some(ImeContentType::Password),
8634 "and the secure surface that field publishes reaches the shell"
8635 );
8636 }
8637
8638 /// The same move, driven from the root's keyboard-class arm
8639 /// (`Scroll | Key | Ime | EditCommand`) rather than a pointer `Down`. That
8640 /// arm honours the claim and nothing else, so a mechanism that only retires
8641 /// a surface's link on a press leaves the session split here.
8642 #[test]
8643 fn a_keyboard_class_claim_in_the_tree_retires_a_surfaces_link() {
8644 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8645 spec.pod_claims_focus = true;
8646 let mut h = OverlayHarness::new(vec![spec]);
8647
8648 h.down(150.0, 30.0);
8649 h.frame();
8650 assert!(h.branch_focus().0, "the surface holds the session");
8651
8652 // A scroll over the field, outside every floated rect: hit-tested into
8653 // the main tree, but routed through the root's non-pointer focus arm.
8654 h.dispatch(&InputEvent::Scroll {
8655 position: Point::new(30.0, 120.0),
8656 delta: ScrollDelta::Lines(0.0, 3.0),
8657 });
8658 h.frame();
8659
8660 assert_eq!(
8661 h.branch_focus(),
8662 (false, true),
8663 "a claim that arrives without a pointer `Down` retires the surface's \
8664 link just the same"
8665 );
8666 assert_eq!(
8667 h.root.ime_state().map(|ime| ime.content_type),
8668 Some(ImeContentType::Password),
8669 );
8670 }
8671
8672 /// A pod dropped while it holds the link takes the session with it. Nothing
8673 /// else can end that session: the widget that owned it no longer exists, so
8674 /// no later pass can reach it to release it, and the root would otherwise go
8675 /// on reporting a live focus session to the shell for a surface nobody can
8676 /// see.
8677 #[test]
8678 fn a_pod_dropped_while_it_holds_the_link_ends_the_session_it_owned() {
8679 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8680 spec.pod_claims_focus = true;
8681 let mut h = OverlayHarness::new(vec![spec]);
8682
8683 h.down(150.0, 30.0);
8684 h.frame();
8685 assert!(h.root.is_focus_active(), "the surface holds the session");
8686
8687 // The owner stops owning the surface: the spec goes, the pod is dropped
8688 // by the next rebuild.
8689 h.state.specs.clear();
8690 h.frame();
8691
8692 assert!(
8693 !h.root.is_focus_active(),
8694 "the session dies with the widget that held it"
8695 );
8696 assert_eq!(h.root.ime_state(), None);
8697
8698 // And the tree is not left de-seeded: the next press focuses normally.
8699 h.down(30.0, 120.0);
8700 h.frame();
8701 assert!(
8702 h.branch_focus().1,
8703 "a field in the tree takes the session as if no surface had existed"
8704 );
8705 assert_eq!(
8706 h.root.ime_state().map(|ime| ime.content_type),
8707 Some(ImeContentType::Password),
8708 );
8709 }
8710
8711 /// The event-route half of the provenance rule. `paint_overlays` refuses a
8712 /// paint-time publish from a pod holding no link; the dispatch that carries
8713 /// an overlay broadcast has to refuse the same publish, or a surface the
8714 /// user merely touched reconfigures the platform keyboard for a secure field
8715 /// it has nothing to do with.
8716 #[test]
8717 fn a_pod_publish_from_an_event_cannot_overwrite_a_field_it_does_not_own() {
8718 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8719 // Publishes from its `event`, and never claims focus.
8720 spec.pod_publishes_ime_on_event = true;
8721 let mut h = OverlayHarness::new(vec![spec]);
8722
8723 h.down(30.0, 120.0);
8724 assert_eq!(
8725 h.root.ime_state().map(|ime| ime.content_type),
8726 Some(ImeContentType::Password),
8727 "the focused field's own surface"
8728 );
8729
8730 // A press inside the surface. The pod publishes, holds no focus link,
8731 // and claims none.
8732 h.down(150.0, 30.0);
8733
8734 assert_eq!(
8735 h.root.ime_state().map(|ime| ime.content_type),
8736 Some(ImeContentType::Password),
8737 "a publish from a branch that owns no session describes nobody's"
8738 );
8739 assert!(
8740 h.root.is_focus_active(),
8741 "and the field it did not own still holds the session"
8742 );
8743 }
8744
8745 /// A pod publishing an IME surface it holds no focus link for describes
8746 /// nobody's session. Accepting it would let a surface overwrite a focused
8747 /// field's — including the content type that configures the platform
8748 /// keyboard as a secure one.
8749 #[test]
8750 fn a_pod_publish_cannot_overwrite_the_surface_of_a_field_it_does_not_own() {
8751 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8752 // Publishes on every paint, and never claims focus.
8753 spec.pod_publishes_ime = true;
8754 let mut h = OverlayHarness::new(vec![spec]);
8755
8756 h.down(30.0, 120.0);
8757 assert_eq!(
8758 h.root.ime_state().map(|ime| ime.content_type),
8759 Some(ImeContentType::Password),
8760 "the focused field's own surface"
8761 );
8762
8763 h.frame();
8764
8765 assert!(
8766 h.root.is_focus_active(),
8767 "the field still holds the session"
8768 );
8769 assert_eq!(
8770 h.root.ime_state().map(|ime| ime.content_type),
8771 Some(ImeContentType::Password),
8772 "and it still describes it: the publishing pod holds no focus link, \
8773 so its surface is not the session's"
8774 );
8775 }
8776
8777 /// The provenance rule is a check, not a refusal: a pod that DOES hold the
8778 /// recorded focus path publishes exactly like a field in the tree.
8779 #[test]
8780 fn a_pod_that_holds_the_focus_path_publishes_its_own_surface() {
8781 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8782 spec.pod_claims_focus = true;
8783 spec.pod_publishes_ime = true;
8784 let mut h = OverlayHarness::new(vec![spec]);
8785
8786 h.down(30.0, 120.0);
8787 h.down(150.0, 30.0);
8788 h.frame();
8789
8790 assert!(h.root.is_focus_active());
8791 assert_eq!(
8792 h.root.ime_state().map(|ime| ime.content_type),
8793 Some(ImeContentType::Normal),
8794 "the surface published by the branch that owns the session"
8795 );
8796 }
8797
8798 #[test]
8799 fn an_outside_press_notifies_a_consuming_surface_and_stops_there() {
8800 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8801 spec.outside_tap = OutsideTap::Notify { consume: true };
8802 let mut h = OverlayHarness::new(vec![spec.clone()]);
8803 h.clear_log();
8804
8805 // Inside the sibling, outside every floated rect.
8806 h.down(30.0, 120.0);
8807
8808 assert_eq!(
8809 h.owner_overlays(),
8810 vec![(spec.key, OverlayEventKind::OutsideDown)],
8811 "the light-dismiss notification carries no position"
8812 );
8813 assert!(
8814 h.sibling_pointers().is_empty(),
8815 "and the press that dismissed the menu did not also activate what was \
8816 underneath it"
8817 );
8818 assert!(
8819 !h.root.is_focus_active(),
8820 "the main tree saw no Down at all, so nothing claimed focus"
8821 );
8822 }
8823
8824 #[test]
8825 fn a_pass_through_outside_press_notifies_and_still_reaches_the_main_tree() {
8826 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8827 spec.outside_tap = OutsideTap::Notify { consume: false };
8828 let mut h = OverlayHarness::new(vec![spec.clone()]);
8829 h.clear_log();
8830
8831 let outcome = h.down(30.0, 120.0);
8832
8833 assert_eq!(
8834 h.owner_overlays(),
8835 vec![(spec.key, OverlayEventKind::OutsideDown)]
8836 );
8837 assert_eq!(
8838 h.sibling_pointers().len(),
8839 1,
8840 "consume: false means both — the owner hears, and the press continues"
8841 );
8842 assert!(h.root.is_focus_active(), "so the tapped field took focus");
8843 assert!(
8844 outcome.handled,
8845 "and the main dispatch's own outcome survives"
8846 );
8847 }
8848
8849 #[test]
8850 fn an_ignoring_surface_hears_nothing_about_an_outside_press() {
8851 // `Ignore` is the default; state it explicitly.
8852 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8853 spec.outside_tap = OutsideTap::Ignore;
8854 let mut h = OverlayHarness::new(vec![spec]);
8855 h.clear_log();
8856
8857 h.down(30.0, 120.0);
8858
8859 assert!(
8860 h.owner_overlays().is_empty(),
8861 "a surface that dismisses some other way is never told"
8862 );
8863 assert_eq!(h.sibling_pointers().len(), 1);
8864 }
8865
8866 #[test]
8867 fn only_a_primary_press_dismisses() {
8868 let mut spec = SurfaceSpec::floating("popover", floating_rect());
8869 spec.outside_tap = OutsideTap::Notify { consume: true };
8870 let mut h = OverlayHarness::new(vec![spec]);
8871 h.clear_log();
8872
8873 // A secondary press is a context gesture, not a dismissal.
8874 h.dispatch(&InputEvent::Pointer(PointerEvent {
8875 phase: PointerPhase::Down,
8876 position: Point::new(30.0, 120.0),
8877 button: PointerButton::Secondary,
8878 }));
8879 assert!(h.owner_overlays().is_empty());
8880 assert_eq!(h.sibling_pointers().len(), 1, "and it reaches the tree");
8881
8882 // Nor does a move or a lift outside the surface.
8883 h.clear_log();
8884 h.dispatch(&pointer(PointerPhase::Move, 30.0, 120.0));
8885 h.dispatch(&pointer(PointerPhase::Up, 30.0, 120.0));
8886 assert!(h.owner_overlays().is_empty());
8887 }
8888
8889 #[test]
8890 fn an_overlay_press_leaves_the_main_trees_hover_standing() {
8891 let spec = SurfaceSpec::floating("popover", floating_rect());
8892 let mut h = OverlayHarness::new(vec![spec]);
8893 let epoch_before = h.root.hover_epoch;
8894
8895 h.down(150.0, 30.0);
8896
8897 assert_eq!(
8898 h.root.hover_epoch, epoch_before,
8899 "an overlay pass advances no hover epoch, so a live hover in the main \
8900 tree is not stranded by a press on a floated surface"
8901 );
8902 }
8903
8904 // ---------------------------------------------------------------------
8905 // Selection toolbar: publish, generation, clear.
8906 // ---------------------------------------------------------------------
8907
8908 /// A request with the menu up, anchored at `x` — the shape a field
8909 /// publishes while its bar stands.
8910 fn toolbar_request(x: f64) -> crate::selection_toolbar::SelectionToolbarRequest {
8911 crate::selection_toolbar::SelectionToolbarRequest {
8912 anchor: Rect::new(x, 100.0, x + 50.0, 120.0),
8913 actions: crate::selection_toolbar::SelectionToolbarActions {
8914 copy: true,
8915 cut: true,
8916 paste: false,
8917 select_all: true,
8918 },
8919 present_menu: true,
8920 }
8921 }
8922
8923 /// The same request with no menu wanted — what a focused field publishes
8924 /// with nothing on screen, so the platform can still answer "may I offer
8925 /// Copy?" for a hardware shortcut.
8926 fn toolbar_level(x: f64) -> crate::selection_toolbar::SelectionToolbarRequest {
8927 crate::selection_toolbar::SelectionToolbarRequest {
8928 present_menu: false,
8929 ..toolbar_request(x)
8930 }
8931 }
8932
8933 #[test]
8934 fn a_selection_toolbar_publish_resolves_and_only_a_menu_edge_moves_the_generation() {
8935 let mut h = OverlayHarness::new(vec![]);
8936 // A toolbar describes the FOCUSED field's selection, so open a session
8937 // first — a publish with nothing focused describes nothing.
8938 h.down(30.0, 120.0);
8939 assert!(h.root.is_focus_active());
8940
8941 h.state.publish = Some(toolbar_request(10.0));
8942 h.frame();
8943 assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(10.0)));
8944 let first_gen = h.root.selection_toolbar_generation();
8945 assert!(first_gen > 0, "appearing is an edge");
8946
8947 // The field republishes the same request every frame its selection
8948 // stands: that must not ask the shell to re-present the menu per vsync.
8949 h.frame();
8950 h.frame();
8951 assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(10.0)));
8952 assert_eq!(h.root.selection_toolbar_generation(), first_gen);
8953
8954 // A moved anchor is stored — a shell re-reads it to place a menu it
8955 // already has on screen — but it is NOT a menu edge. This assertion used
8956 // to read `first_gen + 1`: the anchor is recomputed every painted frame
8957 // and a drag that widens a selection moves it on every touch sample, so
8958 // bumping here asked the platform to re-present its menu per sample.
8959 h.state.publish = Some(toolbar_request(60.0));
8960 h.frame();
8961 assert_eq!(h.root.selection_toolbar(), Some(toolbar_request(60.0)));
8962 assert_eq!(
8963 h.root.selection_toolbar_generation(),
8964 first_gen,
8965 "an anchor following the selection is not a reason to re-present"
8966 );
8967
8968 // A verb changing IS: the menu's own contents just changed.
8969 let mut fewer_verbs = toolbar_request(60.0);
8970 fewer_verbs.actions.select_all = false;
8971 h.state.publish = Some(fewer_verbs);
8972 h.frame();
8973 assert_eq!(h.root.selection_toolbar_generation(), first_gen + 1);
8974
8975 // The field blurs: it stops publishing, and the menu goes away with
8976 // nothing retracted.
8977 h.state.publish = None;
8978 h.frame();
8979 assert_eq!(h.root.selection_toolbar(), None);
8980 assert_eq!(
8981 h.root.selection_toolbar_generation(),
8982 first_gen + 2,
8983 "going away is an edge too, or a shell never learns to dismiss"
8984 );
8985
8986 // ...and staying away is not.
8987 h.frame();
8988 assert_eq!(h.root.selection_toolbar_generation(), first_gen + 2);
8989 }
8990
8991 #[test]
8992 fn only_the_menu_flag_going_up_asks_a_shell_to_present() {
8993 let mut h = OverlayHarness::new(vec![]);
8994 h.down(30.0, 120.0);
8995 assert!(h.root.is_focus_active());
8996
8997 // A focused field with no bar up publishes all the same: the verbs are
8998 // the answer a platform responder chain needs for a hardware shortcut
8999 // that arrives with nothing on screen.
9000 h.state.publish = Some(toolbar_level(10.0));
9001 h.frame();
9002 assert_eq!(h.root.selection_toolbar(), Some(toolbar_level(10.0)));
9003 let level_gen = h.root.selection_toolbar_generation();
9004
9005 // Republishing the same level is not an edge, however many frames it
9006 // stands for.
9007 h.frame();
9008 h.frame();
9009 assert_eq!(h.root.selection_toolbar_generation(), level_gen);
9010
9011 // The gesture fires and the field asks for a menu: THAT is the edge.
9012 h.state.publish = Some(toolbar_request(10.0));
9013 h.frame();
9014 assert_eq!(
9015 h.root.selection_toolbar_generation(),
9016 level_gen + 1,
9017 "the flag going false to true is what presents the menu"
9018 );
9019
9020 // And dropping it again is the dismiss edge, with the field still
9021 // focused and still publishing its verbs.
9022 h.state.publish = Some(toolbar_level(10.0));
9023 h.frame();
9024 assert!(h.root.selection_toolbar().is_some());
9025 assert_eq!(h.root.selection_toolbar_generation(), level_gen + 2);
9026 }
9027
9028 #[test]
9029 fn a_pass_that_publishes_nothing_still_moves_the_generation() {
9030 // `RenderRoot::paint` resolves a pass nobody published in to `None`,
9031 // which reads as "no menu, no verbs" and must differ from whatever
9032 // stood — otherwise a shell holding a presented menu never learns to
9033 // put it away.
9034 let mut h = OverlayHarness::new(vec![]);
9035 h.down(30.0, 120.0);
9036 h.state.publish = Some(toolbar_request(10.0));
9037 h.frame();
9038 let standing = h.root.selection_toolbar_generation();
9039
9040 h.state.publish = None;
9041 h.frame();
9042 assert_eq!(h.root.selection_toolbar(), None);
9043 assert_eq!(h.root.selection_toolbar_generation(), standing + 1);
9044 }
9045
9046 #[test]
9047 fn a_blur_clears_the_selection_toolbar_on_the_event_pass() {
9048 let mut h = OverlayHarness::new(vec![]);
9049 h.down(30.0, 120.0);
9050 h.state.publish = Some(toolbar_request(10.0));
9051 h.frame();
9052 assert!(h.root.selection_toolbar().is_some());
9053 let gen_before = h.root.selection_toolbar_generation();
9054
9055 // A press on chrome that claims no focus ends the session — and the menu
9056 // must go with it immediately, not a frame later.
9057 h.down(150.0, 30.0);
9058 assert!(!h.root.is_focus_active());
9059 assert_eq!(
9060 h.root.selection_toolbar(),
9061 None,
9062 "a toolbar cannot outlive the focus session its selection belonged to"
9063 );
9064 assert_eq!(h.root.selection_toolbar_generation(), gen_before + 1);
9065
9066 // And a field still publishing into the blurred session cannot resurrect
9067 // it — the same refusal the IME republish makes.
9068 h.frame();
9069 assert_eq!(h.root.selection_toolbar(), None);
9070 }
9071}
9072
9073/// The root half of the multi-contact contract (`InputEvent::PointerContact`):
9074/// how a contact's id, the claimant latch and the `capture_contacts` opt-in
9075/// decide where an event goes.
9076#[cfg(test)]
9077mod contact_tests {
9078 use super::*;
9079
9080 /// What every probe saw: which probe, which contact, which phase.
9081 #[derive(Default)]
9082 struct Log {
9083 seen: Vec<(char, PointerId, PointerPhase)>,
9084 }
9085
9086 /// A leaf that logs every pointer event it receives with the contact id its
9087 /// context reports, and on a `Down` captures (optionally opting into the
9088 /// gesture's other contacts) and claims focus.
9089 struct Probe {
9090 tag: char,
9091 opt_in: bool,
9092 }
9093 impl crate::widget::Widget for Probe {
9094 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9095 bc.constrain(Size::new(40.0, 20.0))
9096 }
9097 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
9098 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9099 let InputEvent::Pointer(p) = event else {
9100 return EventResult::Ignored;
9101 };
9102 let id = ctx.pointer_id();
9103 ctx.state_mut::<Log>().seen.push((self.tag, id, p.phase));
9104 if p.phase == PointerPhase::Down {
9105 ctx.capture_pointer();
9106 if self.opt_in {
9107 ctx.capture_contacts();
9108 }
9109 ctx.request_focus();
9110 }
9111 EventResult::Handled
9112 }
9113 }
9114
9115 /// Two probes stacked vertically (A at y 0..20, B at y 30..50) behind the
9116 /// standard recorded-path routing: a captured gesture goes straight to the
9117 /// active child and releases it on `Up`/`Cancel`; anything else is
9118 /// hit-tested, and a `Down` that hits nothing blurs both.
9119 struct Pair {
9120 a: crate::widget::ChildPod,
9121 b: crate::widget::ChildPod,
9122 }
9123 impl crate::widget::Widget for Pair {
9124 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9125 self.a.layout_child(ctx, bc);
9126 self.a.set_origin(Point::new(0.0, 0.0));
9127 self.b.layout_child(ctx, bc);
9128 self.b.set_origin(Point::new(0.0, 30.0));
9129 bc.max()
9130 }
9131 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9132 self.a.paint_child(ctx, scene);
9133 self.b.paint_child(ctx, scene);
9134 }
9135 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9136 let releases = matches!(
9137 event,
9138 InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
9139 );
9140 for pod in [&mut self.a, &mut self.b] {
9141 if pod.is_active() {
9142 let result = pod.event_child(ctx, event);
9143 if releases {
9144 pod.set_active(false);
9145 }
9146 return result;
9147 }
9148 }
9149 let pos = event.position();
9150 let down = matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Down);
9151 for pod in [&mut self.a, &mut self.b] {
9152 if pod.contains(pos) {
9153 return pod.event_child(ctx, event);
9154 }
9155 }
9156 if down {
9157 self.a.set_focused(false);
9158 self.b.set_focused(false);
9159 }
9160 EventResult::Ignored
9161 }
9162 }
9163
9164 struct PairView {
9165 opt_in: bool,
9166 }
9167 impl View<Log> for PairView {
9168 type Element = Pair;
9169 fn build(&self, _ctx: &mut BuildCtx<'_>) -> Pair {
9170 Pair {
9171 a: crate::widget::ChildPod::new(Box::new(Probe {
9172 tag: 'A',
9173 opt_in: self.opt_in,
9174 })),
9175 b: crate::widget::ChildPod::new(Box::new(Probe {
9176 tag: 'B',
9177 opt_in: self.opt_in,
9178 })),
9179 }
9180 }
9181 fn rebuild(&self, _p: &Self, _e: &mut Pair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
9182 ChangeFlags::NONE
9183 }
9184 }
9185
9186 fn root_with(opt_in: bool) -> (RenderRoot<Log, PairView>, Log) {
9187 let mut root: RenderRoot<Log, PairView> = RenderRoot::new();
9188 let mut log = Log::default();
9189 root.rebuild(&mut |_| PairView { opt_in }, &mut log);
9190 root.layout(Size::new(200.0, 200.0));
9191 (root, log)
9192 }
9193
9194 fn ev(phase: PointerPhase, x: f64, y: f64) -> PointerEvent {
9195 PointerEvent {
9196 phase,
9197 position: Point::new(x, y),
9198 button: PointerButton::Primary,
9199 }
9200 }
9201
9202 fn mouse(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
9203 InputEvent::Pointer(ev(phase, x, y))
9204 }
9205
9206 fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
9207 InputEvent::PointerContact {
9208 pointer_id: PointerId::touch(slot),
9209 event: ev(phase, x, y),
9210 }
9211 }
9212
9213 /// Positions: over A, over B, and over neither.
9214 const A: (f64, f64) = (10.0, 10.0);
9215 const B: (f64, f64) = (10.0, 40.0);
9216 const NOWHERE: (f64, f64) = (150.0, 150.0);
9217
9218 #[test]
9219 fn slot_zero_touch_takes_the_same_path_as_a_plain_pointer() {
9220 use PointerPhase::{Down, Move, Up};
9221 let script = [
9222 (Down, A),
9223 (Move, NOWHERE),
9224 (Up, NOWHERE),
9225 (Down, NOWHERE),
9226 (Move, B),
9227 (Down, B),
9228 (Up, B),
9229 ];
9230 let (mut via_pointer, mut pointer_log) = root_with(false);
9231 let (mut via_contact, mut contact_log) = root_with(false);
9232 for (phase, (x, y)) in script {
9233 let a = via_pointer.event(&mut pointer_log, &mouse(phase, x, y));
9234 let b = via_contact.event(&mut contact_log, &touch(0, phase, x, y));
9235 assert_eq!(a, b, "{phase:?} at ({x}, {y}): same outcome");
9236 assert_eq!(
9237 via_pointer.is_pointer_captured(),
9238 via_contact.is_pointer_captured()
9239 );
9240 assert_eq!(via_pointer.is_focus_active(), via_contact.is_focus_active());
9241 assert_eq!(via_pointer.is_hover_active(), via_contact.is_hover_active());
9242 assert_eq!(
9243 via_pointer.focus_ime_generation(),
9244 via_contact.focus_ime_generation()
9245 );
9246 }
9247 // Same widgets, same phases; only the reported id differs.
9248 let strip = |log: &Log| -> Vec<(char, PointerPhase)> {
9249 log.seen.iter().map(|(t, _, p)| (*t, *p)).collect()
9250 };
9251 assert_eq!(strip(&pointer_log), strip(&contact_log));
9252 assert!(
9253 pointer_log
9254 .seen
9255 .iter()
9256 .all(|(_, id, _)| *id == PointerId::MOUSE)
9257 );
9258 assert!(
9259 contact_log
9260 .seen
9261 .iter()
9262 .all(|(_, id, _)| *id == PointerId::touch(0))
9263 );
9264 // The capture the contact's Down took named it as the claimant.
9265 via_contact.event(&mut contact_log, &touch(0, Down, A.0, A.1));
9266 assert_eq!(
9267 via_contact.pointer_capture_claimant(),
9268 Some(PointerId::touch(0))
9269 );
9270 }
9271
9272 #[test]
9273 fn an_additional_contact_with_nothing_captured_is_dropped() {
9274 let (mut root, mut log) = root_with(true);
9275 let outcome = root.event(&mut log, &touch(1, PointerPhase::Down, A.0, A.1));
9276 assert_eq!(outcome, EventOutcome::default());
9277 assert!(log.seen.is_empty(), "no widget saw the slot-1 contact");
9278 assert!(!root.is_pointer_captured());
9279 assert!(!root.is_focus_active(), "a dropped contact claims nothing");
9280 }
9281
9282 #[test]
9283 fn an_opted_in_captor_receives_the_other_contacts_on_the_captured_path() {
9284 use PointerPhase::{Down, Move, Up};
9285 let (mut root, mut log) = root_with(true);
9286 root.event(&mut log, &touch(0, Down, A.0, A.1));
9287 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9288
9289 // The second finger lands over B, yet travels the captured path to A.
9290 let t1 = PointerId::touch(1);
9291 assert!(root.event(&mut log, &touch(1, Down, B.0, B.1)).handled);
9292 root.event(&mut log, &touch(1, Move, NOWHERE.0, NOWHERE.1));
9293 root.event(&mut log, &touch(1, Up, NOWHERE.0, NOWHERE.1));
9294 assert_eq!(
9295 log.seen[1..],
9296 [('A', t1, Down), ('A', t1, Move), ('A', t1, Up)],
9297 "A saw touch(1)'s whole contact, B saw nothing"
9298 );
9299 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9300 }
9301
9302 #[test]
9303 fn a_captor_that_did_not_opt_in_never_sees_the_other_contacts() {
9304 use PointerPhase::{Down, Move, Up};
9305 let (mut root, mut log) = root_with(false);
9306 root.event(&mut log, &touch(0, Down, A.0, A.1));
9307 for phase in [Down, Move, Up] {
9308 let outcome = root.event(&mut log, &touch(1, phase, B.0, B.1));
9309 assert_eq!(
9310 outcome,
9311 EventOutcome::default(),
9312 "touch(1) {phase:?} dropped"
9313 );
9314 }
9315 assert_eq!(log.seen, [('A', PointerId::touch(0), Down)]);
9316 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9317 }
9318
9319 #[test]
9320 fn a_touch_release_never_ends_a_mouse_capture_and_vice_versa() {
9321 use PointerPhase::{Cancel, Down, Move, Up};
9322 // A mouse drag holds A; a finger tapping elsewhere must not break it.
9323 let (mut root, mut log) = root_with(false);
9324 root.event(&mut log, &mouse(Down, A.0, A.1));
9325 root.event(&mut log, &touch(0, Down, B.0, B.1));
9326 root.event(&mut log, &touch(0, Up, B.0, B.1));
9327 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::MOUSE));
9328 root.event(&mut log, &mouse(Move, NOWHERE.0, NOWHERE.1));
9329 assert_eq!(
9330 log.seen.last(),
9331 Some(&('A', PointerId::MOUSE, Move)),
9332 "the drag still reaches its captor"
9333 );
9334 assert!(log.seen.iter().all(|(tag, _, _)| *tag == 'A'));
9335 root.event(&mut log, &mouse(Up, NOWHERE.0, NOWHERE.1));
9336 assert!(!root.is_pointer_captured());
9337
9338 // The mirror image: a touch drag holds B; the mouse cannot end it.
9339 let (mut root, mut log) = root_with(false);
9340 root.event(&mut log, &touch(0, Down, B.0, B.1));
9341 root.event(&mut log, &mouse(Up, A.0, A.1));
9342 root.event(&mut log, &mouse(Cancel, A.0, A.1));
9343 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9344 root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9345 assert_eq!(log.seen.last(), Some(&('B', PointerId::touch(0), Move)));
9346 root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9347 assert!(!root.is_pointer_captured());
9348 }
9349
9350 #[test]
9351 fn only_the_claimants_release_clears_the_latch() {
9352 use PointerPhase::{Cancel, Down, Move, Up};
9353 let (mut root, mut log) = root_with(true);
9354 root.event(&mut log, &touch(0, Down, A.0, A.1));
9355 // Another contact ends twice over — delivered, yet nothing releases:
9356 // not the root latch, and not the container's active link either.
9357 root.event(&mut log, &touch(1, Down, A.0, A.1));
9358 root.event(&mut log, &touch(1, Up, A.0, A.1));
9359 root.event(&mut log, &touch(2, Down, A.0, A.1));
9360 root.event(&mut log, &touch(2, Cancel, A.0, A.1));
9361 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
9362 // Still on the captured path: a claimant move far outside A reaches A.
9363 root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9364 assert_eq!(log.seen.last(), Some(&('A', PointerId::touch(0), Move)));
9365
9366 // The claimant's own release ends it; later contacts fall to rule (b).
9367 root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9368 assert!(!root.is_pointer_captured());
9369 let before = log.seen.len();
9370 root.event(&mut log, &touch(1, Move, A.0, A.1));
9371 assert_eq!(
9372 log.seen.len(),
9373 before,
9374 "touch(1) dropped once the gesture ended"
9375 );
9376 // And the container's link went with it: a fresh contact is hit-tested.
9377 root.event(&mut log, &touch(0, Down, B.0, B.1));
9378 assert_eq!(log.seen.last(), Some(&('B', PointerId::touch(0), Down)));
9379 }
9380
9381 #[test]
9382 fn a_non_claimant_contact_never_blurs_or_moves_hover() {
9383 use PointerPhase::{Down, Up};
9384 let (mut root, mut log) = root_with(true);
9385 root.event(&mut log, &touch(0, Down, A.0, A.1));
9386 assert!(root.is_focus_active());
9387 let generation = root.focus_ime_generation();
9388 root.event(&mut log, &touch(1, Down, NOWHERE.0, NOWHERE.1));
9389 root.event(&mut log, &touch(1, Up, NOWHERE.0, NOWHERE.1));
9390 assert!(
9391 root.is_focus_active(),
9392 "a second finger is not a tap outside"
9393 );
9394 assert_eq!(root.focus_ime_generation(), generation);
9395 }
9396
9397 /// A container in front of [`Pair`] with pointer handling of its own that
9398 /// another contact must never reach: it logs every pointer event it routes
9399 /// (tag `'G'`) and treats a `Down` outside its child as a tap outside — an
9400 /// explicit focus release. With `forwards_broadcasts` unset it drops every
9401 /// broadcast instead of forwarding it (a container the forward-only walk
9402 /// cannot pass).
9403 struct Guard {
9404 inner: crate::widget::ChildPod,
9405 forwards_broadcasts: bool,
9406 }
9407 impl crate::widget::Widget for Guard {
9408 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9409 self.inner
9410 .layout_child(ctx, &BoxConstraints::tight(Size::new(100.0, 60.0)));
9411 self.inner.set_origin(Point::ZERO);
9412 bc.max()
9413 }
9414 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9415 self.inner.paint_child(ctx, scene);
9416 }
9417 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9418 if event.is_broadcast() {
9419 if self.forwards_broadcasts {
9420 self.inner.event_child(ctx, event);
9421 }
9422 return EventResult::Ignored;
9423 }
9424 let InputEvent::Pointer(p) = event else {
9425 return EventResult::Ignored;
9426 };
9427 let id = ctx.pointer_id();
9428 ctx.state_mut::<Log>().seen.push(('G', id, p.phase));
9429 let inside = self.inner.contains(p.position);
9430 let result = if self.inner.is_active() || inside {
9431 self.inner.event_child(ctx, event)
9432 } else {
9433 EventResult::Ignored
9434 };
9435 if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel) {
9436 self.inner.set_active(false);
9437 }
9438 if p.phase == PointerPhase::Down && !inside {
9439 ctx.release_focus();
9440 }
9441 result
9442 }
9443 }
9444
9445 struct GuardView {
9446 forwards_broadcasts: bool,
9447 }
9448 impl View<Log> for GuardView {
9449 type Element = Guard;
9450 fn build(&self, _ctx: &mut BuildCtx<'_>) -> Guard {
9451 Guard {
9452 inner: crate::widget::ChildPod::new(Box::new(Pair {
9453 a: crate::widget::ChildPod::new(Box::new(Probe {
9454 tag: 'A',
9455 opt_in: true,
9456 })),
9457 b: crate::widget::ChildPod::new(Box::new(Probe {
9458 tag: 'B',
9459 opt_in: true,
9460 })),
9461 })),
9462 forwards_broadcasts: self.forwards_broadcasts,
9463 }
9464 }
9465 fn rebuild(&self, _p: &Self, _e: &mut Guard, _c: &mut BuildCtx<'_>) -> ChangeFlags {
9466 ChangeFlags::NONE
9467 }
9468 }
9469
9470 fn guarded_root(forwards_broadcasts: bool) -> (RenderRoot<Log, GuardView>, Log) {
9471 let mut root: RenderRoot<Log, GuardView> = RenderRoot::new();
9472 let mut log = Log::default();
9473 root.rebuild(
9474 &mut |_| GuardView {
9475 forwards_broadcasts,
9476 },
9477 &mut log,
9478 );
9479 root.layout(Size::new(200.0, 200.0));
9480 (root, log)
9481 }
9482
9483 #[test]
9484 fn another_contact_reaches_only_the_captor_and_moves_no_hover_or_focus() {
9485 use PointerPhase::{Down, Move, Up};
9486 let (mut root, mut log) = guarded_root(true);
9487 let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
9488 root.event(&mut log, &touch(0, Down, A.0, A.1));
9489 assert!(root.is_focus_active());
9490 assert_eq!(root.pointer_capture_claimant(), Some(t0));
9491 assert!(root.pointer_capture_contacts());
9492 let (focus_generation, hover) = (root.focus_ime_generation(), root.is_hover_active());
9493
9494 // A second finger landing outside the guard's child: delivered to A down
9495 // the captured path, while the guard — whose own `Down` handling would
9496 // release focus on a tap outside — never runs on it.
9497 assert!(
9498 root.event(&mut log, &touch(1, Down, NOWHERE.0, NOWHERE.1))
9499 .handled
9500 );
9501 root.event(&mut log, &touch(1, Move, B.0, B.1));
9502 root.event(&mut log, &touch(1, Up, B.0, B.1));
9503 assert_eq!(
9504 log.seen,
9505 [
9506 ('G', t0, Down),
9507 ('A', t0, Down),
9508 ('A', t1, Down),
9509 ('A', t1, Move),
9510 ('A', t1, Up),
9511 ],
9512 "only the captor saw touch(1)"
9513 );
9514 assert!(root.is_focus_active(), "no tap outside was seen");
9515 assert_eq!(root.focus_ime_generation(), focus_generation);
9516 assert_eq!(root.is_hover_active(), hover);
9517 assert_eq!(root.pointer_capture_claimant(), Some(t0));
9518 assert!(root.pointer_capture_contacts());
9519
9520 // The claimant itself still takes the ordinary path through the guard.
9521 root.event(&mut log, &touch(0, Move, NOWHERE.0, NOWHERE.1));
9522 assert_eq!(
9523 log.seen[5..],
9524 [('G', t0, Move), ('A', t0, Move)],
9525 "the claimant's own move runs every handler on the path"
9526 );
9527 root.event(&mut log, &touch(0, Up, NOWHERE.0, NOWHERE.1));
9528 assert!(!root.is_pointer_captured());
9529 assert!(!root.pointer_capture_contacts());
9530 }
9531
9532 #[test]
9533 fn a_walk_a_container_cannot_pass_falls_back_to_the_ordinary_delivery() {
9534 use PointerPhase::Down;
9535 let (mut root, mut log) = guarded_root(false);
9536 let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
9537 root.event(&mut log, &touch(0, Down, A.0, A.1));
9538 // The guard drops the carrier, so it is handed the real event instead —
9539 // and the captor still receives it, down the captured path.
9540 assert!(root.event(&mut log, &touch(1, Down, B.0, B.1)).handled);
9541 assert_eq!(
9542 log.seen,
9543 [
9544 ('G', t0, Down),
9545 ('A', t0, Down),
9546 ('G', t1, Down),
9547 ('A', t1, Down),
9548 ]
9549 );
9550 assert_eq!(root.pointer_capture_claimant(), Some(t0));
9551 }
9552
9553 /// A root container that takes a gesture over from its child once the
9554 /// claimant moves more than 10 px: it cancels the child and releases it
9555 /// through [`EventCtx::release_captured_child`]. Logs every pointer event it
9556 /// sees (tag `'T'`); with `opt_in` it also opts into the gesture's other
9557 /// contacts itself, ahead of its child, on the `Down`.
9558 struct TakeOver {
9559 inner: crate::widget::ChildPod,
9560 opt_in: bool,
9561 down_at: Option<Point>,
9562 released_seen: bool,
9563 }
9564 impl crate::widget::Widget for TakeOver {
9565 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9566 self.inner.layout_child(ctx, bc);
9567 self.inner.set_origin(Point::ZERO);
9568 bc.max()
9569 }
9570 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9571 self.inner.paint_child(ctx, scene);
9572 }
9573 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9574 if event.is_broadcast() {
9575 self.inner.event_child(ctx, event);
9576 return EventResult::Ignored;
9577 }
9578 let InputEvent::Pointer(p) = event else {
9579 return EventResult::Ignored;
9580 };
9581 let id = ctx.pointer_id();
9582 ctx.state_mut::<Log>().seen.push(('T', id, p.phase));
9583 match p.phase {
9584 PointerPhase::Down if self.down_at.is_none() => {
9585 self.down_at = Some(p.position);
9586 ctx.capture_pointer();
9587 if self.opt_in {
9588 ctx.capture_contacts();
9589 }
9590 if self.inner.contains(p.position) {
9591 self.inner.event_child(ctx, event);
9592 }
9593 }
9594 PointerPhase::Move => {
9595 let moved = self.down_at.map(|at| (p.position - at).hypot());
9596 if self.inner.is_active() && moved.is_some_and(|d| d > 10.0) {
9597 let cancel = InputEvent::Pointer(PointerEvent {
9598 phase: PointerPhase::Cancel,
9599 ..*p
9600 });
9601 self.inner.event_child(ctx, &cancel);
9602 ctx.release_captured_child(&mut self.inner);
9603 self.released_seen = ctx.is_capture_released();
9604 } else if self.inner.is_active() {
9605 self.inner.event_child(ctx, event);
9606 }
9607 }
9608 PointerPhase::Up | PointerPhase::Cancel => {
9609 if self.inner.is_active() {
9610 self.inner.event_child(ctx, event);
9611 self.inner.set_active(false);
9612 }
9613 self.down_at = None;
9614 }
9615 PointerPhase::Down => {}
9616 }
9617 EventResult::Handled
9618 }
9619 }
9620
9621 struct TakeOverView {
9622 opt_in: bool,
9623 }
9624 impl View<Log> for TakeOverView {
9625 type Element = TakeOver;
9626 fn build(&self, _ctx: &mut BuildCtx<'_>) -> TakeOver {
9627 TakeOver {
9628 inner: crate::widget::ChildPod::new(Box::new(Probe {
9629 tag: 'A',
9630 opt_in: true,
9631 })),
9632 opt_in: self.opt_in,
9633 down_at: None,
9634 released_seen: false,
9635 }
9636 }
9637 fn rebuild(&self, _p: &Self, _e: &mut TakeOver, _c: &mut BuildCtx<'_>) -> ChangeFlags {
9638 ChangeFlags::NONE
9639 }
9640 }
9641
9642 fn take_over_root(opt_in: bool) -> (RenderRoot<Log, TakeOverView>, Log) {
9643 let mut root: RenderRoot<Log, TakeOverView> = RenderRoot::new();
9644 let mut log = Log::default();
9645 root.rebuild(&mut |_| TakeOverView { opt_in }, &mut log);
9646 root.layout(Size::new(200.0, 200.0));
9647 (root, log)
9648 }
9649
9650 fn take_over_widget(root: &mut RenderRoot<Log, TakeOverView>) -> &mut TakeOver {
9651 let id = root.root_id().expect("root built");
9652 root.tree
9653 .pod_mut(id)
9654 .expect("root pod")
9655 .widget_mut()
9656 .downcast_mut::<TakeOver>()
9657 .expect("root is a TakeOver")
9658 }
9659
9660 #[test]
9661 fn a_takeover_ends_the_contact_opt_in_but_keeps_the_claimant() {
9662 use PointerPhase::{Cancel, Down, Move, Up};
9663 let (mut root, mut log) = take_over_root(false);
9664 let t0 = PointerId::touch(0);
9665 root.event(&mut log, &touch(0, Down, A.0, A.1));
9666 assert_eq!(root.pointer_capture_claimant(), Some(t0));
9667 assert!(root.pointer_capture_contacts(), "A opted in");
9668
9669 // The claimant moves past the container's threshold: it cancels A and
9670 // releases it, and the release reaches the root.
9671 root.event(&mut log, &touch(0, Move, A.0, A.1 + 30.0));
9672 assert!(take_over_widget(&mut root).released_seen);
9673 assert_eq!(log.seen[3], ('A', t0, Cancel));
9674 assert_eq!(
9675 root.pointer_capture_claimant(),
9676 Some(t0),
9677 "the gesture is still the claimant's, now held by the container"
9678 );
9679 assert!(
9680 !root.pointer_capture_contacts(),
9681 "the widget that asked for the other contacts is gone"
9682 );
9683
9684 // Another finger is dropped from here on — neither the container nor
9685 // the cancelled captor hears it.
9686 let before = log.seen.len();
9687 let outcome = root.event(&mut log, &touch(1, Down, A.0, A.1));
9688 assert_eq!(outcome, EventOutcome::default());
9689 assert_eq!(log.seen.len(), before);
9690
9691 // The claimant keeps driving the container, and its release ends it.
9692 root.event(&mut log, &touch(0, Move, A.0, A.1 + 60.0));
9693 assert_eq!(log.seen.last(), Some(&('T', t0, Move)));
9694 root.event(&mut log, &touch(0, Up, A.0, A.1 + 60.0));
9695 assert!(!root.is_pointer_captured());
9696 }
9697
9698 #[test]
9699 fn a_takeover_by_the_widget_holding_the_opt_in_keeps_it() {
9700 use PointerPhase::{Down, Move};
9701 let (mut root, mut log) = take_over_root(true);
9702 let (t0, t1) = (PointerId::touch(0), PointerId::touch(1));
9703 root.event(&mut log, &touch(0, Down, A.0, A.1));
9704 root.event(&mut log, &touch(0, Move, A.0, A.1 + 30.0));
9705 assert!(
9706 !take_over_widget(&mut root).released_seen,
9707 "the container itself holds the opt-in, so releasing its child ends nothing"
9708 );
9709 assert!(root.pointer_capture_contacts());
9710 // The container is the captor: another finger reaches it directly.
9711 assert!(root.event(&mut log, &touch(1, Down, A.0, A.1)).handled);
9712 assert_eq!(log.seen.last(), Some(&('T', t1, Down)));
9713 assert_eq!(root.pointer_capture_claimant(), Some(t0));
9714 }
9715}
9716
9717/// [`InputEvent::Scale`]: hit-tested by [`ScaleEvent::focal`] exactly like
9718/// [`InputEvent::Scroll`], translated down the tree the same way, and bubbles
9719/// topmost-first until a widget reports [`EventResult::Handled`].
9720#[cfg(test)]
9721mod scale_tests {
9722 use super::*;
9723 use crate::event::{ScaleEvent, ScalePhase};
9724
9725 /// What every leaf saw: which leaf, which event.
9726 #[derive(Default)]
9727 struct Log {
9728 seen: Vec<(char, ScaleEvent)>,
9729 }
9730
9731 /// Logs every [`InputEvent::Scale`] it receives, in its own local space,
9732 /// and reports `Handled` only when `handles` is set — everything else
9733 /// (including a non-`Scale` event) is ignored.
9734 struct ScaleLeaf {
9735 tag: char,
9736 handles: bool,
9737 }
9738 impl crate::widget::Widget for ScaleLeaf {
9739 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9740 bc.constrain(Size::new(40.0, 40.0))
9741 }
9742 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
9743 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9744 let InputEvent::Scale(scale) = event else {
9745 return EventResult::Ignored;
9746 };
9747 ctx.state_mut::<Log>().seen.push((self.tag, *scale));
9748 if self.handles {
9749 EventResult::Handled
9750 } else {
9751 EventResult::Ignored
9752 }
9753 }
9754 }
9755
9756 /// Two leaves at identical bounds `(0, 0)..(40, 40)` — `top` painted (and
9757 /// hit-tested) before `bottom`, mirroring `frust-widgets::route_event`'s
9758 /// topmost-first, fall-through-on-`Ignored` hit test: a `Scale` landing in
9759 /// the shared rect reaches `top` first, and only reaches `bottom` if `top`
9760 /// ignores it.
9761 struct Overlapping {
9762 top: crate::widget::ChildPod,
9763 bottom: crate::widget::ChildPod,
9764 }
9765 impl crate::widget::Widget for Overlapping {
9766 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9767 self.top.layout_child(ctx, bc);
9768 self.top.set_origin(Point::ZERO);
9769 self.bottom.layout_child(ctx, bc);
9770 self.bottom.set_origin(Point::ZERO);
9771 bc.max()
9772 }
9773 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9774 self.bottom.paint_child(ctx, scene);
9775 self.top.paint_child(ctx, scene);
9776 }
9777 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9778 let pos = event.position();
9779 for pod in [&mut self.top, &mut self.bottom] {
9780 if pod.contains(pos) && pod.event_child(ctx, event) == EventResult::Handled {
9781 return EventResult::Handled;
9782 }
9783 }
9784 EventResult::Ignored
9785 }
9786 }
9787
9788 /// Wraps [`Overlapping`] one container deeper, offset by `(10, 20)` — so
9789 /// the fixture also proves [`InputEvent::translated`] shifts a `Scale`
9790 /// event's focal point correctly across a container boundary.
9791 struct Offset {
9792 inner: crate::widget::ChildPod,
9793 }
9794 impl crate::widget::Widget for Offset {
9795 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
9796 self.inner.layout_child(ctx, bc);
9797 self.inner.set_origin(Point::new(10.0, 20.0));
9798 bc.max()
9799 }
9800 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
9801 self.inner.paint_child(ctx, scene);
9802 }
9803 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
9804 let pos = event.position();
9805 if self.inner.contains(pos) {
9806 return self.inner.event_child(ctx, event);
9807 }
9808 EventResult::Ignored
9809 }
9810 }
9811
9812 struct OffsetView {
9813 top_handles: bool,
9814 }
9815 impl View<Log> for OffsetView {
9816 type Element = Offset;
9817 fn build(&self, _ctx: &mut BuildCtx<'_>) -> Offset {
9818 Offset {
9819 inner: crate::widget::ChildPod::new(Box::new(Overlapping {
9820 top: crate::widget::ChildPod::new(Box::new(ScaleLeaf {
9821 tag: 'T',
9822 handles: self.top_handles,
9823 })),
9824 bottom: crate::widget::ChildPod::new(Box::new(ScaleLeaf {
9825 tag: 'B',
9826 handles: true,
9827 })),
9828 })),
9829 }
9830 }
9831 fn rebuild(
9832 &self,
9833 _prev: &Self,
9834 _element: &mut Offset,
9835 _ctx: &mut BuildCtx<'_>,
9836 ) -> ChangeFlags {
9837 ChangeFlags::NONE
9838 }
9839 }
9840
9841 fn root_with(top_handles: bool) -> (RenderRoot<Log, OffsetView>, Log) {
9842 let mut root: RenderRoot<Log, OffsetView> = RenderRoot::new();
9843 let mut log = Log::default();
9844 root.rebuild(&mut |_| OffsetView { top_handles }, &mut log);
9845 root.layout(Size::new(200.0, 200.0));
9846 (root, log)
9847 }
9848
9849 fn scale(phase: ScalePhase, scale_delta: f64, x: f64, y: f64) -> InputEvent {
9850 InputEvent::Scale(ScaleEvent {
9851 phase,
9852 scale_delta,
9853 focal: Point::new(x, y),
9854 velocity: 0.0,
9855 })
9856 }
9857
9858 #[test]
9859 fn scale_hit_tests_by_focal_point_and_translates_through_a_container() {
9860 let (mut root, mut log) = root_with(true);
9861
9862 // Window (5, 5): outside the offset container entirely (it starts at
9863 // (10, 20)) — nothing is hit, nothing logged.
9864 let outside = root.event(&mut log, &scale(ScalePhase::Begin, 1.1, 5.0, 5.0));
9865 assert!(
9866 log.seen.is_empty(),
9867 "a focal point outside every pod hits nothing"
9868 );
9869 assert!(!outside.handled);
9870
9871 // Window (30, 40): inside the container, local (20, 20) once the
9872 // Offset container's (10, 20) origin is subtracted by
9873 // `InputEvent::translated` — squarely inside both overlapping 40x40
9874 // leaves, so the topmost one (`top`) is the one that sees it.
9875 let inside = root.event(&mut log, &scale(ScalePhase::Update, 1.2, 30.0, 40.0));
9876 assert!(inside.handled);
9877 assert_eq!(log.seen.len(), 1, "the topmost leaf alone handled it");
9878 let (tag, seen) = log.seen[0];
9879 assert_eq!(tag, 'T');
9880 assert_eq!(
9881 seen.focal,
9882 Point::new(20.0, 20.0),
9883 "translated() shifted the focal point into the container's local space"
9884 );
9885 assert_eq!(seen.scale_delta, 1.2);
9886 assert_eq!(seen.phase, ScalePhase::Update);
9887 }
9888
9889 #[test]
9890 fn an_ignored_scale_bubbles_to_the_next_hit_widget() {
9891 // `top` ignores every `Scale` it sees; `bottom`, at the identical
9892 // bounds, still handles it — proving the hit test falls through to
9893 // the next topmost-first candidate instead of stopping (and
9894 // swallowing the event) at the first hit.
9895 let (mut root, mut log) = root_with(false);
9896 let outcome = root.event(&mut log, &scale(ScalePhase::Begin, 0.9, 30.0, 40.0));
9897 assert!(outcome.handled, "the bottom leaf still handled it");
9898 assert_eq!(
9899 log.seen.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
9900 vec!['T', 'B'],
9901 "the ignoring top leaf saw it first, and bottom is what it bubbled to"
9902 );
9903 }
9904}