Skip to main content

frust_core/
event.rs

1//! Layer 2 input: pointer/scroll events and the [`EventCtx`] a widget mutates
2//! while handling them.
3//!
4//! The pipeline mirrors Masonry's corrected pointer model: an [`InputEvent`]
5//! enters the tree at the root ([`crate::app::RenderRoot::event`]) and is routed
6//! down through container [`ChildPod`](crate::widget::ChildPod)s, each of which
7//! translates the event into its child's local coordinate space before
8//! forwarding. A widget reports what it did through [`EventResult`] and can, via
9//! [`EventCtx`], mutate application state, request a redraw, or *capture* the
10//! pointer so subsequent moves/releases route straight back to it.
11//!
12//! Capture here is **by recorded path**, not a global registry: on
13//! [`PointerPhase::Down`] a widget calls [`EventCtx::capture_pointer`]; the
14//! enclosing container reads the flag ([`EventCtx::is_pointer_captured`]) and
15//! records which child was active so it can route later moves/releases directly.
16//! Capture auto-releases on [`PointerPhase::Up`]/[`PointerPhase::Cancel`] (never
17//! on window-leave) — the claimant's own, when more than one contact is down:
18//! [`InputEvent::PointerContact`] states the multi-contact contract, and
19//! [`EventCtx::pointer_id`]/[`EventCtx::capture_contacts`] are a widget's side of it.
20//!
21//! # Hover is a claim, not a phase
22//!
23//! There is no Enter/Leave phase, and [`PointerPhase`] deliberately gains none:
24//! hover is an **opt-in claim** a widget makes from its ordinary uncaptured
25//! [`PointerPhase::Move`] arm ([`EventCtx::claim_hover`]), recorded as a path
26//! through the pod chain the same way focus is. The claim's identity is an
27//! *epoch*: [`crate::app::RenderRoot`] advances one hover epoch per hover pass
28//! (an uncaptured `Move`, or the `Down`/`Up`/`Cancel` that ends a hover
29//! outright), a claim stamps that epoch onto every
30//! [`ChildPod`](crate::widget::ChildPod) from the claimant up to the root, and a
31//! link only counts as hovered while its stamp still matches the live epoch. So
32//! the previous claimant needs no explicit clearing — the pointer moving anywhere
33//! else advances the epoch and its stamp goes stale by construction, which is why
34//! a container that never hears about the move cannot leave a stale path standing.
35//!
36//! Stranding needs a hover pass, and there is exactly one way for a link to lose
37//! its owner without one: a rebuild that *removes* the claimant, which will never
38//! see another `Move`. A dropped [`ChildPod`](crate::widget::ChildPod) holding the
39//! live link therefore reports itself, and
40//! [`RenderRoot::rebuild`](crate::app::RenderRoot::rebuild) ends the hover before
41//! the frame does — the hover twin of the focus-orphan release.
42//!
43//! The recorded thing is a **path**, exactly like focus, and both hover reads
44//! report membership of it: the claimant *and* every ancestor enclosing it read
45//! hovered, the way CSS `:hover` applies to an element while the pointer is over
46//! one of its descendants. Nothing off the path does — a sibling, or a widget
47//! whose descendant did not claim, reads `false`.
48//!
49//! Only an **uncaptured** `Move` may claim: the root marks a captured pass
50//! ineligible outright, and [`ChildPod::event_child`](crate::widget::ChildPod::event_child)
51//! additionally refuses a claim from inside a pod that itself holds the capture
52//! path, so a drag can never paint hover under the finger. At most one claim per
53//! pass is recorded — the **first one recorded wins**, and every later claim in
54//! that pass is ineligible — so at most one path is hovered and two *stacked*
55//! widgets cannot each hold their own link. First-recorded is the topmost
56//! (deepest) claimant only while every container claims **after** routing the
57//! move to its children, which is what [`EventCtx::claim_hover`]'s contract
58//! requires of one: an ancestor that claims *before* it forwards is recorded
59//! first instead, and starves its whole subtree for the pass.
60//!
61//! # The cursor is a per-pass request, on its own channel
62//!
63//! [`EventCtx::set_cursor`] is hover's sibling and deliberately **not** derived
64//! from it: the root's hover mirror is identity-free (it knows *that* something
65//! is hovered, not which widget or what shape that widget wants), so the cursor
66//! gets its own channel — one slot per pass, last writer wins, resolved by
67//! [`crate::app::RenderRoot::event`] into [`crate::app::RenderRoot::cursor`] for
68//! a desktop shell to apply. Absence resolves to [`CursorIcon::Default`], so a
69//! widget that stops asking needs no clearing, and only a pointer
70//! [`PointerPhase::Move`] re-resolves — a captured `Move` included, which is what
71//! lets a drag keep its own cursor outside its bounds.
72
73use std::any::Any;
74use std::cell::Cell;
75use std::fmt;
76use std::thread::LocalKey;
77
78use kurbo::{Affine, Point, Rect, Size, Vec2};
79
80use crate::overlay::OverlayKey;
81
82/// Which physical (or synthetic) button a pointer event carries.
83///
84/// Touch and pen contacts report [`PointerButton::Primary`]; the secondary /
85/// middle variants exist for mouse input (right/middle click).
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum PointerButton {
88    /// The primary button (left mouse, or any touch/pen contact).
89    Primary,
90    /// The secondary button (right mouse).
91    Secondary,
92    /// The middle button (mouse wheel click).
93    Middle,
94}
95
96/// The lifecycle phase of a pointer gesture.
97///
98/// A gesture is a `Down`, zero or more `Move`s, and a terminating `Up` or
99/// `Cancel`. `Cancel` fires when the platform steals the gesture (e.g. a system
100/// gesture recognizer wins) and, like `Up`, releases any capture.
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub enum PointerPhase {
103    /// A contact began (mouse-down / finger-down).
104    Down,
105    /// A contact moved while down.
106    Move,
107    /// A contact ended normally (mouse-up / finger-up). Releases capture.
108    Up,
109    /// The gesture was cancelled by the platform. Releases capture.
110    Cancel,
111}
112
113/// A single pointer event in the coordinate space of the widget receiving it.
114///
115/// `position` is **logical** (density-independent) pixels, already translated
116/// into the receiving widget's local space by the container chain that routed it
117/// (see [`crate::widget::ChildPod::event_child`]).
118#[derive(Clone, Copy, Debug, PartialEq)]
119pub struct PointerEvent {
120    /// The gesture phase.
121    pub phase: PointerPhase,
122    /// The pointer location, in the receiving widget's local logical space.
123    pub position: Point,
124    /// Which button the event carries (`Primary` for touch/pen).
125    pub button: PointerButton,
126}
127
128/// The kind of device a pointer contact comes from — one half of a
129/// [`PointerId`].
130#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
131pub enum PointerSource {
132    /// The mouse (or any single-cursor host pointer a shell reports as one).
133    Mouse,
134    /// A touch contact — one finger on a touchscreen.
135    Touch,
136}
137
138/// The identity of one pointer contact: which device it comes from and which
139/// slot on that device.
140///
141/// It rides **beside** a [`PointerEvent`], never inside it: a shell hands a
142/// touch contact to the root as [`InputEvent::PointerContact`], the root
143/// unwraps it, and the widget receiving the plain [`InputEvent::Pointer`] reads
144/// the identity from [`EventCtx::pointer_id`]. A bare `InputEvent::Pointer`
145/// from a shell means [`PointerId::MOUSE`].
146///
147/// A slot is the shell's own numbering of simultaneous contacts on one source:
148/// slot `0` is the gesture's first contact, and a slot is free again once its
149/// contact ended. The mouse only ever has slot `0`.
150///
151/// # Multi-contact contract
152///
153/// Applied at the root ([`crate::app::RenderRoot::event`]):
154///
155/// * **(a)** With no live pointer capture, a slot-`0` contact is hit-tested
156///   exactly like a plain [`InputEvent::Pointer`] — the same overlay, hover,
157///   focus and blur bookkeeping — and a capture taken on its `Down` latches
158///   *this* id as the gesture's **claimant**.
159/// * **(b)** With no live capture, a contact on slot `1` or above is **dropped**
160///   at the root. Additional contacts exist only inside a captured gesture.
161/// * **(c)** While a capture is live, the claimant's own events take the
162///   captured path as usual. An event with any other id reaches the captor —
163///   and only the captor: the containers above it on that path forward it
164///   without running their own handling — only if the captor opted in with
165///   [`EventCtx::capture_contacts`] on its capturing `Down`; otherwise the root
166///   drops it. Only the claimant's `Up`/`Cancel` ends the capture: another
167///   contact's `Up`/`Cancel` never releases it, whether it came from a touch
168///   while a mouse holds the capture or the other way round. A container that
169///   takes the gesture over from the captor ([`EventCtx::release_captured_child`])
170///   ends the opt-in, and the other contacts are dropped from then on.
171///
172/// See [`InputEvent::PointerContact`] for the full contract.
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
174pub struct PointerId {
175    /// The device the contact comes from.
176    pub source: PointerSource,
177    /// The contact's slot on that device (`0` for the first contact).
178    pub slot: u32,
179}
180
181impl PointerId {
182    /// The mouse pointer: the identity a bare [`InputEvent::Pointer`] carries,
183    /// and what [`EventCtx::pointer_id`] reports when nothing else is known.
184    pub const MOUSE: PointerId = PointerId {
185        source: PointerSource::Mouse,
186        slot: 0,
187    };
188
189    /// The touch contact in `slot` (`0` for the gesture's first finger).
190    pub const fn touch(slot: u32) -> PointerId {
191        PointerId {
192            source: PointerSource::Touch,
193            slot,
194        }
195    }
196}
197
198/// The pointer cursor a widget asks the host to display.
199///
200/// A **request vocabulary**, not a rendering one: platform-neutral names a
201/// widget states its intent in ([`EventCtx::set_cursor`]), which a desktop shell
202/// maps onto its own host API — `frust-shell-desktop` onto winit's own cursor
203/// icons, the one place any of these names touches a platform. Deliberately
204/// tiny: the shapes a desktop-class design system actually needs, not a full CSS
205/// cursor set.
206///
207/// `#[non_exhaustive]` from birth, so widening it later cannot break an
208/// out-of-tree `match` (a shell or design system must carry a wildcard arm and
209/// degrade an unknown request to [`CursorIcon::Default`] rather than fail to
210/// compile).
211///
212/// **Nothing below a desktop shell honours a request.** The mobile shells never
213/// read the resolved value — a touch host has no pointer to shape — so a widget
214/// may set a cursor unconditionally and get the desktop behaviour where it
215/// exists and no behaviour at all where it does not.
216#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
217#[non_exhaustive]
218pub enum CursorIcon {
219    /// The host's ordinary arrow. The resolved value of any pass in which no
220    /// widget asked for anything else, so a widget never has to ask for it to
221    /// "give the cursor back" (see [`EventCtx::set_cursor`]).
222    #[default]
223    Default,
224    /// The clickable hand: buttons, links, and anything else a press activates.
225    Pointer,
226    /// The text I-beam: editable or selectable text.
227    Text,
228    /// An open hand: this is draggable, and no drag has started yet.
229    Grab,
230    /// A closed hand: a drag is in progress.
231    Grabbing,
232    /// A column-resize handle — a divider the pointer moves horizontally.
233    ColResize,
234    /// A row-resize handle — a divider the pointer moves vertically.
235    RowResize,
236    /// The action under the pointer is refused: a disabled control, or a drop
237    /// target rejecting what is being dragged.
238    NotAllowed,
239}
240
241/// A scroll amount, in either discrete lines or continuous pixels.
242///
243/// Line deltas come from mouse wheels (winit `LineDelta`); pixel deltas from
244/// precision trackpads/touch (winit `PixelDelta`). The `(x, y)` order is
245/// horizontal then vertical.
246#[derive(Clone, Copy, Debug, PartialEq)]
247pub enum ScrollDelta {
248    /// A wheel-notch delta measured in lines `(x, y)`.
249    Lines(f64, f64),
250    /// A precision delta measured in logical pixels `(x, y)`.
251    Pixels(f64, f64),
252}
253
254/// The lifecycle phase of a scale (pinch/zoom) gesture.
255///
256/// No `Cancel`: every shipped source — the desktop ctrl/⌘+wheel mapping and
257/// macOS's `PinchGesture` — reports a clean bracket (or, for an ordinary
258/// notch wheel, a lone [`Update`](ScalePhase::Update) with no bracket at
259/// all), so there is nothing yet for a cancelled variant to mean. Widened the
260/// day a source needs one.
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub enum ScalePhase {
263    /// The gesture began.
264    Begin,
265    /// The gesture continued; the event's `scale_delta`/`focal`/`velocity`
266    /// describe this increment.
267    Update,
268    /// The gesture ended normally.
269    End,
270}
271
272/// A scale (pinch/zoom) gesture event — [`InputEvent::Scroll`]'s hit-tested
273/// sibling, carrying a *multiplicative* delta and a focal point rather than
274/// an additive one.
275///
276/// Any source that reports a scale-factor change rather than individual
277/// contact moves reduces to this one event — a desktop shell's ctrl/⌘+wheel
278/// mapping, macOS's `PinchGesture`, and eventually a touch two-finger pinch
279/// recognizer — so a widget reacts to pinch-to-zoom the same way regardless
280/// of input device.
281#[derive(Clone, Copy, Debug, PartialEq)]
282pub struct ScaleEvent {
283    /// The gesture phase.
284    pub phase: ScalePhase,
285    /// The multiplicative scale change this event represents — not a running
286    /// total. A consumer multiplies its own accumulated scale by this value
287    /// each time an event arrives: `1.0` is a no-op, `>1.0` zooms in, `<1.0`
288    /// zooms out.
289    pub scale_delta: f64,
290    /// Where the gesture is centered, in the receiving widget's local
291    /// logical space — the point that must stay visually fixed while scale
292    /// changes. Translated like [`InputEvent::Pointer`]'s position by the
293    /// container chain that routes it (see [`InputEvent::translated`]).
294    pub focal: Point,
295    /// The gesture's current rate of scale change, per second. `0.0` when the
296    /// source reports none — every shipped desktop source today, since
297    /// neither a wheel notch nor winit's `PinchGesture` carries a velocity —
298    /// reserved for a recognizer that tracks contact velocity directly.
299    pub velocity: f64,
300}
301
302/// A named (non-character) key: the control keys an editable widget reacts to.
303///
304/// Character-producing keys arrive as [`Key::Character`] (already resolved to the
305/// typed text, so dead keys / smart quotes / IME are handled upstream); only the
306/// keys with editing *semantics* are enumerated here.
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub enum NamedKey {
309    /// Return / Enter — submit or newline.
310    Enter,
311    /// Backspace — delete the grapheme before the caret.
312    Backspace,
313    /// Forward delete — delete the grapheme after the caret.
314    Delete,
315    /// Move / extend the caret left.
316    ArrowLeft,
317    /// Move / extend the caret right.
318    ArrowRight,
319    /// Move / extend the caret up.
320    ArrowUp,
321    /// Move / extend the caret down.
322    ArrowDown,
323    /// Move to line / document start.
324    Home,
325    /// Move to line / document end.
326    End,
327    /// Cancel / dismiss (blur, drop composition).
328    Escape,
329    /// Tab — focus traversal or literal tab (widget's choice).
330    Tab,
331    /// The dedicated hardware **Copy** key (winit's `NamedKey::Copy`), present on
332    /// full-size and multimedia keyboards. Semantically identical to the
333    /// platform copy chord, but it arrives as a key rather than as a modifier
334    /// combination, so a shell maps it straight onto
335    /// [`EditCommand::Copy`] instead of asking a widget to decode a chord.
336    Copy,
337    /// The dedicated hardware **Cut** key (winit's `NamedKey::Cut`) — the
338    /// [`Copy`](NamedKey::Copy) note applies verbatim, mapping onto
339    /// [`EditCommand::Cut`].
340    Cut,
341    /// The dedicated hardware **Paste** key (winit's `NamedKey::Paste`) — the
342    /// [`Copy`](NamedKey::Copy) note applies verbatim. A shell answers it the way
343    /// it answers any paste: by reading the host clipboard and dispatching
344    /// [`EditCommand::Paste`] with the text.
345    Paste,
346    /// **Insert** — carried for the legacy clipboard chords rather than for an
347    /// overtype mode: `Shift+Insert` is paste and `Ctrl+Insert` is copy on
348    /// Windows, Linux, and most X11 terminals, which is the only reason this key
349    /// is enumerated here (nothing in this workspace toggles overtype).
350    Insert,
351}
352
353/// A logical key press: either a semantic [`NamedKey`] or a run of typed text.
354///
355/// [`Key::Character`] carries the *resolved* text a key produced (winit's
356/// `KeyEvent.text` / a platform character), so widgets insert it verbatim without
357/// re-deriving it from a keycode + modifiers.
358#[derive(Clone, Debug, PartialEq, Eq)]
359pub enum Key {
360    /// A control / navigation key with editing semantics.
361    Named(NamedKey),
362    /// Typed text to insert as-is (usually a single grapheme).
363    Character(String),
364}
365
366/// The chord of modifier keys held when a [`KeyEvent`] fired.
367///
368/// `meta` is Command on macOS and the Windows/Super key elsewhere; widgets use
369/// it (with `ctrl`) for shortcuts like select-all.
370#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
371pub struct Modifiers {
372    /// Shift held (extends selection on arrow keys).
373    pub shift: bool,
374    /// Control held.
375    pub ctrl: bool,
376    /// Alt / Option held.
377    pub alt: bool,
378    /// Meta held (Command on macOS, Super/Windows elsewhere).
379    pub meta: bool,
380}
381
382/// A keyboard key event delivered down the focus path (never hit-tested).
383#[derive(Clone, Debug, PartialEq, Eq)]
384pub struct KeyEvent {
385    /// The logical key (a [`NamedKey`] or typed [`Key::Character`] text).
386    pub key: Key,
387    /// The modifier chord held when the key fired.
388    pub modifiers: Modifiers,
389    /// Whether this is an auto-repeat (key held down), not a fresh press.
390    pub repeat: bool,
391}
392
393/// A semantic clipboard / selection command delivered to the focused editable.
394///
395/// The **decoded** form of a platform gesture, not the gesture itself: a shell
396/// resolves `Cmd+C` / `Ctrl+C` / [`NamedKey::Copy`] / `Ctrl+Insert` / an Android
397/// `ACTION_PROCESS_TEXT` / an iOS edit-menu tap into one of these variants and
398/// dispatches it as [`InputEvent::EditCommand`], so no widget has to know which
399/// chord means copy on which OS. Every widget sees the same four verbs.
400///
401/// # Why paste carries its text and copy does not
402///
403/// The clipboard itself lives in the shell (only the shell has a host clipboard
404/// to talk to), and the two directions are deliberately asymmetric:
405///
406/// * [`Copy`](EditCommand::Copy) / [`Cut`](EditCommand::Cut) carry nothing —
407///   the widget owns the selection, so it answers by writing its own text into
408///   the pass's clipboard slot ([`EventCtx::write_clipboard`]), which the shell
409///   drains and hands to the host.
410/// * [`Paste`](EditCommand::Paste) carries the text — the *shell* owns the
411///   host clipboard, so by the time the command reaches the tree the read has
412///   already happened. A widget that wants a paste it did not receive asks for
413///   one ([`EventCtx::request_paste`]) and the shell answers with this variant.
414///
415/// # Refusal is the widget's call
416///
417/// Nothing here is a permission: a read-only or secret field is free to ignore
418/// a [`Copy`](EditCommand::Copy)/[`Cut`](EditCommand::Cut) it does not want to
419/// honour, and a widget with no selection simply reports
420/// [`EventResult::Ignored`]. The vocabulary states what was *asked for*.
421///
422/// Exhaustive on purpose (no `#[non_exhaustive]`): these four verbs are the
423/// whole clipboard contract, and a widget matching on them should be told by
424/// the compiler if that ever stops being true.
425#[derive(Clone, PartialEq, Eq)]
426pub enum EditCommand {
427    /// Copy the current selection to the host clipboard, leaving the document
428    /// unchanged. A widget answers by calling [`EventCtx::write_clipboard`].
429    Copy,
430    /// Copy the current selection and delete it. A widget answers by calling
431    /// [`EventCtx::write_clipboard`] *and* mutating its own text — the shell
432    /// sees one clipboard write either way (see that method's last-writer rule).
433    Cut,
434    /// Replace the current selection with this text (insert it at the caret when
435    /// there is no selection). Already read from the host clipboard by the shell.
436    Paste(String),
437    /// Select the widget's entire content — the selection half of this
438    /// vocabulary, carried here because it arrives through the same platform
439    /// chords and edit menus as the other three.
440    SelectAll,
441}
442
443impl fmt::Debug for EditCommand {
444    /// Hand-written so pasted text never reaches a log.
445    ///
446    /// [`ImeState`]'s reason, one step earlier in the pipeline (see its `Debug`):
447    /// a paste payload is arbitrary host-clipboard content — a password manager's
448    /// fill, a copied token, a recovery phrase — and unlike an IME surface there
449    /// is no content-type hint to key the decision off, because the *clipboard*
450    /// has no owner to state one. So the payload is unconditionally replaced by
451    /// `<redacted>` (no length, which would itself leak), and the variant name
452    /// still prints so a trace stays readable.
453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454        match self {
455            EditCommand::Copy => f.write_str("Copy"),
456            EditCommand::Cut => f.write_str("Cut"),
457            EditCommand::Paste(_) => f.debug_tuple("Paste").field(&"<redacted>").finish(),
458            EditCommand::SelectAll => f.write_str("SelectAll"),
459        }
460    }
461}
462
463/// The full editing state of a text field, the one struct every IME bridge syncs.
464///
465/// This mirrors Flutter's canonical editing-state shape (`−1` = "none" for the
466/// selection/composing anchors). It is the value pushed across the framework↔
467/// platform seam in both directions.
468///
469/// # Index boundary rule
470///
471/// **An `EditingState` crossing the `AppTree`/shell seam is UTF-16 code-unit
472/// indexed** (`selection_*`/`composing_*` count UTF-16 units, the platform-native
473/// unit for both Android `Editable` and iOS `NSMutableString`). Widgets and
474/// `frust-text` convert to/from Rust byte offsets at their own boundary.
475/// Core carries the value opaquely and makes no index interpretation.
476#[derive(Clone, Debug, PartialEq, Eq)]
477pub struct EditingState {
478    /// The full text content.
479    pub text: String,
480    /// Selection anchor (UTF-16 unit index at the shell seam; `−1` = none).
481    pub selection_base: i32,
482    /// Selection focus (UTF-16 unit index at the shell seam; `−1` = none).
483    pub selection_extent: i32,
484    /// Composing-region start (UTF-16 unit index; `−1` = not composing).
485    pub composing_base: i32,
486    /// Composing-region end (UTF-16 unit index; `−1` = not composing).
487    pub composing_extent: i32,
488}
489
490impl Default for EditingState {
491    /// An empty field with no selection and no composing region.
492    ///
493    /// Note this is **not** the derived default: the anchors are the `−1`
494    /// "none" sentinel, not `0` (which would mean a real caret at offset 0).
495    fn default() -> Self {
496        Self {
497            text: String::new(),
498            selection_base: -1,
499            selection_extent: -1,
500            composing_base: -1,
501            composing_extent: -1,
502        }
503    }
504}
505
506/// An input-method (IME) event delivered down the focus path (never hit-tested).
507///
508/// Desktop drives [`ImeEvent::Compose`]/[`ImeEvent::Commit`] from winit's
509/// `Ime::Preedit`/`Ime::Commit`; the mobile bridges push whole values via
510/// [`ImeEvent::ApplyEditingState`] (state-sync, not op-forwarding).
511/// [`ImeEvent::Enabled`]/[`ImeEvent::Disabled`] bracket a
512/// composition session.
513#[derive(Clone, Debug, PartialEq, Eq)]
514pub enum ImeEvent {
515    /// Preedit / marked text: `text` is the composing string, `cursor` its
516    /// optional `(start, end)` selection within that string (byte indices, as
517    /// winit reports).
518    Compose {
519        /// The composing (marked) text.
520        text: String,
521        /// Optional caret/selection `(start, end)` inside `text`.
522        cursor: Option<(usize, usize)>,
523    },
524    /// Commit finished composition: insert `text` and clear the composing region.
525    Commit(String),
526    /// Replace the whole editing state (mobile state-sync path).
527    ApplyEditingState(EditingState),
528    /// The platform enabled IME on the focused field (composition may begin).
529    Enabled,
530    /// The platform disabled IME (composition ended / focus left).
531    Disabled,
532}
533
534/// What an [`InputEvent::Overlay`] carries into a floated surface.
535///
536/// The key names the surface's owner (see [`OverlayKey`]); the kind is the input
537/// itself, always in **absolute window space** rather than in anyone's local
538/// space — see [`OverlayEventKind`].
539#[derive(Clone, Debug, PartialEq)]
540pub struct OverlayEvent {
541    /// The owner whose registered surface the root hit. Every other widget in
542    /// the tree sees this broadcast and must ignore it.
543    pub key: OverlayKey,
544    /// What happened.
545    pub kind: OverlayEventKind,
546}
547
548/// The input an [`OverlayEvent`] delivers.
549///
550/// # Window space, not local space
551///
552/// Every position here is absolute logical window space, deliberately: the
553/// broadcast reaches the owner by travelling the *main* tree, so the translation
554/// chain it passes through on the way (`ChildPod::event_child` subtracting each
555/// container's origin) describes the owner's position, not the floated pod's.
556/// Translating the payload would therefore corrupt it. The owner instead
557/// subtracts its own registered
558/// [`window_rect`](crate::overlay::OverlayEntry::window_rect) origin before
559/// forwarding into the pod, which is the only offset that means anything — which
560/// is also why [`InputEvent::translated`] returns an overlay event unchanged.
561#[derive(Clone, Debug, PartialEq)]
562pub enum OverlayEventKind {
563    /// A pointer event inside the surface's rect, positioned in window space.
564    Pointer(PointerEvent),
565    /// A scroll inside the surface's rect, positioned in window space.
566    Scroll {
567        /// Where the scroll occurred, in absolute window space.
568        position: Point,
569        /// How much to scroll.
570        delta: ScrollDelta,
571    },
572    /// A scale gesture inside the surface's rect, focal point in window
573    /// space — the overlay mirror of [`InputEvent::Scale`], routed here on
574    /// exactly the same hit-test terms as [`Scroll`](OverlayEventKind::Scroll).
575    Scale {
576        /// Where the gesture is centered, in absolute window space.
577        focal: Point,
578        /// The gesture phase.
579        phase: ScalePhase,
580        /// The multiplicative scale change this event represents.
581        scale_delta: f64,
582        /// The gesture's current rate of scale change, per second.
583        velocity: f64,
584    },
585    /// A primary press landed outside **every** registered surface — the
586    /// light-dismiss notification, delivered only to entries registered
587    /// [`OutsideTap::Notify`](crate::overlay::OutsideTap::Notify). It carries no
588    /// position: where the press landed is the main tree's business, and an
589    /// owner that wants it can register `consume: false` and watch the press
590    /// arrive there normally.
591    OutsideDown,
592}
593
594/// An input event delivered to the widget tree.
595///
596/// Pointer gestures, scroll, and scale are **hit-tested** (routed by position);
597/// keyboard, IME, and edit-command events are **focus-routed** — delivered
598/// straight down the recorded focus chain with no hit test and no meaningful
599/// position (see [`crate::widget::ChildPod`]'s focus bookkeeping and
600/// `frust-widgets`' `route_event`). [`InputEvent::Housekeeping`] and
601/// [`InputEvent::Overlay`] are neither: they are **broadcasts** that reach
602/// every child unconditionally.
603#[derive(Clone, Debug, PartialEq)]
604pub enum InputEvent {
605    /// A pointer (mouse/touch/pen) gesture event.
606    ///
607    /// From a shell it means the same as
608    /// [`PointerContact`](InputEvent::PointerContact) with [`PointerId::MOUSE`].
609    /// It is also the **only** pointer form a widget ever receives: the root
610    /// unwraps a `PointerContact` into this variant and reports the contact's
611    /// identity through [`EventCtx::pointer_id`].
612    Pointer(PointerEvent),
613    /// One identified pointer contact — the shell-facing carrier for a touch
614    /// contact (or any pointer that is not [`PointerId::MOUSE`]).
615    ///
616    /// **Widgets never receive this variant.**
617    /// [`RenderRoot::event`](crate::app::RenderRoot::event) unwraps it and
618    /// dispatches [`InputEvent::Pointer`]`(event)` with
619    /// [`EventCtx::pointer_id`] reporting `pointer_id`, so every existing
620    /// `match` on `InputEvent::Pointer` keeps working unchanged and a widget
621    /// that does not care which contact it is seeing never has to ask.
622    ///
623    /// # Multi-contact contract
624    ///
625    /// The root decides what a contact does from its id and the capture latch
626    /// it holds (the latch records the **claimant**: the id whose `Down` took the
627    /// capture).
628    ///
629    /// * **(a) Slot 0 with no live capture is hit-tested exactly like
630    ///   [`InputEvent::Pointer`].** The overlay pre-pass, hover, focus and
631    ///   blur-on-outside-tap bookkeeping are the same code path, so a
632    ///   single-finger gesture behaves identically whichever carrier delivered
633    ///   it. A capture taken on its `Down` latches `pointer_id` as the claimant.
634    /// * **(b) Slot 1 and above with no live capture is dropped at the root.**
635    ///   Additional contacts exist only inside a captured gesture; one that
636    ///   arrives while nothing holds the pointer reaches no widget and moves no
637    ///   root state.
638    /// * **(c) While a capture is live, routing is keyed on the claimant.** The
639    ///   claimant's own events take the captured path exactly as before. An
640    ///   event from **any other id** is delivered down the same captured path
641    ///   to the captor — as `InputEvent::Pointer`, with
642    ///   [`EventCtx::pointer_id`] reporting that id — only if the captor called
643    ///   [`EventCtx::capture_contacts`] on the `Down` it captured with;
644    ///   otherwise it is dropped at the root. **Only the claimant's
645    ///   `Up`/`Cancel` releases the capture.** Another contact's `Up`/`Cancel`
646    ///   never does — at the root or in any container's recorded active path —
647    ///   so a finger lifting elsewhere cannot break a mouse drag, nor a mouse
648    ///   release a touch drag.
649    ///
650    ///   **The captor is the only widget that sees another contact.** The
651    ///   delivery walks the recorded active path *forward-only*: every
652    ///   container between the root and the captor hands it on without its own
653    ///   pointer handling running (see
654    ///   [`ChildPod::event_child`](crate::widget::ChildPod::event_child) for the
655    ///   mechanism), so a scroll view or gesture detector enclosing a pinch
656    ///   recognizer never sees the second finger as a `Down` of its own; the
657    ///   captor's own handler, and whatever it routes below itself, run as
658    ///   usual. If the walk cannot reach the captor through a container (an
659    ///   overlay owner whose captured pod is a floated surface), that container
660    ///   alone is handed the event the ordinary way.
661    ///
662    ///   **A takeover ends the opt-in.** A container that cancels the captor
663    ///   and keeps the gesture for itself releases it with
664    ///   [`EventCtx::release_captured_child`]; the root then stops routing the
665    ///   other contacts (they fall under rule (c)'s drop branch), while the
666    ///   claimant keeps the capture — now held by that container — until its own
667    ///   `Up`/`Cancel`.
668    ///
669    /// A delivered non-claimant contact is not a gesture of its own: it opens or
670    /// moves no capture, takes no hover pass, resolves no cursor, and blurs
671    /// nothing (an explicit [`EventCtx::request_focus`]/[`EventCtx::release_focus`]
672    /// from its handler is still honoured, as it is for a scroll). When the
673    /// claimant's `Up`/`Cancel` ends the capture, the captor must treat every
674    /// other contact it was tracking as ended too: their later events fall under
675    /// rule (b) and never reach it.
676    ///
677    /// A bare [`InputEvent::Pointer`] from a shell is this variant with
678    /// [`PointerId::MOUSE`], so a host that emits only `Pointer` (desktop, web)
679    /// sees exactly the single-pointer behaviour it always had.
680    PointerContact {
681        /// Which contact this is.
682        pointer_id: PointerId,
683        /// The contact's event, positioned like any [`InputEvent::Pointer`].
684        event: PointerEvent,
685    },
686    /// A scroll event at `position` (local logical space) carrying `delta`.
687    Scroll {
688        /// Where the scroll occurred, in the receiving widget's local space.
689        position: Point,
690        /// How much to scroll.
691        delta: ScrollDelta,
692    },
693    /// A scale (pinch/zoom) gesture event — hit-tested exactly like
694    /// [`Scroll`](InputEvent::Scroll), by its [`ScaleEvent::focal`] point, and
695    /// bubbles up the tree until a widget reports [`EventResult::Handled`].
696    /// See [`ScaleEvent`] for the field contract.
697    Scale(ScaleEvent),
698    /// A keyboard key event, routed down the focus path (no hit test).
699    Key(KeyEvent),
700    /// An IME event, routed down the focus path (no hit test).
701    Ime(ImeEvent),
702    /// A decoded clipboard / selection command, routed down the focus path (no
703    /// hit test) exactly like [`Key`](InputEvent::Key) and [`Ime`](InputEvent::Ime).
704    ///
705    /// Focus-routed rather than hit-tested because a clipboard verb is *about
706    /// the selection*, and the selection lives wherever focus is — a `Cmd+V`
707    /// carries no pointer position, and an edit-menu tap's position is the
708    /// menu's, not the field's. Focus routing is also what makes a paste with
709    /// nothing focused a harmless no-op: the event reaches no widget and is
710    /// dropped, so a shell may answer a stale paste request unconditionally
711    /// (see [`EventCtx::request_paste`]).
712    EditCommand(EditCommand),
713    /// **Not user input**: a state-bearing housekeeping pass, broadcast to the
714    /// whole tree so a widget that queued a callback needing `&mut State` during
715    /// a state-free `BuildCtx` pass can run it.
716    ///
717    /// # Why it exists
718    ///
719    /// [`crate::app::RenderRoot::rebuild`] is the only unconditional per-frame
720    /// pass holding `&mut State`, and it hands that state to the build closure alone —
721    /// the view diff itself (and therefore every `View::rebuild`, where a
722    /// navigator applies its queued push/pop ops) is state-free. A widget that
723    /// needs to call back into app state from there had, before this variant, no
724    /// pass to run in except the *next event*, which on a touch device may be
725    /// seconds away or may never reach that widget at all (a pop-result
726    /// callback measured 3.2s late on device, and was lost entirely when the
727    /// next tap was consumed by chrome outside the navigator).
728    /// `rebuild` now dispatches this variant instead, so the deferred callback
729    /// runs on the very frame that queued it.
730    ///
731    /// # Routing contract
732    ///
733    /// **Broadcast, never consumed.** It carries no position, is not hit-tested,
734    /// and is not focus-routed: a container forwards it to *every* child
735    /// unconditionally (before any capture/focus/hit-test branch) and reports
736    /// [`EventResult::Ignored`] regardless of what the children returned, so no
737    /// "first handler wins" short-circuit can hide a subtree from it. A leaf
738    /// widget with nothing deferred simply ignores it — the fall-through is
739    /// harmless by construction. It never opens or releases a capture, never
740    /// moves focus, and never blurs.
741    ///
742    /// # Naming
743    ///
744    /// Deliberately *not* `Tick`: `Tick` already means frame pacing in this
745    /// codebase ([`crate::widget::TickClass`]), and this variant has nothing to
746    /// do with the frame gate.
747    Housekeeping,
748    /// **Not user input either**: one floated overlay surface's own input,
749    /// broadcast to the whole tree so it reaches the owner that registered the
750    /// surface, wherever in the tree that owner sits.
751    ///
752    /// # Why a broadcast
753    ///
754    /// The owner of a floated surface is an ordinary widget somewhere in the
755    /// tree, and the pointer that hit its surface is nowhere near its own bounds
756    /// — that is the entire point of floating. Hit-testing the event would
757    /// therefore deliver it to whatever the main tree has under the pointer, and
758    /// focus-routing it would deliver it to a text field that has nothing to do
759    /// with the surface. Broadcasting is the only route that reaches the owner
760    /// without knowing where it is, so this is the **second** broadcast variant
761    /// (see [`InputEvent::is_broadcast`]), and every routing helper's existing
762    /// broadcast-first branch already forwards it correctly with no change.
763    ///
764    /// # Routing contract
765    ///
766    /// **Only the owner whose [`OverlayKey`] matches acts on it; every other
767    /// widget ignores it.** A container forwards it to every child
768    /// unconditionally — no hit test, no capture fast path, no focus gate — and
769    /// reports [`EventResult::Ignored`] regardless, exactly like
770    /// [`Housekeeping`](InputEvent::Housekeeping). A widget that is not an
771    /// overlay owner, or whose key differs, must fall through: the key
772    /// comparison is the whole addressing mechanism.
773    ///
774    /// At the root it is inert in the ways a broadcast must be — it advances no
775    /// hover epoch and never blurs — but, unlike `Housekeeping`, it *is* a real
776    /// user gesture underneath, so a focus request or a pointer capture bubbled
777    /// from inside the surface is honoured (see
778    /// [`crate::app::RenderRoot::event`]).
779    Overlay(OverlayEvent),
780}
781
782impl InputEvent {
783    /// The event's location, in the receiving widget's local coordinate space.
784    ///
785    /// [`InputEvent::Scale`] reports its [`ScaleEvent::focal`] point here, the
786    /// same way [`InputEvent::Scroll`] reports `position`. Focus-routed events
787    /// ([`InputEvent::Key`]/[`InputEvent::Ime`]/
788    /// [`InputEvent::EditCommand`]) and the two broadcasts
789    /// ([`Housekeeping`](InputEvent::Housekeeping) and
790    /// [`Overlay`](InputEvent::Overlay)) have no spatial position — they are
791    /// delivered down the focus chain, or to every child, not hit-tested — so
792    /// this reports [`Point::ZERO`] for them; callers must never hit-test on it
793    /// (routing helpers early-return both classes). An overlay event's *payload*
794    /// does carry a position, but in window space rather than in the receiver's
795    /// local space, which is precisely why it is not reported here (see
796    /// [`OverlayEventKind`]).
797    pub fn position(&self) -> Point {
798        match self {
799            InputEvent::Pointer(p) | InputEvent::PointerContact { event: p, .. } => p.position,
800            InputEvent::Scroll { position, .. } => *position,
801            InputEvent::Scale(scale) => scale.focal,
802            InputEvent::Key(_)
803            | InputEvent::Ime(_)
804            | InputEvent::EditCommand(_)
805            | InputEvent::Housekeeping
806            | InputEvent::Overlay(_) => Point::ZERO,
807        }
808    }
809
810    /// Return a copy of this event with its position shifted by `offset`.
811    ///
812    /// Containers use this (with `offset = -child_origin`) to translate an event
813    /// from their own coordinate space into a child's local space before
814    /// forwarding it — see [`crate::widget::ChildPod::event_child`].
815    /// [`InputEvent::Scale`] shifts its [`ScaleEvent::focal`] point the same way
816    /// [`InputEvent::Scroll`] shifts its `position`. Focus-routed
817    /// events ([`InputEvent::Key`]/[`InputEvent::Ime`]/
818    /// [`InputEvent::EditCommand`]) and the
819    /// [`Housekeeping`](InputEvent::Housekeeping) broadcast carry no position, so
820    /// they are returned unchanged (cloned). An
821    /// [`Overlay`](InputEvent::Overlay) event is returned unchanged for the
822    /// opposite reason — its payload carries a **window-space** position that the
823    /// container chain between the root and the owner must not shift, since that
824    /// chain describes where the *owner* sits and not where the floated surface
825    /// does (see [`OverlayEventKind`]).
826    pub fn translated(&self, offset: Vec2) -> InputEvent {
827        match self {
828            InputEvent::Pointer(p) => InputEvent::Pointer(PointerEvent {
829                position: p.position + offset,
830                ..*p
831            }),
832            InputEvent::PointerContact { pointer_id, event } => InputEvent::PointerContact {
833                pointer_id: *pointer_id,
834                event: PointerEvent {
835                    position: event.position + offset,
836                    ..*event
837                },
838            },
839            InputEvent::Scroll { position, delta } => InputEvent::Scroll {
840                position: *position + offset,
841                delta: *delta,
842            },
843            InputEvent::Scale(scale) => InputEvent::Scale(ScaleEvent {
844                focal: scale.focal + offset,
845                ..*scale
846            }),
847            InputEvent::Key(_)
848            | InputEvent::Ime(_)
849            | InputEvent::EditCommand(_)
850            | InputEvent::Housekeeping
851            | InputEvent::Overlay(_) => self.clone(),
852        }
853    }
854
855    /// Return a copy of this event with its position mapped through `affine` —
856    /// the general form of [`InputEvent::translated`], for a container that
857    /// places a child under an arbitrary transform
858    /// ([`crate::widget::ChildPod::set_transform`]).
859    ///
860    /// Maps exactly the positions `translated` shifts, and leaves alone exactly
861    /// what it leaves alone: [`InputEvent::Pointer`]'s position, the inner event
862    /// of an [`InputEvent::PointerContact`], [`InputEvent::Scroll`]'s `position`
863    /// and [`InputEvent::Scale`]'s [`ScaleEvent::focal`] are mapped; the
864    /// focus-routed events, the [`Housekeeping`](InputEvent::Housekeeping)
865    /// broadcast and the window-space [`Overlay`](InputEvent::Overlay) payload are
866    /// returned unchanged (cloned), for the reasons `translated` gives.
867    ///
868    /// Only *positions* are mapped. A scroll `delta`, a scale's multiplicative
869    /// `scale_delta` and its `velocity` are carried over as-is: they describe the
870    /// gesture's magnitude in the input device's terms, not a point in the
871    /// receiver's space.
872    ///
873    /// A container routing into a transformed child passes the **inverse** of the
874    /// child's local→container mapping here; the caller owns checking that the
875    /// inverse exists (see [`crate::hit::checked_inverse`]).
876    pub fn transformed(&self, affine: &Affine) -> InputEvent {
877        match self {
878            InputEvent::Pointer(p) => InputEvent::Pointer(PointerEvent {
879                position: *affine * p.position,
880                ..*p
881            }),
882            InputEvent::PointerContact { pointer_id, event } => InputEvent::PointerContact {
883                pointer_id: *pointer_id,
884                event: PointerEvent {
885                    position: *affine * event.position,
886                    ..*event
887                },
888            },
889            InputEvent::Scroll { position, delta } => InputEvent::Scroll {
890                position: *affine * *position,
891                delta: *delta,
892            },
893            InputEvent::Scale(scale) => InputEvent::Scale(ScaleEvent {
894                focal: *affine * scale.focal,
895                ..*scale
896            }),
897            InputEvent::Key(_)
898            | InputEvent::Ime(_)
899            | InputEvent::EditCommand(_)
900            | InputEvent::Housekeeping
901            | InputEvent::Overlay(_) => self.clone(),
902        }
903    }
904
905    /// Whether this event is focus-routed (delivered down the focus chain with no
906    /// hit test) rather than hit-tested by position.
907    ///
908    /// [`Housekeeping`](InputEvent::Housekeeping) is **not** focus-routed — it
909    /// reaches every child, focused or not; see
910    /// [`is_broadcast`](InputEvent::is_broadcast).
911    pub fn is_focus_routed(&self) -> bool {
912        matches!(
913            self,
914            InputEvent::Key(_) | InputEvent::Ime(_) | InputEvent::EditCommand(_)
915        )
916    }
917
918    /// Whether this event is a broadcast: forwarded to **every** child
919    /// unconditionally, with no hit test, no capture fast-path, and no focus
920    /// routing — [`InputEvent::Housekeeping`] and [`InputEvent::Overlay`].
921    ///
922    /// Every routing helper branches on this **first**, before its capture,
923    /// focus, and hit-test branches (`frust-widgets`'
924    /// `route_event`/`route_event_single`, and this crate's own
925    /// [`crate::component`] mirror), so a broadcast can never be swallowed by a
926    /// captured child or a `contains()` miss. That existing branch is exactly
927    /// what carries an overlay event to its owner with no router change: the two
928    /// variants differ in what they *mean* (a deferred callback flush vs one
929    /// floated surface's own input), not in how they travel.
930    pub fn is_broadcast(&self) -> bool {
931        matches!(self, InputEvent::Housekeeping | InputEvent::Overlay(_))
932    }
933}
934
935thread_local! {
936    /// The "a deferred state-bearing callback is queued somewhere in this
937    /// thread's tree" flag, raised by [`mark_pending_result_flush`] and drained
938    /// by [`take_pending_result_flush`].
939    ///
940    /// A side channel for the same reason [`crate::widget::report_retired_slot`]'s
941    /// `RETIRED_SLOTS` list is one: the widget that queues the callback is deep
942    /// inside a `View::rebuild` (a `BuildCtx` pass) with no
943    /// [`crate::app::RenderRoot`] handle to reach, and — unlike a paint pass — no
944    /// threaded per-frame sink.
945    ///
946    /// **Data-free on purpose.** Only the *fact* that a flush is owed rides here;
947    /// the callbacks themselves stay in the widget that queued them. Those
948    /// callbacks are `Rc<dyn Fn>` (`!Send`), so they can only ever be run on the
949    /// thread that queued them — which is exactly why this is `thread_local`
950    /// rather than a process-global `AtomicBool`. A global would let a
951    /// [`RenderRoot`](crate::app::RenderRoot) on one thread *drain a mark raised
952    /// on another*, broadcasting into a tree with nothing pending while the tree
953    /// that actually owes the flush is left waiting — silently reintroducing the
954    /// failure this broadcast exists to fix. UI-thread affinity is the same argument
955    /// `frust-reactive`'s `CAN_POP_PROVIDER` and `frust-widgets`' `PAGE_REACH`
956    /// make for their own `Rc`-backed state.
957    static PENDING_RESULT_FLUSH: Cell<bool> = const { Cell::new(false) };
958}
959
960/// Record that a widget queued a callback needing `&mut State` during a
961/// state-free pass, so [`crate::app::RenderRoot::rebuild`] dispatches an
962/// [`InputEvent::Housekeeping`] broadcast before the frame ends.
963///
964/// Idempotent: marking twice in one pass owes exactly one broadcast, and the
965/// broadcast reaches every widget that queued anything (see the variant's
966/// routing contract).
967///
968/// Thread-affine: the mark is visible only to the thread that raised it, which
969/// is also the only thread that can run the `!Send` callback it stands for.
970pub fn mark_pending_result_flush() {
971    PENDING_RESULT_FLUSH.with(|flag| flag.set(true));
972}
973
974/// Take (and clear) the [`mark_pending_result_flush`] flag.
975///
976/// Drained by [`crate::app::RenderRoot::rebuild`], which dispatches one
977/// [`InputEvent::Housekeeping`] broadcast per `true` it takes. Destructive,
978/// mirroring [`crate::app::RenderRoot::take_change_flags`]: a caller that drains
979/// and drops the result loses that flush until something marks again.
980pub fn take_pending_result_flush() -> bool {
981    PENDING_RESULT_FLUSH.with(|flag| flag.replace(false))
982}
983
984/// Non-draining peek at the [`mark_pending_result_flush`] flag — whether a
985/// deferred state-bearing callback is owed a [`InputEvent::Housekeeping`]
986/// broadcast, without consuming the mark.
987///
988/// The [`take_change_flags`](crate::app::RenderRoot::take_change_flags) /
989/// [`has_pending_change_flags`](crate::app::RenderRoot::has_pending_change_flags)
990/// pairing, one layer down: the mobile shells read this while gathering their
991/// frame-gate inputs (`FrameInputs::deferred_callbacks_pending`) *before*
992/// deciding whether the frame runs at all, so a frame the gate would otherwise
993/// skip still runs and reaches the [`crate::app::RenderRoot::rebuild`] that
994/// drains the mark. Peeking must not consume it — draining stays that rebuild's
995/// job.
996///
997/// Thread-affine like both of its neighbours: it reports only marks raised on
998/// the calling thread (see the `PENDING_RESULT_FLUSH` doc for why the flag is
999/// thread-local rather than a process-global `AtomicBool`).
1000pub fn has_pending_result_flush() -> bool {
1001    PENDING_RESULT_FLUSH.with(|flag| flag.get())
1002}
1003
1004thread_local! {
1005    /// The "a focused child pod lost its identity during this thread's view
1006    /// diff" flag, raised by [`mark_focus_orphaned`] and drained by
1007    /// [`take_focus_orphaned`].
1008    ///
1009    /// A side channel for exactly the reason [`PENDING_RESULT_FLUSH`] above is
1010    /// one: the reconciler that tears a focused pod down runs deep inside a
1011    /// `View::rebuild` (a [`crate::view::BuildCtx`] pass) with no
1012    /// [`RenderRoot`](crate::app::RenderRoot) handle to reach, so it cannot
1013    /// clear the root's `focus_active`/`ime_state` mirror itself — the
1014    /// long-standing desync `frust-widgets`' `cancel_active_children` documents.
1015    /// *Which* pods may raise it is narrowed by the pass's own focus chain
1016    /// ([`crate::view::BuildCtx::has_focus`]) — see [`mark_focus_orphaned`].
1017    ///
1018    /// **Data-free on purpose, and idempotent.** Only the *fact* that some
1019    /// focused pod died rides here; there is nothing useful to carry (the root
1020    /// keeps no id of the focused widget, only the boolean mirror). Several pods
1021    /// cleared in one diff owe exactly one release.
1022    ///
1023    /// Thread-local rather than a process-global `AtomicBool` for the same
1024    /// UI-thread-affinity reason: the tree that lost the focus, and the
1025    /// `RenderRoot` that must release the session, live on one thread. A global
1026    /// would let a root on one thread release a session another thread's tree
1027    /// still holds.
1028    static FOCUS_ORPHANED: Cell<bool> = const { Cell::new(false) };
1029}
1030
1031/// Record that a structural rebuild severed the recorded focus path — a focused
1032/// [`ChildPod`](crate::widget::ChildPod) was torn down, type-swapped, or had its
1033/// `focused` flag cleared by a reconciler — so
1034/// [`RenderRoot::rebuild`](crate::app::RenderRoot::rebuild) releases the whole
1035/// focus/IME session before the frame ends.
1036///
1037/// # The invariant: a mark means a LIVE session lost its owner
1038///
1039/// Raise this only when the severed link was on the **live focus chain** — the
1040/// pod's own `focused` flag AND
1041/// [`BuildCtx::has_focus`](crate::view::BuildCtx::has_focus), the composed chain
1042/// from the root down to it. A `focused` flag on its own is not evidence of a
1043/// session: a container-routed blur clears the focus link at the nearest common
1044/// ancestor only, so flags deeper in the blurred branch legitimately stay set,
1045/// and marking on one of those releases whatever field is *actually* focused
1046/// elsewhere in the tree — the keyboard dropping mid-typing because an unrelated
1047/// list recycled a row. Every drain here performs a real, user-visible release;
1048/// it must never fire on speculation.
1049///
1050/// Raised by `frust-widgets`' reconcilers (`teardown_child`,
1051/// `cancel_active_children`, and the type-swap arms of both the keyed reconciler
1052/// and the single-child `rebuild_child`), all four through one shared gate
1053/// (`mark_orphan_if_live`), and by
1054/// [`ComponentView::rebuild`](crate::component::ComponentView)'s own swap arm,
1055/// which spells the identical gate by hand because this crate sits below
1056/// `frust-widgets`. A hand-rolled container that clears a focused pod itself
1057/// should raise it under the same condition.
1058///
1059/// Idempotent and thread-affine, exactly like [`mark_pending_result_flush`].
1060pub fn mark_focus_orphaned() {
1061    FOCUS_ORPHANED.with(|flag| flag.set(true));
1062}
1063
1064/// Take (and clear) the [`mark_focus_orphaned`] flag.
1065///
1066/// Drained by [`RenderRoot::rebuild`](crate::app::RenderRoot::rebuild), which
1067/// performs one full focus/IME session release per `true` it takes. Destructive,
1068/// mirroring [`take_pending_result_flush`]: a caller that drains and drops the
1069/// result loses that release until something marks again.
1070///
1071/// A mark can only be raised *during* a view diff, and the diff's own
1072/// `RenderRoot::rebuild` drains it before returning, so the flag never survives
1073/// a frame — there is no peeking counterpart (unlike
1074/// [`has_pending_result_flush`], which a frame gate must consult before deciding
1075/// whether to run the rebuild that drains it at all).
1076pub fn take_focus_orphaned() -> bool {
1077    FOCUS_ORPHANED.with(|flag| flag.replace(false))
1078}
1079
1080thread_local! {
1081    /// Which root owes a hover end because a pod holding its LIVE hover link was
1082    /// dropped by this thread's view diff — raised by [`mark_hover_orphaned`] and
1083    /// drained by [`take_hover_orphaned`]. `None` when nothing is owed.
1084    ///
1085    /// Hover's analog of [`FOCUS_ORPHANED`], and a side channel for the same
1086    /// missing-handle reason: the reconciler that drops the claimant's
1087    /// [`ChildPod`](crate::widget::ChildPod) runs inside a
1088    /// [`View::rebuild`](crate::view::View::rebuild) with no
1089    /// [`RenderRoot`](crate::app::RenderRoot) to clear the root's hover mirror
1090    /// with. Idempotent for the same reason too: several pods severed in one diff
1091    /// owe exactly one hover end.
1092    ///
1093    /// **Root-qualified rather than data-free**, which is where it diverges from
1094    /// its focus neighbour. Focus is raised *and* drained inside one root's own
1095    /// `rebuild`, so a bare bool cannot reach a second root. A hover mark comes
1096    /// from a destructor, which fires whenever a pod happens to die — including
1097    /// while another root on the same thread is the one that rebuilds next — so
1098    /// the mark carries the identity of the root whose link died and only that
1099    /// root's drain consumes it. Two roots' epoch counters legitimately collide
1100    /// (each starts at `1` and advances per hover pass), so the identity, not the
1101    /// epoch, is what keeps them apart.
1102    ///
1103    /// One slot, so two roots severed between the same pair of rebuilds leave the
1104    /// later mark standing and the earlier root's mirror to lapse on its own next
1105    /// hover pass — the pre-existing degradation, never a release of a link that
1106    /// is still held.
1107    static HOVER_ORPHANED: Cell<Option<u64>> = const { Cell::new(None) };
1108
1109    /// The hover link standing on this thread right now as `(root identity,
1110    /// epoch)`, or `(0, 0)` when nothing holds one — published by
1111    /// [`RenderRoot::event`](crate::app::RenderRoot::event) whenever it closes a
1112    /// hover pass, and read by a dropping pod to tell a live link from a stale
1113    /// stamp ([`live_hover_link_is`]).
1114    ///
1115    /// The root would otherwise be unreachable from a destructor, and the
1116    /// distinction is the whole invariant: pods carrying *stale* stamps are
1117    /// dropped constantly (any recycled list row that was hovered at some point),
1118    /// and ending the hover on one of those would drop the chrome of whatever is
1119    /// hovered now.
1120    ///
1121    /// Thread-local for its neighbours' UI-thread-affinity reason. It mirrors
1122    /// **one** root, so a second `RenderRoot` driving passes on the same thread
1123    /// overwrites it — costing the first root's hover the drop-time check (its
1124    /// link then lapses on the next `Move`, the pre-existing behaviour) rather
1125    /// than corrupting anything. The root identity in the pair is what makes that
1126    /// last clause true: a pod of the overwritten root can no longer match the
1127    /// published link by an epoch integer the two roots happen to share, so it
1128    /// marks nothing instead of ending the *other* root's live hover.
1129    static LIVE_HOVER_LINK: Cell<(u64, u64)> = const { Cell::new((0, 0)) };
1130}
1131
1132/// Record that a [`ChildPod`](crate::widget::ChildPod) holding the **live** hover
1133/// link was dropped, so [`RenderRoot::rebuild`](crate::app::RenderRoot::rebuild)
1134/// ends the hover before the frame ends.
1135///
1136/// # Why a destructor, and not the reconcilers
1137///
1138/// [`mark_focus_orphaned`]'s callers are the reconcilers themselves, because a
1139/// focused pod's link is a flag they own and clear (`set_focused`). Hover has no
1140/// such flag and no setter: the link is an epoch stamp
1141/// ([`ChildPod::hover_epoch`](crate::widget::ChildPod::hover_epoch)) that only
1142/// [`ChildPod::event_child`](crate::widget::ChildPod::event_child) may write,
1143/// deliberately, so that no container can record or clear a hover by hand. A
1144/// container therefore *cannot* report its own severance, and hand-rolled
1145/// containers outside this workspace could never opt in. The pod reports instead,
1146/// from `Drop`, which covers every removal route — a truncated `Vec`, a
1147/// `None`-ed `Option`, a keyed reconciler's dropped entry, a whole subtree torn
1148/// down — with nothing to remember to call.
1149///
1150/// # The invariant: a mark means the LIVE link lost its owner
1151///
1152/// Exactly [`mark_focus_orphaned`]'s invariant, enforced by the stamp comparison
1153/// instead of a chain: a pod marks only when its own stamp is non-zero *and*
1154/// names the published link — same root identity, same epoch
1155/// ([`live_hover_link_is`]). A stale stamp — the far commoner case, since a
1156/// stamp is never cleared, only stranded by the next epoch advance — marks
1157/// nothing, and so does a stamp from a *different* root that happens to carry the
1158/// same epoch integer.
1159///
1160/// `root` is the identity the claim was stamped with (the root that ran the hover
1161/// pass), so only that root's [`take_hover_orphaned`] consumes the mark.
1162///
1163/// Idempotent and thread-affine, exactly like [`mark_focus_orphaned`].
1164pub(crate) fn mark_hover_orphaned(root: u64) {
1165    HOVER_ORPHANED.with(|slot| slot.set(Some(root)));
1166}
1167
1168/// Take (and clear) a [`mark_hover_orphaned`] mark raised for `root`.
1169///
1170/// Drained by [`RenderRoot::rebuild`](crate::app::RenderRoot::rebuild), which
1171/// ends its own standing hover link per `true` it takes. Destructive for the
1172/// matching root only, mirroring [`take_focus_orphaned`]: a mark another root
1173/// raised is left standing rather than consumed, which is what keeps two roots on
1174/// one thread from ending each other's hover.
1175pub(crate) fn take_hover_orphaned(root: u64) -> bool {
1176    HOVER_ORPHANED.with(|slot| {
1177        if slot.get() == Some(root) {
1178            slot.set(None);
1179            true
1180        } else {
1181            false
1182        }
1183    })
1184}
1185
1186/// Publish the hover link standing on this thread — `root`'s live epoch while one
1187/// of its widgets holds the link, epoch `0` while none does.
1188///
1189/// Called by [`RenderRoot::event`](crate::app::RenderRoot::event) as it closes a
1190/// hover pass, and by the rebuild-time end that [`take_hover_orphaned`] drives.
1191pub(crate) fn set_live_hover_link(root: u64, epoch: u64) {
1192    LIVE_HOVER_LINK.with(|slot| slot.set((root, epoch)));
1193}
1194
1195/// Whether `(root, epoch)` is the hover link standing on this thread — what a
1196/// dropping [`ChildPod`](crate::widget::ChildPod) compares its own stamp against
1197/// (see [`mark_hover_orphaned`]). Epoch `0` is "no link" and never matches.
1198pub(crate) fn live_hover_link_is(root: u64, epoch: u64) -> bool {
1199    epoch != 0 && LIVE_HOVER_LINK.with(|slot| slot.get()) == (root, epoch)
1200}
1201
1202thread_local! {
1203    /// The cursor a widget asked for during the request pass currently running on
1204    /// this thread — written by [`EventCtx::set_cursor`], bracketed by the
1205    /// [`RequestPass`] guard [`crate::app::RenderRoot::event`] holds for the
1206    /// length of its dispatch.
1207    ///
1208    /// A side channel for a *routing* reason rather than the missing-handle
1209    /// reason [`PENDING_RESULT_FLUSH`] and [`FOCUS_ORPHANED`] above have. Unlike
1210    /// capture, focus, and hover, a cursor request has nothing to record **per
1211    /// pod**: the root wants one value — whichever widget on the routed path
1212    /// spoke last — and no container between that widget and the root reads it or
1213    /// acts on it. Bubbling it pod by pod would mean widening every container's
1214    /// fold to carry a value no container uses.
1215    ///
1216    /// **Pass-scoped, not persistent.** The guard clears the slot before
1217    /// dispatching and drains it after, so a request never outlives its pass, and
1218    /// a [`EventCtx::set_cursor`] made from a dispatch no root drives (the
1219    /// `Cancel` a reconciler synthesizes during a rebuild, say) is dropped by the
1220    /// next pass's clear rather than leaking into it. Last write wins, which is
1221    /// what makes the innermost widget the routed path reaches the one that
1222    /// decides. A *nested* pass is scoped the same way and hands the slot back
1223    /// (see [`RequestPass`]).
1224    ///
1225    /// Thread-local rather than a process-global for the same UI-thread-affinity
1226    /// reason as its two neighbours: the tree that requests a cursor and the
1227    /// `RenderRoot` whose shell applies it live on one thread, and a global would
1228    /// let a hover on one thread reshape another window's pointer.
1229    static CURSOR_REQUEST: Cell<Option<CursorIcon>> = const { Cell::new(None) };
1230
1231    /// The text a widget asked the shell to put on the host clipboard during the
1232    /// request pass currently running on this thread — written by
1233    /// [`EventCtx::write_clipboard`], bracketed by the same [`RequestPass`] guard,
1234    /// and resolved by [`crate::app::RenderRoot::event`] into the value a shell
1235    /// drains through
1236    /// [`RenderRoot::take_clipboard_write`](crate::app::RenderRoot::take_clipboard_write).
1237    ///
1238    /// **A slot rather than a bubbled field, for [`CURSOR_REQUEST`]'s routing
1239    /// reason verbatim** (above): the root wants one value — whichever widget on
1240    /// the routed path spoke last — and no container between the copying widget
1241    /// and the root reads it or acts on it, so recording it per pod would widen
1242    /// every container's fold ([`EventCtx::absorb_child`], and with it
1243    /// [`crate::widget::ChildPod::event_child`] and every hand-written router in
1244    /// `frust-widgets`) to carry a payload no container uses. `ImeState` is
1245    /// bubbled precisely because containers *do* re-publish it; a clipboard write
1246    /// is a one-way message to the shell.
1247    ///
1248    /// **Pass-scoped and last-writer-wins**, exactly like the cursor: a write
1249    /// made outside any pass (a reconciler's synthesized `Cancel`) is dropped
1250    /// rather than leaked into the next pass, and a `Cut` that writes from an
1251    /// inner widget after its container wrote something else sends the inner
1252    /// widget's text.
1253    ///
1254    /// Thread-local for its neighbours' UI-thread-affinity reason: the tree that
1255    /// copies and the `RenderRoot` whose shell owns the host clipboard live on
1256    /// one thread.
1257    static CLIPBOARD_WRITE: Cell<Option<String>> = const { Cell::new(None) };
1258
1259    /// Whether a widget asked the shell to hand it the host clipboard's contents
1260    /// during the request pass currently running on this thread — raised by
1261    /// [`EventCtx::request_paste`], bracketed by the same [`RequestPass`] guard,
1262    /// and resolved by [`crate::app::RenderRoot::event`] into the flag a shell
1263    /// drains through
1264    /// [`RenderRoot::take_paste_request`](crate::app::RenderRoot::take_paste_request).
1265    ///
1266    /// **Data-free and idempotent**, like [`PENDING_RESULT_FLUSH`]: only the
1267    /// *fact* that a paste was asked for rides here, because the answer is the
1268    /// shell's to compose (it reads the host clipboard and dispatches
1269    /// [`InputEvent::EditCommand`]`(`[`EditCommand::Paste`]`)`). Two widgets
1270    /// asking in one pass owe exactly one read — there is one host clipboard and
1271    /// one focused widget to deliver it to, so "who asked" adds nothing.
1272    ///
1273    /// Pass-scoped and thread-local for [`CLIPBOARD_WRITE`]'s reasons.
1274    static PASTE_REQUEST: Cell<bool> = const { Cell::new(false) };
1275
1276    /// Edit commands one widget dispatched to another during the request pass
1277    /// currently running on this thread — pushed by
1278    /// [`EventCtx::dispatch_edit_command`] and drained, in order, by
1279    /// [`EventCtx::take_edit_commands`].
1280    ///
1281    /// **A widget-to-widget channel, not a shell-facing one**, which is what
1282    /// makes it different from its three neighbours above: the cursor, the
1283    /// clipboard write and the paste request all resolve *at the root* into
1284    /// something a shell reads, whereas this queue is drained by another widget
1285    /// in the same pass and the root never looks at it. It rides here anyway
1286    /// because the two widgets cannot reach each other any other way — a
1287    /// selection toolbar floated through [`crate::overlay`] is a pod the *text
1288    /// input* owns but does not contain, so the toolbar's "Copy" tap has no
1289    /// container path down which to hand the verb back.
1290    ///
1291    /// **A FIFO, not a last-writer-wins slot**: a toolbar may answer one tap with
1292    /// several verbs (a "cut" that is a copy then a delete), and order is
1293    /// meaning.
1294    ///
1295    /// Pass-scoped like its neighbours, and for a sharper reason: an undrained
1296    /// command must never re-fire in a later pass — a stale `Cut` applied to
1297    /// whatever is selected two gestures later would silently destroy text. A
1298    /// pass that ends with the queue non-empty therefore clears it (and says so
1299    /// in a debug build — see [`RequestPass::take`]) rather than carrying it.
1300    ///
1301    /// Thread-local for its neighbours' UI-thread-affinity reason.
1302    static EDIT_COMMAND_QUEUE: Cell<Vec<EditCommand>> = const { Cell::new(Vec::new()) };
1303
1304    /// Whether a request pass is open on this thread — `false` at rest, `true` for
1305    /// the length of one, however many are nested. Owned by [`RequestPass`], which
1306    /// is the only thing that reads or writes it: [`RequestPass::enter`] captures
1307    /// the previous value into the guard and `Drop` puts exactly that value back,
1308    /// so an unwind through a nested pass restores the enclosing pass's state
1309    /// rather than leaving a counter to unwind correctly on its own.
1310    ///
1311    /// The one question it answers is whether [`RequestPass::enter`] found an
1312    /// *enclosing* pass's in-progress requests in the slots (restore them on exit)
1313    /// or stray requests made outside any pass (drop them), which the slots' own
1314    /// contents cannot distinguish. One flag covers all three slots because one
1315    /// guard brackets all three: they open and close together, per dispatch.
1316    static REQUEST_PASS_OPEN: Cell<bool> = const { Cell::new(false) };
1317}
1318
1319/// One pass-scoped request slot's save/restore half — the mechanism [`RequestPass`]
1320/// owns three of.
1321///
1322/// Generic over the slot's payload rather than written out per channel: the
1323/// cursor, the clipboard write, and the paste flag differ only in what they
1324/// carry, and three hand-copied guards would be three places for the
1325/// stash-and-restore invariant to drift. `T::default()` is each slot's "nobody
1326/// asked" state (`None`, `None`, `false`), which is exactly what makes absence
1327/// the answer rather than a missing answer.
1328pub(crate) struct PassSlot<T: Default + 'static> {
1329    /// The thread-local this half brackets. A `&'static` handle so one generic
1330    /// body serves every channel — [`LocalKey::with`] needs the `'static`
1331    /// reference anyway.
1332    slot: &'static LocalKey<Cell<T>>,
1333    /// What the slot is restored to when this pass ends: the enclosing pass's
1334    /// in-progress request when nested, `T::default()` at the outermost level.
1335    restore: T,
1336}
1337
1338impl<T: Default + 'static> PassSlot<T> {
1339    /// Open this slot for a pass, starting it from "nobody has asked for
1340    /// anything" and stashing whatever an enclosing pass had collected.
1341    ///
1342    /// `nested` is the shared [`REQUEST_PASS_OPEN`] answer: only an enclosing
1343    /// pass is owed its value back, since a value found in the slot with no pass
1344    /// open is a stray (see [`CURSOR_REQUEST`]).
1345    pub(crate) fn enter(slot: &'static LocalKey<Cell<T>>, nested: bool) -> Self {
1346        let stashed = slot.with(|cell| cell.take());
1347        Self {
1348            slot,
1349            restore: if nested { stashed } else { T::default() },
1350        }
1351    }
1352
1353    /// Take what *this* pass recorded in this slot, leaving it empty.
1354    pub(crate) fn take(&self) -> T {
1355        self.slot.with(|cell| cell.take())
1356    }
1357}
1358
1359impl<T: Default + 'static> Drop for PassSlot<T> {
1360    fn drop(&mut self) {
1361        let restore = std::mem::take(&mut self.restore);
1362        self.slot.with(|cell| cell.set(restore));
1363    }
1364}
1365
1366/// A pass-scoped slot **carrying its own open flag** — [`PassSlot`] made
1367/// self-contained, for a channel bracketed by a *different* pass than the event
1368/// dispatch.
1369///
1370/// [`RequestPass`] brackets three channels that open and close together, so one
1371/// [`REQUEST_PASS_OPEN`] flag serves all three. A channel scoped to the **paint**
1372/// pass instead (the overlay registry and the selection-toolbar publish slot —
1373/// see [`crate::overlay`] and [`crate::selection_toolbar`]) cannot share that
1374/// flag: a paint pass runs with no event pass open, and an event pass with no
1375/// paint pass open, so borrowing the other's flag would answer "is an enclosing
1376/// pass of MY kind open?" with another kind's state and either restore a stray
1377/// or drop an enclosing pass's work. One flag per bracket is what keeps the
1378/// question well-posed.
1379///
1380/// Everything else is [`PassSlot`]'s, verbatim: enter stashes, [`take`](Self::take)
1381/// drains what this pass alone recorded, and `Drop` puts the enclosing pass's
1382/// stash back (or leaves the slot clear at the outermost level, so a value
1383/// written with no pass open is dropped rather than leaked into the next one).
1384///
1385/// Unlike [`RequestPass::take`] this drains behind `&self` rather than consuming
1386/// the guard: a paint pass resolves its channels *and then* keeps painting
1387/// (`RenderRoot::paint` drains the registry, then paints what it drained), so the
1388/// bracket has to outlive its own drain.
1389pub(crate) struct PassBracket<T: Default + 'static> {
1390    /// The value half, which owns the stash/restore invariant.
1391    slot: PassSlot<T>,
1392    /// This bracket's own "a pass is open" flag.
1393    open: &'static LocalKey<Cell<bool>>,
1394    /// Whether a pass of this kind was already open when this one entered — put
1395    /// back verbatim by `Drop`.
1396    was_open: bool,
1397}
1398
1399impl<T: Default + 'static> PassBracket<T> {
1400    /// Open a pass over `slot`, tracked by `open`, starting from
1401    /// `T::default()` ("nobody has recorded anything").
1402    pub(crate) fn enter(
1403        slot: &'static LocalKey<Cell<T>>,
1404        open: &'static LocalKey<Cell<bool>>,
1405    ) -> Self {
1406        let was_open = open.with(|flag| flag.replace(true));
1407        Self {
1408            slot: PassSlot::enter(slot, was_open),
1409            open,
1410            was_open,
1411        }
1412    }
1413
1414    /// Take what *this* pass recorded, leaving the slot empty.
1415    pub(crate) fn take(&self) -> T {
1416        self.slot.take()
1417    }
1418}
1419
1420impl<T: Default + 'static> Drop for PassBracket<T> {
1421    fn drop(&mut self) {
1422        // The inner `PassSlot` restores the value as it drops, right after this.
1423        self.open.with(|flag| flag.set(self.was_open));
1424    }
1425}
1426
1427/// Everything one request pass resolved: the three shell-facing values a
1428/// dispatch can produce, drained together by [`RequestPass::take`].
1429pub(crate) struct PassRequests {
1430    /// The cursor the pass's last [`EventCtx::set_cursor`] asked for; `None` when
1431    /// no widget asked, which resolves to [`CursorIcon::Default`].
1432    pub(crate) cursor: Option<CursorIcon>,
1433    /// The text the pass's last [`EventCtx::write_clipboard`] asked the shell to
1434    /// put on the host clipboard; `None` when no widget copied.
1435    pub(crate) clipboard_write: Option<String>,
1436    /// Whether any widget in the pass called [`EventCtx::request_paste`].
1437    pub(crate) paste_request: bool,
1438}
1439
1440/// The open/close bracket around one request pass, and the guard that makes the
1441/// pass-scoped slots above survive re-entrancy.
1442///
1443/// # Why a guard rather than a bare clear/take pair
1444///
1445/// Each slot is *pass-scoped*: cleared before a dispatch, drained after it, so a
1446/// request never outlives the pass that made it. Spelled as a bare
1447/// `clear_cursor_request` + `take_cursor_request` pair that contract holds
1448/// only while passes never nest — a nested dispatch's clear would erase a request
1449/// the enclosing pass had already collected, and its drain would take one the
1450/// enclosing pass was still owed. Nothing in this workspace nests a pass today
1451/// ([`RenderRoot::event`](crate::app::RenderRoot::event) documents the rule, and
1452/// the devtools injector hops its synthetic events onto the UI thread's queue
1453/// rather than calling into a live dispatch), but the failure is silent and the
1454/// cost of ruling it out is one stack slot.
1455///
1456/// # One guard, three channels
1457///
1458/// The cursor, the clipboard write and the paste request are all "one value the
1459/// root resolves at the end of the dispatch", so they share a bracket and a
1460/// single [`REQUEST_PASS_OPEN`] flag rather than three copies of this reasoning;
1461/// the per-slot half is [`PassSlot`], instantiated once per channel. What differs
1462/// is only what the root *does* with each value — see
1463/// [`RenderRoot::event`](crate::app::RenderRoot::event), where the cursor commits
1464/// on pointer-move passes alone while the two clipboard values commit on every
1465/// pass.
1466///
1467/// # What nesting resolves to
1468///
1469/// Save-and-restore, so **every** pass — nested or not — resolves exactly the
1470/// requests made inside it, and an inner pass returns the slots to the enclosing
1471/// pass untouched:
1472///
1473/// * [`RequestPass::enter`] stashes whatever the enclosing pass had collected and
1474///   starts the inner pass from empty (the same "absence *is* the answer" state a
1475///   top-level pass starts from).
1476/// * [`RequestPass::take`] drains what this pass alone recorded, and **consumes
1477///   the guard**: a pass resolves exactly once, and the drain is what closes it.
1478/// * `Drop` puts the enclosing pass's stash back — or, at the outermost level,
1479///   leaves the slots clear, exactly as the bare pair did, so a request made
1480///   outside any pass (a reconciler's synthesized `Cancel`) is still dropped
1481///   rather than leaked into the next one.
1482pub(crate) struct RequestPass {
1483    /// The cursor half of the bracket.
1484    cursor: PassSlot<Option<CursorIcon>>,
1485    /// The clipboard-write half.
1486    clipboard_write: PassSlot<Option<String>>,
1487    /// The paste-request half.
1488    paste_request: PassSlot<bool>,
1489    /// The widget-to-widget edit-command queue. Bracketed here rather than
1490    /// resolved into [`PassRequests`]: the root must never see it (its consumer
1491    /// is another widget in the same pass), so this half exists only to bound the
1492    /// queue's lifetime to the pass — see [`EDIT_COMMAND_QUEUE`].
1493    edit_commands: PassSlot<Vec<EditCommand>>,
1494    /// Whether a pass was already open when this one entered — put back verbatim
1495    /// by `Drop`, so an inner pass leaves the enclosing one open and the
1496    /// outermost leaves the thread at rest.
1497    was_open: bool,
1498}
1499
1500impl RequestPass {
1501    /// Open a request pass, starting every slot from "nobody has asked for
1502    /// anything".
1503    pub(crate) fn enter() -> Self {
1504        let was_open = REQUEST_PASS_OPEN.with(|open| open.replace(true));
1505        Self {
1506            cursor: PassSlot::enter(&CURSOR_REQUEST, was_open),
1507            clipboard_write: PassSlot::enter(&CLIPBOARD_WRITE, was_open),
1508            paste_request: PassSlot::enter(&PASTE_REQUEST, was_open),
1509            edit_commands: PassSlot::enter(&EDIT_COMMAND_QUEUE, was_open),
1510            was_open,
1511        }
1512    }
1513
1514    /// Take what *this* pass recorded — the values the root resolves into its
1515    /// shell-facing cursor, clipboard write and paste request.
1516    ///
1517    /// Consumes the guard, so the pass ends here: a second drain of the same pass
1518    /// is unrepresentable rather than a silent set of empties (the slots are
1519    /// drained destructively, so a repeat call would report "nobody asked" for a
1520    /// pass that had already resolved).
1521    pub(crate) fn take(self) -> PassRequests {
1522        // The edit-command queue is drained here but NOT reported: anything left
1523        // in it is a command whose intended consumer never called
1524        // `EventCtx::take_edit_commands` — a wiring bug in the dispatching
1525        // widget, not something the root can act on. Dropping it is the safe
1526        // resolution (a command that survived into a later pass would apply to
1527        // whatever is selected *then*), and a debug build says so rather than
1528        // swallowing it silently.
1529        let leaked = self.edit_commands.take();
1530        #[cfg(debug_assertions)]
1531        if !leaked.is_empty() {
1532            eprintln!(
1533                "frust-core: {} edit command(s) dispatched but never taken in this \
1534                 pass ({leaked:?}); clearing — the dispatching widget's consumer \
1535                 must call EventCtx::take_edit_commands in the same pass",
1536                leaked.len()
1537            );
1538        }
1539        drop(leaked);
1540        PassRequests {
1541            cursor: self.cursor.take(),
1542            clipboard_write: self.clipboard_write.take(),
1543            paste_request: self.paste_request.take(),
1544        }
1545    }
1546}
1547
1548impl Drop for RequestPass {
1549    fn drop(&mut self) {
1550        // Each `PassSlot` restores its own slot as it drops, right after this.
1551        REQUEST_PASS_OPEN.with(|open| open.set(self.was_open));
1552    }
1553}
1554
1555/// Clear any pending cursor request, so the pass about to run starts from
1556/// "nobody has asked for anything".
1557///
1558/// Absence is not a missing answer — it *is* the answer
1559/// ([`CursorIcon::Default`]), which is why the clear is what makes the request
1560/// model stateless: a widget that stops asking stops being obeyed, with nothing
1561/// to release.
1562///
1563/// **Test-only.** Production code brackets a pass with [`RequestPass`], which
1564/// owns both ends; this is the bare clear a test that drives a widget *with no
1565/// root above it* needs to start from a known slot (`component.rs`'s
1566/// component-boundary cursor test is the one caller).
1567#[cfg(test)]
1568pub(crate) fn clear_cursor_request() {
1569    CURSOR_REQUEST.with(|slot| slot.set(None));
1570}
1571
1572/// Take (and clear) the cursor requested during this pass, `None` when no widget
1573/// asked — the drain half of the test-only pair (see
1574/// [`clear_cursor_request`]); a root reaches the same value through
1575/// [`RequestPass::take`].
1576#[cfg(test)]
1577pub(crate) fn take_cursor_request() -> Option<CursorIcon> {
1578    CURSOR_REQUEST.with(|slot| slot.take())
1579}
1580
1581/// What the contact pass running on this thread knows about the pointer it is
1582/// dispatching — the state [`ContactPass`] brackets.
1583#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1584struct ContactPassState {
1585    /// Whether a root opened a contact pass at all. A
1586    /// [`EventCtx::capture_contacts`] made outside one (a widget driven with no
1587    /// root above it) records nothing here.
1588    open: bool,
1589    /// The contact being dispatched — what [`EventCtx::new`] seeds a fresh
1590    /// context's [`EventCtx::pointer_id`] with.
1591    pointer_id: PointerId,
1592    /// Whether the dispatch is a **non-claimant** contact travelling down a live
1593    /// capture's path (rule (c) of [`InputEvent::PointerContact`]'s contract).
1594    /// While set, [`crate::widget::ChildPod::set_active`] refuses to drop a
1595    /// recorded active link, so another contact's `Up`/`Cancel` cannot release
1596    /// the claimant's capture inside any container.
1597    secondary: bool,
1598    /// Whether a widget called [`EventCtx::capture_contacts`] during this pass
1599    /// — since the last [`EventCtx::release_captured_child`] that released the
1600    /// opted-in widget, which resets it (so the flag ends the pass naming only
1601    /// an opt-in that is still on the active path).
1602    contacts_requested: bool,
1603    /// Whether a container released the gesture's opted-in widget from the
1604    /// active path during this pass ([`EventCtx::release_captured_child`]).
1605    capture_released: bool,
1606}
1607
1608impl ContactPassState {
1609    /// Nothing open: the mouse, no secondary contact, nothing requested.
1610    const IDLE: ContactPassState = ContactPassState {
1611        open: false,
1612        pointer_id: PointerId::MOUSE,
1613        secondary: false,
1614        contacts_requested: false,
1615        capture_released: false,
1616    };
1617}
1618
1619thread_local! {
1620    /// The contact pass currently running on this thread — opened by
1621    /// [`crate::app::RenderRoot::event`] for each dispatch through a
1622    /// [`ContactPass`] guard.
1623    ///
1624    /// **A slot as well as a per-context field**, for the component boundary's
1625    /// sake: a [`crate::component`] element dispatches its subtree through a
1626    /// *fresh* [`EventCtx`] over its own local state, so anything carried only
1627    /// in the context would stop at it. Seeding [`EventCtx::new`] from here
1628    /// keeps [`EventCtx::pointer_id`] right below a component, and recording
1629    /// [`EventCtx::capture_contacts`] here lets the root see an opt-in made
1630    /// anywhere in the tree — the same reason the cursor request rides a slot
1631    /// (see [`CURSOR_REQUEST`]).
1632    ///
1633    /// Thread-local for its neighbours' UI-thread-affinity reason.
1634    static CONTACT_PASS: Cell<ContactPassState> = const { Cell::new(ContactPassState::IDLE) };
1635}
1636
1637/// The bracket around one root dispatch's contact identity: entering publishes
1638/// which contact is being dispatched (and whether it is a non-claimant one),
1639/// dropping restores whatever the enclosing pass had — so a dispatch that
1640/// re-enters [`crate::app::RenderRoot::event`] (the overlay pre-pass does)
1641/// scopes its own opt-in and hands the slot back on exit, unwind included.
1642pub(crate) struct ContactPass {
1643    saved: ContactPassState,
1644}
1645
1646impl ContactPass {
1647    /// Open a pass dispatching `pointer_id`; `secondary` marks a non-claimant
1648    /// contact routed down a live capture's path.
1649    pub(crate) fn enter(pointer_id: PointerId, secondary: bool) -> Self {
1650        let saved = CONTACT_PASS.with(|slot| {
1651            slot.replace(ContactPassState {
1652                open: true,
1653                pointer_id,
1654                secondary,
1655                contacts_requested: false,
1656                capture_released: false,
1657            })
1658        });
1659        Self { saved }
1660    }
1661
1662    /// Whether a widget called [`EventCtx::capture_contacts`] during this pass
1663    /// (and no later [`EventCtx::release_captured_child`] in it released that
1664    /// widget from the active path).
1665    pub(crate) fn contacts_requested(&self) -> bool {
1666        CONTACT_PASS.with(|slot| slot.get().contacts_requested)
1667    }
1668
1669    /// Whether a container released the gesture's opted-in widget from the
1670    /// active path during this pass ([`EventCtx::release_captured_child`]).
1671    pub(crate) fn capture_released(&self) -> bool {
1672        CONTACT_PASS.with(|slot| slot.get().capture_released)
1673    }
1674}
1675
1676impl Drop for ContactPass {
1677    fn drop(&mut self) {
1678        CONTACT_PASS.with(|slot| slot.set(self.saved));
1679    }
1680}
1681
1682/// The contact the pass running on this thread is dispatching —
1683/// [`PointerId::MOUSE`] outside any pass.
1684pub(crate) fn current_pointer_id() -> PointerId {
1685    CONTACT_PASS.with(|slot| slot.get().pointer_id)
1686}
1687
1688/// Whether the pass running on this thread is delivering a non-claimant
1689/// contact down a live capture's path (see [`ContactPassState::secondary`]).
1690pub(crate) fn in_secondary_contact_pass() -> bool {
1691    CONTACT_PASS.with(|slot| slot.get().secondary)
1692}
1693
1694/// Record a [`EventCtx::capture_contacts`] call in the open pass, if any, and
1695/// in the dispatch frame that made it.
1696fn note_contacts_requested() {
1697    CONTACT_PASS.with(|slot| {
1698        let mut state = slot.get();
1699        if state.open {
1700            state.contacts_requested = true;
1701            slot.set(state);
1702        }
1703    });
1704    CONTACT_FRAME.with(|slot| {
1705        let mut frame = slot.get();
1706        frame.opted_in = true;
1707        slot.set(frame);
1708    });
1709}
1710
1711/// Record an [`EventCtx::release_captured_child`] that took the opted-in
1712/// widget off the active path: the pass's opt-in is void from here on, unless a
1713/// later [`EventCtx::capture_contacts`] in the same pass (the container that
1714/// took over opting in itself) records a fresh one.
1715fn note_capture_released() {
1716    CONTACT_PASS.with(|slot| {
1717        let mut state = slot.get();
1718        if state.open {
1719            state.capture_released = true;
1720            state.contacts_requested = false;
1721            slot.set(state);
1722        }
1723    });
1724}
1725
1726/// What one widget dispatch — a [`crate::widget::ChildPod::event_child`] call,
1727/// or the root's own call into its root widget — learned about the gesture's
1728/// contact opt-in. The state [`ContactFrame`] brackets.
1729#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1730struct ContactFrameState {
1731    /// The dispatched widget itself called [`EventCtx::capture_contacts`].
1732    opted_in: bool,
1733    /// A widget below it did (folded in when each nested frame closes).
1734    opted_in_below: bool,
1735    /// The dispatch runs inside the handler of the widget that holds the live
1736    /// opt-in (that widget's own frame included): a release made here never
1737    /// takes the opt-in off the active path, because its holder stays on it.
1738    under_captor: bool,
1739}
1740
1741thread_local! {
1742    /// The innermost widget dispatch's [`ContactFrameState`]. A slot rather
1743    /// than an [`EventCtx`] field for [`CONTACT_PASS`]'s reason: a component
1744    /// boundary dispatches through a fresh context, and the opt-in must still be
1745    /// attributed to the pod it was made under.
1746    static CONTACT_FRAME: Cell<ContactFrameState> =
1747        const { Cell::new(ContactFrameState { opted_in: false, opted_in_below: false, under_captor: false }) };
1748}
1749
1750/// The bracket around one widget dispatch's contact opt-in bookkeeping: entering
1751/// opens a clean frame, [`ContactFrame::close`] reports whether the dispatched
1752/// widget opted in itself and whether anything at or below it did, and dropping
1753/// restores the enclosing frame with this one's opt-in folded into its
1754/// `opted_in_below` (unwind included).
1755pub(crate) struct ContactFrame {
1756    saved: ContactFrameState,
1757}
1758
1759impl ContactFrame {
1760    /// Open a frame for one widget dispatch; `captor` marks the dispatched
1761    /// widget as the holder of the live opt-in.
1762    pub(crate) fn enter(captor: bool) -> Self {
1763        let saved = CONTACT_FRAME.with(|slot| {
1764            let saved = slot.get();
1765            slot.set(ContactFrameState {
1766                opted_in: false,
1767                opted_in_below: false,
1768                under_captor: saved.under_captor || captor,
1769            });
1770            saved
1771        });
1772        Self { saved }
1773    }
1774
1775    /// Close the frame: `(opted_in, opted_in_at_or_below)` for the dispatched
1776    /// widget.
1777    pub(crate) fn close(self) -> (bool, bool) {
1778        let frame = CONTACT_FRAME.with(|slot| slot.get());
1779        (frame.opted_in, frame.opted_in || frame.opted_in_below)
1780    }
1781}
1782
1783impl Drop for ContactFrame {
1784    fn drop(&mut self) {
1785        CONTACT_FRAME.with(|slot| {
1786            let frame = slot.get();
1787            let mut restored = self.saved;
1788            restored.opted_in_below |= frame.opted_in || frame.opted_in_below;
1789            slot.set(restored);
1790        });
1791    }
1792}
1793
1794/// Whether the dispatch running on this thread is inside the handler of the
1795/// widget that holds the live contact opt-in (see
1796/// [`ContactFrameState::under_captor`]).
1797fn under_contact_captor() -> bool {
1798    CONTACT_FRAME.with(|slot| slot.get().under_captor)
1799}
1800
1801/// One level of a **secondary-contact walk**: the real event, in the coordinate
1802/// space of the container currently being handed the walk's carrier, and what
1803/// the walk delivered below that container.
1804struct SecondaryWalkFrame {
1805    /// The non-claimant contact's event, in the space of the container whose
1806    /// handler is running (i.e. what that container's own `ChildPod`s
1807    /// receive in their parent's space).
1808    event: InputEvent,
1809    /// The result of the delivery the walk made below this container, `None`
1810    /// until one happened.
1811    delivered: Option<EventResult>,
1812}
1813
1814thread_local! {
1815    /// The secondary-contact walk running on this thread, if any — innermost
1816    /// level only; each level saves and restores its enclosing one.
1817    ///
1818    /// A slot for the same component-boundary reason as [`CONTACT_PASS`], and
1819    /// because the walk's real event has to cross a container's handler that is
1820    /// only ever handed the inert [`secondary_walk_carrier`].
1821    static SECONDARY_WALK: std::cell::RefCell<Option<SecondaryWalkFrame>> =
1822        const { std::cell::RefCell::new(None) };
1823
1824    /// The [`OverlayKey`] the walk's carrier is addressed to — allocated once
1825    /// per thread from [`OverlayKey::next`], so it is distinct from every key an
1826    /// overlay owner holds and the carrier is ignored by all of them.
1827    static SECONDARY_WALK_KEY: OverlayKey = OverlayKey::next();
1828}
1829
1830/// The event a container on the active chain is handed while a non-claimant
1831/// contact walks past it: an [`InputEvent::Overlay`] broadcast addressed to a
1832/// key no owner holds. Every container already forwards a broadcast to its
1833/// children before running any gesture, capture, focus or hit-test logic of its
1834/// own (the broadcast-first rule), and every widget ignores an overlay event
1835/// addressed to someone else — so a container's handler runs, but none of its
1836/// pointer machinery does.
1837pub(crate) fn secondary_walk_carrier() -> InputEvent {
1838    InputEvent::Overlay(OverlayEvent {
1839        key: SECONDARY_WALK_KEY.with(|key| *key),
1840        kind: OverlayEventKind::OutsideDown,
1841    })
1842}
1843
1844/// Whether `event` is [`secondary_walk_carrier`]'s carrier.
1845fn is_secondary_walk_carrier(event: &InputEvent) -> bool {
1846    matches!(event, InputEvent::Overlay(overlay)
1847        if overlay.key == SECONDARY_WALK_KEY.with(|key| *key))
1848}
1849
1850/// The real event a secondary-contact walk is carrying past the container
1851/// that just routed `event` — `Some` only when a walk is running **and** `event`
1852/// is its carrier (a container that synthesizes an event of its own mid-walk
1853/// dispatches it normally).
1854pub(crate) fn secondary_walk_event(event: &InputEvent) -> Option<InputEvent> {
1855    if !is_secondary_walk_carrier(event) {
1856        return None;
1857    }
1858    SECONDARY_WALK.with(|slot| slot.borrow().as_ref().map(|frame| frame.event.clone()))
1859}
1860
1861/// Restores the enclosing walk level on drop (unwind included).
1862struct SecondaryWalkRestore(Option<SecondaryWalkFrame>);
1863
1864impl Drop for SecondaryWalkRestore {
1865    fn drop(&mut self) {
1866        let saved = self.0.take();
1867        SECONDARY_WALK.with(|slot| *slot.borrow_mut() = saved);
1868    }
1869}
1870
1871/// Run `f` as one level of a secondary-contact walk carrying `event` (in the
1872/// space of the container `f` hands the carrier to), returning `f`'s result and
1873/// what the walk delivered below it — `None` when the carrier never reached a
1874/// `ChildPod` on the active chain.
1875pub(crate) fn run_secondary_walk<R>(
1876    event: InputEvent,
1877    f: impl FnOnce() -> R,
1878) -> (R, Option<EventResult>) {
1879    let saved = SECONDARY_WALK.with(|slot| {
1880        slot.borrow_mut().replace(SecondaryWalkFrame {
1881            event,
1882            delivered: None,
1883        })
1884    });
1885    let restore = SecondaryWalkRestore(saved);
1886    let result = f();
1887    let delivered =
1888        SECONDARY_WALK.with(|slot| slot.borrow().as_ref().and_then(|frame| frame.delivered));
1889    drop(restore);
1890    (result, delivered)
1891}
1892
1893/// Run `f` with no secondary-contact walk in force — how the walk hands the
1894/// real event to the widget it ends at, whose own subtree then routes it the
1895/// ordinary way.
1896pub(crate) fn without_secondary_walk<R>(f: impl FnOnce() -> R) -> R {
1897    let saved = SECONDARY_WALK.with(|slot| slot.borrow_mut().take());
1898    let _restore = SecondaryWalkRestore(saved);
1899    f()
1900}
1901
1902/// Record that the walk level in force delivered the real event below its
1903/// container, with `result`.
1904pub(crate) fn note_secondary_delivered(result: EventResult) {
1905    SECONDARY_WALK.with(|slot| {
1906        if let Some(frame) = slot.borrow_mut().as_mut() {
1907            frame.delivered = Some(result);
1908        }
1909    });
1910}
1911
1912/// What kind of content a focused editable field holds — the hint a widget
1913/// publishes so each shell can configure the platform input method.
1914///
1915/// This is the framework's **input-purpose vocabulary**: renderer- and
1916/// platform-neutral names a widget states its intent in, which each shell maps
1917/// onto its own host API. It is deliberately tiny — it exists to let a secret
1918/// field tell the platform it is secret, not to model every keyboard layout.
1919///
1920/// # Why this exists (security, not ergonomics)
1921///
1922/// Visual masking (`TextInput::obscured`) hides the glyphs the *app* draws; it
1923/// says nothing to the input method. A stock soft keyboard given no hint will
1924/// happily render the field's text in its suggestion strip **above** the masked
1925/// field, and may commit it to its persistent learned-word dictionary. Only a
1926/// content-type hint suppresses that; an accessibility `Role::PasswordInput`
1927/// does not.
1928///
1929/// # Platform mapping
1930///
1931/// Each shell owns its own constants (core holds no platform integers). The
1932/// intended mapping, which downstream shell work must honour:
1933///
1934/// | Variant | Android (`InputType` / `EditorInfo.imeOptions`) | iOS (`UITextInputTraits`) | Desktop (winit) |
1935/// |---|---|---|---|
1936/// | [`Normal`](Self::Normal) | `TYPE_CLASS_TEXT` | platform defaults | `ImePurpose::Normal` |
1937/// | [`Password`](Self::Password) | `TYPE_CLASS_TEXT \| TYPE_TEXT_VARIATION_PASSWORD`, plus `TYPE_TEXT_FLAG_NO_SUGGESTIONS` and `IME_FLAG_NO_PERSONALIZED_LEARNING` | `isSecureTextEntry = true`, `textContentType = .password`, `autocorrectionType = .no`, `spellCheckingType = .no`, plus smart-punctuation suppression (see [`Terminal`](Self::Terminal)) | `ImePurpose::Password` |
1938/// | [`NoSuggestions`](Self::NoSuggestions) | `TYPE_CLASS_TEXT \| TYPE_TEXT_FLAG_NO_SUGGESTIONS`, plus `IME_FLAG_NO_PERSONALIZED_LEARNING` | `autocorrectionType = .no`, `spellCheckingType = .no`, plus smart-punctuation suppression | no equivalent — `ImePurpose::Normal` |
1939/// | [`Terminal`](Self::Terminal) | `TYPE_CLASS_TEXT \| TYPE_TEXT_FLAG_NO_SUGGESTIONS`, plus `IME_FLAG_NO_PERSONALIZED_LEARNING` (same as `NoSuggestions`) | `isSecureTextEntry = false`, `autocorrectionType = .no`, `spellCheckingType = .no`, `smartQuotesType = .no`, `smartDashesType = .no`, `smartInsertDeleteType = .no`, `autocapitalizationType = .none`, `textContentType = nil` | `ImePurpose::Terminal` |
1940///
1941/// Sources: Android `android.text.InputType` / `android.view.inputmethod.EditorInfo`
1942/// and Apple `UITextInputTraits` reference docs, retrieved 2026-08-01.
1943///
1944/// **Unsupported is a first-class outcome.** winit 0.30's
1945/// `Window::set_ime_purpose` is documented as unsupported on iOS/Android/Web/
1946/// Windows/X11/macOS/Orbital (Wayland text-input-v3 is the only implementation),
1947/// so the desktop shell may legitimately honour nothing here. A shell that
1948/// cannot express a hint drops it — it must never refuse to publish, and core
1949/// never asserts that a hint took effect.
1950///
1951/// # Matching rule for shells
1952///
1953/// This enum is `#[non_exhaustive]`: adding a variant later (numeric password,
1954/// email, one-time code…) must not break a shell. So a shell branches its
1955/// **security** behaviour on [`is_secret`](Self::is_secret) /
1956/// [`suppresses_suggestions`](Self::suppresses_suggestions), never on a variant
1957/// match with a `_ =>` fallback — a catch-all arm would silently downgrade a
1958/// future secret variant to a non-secret keyboard, which is exactly the leak
1959/// this type exists to close. Variant matching is fine for the *cosmetic*
1960/// choice (which keyboard layout to request).
1961#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1962#[non_exhaustive]
1963pub enum ImeContentType {
1964    /// No hint: ordinary text, platform defaults (suggestions, autocorrect and
1965    /// personalized learning all as the user configured them).
1966    ///
1967    /// The default, and what every field publishes unless it opts in.
1968    #[default]
1969    Normal,
1970    /// Secret text (password / passphrase / PIN entered as text).
1971    ///
1972    /// The shell must request secure entry *and* suppress suggestions and
1973    /// personalized learning.
1974    Password,
1975    /// Non-secret text that must not be autocorrected, suggested, or learned
1976    /// (recovery codes, identifiers, usernames).
1977    ///
1978    /// Distinct from [`Password`](Self::Password): the platform does **not**
1979    /// switch to secure entry, so autofill/reveal-last-character behaviour is
1980    /// unchanged; only the suggestion/learning channel is closed.
1981    NoSuggestions,
1982    /// A raw byte-entry surface (a terminal/shell keystroke source): no
1983    /// suggestion strip, no autocorrect, no smart quotes/dashes/insert-delete,
1984    /// no autocapitalization. Text is **not** masked — this is not a secret
1985    /// field, it is a field where every character the user typed must reach
1986    /// the app byte-for-byte with zero platform "correction" applied to it.
1987    ///
1988    /// The defect this closes is the same class [`Password`](Self::Password)
1989    /// closes for secrets: a smart keyboard silently substituting `"` for a
1990    /// curly quote or `--` for an em dash corrupts a shell command exactly as
1991    /// it corrupts a password, just without the confidentiality angle. Distinct
1992    /// from [`NoSuggestions`](Self::NoSuggestions): that variant suppresses the
1993    /// suggestion/learning channel only, while `Terminal` additionally
1994    /// suppresses smart punctuation and autocapitalization, both of which
1995    /// silently rewrite the text a suggestion-only hint leaves untouched.
1996    Terminal,
1997}
1998
1999impl ImeContentType {
2000    /// Whether the field holds a secret the platform must treat as such
2001    /// (secure entry on iOS, a password `InputType` variation on Android).
2002    ///
2003    /// Shells gate secure-entry configuration on this, not on a variant match
2004    /// (see the type docs' matching rule).
2005    ///
2006    /// This predicate and [`suppresses_suggestions`](Self::suppresses_suggestions)
2007    /// match exhaustively (no `_` arm) on purpose: adding a variant to this enum
2008    /// is a compile error here until it is classified as secret or not.
2009    pub fn is_secret(self) -> bool {
2010        match self {
2011            Self::Password => true,
2012            Self::Normal | Self::NoSuggestions | Self::Terminal => false,
2013        }
2014    }
2015
2016    /// Whether the platform must suppress its suggestion strip, autocorrect,
2017    /// and persistent word learning for this field.
2018    ///
2019    /// True for every secret content type and for
2020    /// [`NoSuggestions`](Self::NoSuggestions) and [`Terminal`](Self::Terminal).
2021    pub fn suppresses_suggestions(self) -> bool {
2022        match self {
2023            Self::Password | Self::NoSuggestions | Self::Terminal => true,
2024            Self::Normal => false,
2025        }
2026    }
2027}
2028
2029/// The IME-relevant surface a focused editable widget publishes for the shell.
2030///
2031/// Written by the focused widget through [`EventCtx::publish_ime_state`], it
2032/// bubbles up the focus chain and is stored on [`crate::app::RenderRoot`], where
2033/// the shell reads it via [`crate::app::RenderRoot::ime_state`] to drive the
2034/// platform IME (winit `set_ime_cursor_area`, Android `updateSelection`, iOS
2035/// `inputDelegate`). See the module docs for the index boundary rule.
2036///
2037/// # `editing` carries the real text, even for a secret field
2038///
2039/// [`content_type`](Self::content_type) marks a field secret; it does **not**
2040/// redact [`editing`](Self::editing). That is deliberate: this struct is one
2041/// half of a **bidirectional state-sync mirror** (see `docs/CODE_STANDARDS.md`'s
2042/// state-sync rule) — the platform keeps a local `Editable`/`UITextInput` mirror
2043/// seeded from these exact fields and hands a whole reconciled
2044/// [`EditingState`] back through [`ImeEvent::ApplyEditingState`]. Publishing
2045/// redacted or masked text would desynchronize that mirror (the platform would
2046/// compute deletions/replacements against text the widget does not have, and
2047/// would echo the mask back as the field's new value), and it would not close
2048/// the leak anyway: the keyboard process is where the characters originate.
2049/// What a hint *does* close is the suggestion strip reading the field's text and
2050/// the IME persisting it to a learned-word dictionary.
2051///
2052/// **Residual exposure:** the plaintext still crosses the FFI seam into the
2053/// platform IME. A hostile or non-compliant third-party keyboard can read it.
2054/// That is unavoidable on both mobile platforms short of not using the platform
2055/// IME at all. As partial mitigation, this type's [`fmt::Debug`] redacts the
2056/// text whenever the content type is secret, so a trace log never carries it.
2057#[derive(Clone, PartialEq)]
2058pub struct ImeState {
2059    /// Whether the focused widget currently wants IME active.
2060    pub active: bool,
2061    /// The current editing state (UTF-16 indexed at this shell-facing surface).
2062    pub editing: EditingState,
2063    /// The caret rectangle in logical coordinates, for IME candidate placement.
2064    pub caret: Option<Rect>,
2065    /// What kind of content the field holds, so the shell can configure the
2066    /// platform IME. Defaults to [`ImeContentType::Normal`] — a field that says
2067    /// nothing behaves exactly as it did before this hint existed.
2068    pub content_type: ImeContentType,
2069    /// Whether the field wants the platform input surface **without** an
2070    /// on-screen keyboard.
2071    ///
2072    /// A field whose text cannot be changed is still focusable and copyable
2073    /// (Material 3 and Apple's HIG both keep it so), and copying is exactly
2074    /// what needs the surface: the web overlay `<input>`'s DOM `copy`
2075    /// listener, Android's `InputConnection` and iOS's first responder are
2076    /// each the route a clipboard verb travels, and all three exist only while
2077    /// [`active`](Self::active) holds. What such a field does not need is
2078    /// somewhere to type — so this asks the shell to keep the surface wired and
2079    /// suppress the soft keyboard it would otherwise raise.
2080    ///
2081    /// Defaults to `false` — a field that says nothing behaves exactly as it
2082    /// did before this hint existed. It says nothing about an inactive surface
2083    /// (there is no keyboard up to suppress), and a shell with no on-screen
2084    /// keyboard of its own has nothing to do for it.
2085    pub suppress_soft_keyboard: bool,
2086}
2087
2088impl Default for ImeState {
2089    /// A cleared, inactive surface with no hint — what a container publishes
2090    /// when it stops routing to an editable child.
2091    fn default() -> Self {
2092        Self {
2093            active: false,
2094            editing: EditingState::default(),
2095            caret: None,
2096            content_type: ImeContentType::Normal,
2097            suppress_soft_keyboard: false,
2098        }
2099    }
2100}
2101
2102impl fmt::Debug for ImeState {
2103    /// Hand-written so a secret field's text never reaches a log.
2104    ///
2105    /// Everything except [`EditingState::text`] prints as derived; for a secret
2106    /// [`content_type`](Self::content_type) the text is replaced by
2107    /// `<redacted>` (no length, which would itself leak).
2108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2109        struct Redacted<'a>(&'a EditingState);
2110        impl fmt::Debug for Redacted<'_> {
2111            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2112                f.debug_struct("EditingState")
2113                    .field("text", &"<redacted>")
2114                    .field("selection_base", &self.0.selection_base)
2115                    .field("selection_extent", &self.0.selection_extent)
2116                    .field("composing_base", &self.0.composing_base)
2117                    .field("composing_extent", &self.0.composing_extent)
2118                    .finish()
2119            }
2120        }
2121
2122        let mut s = f.debug_struct("ImeState");
2123        s.field("active", &self.active);
2124        if self.content_type.is_secret() {
2125            s.field("editing", &Redacted(&self.editing));
2126        } else {
2127            s.field("editing", &self.editing);
2128        }
2129        s.field("caret", &self.caret)
2130            .field("content_type", &self.content_type)
2131            .field("suppress_soft_keyboard", &self.suppress_soft_keyboard)
2132            .finish()
2133    }
2134}
2135
2136/// What a widget did with an event.
2137///
2138/// `Handled` stops the enclosing container from offering the event to further
2139/// siblings and marks the frame dirty; `Ignored` lets routing continue.
2140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2141pub enum EventResult {
2142    /// The widget did not consume the event.
2143    Ignored,
2144    /// The widget consumed the event.
2145    Handled,
2146}
2147
2148/// The result of a whole [`crate::app::RenderRoot::event`] pass.
2149///
2150/// `handled` is whether any widget consumed the event; `needs_redraw` is whether
2151/// the shell should schedule a repaint (a handled event or an explicit
2152/// [`EventCtx::request_redraw`]). The shell turns `needs_redraw` into a
2153/// `window.request_redraw()` — the event pass itself never rebuilds or repaints.
2154#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2155pub struct EventOutcome {
2156    /// Whether the event was consumed by the tree.
2157    pub handled: bool,
2158    /// Whether the shell should schedule a redraw as a result.
2159    pub needs_redraw: bool,
2160}
2161
2162/// Context threaded into [`crate::widget::Widget::event`].
2163///
2164/// Gives an event handler three capabilities: mutate the (type-erased)
2165/// application state, request a redraw, and capture the pointer. It also carries
2166/// the receiving widget's own geometry ([`EventCtx::origin`]/[`EventCtx::size`])
2167/// so handlers can do local-coordinate math (the event `position` is already in
2168/// the widget's local space; origin/size describe where that widget sits in and
2169/// how big it is within its parent).
2170///
2171/// `State` is erased as `&mut dyn Any` — the same pattern
2172/// [`crate::widget::LayoutCtx`] uses for the text context — so `frust-core`
2173/// carries no knowledge of the concrete app state type; a handler recovers it
2174/// with [`EventCtx::state_mut`].
2175pub struct EventCtx<'a> {
2176    state: &'a mut dyn Any,
2177    needs_redraw: bool,
2178    capture_requested: bool,
2179    /// Set by [`EventCtx::capture_contacts`]; bubbles up beside
2180    /// `capture_requested` so a container can see the opt-in.
2181    contacts_requested: bool,
2182    /// Set by [`EventCtx::release_captured_child`] when the release took the
2183    /// gesture's contact opt-in off the active path; bubbles up beside
2184    /// `capture_requested` ([`EventCtx::is_capture_released`]).
2185    capture_released: bool,
2186    /// Which contact this dispatch carries ([`EventCtx::pointer_id`]); threaded
2187    /// unchanged from parent to child.
2188    pointer_id: PointerId,
2189    /// Set by [`EventCtx::request_focus`]; read by the enclosing container to
2190    /// record which child holds the focus path (mirrors `capture_requested`).
2191    focus_requested: bool,
2192    /// Set by [`EventCtx::release_focus`]; drops the recorded focus path.
2193    focus_released: bool,
2194    /// Whether the receiving widget currently holds focus (threaded down from its
2195    /// pod's recorded focus flag; seeded from the root focus state at the root).
2196    has_focus: bool,
2197    /// Whether the receiving widget is on the recorded hover path — the pointer is
2198    /// over it or over a descendant of it, as of the last completed hover pass.
2199    /// Threaded down from its
2200    /// pod's recorded hover stamp (see `hover_epoch`), seeded from
2201    /// [`crate::app::RenderRoot`]'s own hover mirror at the root. The event-pass
2202    /// mirror of [`PaintCtx::is_hovered`](crate::widget::PaintCtx::is_hovered).
2203    hovered: bool,
2204    /// Set by [`EventCtx::claim_hover`]; read by the enclosing container, which
2205    /// stamps the claim onto its child's pod and bubbles it further up (the hover
2206    /// mirror of `focus_requested`).
2207    hover_claimed: bool,
2208    /// Whether a [`EventCtx::claim_hover`] call in this (sub)dispatch records
2209    /// anything at all. `false` unless the root marked this pass an uncaptured
2210    /// [`PointerPhase::Move`], and narrowed further on the way down: a pod holding
2211    /// the capture path, or a pass in which a claim was already recorded, hands
2212    /// its child an ineligible context. This is what makes "a captured pointer
2213    /// never creates hover" and "at most one claimant per pass" true by
2214    /// construction rather than by a check the root has to remember.
2215    hover_eligible: bool,
2216    /// The hover epoch of the last **completed** hover pass — what a pod's
2217    /// recorded stamp must equal for its link to still count
2218    /// ([`EventCtx::hover_epoch`]). A claim made during *this* pass records
2219    /// [`EventCtx::hover_claim_epoch`] instead (one past this value), because the
2220    /// root advances its own epoch when the pass ends.
2221    hover_epoch: u64,
2222    /// The identity of the [`crate::app::RenderRoot`] running this pass, stamped
2223    /// onto a pod beside the claim epoch ([`EventCtx::hover_root`]) so a pod
2224    /// dropped later can tell its own root's live link from another root's
2225    /// identically-numbered epoch. `0` outside a root-driven pass.
2226    hover_root: u64,
2227    /// The IME surface the focused widget published this dispatch, if any; bubbles
2228    /// up the focus chain to [`crate::app::RenderRoot`].
2229    ime_state: Option<ImeState>,
2230    origin: Point,
2231    size: Size,
2232}
2233
2234impl<'a> EventCtx<'a> {
2235    /// Build a root event context over the erased application `state` for a
2236    /// widget placed at `origin` with `size`.
2237    pub fn new(state: &'a mut dyn Any, origin: Point, size: Size) -> Self {
2238        Self {
2239            state,
2240            needs_redraw: false,
2241            capture_requested: false,
2242            contacts_requested: false,
2243            capture_released: false,
2244            // The contact the running root pass is dispatching, so a context
2245            // built mid-pass (a component's inner one) reports the same id its
2246            // enclosing context does; the mouse outside any pass.
2247            pointer_id: current_pointer_id(),
2248            focus_requested: false,
2249            focus_released: false,
2250            has_focus: false,
2251            hovered: false,
2252            hover_claimed: false,
2253            hover_eligible: false,
2254            hover_epoch: 0,
2255            hover_root: 0,
2256            ime_state: None,
2257            origin,
2258            size,
2259        }
2260    }
2261
2262    /// Recover the application state as `&mut T`.
2263    ///
2264    /// Panics if `T` is not the concrete state type the render root erased — a
2265    /// shell/wiring bug, not a runtime-data condition (mirrors
2266    /// [`crate::widget::LayoutCtx::text_context`]).
2267    pub fn state_mut<T: Any>(&mut self) -> &mut T {
2268        self.state
2269            .downcast_mut::<T>()
2270            .expect("event state is not the expected application-state type")
2271    }
2272
2273    /// Request that the shell schedule a repaint after this event pass.
2274    pub fn request_redraw(&mut self) {
2275        self.needs_redraw = true;
2276    }
2277
2278    /// Whether a redraw was requested during this (sub)dispatch.
2279    pub fn needs_redraw(&self) -> bool {
2280        self.needs_redraw
2281    }
2282
2283    /// Capture the pointer: subsequent moves/releases should route back to this
2284    /// widget. The [`ChildPod::event_child`](crate::widget::ChildPod::event_child)
2285    /// call that delivered the event reads [`EventCtx::is_pointer_captured`]
2286    /// after the dispatch returns and records the active path on its pod; the
2287    /// container clears it on `Up`/`Cancel`.
2288    ///
2289    /// **Capture is a `Down`-time concept here.** For a hit-tested pointer event,
2290    /// only the `Down` arm of [`RenderRoot::event`](crate::app::RenderRoot::event)
2291    /// folds a request into the root's own capture mirror, so a capture opened
2292    /// from a `Move` records the pod's active path (routing works) while the root
2293    /// still reads uncaptured. (A floated overlay surface's own input is the one
2294    /// exception: the root mirrors a capture claimed through it on any phase —
2295    /// see [`InputEvent::Overlay`].) For hover that means a `Move` that both
2296    /// captures and [`claim_hover`](EventCtx::claim_hover)s records the claim — the pod's
2297    /// eligibility gate reads the active flag as it stood *before* this dispatch —
2298    /// and then lapses on the next `Move`, where the now-active pod is ineligible.
2299    /// A gesture that wants hover chrome for its whole drag keeps its own pressed
2300    /// flag rather than relying on the link.
2301    pub fn capture_pointer(&mut self) {
2302        self.capture_requested = true;
2303    }
2304
2305    /// Whether the widget requested pointer capture during this (sub)dispatch.
2306    pub fn is_pointer_captured(&self) -> bool {
2307        self.capture_requested
2308    }
2309
2310    /// Which pointer contact this event comes from.
2311    ///
2312    /// [`PointerId::MOUSE`] unless the dispatch says otherwise: a shell's bare
2313    /// [`InputEvent::Pointer`] is the mouse, and a touch contact arrives as
2314    /// [`InputEvent::PointerContact`], which the root unwraps into
2315    /// `InputEvent::Pointer` while this reports its id. Meaningful for pointer
2316    /// events only; any other event reports whatever contact the pass carries
2317    /// (the mouse at the top level).
2318    ///
2319    /// A widget that tracks one gesture at a time never needs this — the root
2320    /// only ever delivers it the claimant's contact unless it opted in with
2321    /// [`EventCtx::capture_contacts`]. One that did opt in tells the contacts
2322    /// apart with it.
2323    pub fn pointer_id(&self) -> PointerId {
2324        self.pointer_id
2325    }
2326
2327    /// Opt into the **other** contacts of the gesture this widget is capturing:
2328    /// call it on the same `Down` that calls [`EventCtx::capture_pointer`], and
2329    /// while that capture lives every additional contact's
2330    /// `Down`/`Move`/`Up`/`Cancel` is delivered here down the captured path, as
2331    /// [`InputEvent::Pointer`] with [`EventCtx::pointer_id`] naming the contact.
2332    /// A pinch or rotate recognizer is the intended caller.
2333    ///
2334    /// The opt-in is read with the capture it accompanies: on a `Down` that
2335    /// captures nothing it does nothing, and it ends with the capture.
2336    ///
2337    /// # Multi-contact contract
2338    ///
2339    /// * **(a)** With no live capture, a slot-`0` contact is hit-tested exactly
2340    ///   like a plain [`InputEvent::Pointer`]; the `Down` this widget captures
2341    ///   on makes that contact the gesture's **claimant**.
2342    /// * **(b)** With no live capture, a contact on slot `1` or above is dropped
2343    ///   at the root — so an additional finger only ever reaches a widget
2344    ///   through this opt-in.
2345    /// * **(c)** While the capture lives, the claimant's events arrive as usual;
2346    ///   every other contact's events arrive only because of this call (a
2347    ///   captor that did not make it never sees them), and **only here**: the
2348    ///   containers between the root and this widget on the recorded active
2349    ///   path forward them without running their own pointer handling (see
2350    ///   [`ChildPod::event_child`](crate::widget::ChildPod::event_child)), so an
2351    ///   enclosing scroll view or gesture detector never mistakes a second
2352    ///   finger for its own. Only the claimant's `Up`/`Cancel` ends the
2353    ///   capture — another contact's `Up`/`Cancel` is delivered but releases
2354    ///   nothing, so the handler must not treat it as the end of the gesture.
2355    ///   When the claimant's `Up`/`Cancel` arrives, the captor must drop every
2356    ///   other contact it was tracking: their later events no longer reach it.
2357    ///   The opt-in also ends early if an enclosing container takes the gesture
2358    ///   over and releases this widget from the active path
2359    ///   ([`EventCtx::release_captured_child`]).
2360    ///
2361    /// See [`InputEvent::PointerContact`] for the full contract.
2362    pub fn capture_contacts(&mut self) {
2363        self.contacts_requested = true;
2364        note_contacts_requested();
2365    }
2366
2367    /// Whether a widget opted into the gesture's other contacts
2368    /// ([`EventCtx::capture_contacts`]) during this (sub)dispatch — the
2369    /// container-side read, bubbled by
2370    /// [`ChildPod::event_child`](crate::widget::ChildPod::event_child) exactly
2371    /// like [`EventCtx::is_pointer_captured`]. The root does not depend on the
2372    /// bubble: it reads the opt-in from the pass it opened, so a component
2373    /// boundary (whose fresh inner context this flag does not cross) cannot
2374    /// hide it.
2375    pub fn is_contact_capture_requested(&self) -> bool {
2376        self.contacts_requested
2377    }
2378
2379    /// Release a captured child: the container-side half of a **takeover**, for
2380    /// a container that cancels the gesture its captured child was handling and
2381    /// keeps the gesture for itself (a scroll view crossing its drag slop).
2382    ///
2383    /// Clears `child`'s recorded active path exactly like
2384    /// [`ChildPod::set_active`](crate::widget::ChildPod::set_active)`(false)`
2385    /// (call it after delivering the child its `Cancel`), and when the released
2386    /// subtree held the widget that opted into the gesture's other contacts
2387    /// ([`EventCtx::capture_contacts`]) it also tells the root, which then stops
2388    /// routing those contacts — the widget that asked for them is no longer on
2389    /// the active path, and nothing else asked. The signal bubbles like
2390    /// [`EventCtx::is_pointer_captured`] ([`EventCtx::is_capture_released`]).
2391    ///
2392    /// The capture itself stays with the gesture's claimant: the container that
2393    /// took over is still on the active path (it was the released child's
2394    /// ancestor), so the claimant's later events keep reaching it and only the
2395    /// claimant's `Up`/`Cancel` ends the gesture. A container that wants the
2396    /// other contacts for itself calls [`EventCtx::capture_contacts`] after
2397    /// this, in the same dispatch.
2398    ///
2399    /// Nothing is released or signalled while the child holds no active path,
2400    /// while a non-claimant contact is being delivered (whose `Up`/`Cancel`
2401    /// must never break the claimant's gesture — see
2402    /// [`ChildPod::set_active`](crate::widget::ChildPod::set_active)), or when
2403    /// the opted-in widget is this container or one of its ancestors (it stays
2404    /// on the active path, so its opt-in stands).
2405    pub fn release_captured_child(&mut self, child: &mut crate::widget::ChildPod) {
2406        if !child.is_active() {
2407            return;
2408        }
2409        let held_opt_in = child.holds_contact_opt_in();
2410        child.set_active(false);
2411        if child.is_active() || !held_opt_in || under_contact_captor() {
2412            return;
2413        }
2414        self.capture_released = true;
2415        note_capture_released();
2416    }
2417
2418    /// Whether a container released the gesture's opted-in widget from the
2419    /// active path during this (sub)dispatch
2420    /// ([`EventCtx::release_captured_child`]) — bubbled by
2421    /// [`ChildPod::event_child`](crate::widget::ChildPod::event_child) exactly
2422    /// like [`EventCtx::is_pointer_captured`]. As with the opt-in, the root reads
2423    /// the release from the pass it opened, so a component boundary cannot hide
2424    /// it.
2425    pub fn is_capture_released(&self) -> bool {
2426        self.capture_released
2427    }
2428
2429    /// Request focus: subsequent keyboard/IME events should route to this widget.
2430    ///
2431    /// The [`ChildPod::event_child`](crate::widget::ChildPod::event_child) call
2432    /// that delivered the event reads the flag after the dispatch returns and
2433    /// records its pod as the focused path, stamped with the live focus session
2434    /// (the focus mirror of [`EventCtx::capture_pointer`]); the request bubbles,
2435    /// so every pod up to the root records it. Focus-routed events are delivered
2436    /// down that recorded chain with no hit test.
2437    pub fn request_focus(&mut self) {
2438        self.focus_requested = true;
2439    }
2440
2441    /// Release focus: drop the recorded focus path (e.g. Escape / blur).
2442    pub fn release_focus(&mut self) {
2443        self.focus_released = true;
2444    }
2445
2446    /// Whether the receiving widget currently holds the focus path.
2447    ///
2448    /// Threaded down from the widget's pod ([`crate::widget::ChildPod::is_focused`]);
2449    /// a keyboard/IME event only reaches a widget along this chain, so a widget
2450    /// handling such an event is by construction focused.
2451    pub fn has_focus(&self) -> bool {
2452        self.has_focus
2453    }
2454
2455    /// Claim the hover link: the pointer is over *this* widget, so the next paint
2456    /// pass reports [`PaintCtx::is_hovered`](crate::widget::PaintCtx::is_hovered)
2457    /// for it — and, because the claim is recorded as a path, for every ancestor
2458    /// enclosing it as well (see the [module docs](crate::event)).
2459    ///
2460    /// # The consumer contract
2461    ///
2462    /// Three things together, all three required:
2463    ///
2464    /// 1. **Claim from the [`PointerPhase::Move`] arm**, once the widget has
2465    ///    hit-tested the event's `position` inside its own bounds — the same local
2466    ///    test a press arm does on `Up`.
2467    /// 2. **Keep an internal hover flag**, updated from that same hit test, and
2468    ///    gate `request_redraw` on its *changed*-return. This call requests no
2469    ///    frame of its own (below), and the root manufactures one only when a hover
2470    ///    ends with nothing taking it — so a widget without this flag paints no
2471    ///    hover chrome on entry, and none when the link moves from a sibling to it.
2472    /// 3. **Read [`PaintCtx::is_hovered`](crate::widget::PaintCtx::is_hovered) in
2473    ///    `paint` and self-correct the flag from it.** It is authoritative: the
2474    ///    flag can be stale (a pointer that left the widget never delivers it
2475    ///    another event; a container clearing or lapsing a link never tells the
2476    ///    widget either), and this read is what fixes it.
2477    ///
2478    /// ```ignore
2479    /// PointerPhase::Move => {
2480    ///     if !self.captured {
2481    ///         // Uncaptured move: this is the hover pass.
2482    ///         let over = inside(p.position, ctx.size());
2483    ///         if over { ctx.claim_hover(); }
2484    ///         if self.state_layer.set_hovered(over) { ctx.request_redraw(); }
2485    ///         return EventResult::Ignored;
2486    ///     }
2487    ///     // ... captured drag handling
2488    /// }
2489    ///
2490    /// fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2491    ///     // Authoritative; corrects the flag above whenever it went stale.
2492    ///     self.state_layer.set_hovered(ctx.is_hovered());
2493    ///     // ... paint the overlay
2494    /// }
2495    /// ```
2496    ///
2497    /// **Claim on every qualifying `Move`, not just on entry.** The claim is
2498    /// per-pass, not sticky: a widget that stops claiming stops being hovered on
2499    /// the next hover pass. That is the mechanism, not a defect — it is what makes
2500    /// "the pointer moved somewhere else" self-clearing with no leave event to
2501    /// deliver.
2502    ///
2503    /// **A container claims *after* routing the `Move` to its children, never
2504    /// before.** One claim per pass is recorded and the first one recorded wins, so
2505    /// an ancestor that claims before it forwards makes every descendant ineligible
2506    /// for the pass: the child under the pointer reads
2507    /// [`is_hovered`](EventCtx::is_hovered) `== false` forever while step 2 above
2508    /// keeps flipping its flag and asking for a frame on every move — hover chrome
2509    /// that never appears, plus a repaint per event. Claiming after routing is
2510    /// correct in every case: a descendant's claim is recorded first and wins, the
2511    /// container's own late call is then a silent no-op yet it still reads hovered
2512    /// through the stamped path (below), and when no descendant claims, the
2513    /// container's claim is what records, so its own chrome still works. A
2514    /// container therefore never arbitrates — it orders.
2515    ///
2516    /// **Its sibling channel resolves the opposite way.** A claim is
2517    /// *first*-writer-wins; [`set_cursor`](EventCtx::set_cursor) is
2518    /// *last*-writer-wins. So the same "claim/ask after routing" placement means
2519    /// two different things in one handler: the container's claim is a **fallback**
2520    /// its child beats, while the container's cursor request is an **override**
2521    /// that beats its child's. A container that wants the child's cursor to win
2522    /// must ask *before* it routes — the mirror image of the ordering here.
2523    ///
2524    /// # When it does nothing
2525    ///
2526    /// A call is silently ignored unless the pass is hover-eligible: a captured
2527    /// pointer (anywhere on the path), any phase other than an uncaptured `Move`,
2528    /// and any claim after the first one in the same pass all record nothing. A
2529    /// **leaf** therefore never has to ask whether claiming is allowed — it claims
2530    /// whenever the pointer is over it and the pipeline decides. A **container**
2531    /// gets the same freedom only by claiming after it routes: it never asks
2532    /// either, but *when* it claims decides whether its children may, per the
2533    /// ordering rule above.
2534    ///
2535    /// A `Down`, `Up`, or `Cancel` *ends* whatever hover stood without opening a
2536    /// new one, so a consumer re-claims on the next `Move` rather than expecting
2537    /// its chrome to survive a click.
2538    ///
2539    /// # It does not request a redraw
2540    ///
2541    /// Deliberately: a pointer moving *within* one widget claims on every event,
2542    /// and repainting each time would be pure waste. The widget owns the change
2543    /// detection instead — which is what makes step 2 above part of the contract
2544    /// rather than an optimization.
2545    pub fn claim_hover(&mut self) {
2546        if self.hover_eligible {
2547            self.hover_claimed = true;
2548        }
2549    }
2550
2551    /// Whether the receiving widget **or a descendant of it** holds the hover link
2552    /// — i.e. whether the last completed hover pass recorded a claim path running
2553    /// through this widget.
2554    ///
2555    /// So a container reads `true` while the pointer is over a claiming child
2556    /// (CSS `:hover` semantics), and a widget that never claims can still read
2557    /// `true` when a descendant does; a sibling or any other off-path widget reads
2558    /// `false`.
2559    ///
2560    /// Threaded down from the widget's pod
2561    /// ([`ChildPod::hover_epoch`](crate::widget::ChildPod::hover_epoch) against
2562    /// the live epoch) and seeded at the root from `RenderRoot`'s hover mirror, so
2563    /// it reflects state as of *before* this dispatch: a
2564    /// [`claim_hover`](EventCtx::claim_hover) made in this pass does not flip it.
2565    /// Mirrors [`EventCtx::has_focus`]; the paint-pass form is
2566    /// [`PaintCtx::is_hovered`](crate::widget::PaintCtx::is_hovered), which is the
2567    /// authoritative read for a widget's own hover chrome.
2568    pub fn is_hovered(&self) -> bool {
2569        self.hovered
2570    }
2571
2572    /// Ask the host to show `icon` while the pointer is where it is now.
2573    ///
2574    /// The request is per-pass and stateless, exactly like
2575    /// [`request_redraw`](EventCtx::request_redraw) and
2576    /// [`claim_hover`](EventCtx::claim_hover): it says what the cursor should be
2577    /// *for this pass*, and a widget that stops asking falls back to
2578    /// [`CursorIcon::Default`] with nothing to clear.
2579    ///
2580    /// # When to call it
2581    ///
2582    /// From a [`PointerPhase::Move`] arm, on the same hit test a
2583    /// [`claim_hover`](EventCtx::claim_hover) rides — the two are siblings, and a
2584    /// widget that wants hover chrome usually wants a cursor too:
2585    ///
2586    /// ```ignore
2587    /// PointerPhase::Move => {
2588    ///     if !self.captured {
2589    ///         if inside(p.position, ctx.size()) {
2590    ///             ctx.claim_hover();
2591    ///             ctx.set_cursor(CursorIcon::Pointer);
2592    ///         }
2593    ///         return EventResult::Ignored;
2594    ///     }
2595    ///     // Captured drag: this widget owns the pass, so its request wins
2596    ///     // wherever the pointer has gone.
2597    ///     ctx.set_cursor(CursorIcon::Grabbing);
2598    ///     // ... drag handling
2599    /// }
2600    /// ```
2601    ///
2602    /// **Ask on every `Move`, not just on entry**, and ask from the captured
2603    /// `Move`s too if a drag should keep its own shape: a captured pass routes
2604    /// only to the capturing widget, so re-asking there is what keeps a
2605    /// `Grabbing` cursor alive while the pointer is dragged outside the widget's
2606    /// own bounds.
2607    ///
2608    /// # Which pass the root actually resolves
2609    ///
2610    /// Only a pointer [`PointerPhase::Move`] — captured or not — re-resolves the
2611    /// cursor ([`crate::app::RenderRoot::cursor`]). A request made on any other
2612    /// pass records nothing, and, just as importantly, no other pass *resets* the
2613    /// cursor: a `Down`/`Up` whose handlers say nothing about the cursor leaves
2614    /// the standing shape alone rather than blinking it back to `Default` for the
2615    /// duration of a click. A widget wanting a press-specific cursor therefore
2616    /// keys it off its own pressed state from the `Move` arm rather than setting
2617    /// it on `Down`.
2618    ///
2619    /// # Last writer wins
2620    ///
2621    /// One value is resolved per pass, and the last `set_cursor` of the pass is
2622    /// it. Because a container routes to its child from the middle of its own
2623    /// handler, the innermost widget the route reaches normally speaks last and
2624    /// therefore wins — which is what makes a specific control override the
2625    /// generic surface behind it. A container that deliberately overrides its
2626    /// children sets the cursor *after* routing.
2627    ///
2628    /// **Note the asymmetry with [`claim_hover`](EventCtx::claim_hover)**, which
2629    /// is first-writer-wins: a container claiming after routing yields hover to
2630    /// its child, while a container asking for a cursor after routing overrides
2631    /// its child. Placing the two calls side by side in one `Move` arm — the
2632    /// example above — is correct precisely because a leaf has no child to order
2633    /// against; a *container* writing both has to place them separately.
2634    ///
2635    /// # It does not request a redraw
2636    ///
2637    /// Deliberately, for [`claim_hover`](EventCtx::claim_hover)'s reason: a
2638    /// pointer moving within one widget re-asks on every event, and the shell
2639    /// applies the resolved cursor whether or not a frame is painted.
2640    ///
2641    /// Takes `&mut self` like every other request on this context even though the
2642    /// pass's request slot is not a field of it (`CURSOR_REQUEST`, above): asking
2643    /// is something a widget does *through its context*, and keeping the signature
2644    /// honest about that leaves the storage free to move.
2645    pub fn set_cursor(&mut self, icon: CursorIcon) {
2646        CURSOR_REQUEST.with(|slot| slot.set(Some(icon)));
2647    }
2648
2649    /// Ask the shell to put `text` on the host clipboard.
2650    ///
2651    /// The answer to an [`InputEvent::EditCommand`]`(`[`EditCommand::Copy`]`)` or
2652    /// [`EditCommand::Cut`]: the widget owns the selection, so it is the only
2653    /// thing that can say what "copy" means, and the shell owns the host
2654    /// clipboard, so it is the only thing that can perform the write. A widget
2655    /// with nothing selected simply does not call this, and nothing is written.
2656    ///
2657    /// ```ignore
2658    /// InputEvent::EditCommand(EditCommand::Cut) => {
2659    ///     if let Some(sel) = self.selected_text() {
2660    ///         ctx.write_clipboard(sel);
2661    ///         self.delete_selection();
2662    ///         ctx.request_redraw();
2663    ///     }
2664    ///     EventResult::Handled
2665    /// }
2666    /// ```
2667    ///
2668    /// # Pass-scoped, last writer wins
2669    ///
2670    /// The request rides the same kind of per-pass slot as
2671    /// [`set_cursor`](EventCtx::set_cursor) (`CLIPBOARD_WRITE`, bracketed by the
2672    /// same [`RequestPass`] guard), so exactly one write is resolved per dispatch
2673    /// and the pass's last caller is it — which, since a container routes to its
2674    /// child from the middle of its own handler, normally makes the innermost
2675    /// widget the route reaches the one that speaks. Nothing accumulates between
2676    /// passes and there is nothing to clear: a widget that stops copying stops
2677    /// writing.
2678    ///
2679    /// The root resolves the slot at the end of **every** pass (not just a
2680    /// clipboard one — a copy can be answered from a key chord a widget decoded
2681    /// itself), and a shell drains it with
2682    /// [`RenderRoot::take_clipboard_write`](crate::app::RenderRoot::take_clipboard_write)
2683    /// immediately after the dispatch, beside
2684    /// [`cursor()`](crate::app::RenderRoot::cursor) and
2685    /// [`ime_state()`](crate::app::RenderRoot::ime_state).
2686    ///
2687    /// # It does not request a redraw
2688    ///
2689    /// [`set_cursor`](EventCtx::set_cursor)'s reason: copying paints nothing. A
2690    /// `Cut` that mutates the document asks for its own redraw, for the mutation.
2691    ///
2692    /// Takes `&mut self` like every other request on this context even though the
2693    /// pass's slot is not a field of it: asking is something a widget does
2694    /// *through its context*, and keeping the signature honest about that leaves
2695    /// the storage free to move.
2696    pub fn write_clipboard(&mut self, text: String) {
2697        CLIPBOARD_WRITE.with(|slot| slot.set(Some(text)));
2698    }
2699
2700    /// Ask the shell to read the host clipboard and deliver it back as an
2701    /// [`InputEvent::EditCommand`]`(`[`EditCommand::Paste`]`)`.
2702    ///
2703    /// The inverse of [`write_clipboard`](EventCtx::write_clipboard), and the
2704    /// reason a paste arrives with its text already attached: only the shell can
2705    /// touch the host clipboard, so a widget that wants a paste it was not given
2706    /// — an in-widget context-menu item, a chord the widget decoded itself —
2707    /// raises this flag and receives the text on a *later* dispatch rather than
2708    /// inline.
2709    ///
2710    /// # Idempotent, pass-scoped, and answered out of band
2711    ///
2712    /// Data-free: two widgets asking in one pass owe exactly one clipboard read,
2713    /// because there is one host clipboard and one focused widget to deliver it
2714    /// to. The flag rides a per-pass slot bracketed by the same [`RequestPass`]
2715    /// guard as the cursor, so an ask made outside any dispatch is dropped rather
2716    /// than leaking into the next pass; the root resolves it at the end of every
2717    /// pass and a shell drains it with
2718    /// [`RenderRoot::take_paste_request`](crate::app::RenderRoot::take_paste_request).
2719    ///
2720    /// The answer is a **new dispatch**, never a return value: the shell's read
2721    /// may be asynchronous (a permission prompt, a cross-process fetch), and by
2722    /// the time it lands the pass that asked is long over. The synthesized
2723    /// [`EditCommand::Paste`] carries text and no identity of its own, and focus
2724    /// routing hands it to whoever holds focus *at delivery*: a **release** does
2725    /// drop it — with nothing focused it reaches no widget — but a focus *move*
2726    /// lands it in the new field, not in the one that asked.
2727    ///
2728    /// A synchronous read has no in-flight window and needs no guard. An
2729    /// asynchronous one must bind its answer to the session that asked:
2730    /// snapshot [`RenderRoot::focus_epoch`](crate::app::RenderRoot::focus_epoch)
2731    /// (reached shell-side as `AppTree::focus_epoch`) when the request is
2732    /// drained, and drop an answer whose epoch no longer matches — never
2733    /// [`focus_ime_generation`](crate::app::RenderRoot::focus_ime_generation),
2734    /// which an edit or a caret move inside one session also moves.
2735    ///
2736    /// A `Cut` may write and ask in the same pass; the two slots are independent.
2737    pub fn request_paste(&mut self) {
2738        PASTE_REQUEST.with(|slot| slot.set(true));
2739    }
2740
2741    /// Hand an [`EditCommand`] to whichever widget drains the queue later in
2742    /// **this** pass — the widget-to-widget half of the clipboard story.
2743    ///
2744    /// # Why this is not just an `InputEvent::EditCommand`
2745    ///
2746    /// A selection toolbar and the text input it acts on are two different
2747    /// widgets, and the toolbar is a pod its owner floats rather than contains
2748    /// (see [`crate::overlay`]), so there is no container path from the toolbar's
2749    /// "Copy" tap back down to the field. Re-entering
2750    /// [`crate::app::RenderRoot::event`] with a focus-routed
2751    /// [`InputEvent::EditCommand`] would be the other option, and is worse: a
2752    /// dispatch may not re-enter the root (see that method's reentrancy
2753    /// contract), and the toolbar's tap is *already* being routed as an overlay
2754    /// broadcast when it decides. So the verb rides a pass-scoped FIFO the owner
2755    /// drains the instant its forward returns, and applies to the field itself —
2756    /// synchronously, inside the same dispatch.
2757    ///
2758    /// Order is preserved: commands drain in the order they were dispatched.
2759    ///
2760    /// A command nobody takes before the pass ends is **dropped** (with a
2761    /// debug-build diagnostic) rather than carried into the next pass, where it
2762    /// would apply to whatever happened to be selected by then.
2763    pub fn dispatch_edit_command(&mut self, cmd: EditCommand) {
2764        EDIT_COMMAND_QUEUE.with(|slot| {
2765            let mut queue = slot.take();
2766            queue.push(cmd);
2767            slot.set(queue);
2768        });
2769    }
2770
2771    /// Drain everything [`EventCtx::dispatch_edit_command`] queued so far in this
2772    /// pass, in dispatch order, leaving the queue empty.
2773    ///
2774    /// An overlay owner calls this immediately after forwarding an event into its
2775    /// floated pod, and applies what comes back to itself. Draining the queue
2776    /// (rather than peeking) is what keeps a verb from being applied twice when
2777    /// two owners forward in the same pass.
2778    pub fn take_edit_commands(&mut self) -> Vec<EditCommand> {
2779        EDIT_COMMAND_QUEUE.with(|slot| slot.take())
2780    }
2781
2782    /// Publish this widget's IME surface (editing state + caret) for the shell.
2783    ///
2784    /// The value bubbles up the focus chain to [`crate::app::RenderRoot`], where
2785    /// the shell reads it via [`crate::app::RenderRoot::ime_state`]. Called by the
2786    /// focused editable widget after any state change so the platform IME stays in
2787    /// sync.
2788    ///
2789    /// Core carries the whole [`ImeState`] — including its
2790    /// [`ImeContentType`](ImeState::content_type) hint — opaquely: nothing
2791    /// between here and the shell inspects or rewrites it.
2792    pub fn publish_ime_state(&mut self, state: ImeState) {
2793        self.ime_state = Some(state);
2794    }
2795
2796    /// Whether this widget requested focus during this (sub)dispatch (container-side).
2797    pub(crate) fn is_focus_requested(&self) -> bool {
2798        self.focus_requested
2799    }
2800
2801    /// Whether this widget released focus during this (sub)dispatch (container-side).
2802    pub(crate) fn is_focus_released(&self) -> bool {
2803        self.focus_released
2804    }
2805
2806    /// Take the IME surface published during this (sub)dispatch, leaving `None`.
2807    pub(crate) fn take_ime_state(&mut self) -> Option<ImeState> {
2808        self.ime_state.take()
2809    }
2810
2811    /// Seed whether the receiving (root) widget holds focus — used by
2812    /// [`crate::app::RenderRoot::event`] when it dispatches straight to the root.
2813    pub(crate) fn set_has_focus(&mut self, has_focus: bool) {
2814        self.has_focus = has_focus;
2815    }
2816
2817    /// Whether a widget claimed hover during this (sub)dispatch (container-side).
2818    pub(crate) fn is_hover_claimed(&self) -> bool {
2819        self.hover_claimed
2820    }
2821
2822    /// Whether a [`EventCtx::claim_hover`] call in this (sub)dispatch would record
2823    /// anything — read by [`crate::widget::ChildPod::event_child`], which narrows
2824    /// it further before handing it to a child.
2825    pub(crate) fn is_hover_eligible(&self) -> bool {
2826        self.hover_eligible
2827    }
2828
2829    /// Seed whether the receiving (root) widget holds the hover link — the hover
2830    /// mirror of [`EventCtx::set_has_focus`].
2831    pub(crate) fn set_hovered(&mut self, hovered: bool) {
2832        self.hovered = hovered;
2833    }
2834
2835    /// Seed whether this pass may record a hover claim at all. Called by
2836    /// [`crate::app::RenderRoot::event`], which sets it only for an **uncaptured**
2837    /// [`PointerPhase::Move`].
2838    pub(crate) fn set_hover_eligible(&mut self, eligible: bool) {
2839        self.hover_eligible = eligible;
2840    }
2841
2842    /// Seed the live hover epoch (the last completed hover pass's). Called by
2843    /// [`crate::app::RenderRoot::event`] at the root and threaded unchanged into
2844    /// every child by [`crate::widget::ChildPod::event_child`].
2845    pub(crate) fn set_hover_epoch(&mut self, epoch: u64) {
2846        self.hover_epoch = epoch;
2847    }
2848
2849    /// The live hover epoch: a pod whose recorded stamp equals this still holds
2850    /// the hover link.
2851    pub(crate) fn hover_epoch(&self) -> u64 {
2852        self.hover_epoch
2853    }
2854
2855    /// Seed the identity of the root running this pass. Called by
2856    /// [`crate::app::RenderRoot::event`] at the root and threaded unchanged into
2857    /// every child by [`crate::widget::ChildPod::event_child`], which stamps it
2858    /// beside the claim epoch.
2859    pub(crate) fn set_hover_root(&mut self, root: u64) {
2860        self.hover_root = root;
2861    }
2862
2863    /// The identity of the root running this pass — stamped onto a claiming pod
2864    /// so its destructor can qualify its epoch (see [`mark_hover_orphaned`]).
2865    pub(crate) fn hover_root(&self) -> u64 {
2866        self.hover_root
2867    }
2868
2869    /// The epoch a claim recorded during *this* pass takes — one past the live
2870    /// one, because [`crate::app::RenderRoot::event`] advances its epoch when the
2871    /// hover pass ends. Wrapping is deliberate and harmless: the stamp is only
2872    /// ever compared for equality, never ordered, and a wrap would need 2^64 hover
2873    /// passes to collide with a link recorded before it.
2874    pub(crate) fn hover_claim_epoch(&self) -> u64 {
2875        self.hover_epoch.wrapping_add(1)
2876    }
2877
2878    /// The receiving widget's origin in its **parent's** coordinate space.
2879    ///
2880    /// Not the window-space origin, and **not** the same frame of reference as
2881    /// [`PaintCtx::origin`](crate::widget::PaintCtx::origin), which is absolute:
2882    /// the paint pass accumulates each child's parent-relative offset onto its
2883    /// parent's already-absolute origin, while the event pass instead translates
2884    /// the *event* into the child's local space
2885    /// ([`ChildPod::event_child`](crate::widget::ChildPod::event_child)) and hands
2886    /// down the pod's own offset unaccumulated. So an event position is already
2887    /// local (compare it against `Point::ZERO` and [`EventCtx::size`], never
2888    /// against this), and anything anchored in window space — an overlay, a
2889    /// popup, a reported rect — must be computed from `PaintCtx::origin` in
2890    /// `paint`, not from this value.
2891    pub fn origin(&self) -> Point {
2892        self.origin
2893    }
2894
2895    /// The receiving widget's resolved size.
2896    pub fn size(&self) -> Size {
2897        self.size
2898    }
2899
2900    /// Create a fresh sub-context for a child at `origin`/`size`, reborrowing the
2901    /// same erased state. The child's `needs_redraw`/`capture_requested`/focus and
2902    /// hover-claim flags start clear; `has_focus` reflects the child pod's recorded
2903    /// focus flag, `hovered` its recorded hover link, and `hover_eligible` whether
2904    /// the child may claim hover at all (the caller narrows it — see
2905    /// [`crate::widget::ChildPod::event_child`]). The live hover epoch, the
2906    /// running root's identity and the dispatch's [`EventCtx::pointer_id`] are
2907    /// threaded down unchanged (a claim anywhere in the subtree is stamped with
2908    /// the first two). The parent folds the results back in with
2909    /// [`EventCtx::absorb_child`].
2910    pub(crate) fn child_ctx(
2911        &mut self,
2912        origin: Point,
2913        size: Size,
2914        focused: bool,
2915        hovered: bool,
2916        hover_eligible: bool,
2917    ) -> EventCtx<'_> {
2918        EventCtx {
2919            state: &mut *self.state,
2920            needs_redraw: false,
2921            capture_requested: false,
2922            contacts_requested: false,
2923            capture_released: false,
2924            pointer_id: self.pointer_id,
2925            focus_requested: false,
2926            focus_released: false,
2927            has_focus: focused,
2928            hovered,
2929            hover_claimed: false,
2930            hover_eligible,
2931            hover_epoch: self.hover_epoch,
2932            hover_root: self.hover_root,
2933            ime_state: None,
2934            origin,
2935            size,
2936        }
2937    }
2938
2939    /// Fold a child dispatch's redraw/capture/contact-opt-in/capture-release/
2940    /// hover-claim/focus flags (and any published IME surface) back into this
2941    /// context.
2942    #[allow(clippy::too_many_arguments)]
2943    pub(crate) fn absorb_child(
2944        &mut self,
2945        child_needs_redraw: bool,
2946        child_captured: bool,
2947        child_contacts_requested: bool,
2948        child_capture_released: bool,
2949        child_hover_claimed: bool,
2950        child_focus_requested: bool,
2951        child_focus_released: bool,
2952        child_ime_state: Option<ImeState>,
2953    ) {
2954        self.needs_redraw |= child_needs_redraw;
2955        self.capture_requested |= child_captured;
2956        self.contacts_requested |= child_contacts_requested;
2957        self.capture_released |= child_capture_released;
2958        // A claim bubbles like a focus request: every pod between the claimant and
2959        // the root records it, so the whole path carries the same stamp.
2960        self.hover_claimed |= child_hover_claimed;
2961        self.focus_requested |= child_focus_requested;
2962        self.focus_released |= child_focus_released;
2963        if child_ime_state.is_some() {
2964            self.ime_state = child_ime_state;
2965        }
2966    }
2967}
2968
2969#[cfg(test)]
2970mod tests {
2971    use super::*;
2972
2973    fn down(x: f64, y: f64) -> InputEvent {
2974        InputEvent::Pointer(PointerEvent {
2975            phase: PointerPhase::Down,
2976            position: Point::new(x, y),
2977            button: PointerButton::Primary,
2978        })
2979    }
2980
2981    #[test]
2982    fn state_mut_recovers_concrete_state() {
2983        let mut count = 3u32;
2984        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(10.0, 10.0));
2985        *ctx.state_mut::<u32>() += 1;
2986        assert_eq!(count, 4);
2987    }
2988
2989    #[test]
2990    fn request_redraw_and_capture_set_flags() {
2991        let mut count = 0u32;
2992        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
2993        assert!(!ctx.needs_redraw());
2994        assert!(!ctx.is_pointer_captured());
2995        ctx.request_redraw();
2996        ctx.capture_pointer();
2997        assert!(ctx.needs_redraw());
2998        assert!(ctx.is_pointer_captured());
2999    }
3000
3001    #[test]
3002    fn translated_shifts_pointer_position() {
3003        let e = down(20.0, 30.0);
3004        let local = e.translated(-Vec2::new(5.0, 7.0));
3005        assert_eq!(local.position(), Point::new(15.0, 23.0));
3006        // The original is untouched.
3007        assert_eq!(e.position(), Point::new(20.0, 30.0));
3008    }
3009
3010    #[test]
3011    fn transformed_maps_every_positioned_variant_like_translated() {
3012        // A pure translation through `transformed` must agree with `translated`
3013        // for every variant, positioned or not.
3014        let offset = Vec2::new(-5.0, -7.0);
3015        let affine = Affine::translate(offset);
3016        let scale = ScaleEvent {
3017            phase: ScalePhase::Update,
3018            scale_delta: 1.5,
3019            focal: Point::new(20.0, 30.0),
3020            velocity: 0.25,
3021        };
3022        let events = [
3023            down(20.0, 30.0),
3024            InputEvent::PointerContact {
3025                pointer_id: PointerId::touch(1),
3026                event: PointerEvent {
3027                    phase: PointerPhase::Move,
3028                    position: Point::new(20.0, 30.0),
3029                    button: PointerButton::Primary,
3030                },
3031            },
3032            InputEvent::Scroll {
3033                position: Point::new(20.0, 30.0),
3034                delta: ScrollDelta::Pixels(3.0, 4.0),
3035            },
3036            InputEvent::Scale(scale),
3037            InputEvent::Housekeeping,
3038        ];
3039        for event in &events {
3040            assert_eq!(event.transformed(&affine), event.translated(offset));
3041        }
3042    }
3043
3044    #[test]
3045    fn transformed_maps_positions_through_scale_and_keeps_magnitudes() {
3046        // Inverse of scale(2) then translate(10, 20): container (30, 60) is
3047        // local (10, 20).
3048        let inverse = (Affine::translate(Vec2::new(10.0, 20.0)) * Affine::scale(2.0)).inverse();
3049        assert_eq!(
3050            down(30.0, 60.0).transformed(&inverse).position(),
3051            Point::new(10.0, 20.0)
3052        );
3053        let scroll = InputEvent::Scroll {
3054            position: Point::new(30.0, 60.0),
3055            delta: ScrollDelta::Pixels(3.0, 4.0),
3056        };
3057        assert_eq!(
3058            scroll.transformed(&inverse),
3059            InputEvent::Scroll {
3060                position: Point::new(10.0, 20.0),
3061                delta: ScrollDelta::Pixels(3.0, 4.0),
3062            }
3063        );
3064        let scale = InputEvent::Scale(ScaleEvent {
3065            phase: ScalePhase::Begin,
3066            scale_delta: 1.25,
3067            focal: Point::new(30.0, 60.0),
3068            velocity: 2.0,
3069        });
3070        assert_eq!(
3071            scale.transformed(&inverse),
3072            InputEvent::Scale(ScaleEvent {
3073                phase: ScalePhase::Begin,
3074                scale_delta: 1.25,
3075                focal: Point::new(10.0, 20.0),
3076                velocity: 2.0,
3077            })
3078        );
3079    }
3080
3081    #[test]
3082    fn absorb_child_folds_flags_upward() {
3083        let mut count = 0u32;
3084        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3085        {
3086            let mut child = ctx.child_ctx(
3087                Point::new(1.0, 2.0),
3088                Size::new(3.0, 4.0),
3089                false,
3090                false,
3091                false,
3092            );
3093            child.request_redraw();
3094            child.capture_pointer();
3095            let (redraw, cap) = (child.needs_redraw(), child.is_pointer_captured());
3096            ctx.absorb_child(redraw, cap, false, false, false, false, false, None);
3097        }
3098        assert!(ctx.needs_redraw());
3099        assert!(ctx.is_pointer_captured());
3100    }
3101
3102    #[test]
3103    fn request_and_release_focus_set_flags() {
3104        let mut count = 0u32;
3105        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3106        assert!(!ctx.is_focus_requested());
3107        assert!(!ctx.is_focus_released());
3108        assert!(!ctx.has_focus());
3109        ctx.request_focus();
3110        ctx.release_focus();
3111        assert!(ctx.is_focus_requested());
3112        assert!(ctx.is_focus_released());
3113    }
3114
3115    #[test]
3116    fn child_ctx_seeds_has_focus_from_pod_flag() {
3117        let mut count = 0u32;
3118        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3119        let focused_child = ctx.child_ctx(Point::ZERO, Size::ZERO, true, false, false);
3120        assert!(focused_child.has_focus());
3121        let unfocused_child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, false, false);
3122        assert!(!unfocused_child.has_focus());
3123    }
3124
3125    #[test]
3126    fn absorb_child_folds_focus_and_ime_upward() {
3127        let mut count = 0u32;
3128        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3129        let published = ImeState {
3130            active: true,
3131            editing: EditingState {
3132                text: "hi".to_string(),
3133                selection_base: 2,
3134                selection_extent: 2,
3135                composing_base: -1,
3136                composing_extent: -1,
3137            },
3138            caret: Some(Rect::new(0.0, 0.0, 1.0, 10.0)),
3139            content_type: ImeContentType::Normal,
3140            suppress_soft_keyboard: false,
3141        };
3142        {
3143            let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, false, false);
3144            child.request_focus();
3145            child.publish_ime_state(published.clone());
3146            let (fr, frl, ime) = (
3147                child.is_focus_requested(),
3148                child.is_focus_released(),
3149                child.take_ime_state(),
3150            );
3151            ctx.absorb_child(false, false, false, false, false, fr, frl, ime);
3152        }
3153        assert!(ctx.is_focus_requested());
3154        assert!(!ctx.is_focus_released());
3155        assert_eq!(ctx.take_ime_state(), Some(published));
3156    }
3157
3158    #[test]
3159    fn claim_hover_records_nothing_unless_the_pass_is_eligible() {
3160        let mut count = 0u32;
3161        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3162        // A bare context is ineligible by default — the safe direction: a widget
3163        // that claims on a pass the root never marked a hover pass records nothing.
3164        assert!(!ctx.is_hover_eligible());
3165        ctx.claim_hover();
3166        assert!(
3167            !ctx.is_hover_claimed(),
3168            "an ineligible claim records nothing"
3169        );
3170
3171        ctx.set_hover_eligible(true);
3172        ctx.claim_hover();
3173        assert!(ctx.is_hover_claimed());
3174        // A claim never touches the redraw channel: the widget owns change
3175        // detection (see `claim_hover`'s docs).
3176        assert!(!ctx.needs_redraw());
3177    }
3178
3179    #[test]
3180    fn child_ctx_seeds_hover_and_absorb_bubbles_a_claim() {
3181        let mut count = 0u32;
3182        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3183        ctx.set_hover_epoch(7);
3184        {
3185            // A hovered, eligible child: it sees its own link and its claim
3186            // bubbles into the parent so the whole path records the same stamp.
3187            let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, true, true);
3188            assert!(child.is_hovered());
3189            assert_eq!(child.hover_epoch(), 7, "the live epoch threads down");
3190            assert_eq!(child.hover_claim_epoch(), 8, "a claim takes the next epoch");
3191            child.claim_hover();
3192            let claimed = child.is_hover_claimed();
3193            ctx.absorb_child(false, false, false, false, claimed, false, false, None);
3194        }
3195        assert!(ctx.is_hover_claimed());
3196    }
3197
3198    #[test]
3199    fn content_type_defaults_to_no_hint() {
3200        assert_eq!(ImeContentType::default(), ImeContentType::Normal);
3201        assert!(!ImeContentType::default().is_secret());
3202        assert!(!ImeContentType::default().suppresses_suggestions());
3203    }
3204
3205    #[test]
3206    fn content_type_predicates_classify_every_variant() {
3207        // `is_secret` gates secure entry; `suppresses_suggestions` gates the
3208        // suggestion strip + personalized learning. A shell branches on these,
3209        // never on a `_` arm (the enum is `#[non_exhaustive]`).
3210        assert!(ImeContentType::Password.is_secret());
3211        assert!(ImeContentType::Password.suppresses_suggestions());
3212
3213        assert!(!ImeContentType::NoSuggestions.is_secret());
3214        assert!(ImeContentType::NoSuggestions.suppresses_suggestions());
3215
3216        assert!(!ImeContentType::Terminal.is_secret());
3217        assert!(ImeContentType::Terminal.suppresses_suggestions());
3218
3219        assert!(!ImeContentType::Normal.is_secret());
3220        assert!(!ImeContentType::Normal.suppresses_suggestions());
3221    }
3222
3223    #[test]
3224    fn default_ime_state_is_cleared_and_unhinted() {
3225        let s = ImeState::default();
3226        assert!(!s.active);
3227        assert!(s.caret.is_none());
3228        assert_eq!(s.content_type, ImeContentType::Normal);
3229        // `−1` sentinels, not the derived zeros: no selection, no composition.
3230        assert_eq!(
3231            s.editing,
3232            EditingState {
3233                text: String::new(),
3234                selection_base: -1,
3235                selection_extent: -1,
3236                composing_base: -1,
3237                composing_extent: -1,
3238            }
3239        );
3240    }
3241
3242    /// The content-type hint must survive core's opaque passthrough untouched —
3243    /// core never inspects or rewrites it, it only carries it to the shell.
3244    #[test]
3245    fn publish_ime_state_round_trips_the_content_type() {
3246        let mut count = 0u32;
3247        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3248        let published = ImeState {
3249            active: true,
3250            editing: EditingState {
3251                text: "hunter2".to_string(),
3252                selection_base: 7,
3253                selection_extent: 7,
3254                composing_base: -1,
3255                composing_extent: -1,
3256            },
3257            caret: Some(Rect::new(0.0, 0.0, 1.0, 10.0)),
3258            content_type: ImeContentType::Password,
3259            suppress_soft_keyboard: false,
3260        };
3261        ctx.publish_ime_state(published.clone());
3262        let taken = ctx.take_ime_state().expect("published state");
3263        assert_eq!(taken, published);
3264        assert_eq!(taken.content_type, ImeContentType::Password);
3265        // The secret's text is published verbatim — the platform IME mirror
3266        // needs it (see `ImeState`'s docs); the hint, not redaction, is what
3267        // tells the shell to lock the keyboard down.
3268        assert_eq!(taken.editing.text, "hunter2");
3269    }
3270
3271    /// …and it survives the focus-chain bubble a real widget publication takes.
3272    #[test]
3273    fn content_type_bubbles_up_the_focus_chain() {
3274        let mut count = 0u32;
3275        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3276        let published = ImeState {
3277            content_type: ImeContentType::NoSuggestions,
3278            ..ImeState::default()
3279        };
3280        {
3281            let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, true, false, false);
3282            child.publish_ime_state(published.clone());
3283            let ime = child.take_ime_state();
3284            ctx.absorb_child(false, false, false, false, false, false, false, ime);
3285        }
3286        assert_eq!(ctx.take_ime_state(), Some(published));
3287    }
3288
3289    /// The keyboard-suppression hint is carried, not interpreted: core passes
3290    /// it through untouched, and it prints plainly (it is a routing hint, not a
3291    /// secret) so a trace shows why no keyboard came up.
3292    #[test]
3293    fn suppress_soft_keyboard_defaults_off_round_trips_and_prints_plainly() {
3294        assert!(
3295            !ImeState::default().suppress_soft_keyboard,
3296            "a publisher that says nothing must behave as it did before the hint existed"
3297        );
3298        let mut count = 0u32;
3299        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3300        let published = ImeState {
3301            active: true,
3302            suppress_soft_keyboard: true,
3303            ..ImeState::default()
3304        };
3305        ctx.publish_ime_state(published.clone());
3306        let taken = ctx.take_ime_state().expect("published state");
3307        assert_eq!(taken, published);
3308        assert!(taken.suppress_soft_keyboard);
3309        assert!(format!("{taken:?}").contains("suppress_soft_keyboard: true"));
3310    }
3311
3312    #[test]
3313    fn debug_redacts_a_secret_field_but_not_a_normal_one() {
3314        let secret = ImeState {
3315            active: true,
3316            editing: EditingState {
3317                text: "hunter2".to_string(),
3318                ..EditingState::default()
3319            },
3320            caret: None,
3321            content_type: ImeContentType::Password,
3322            suppress_soft_keyboard: false,
3323        };
3324        let rendered = format!("{secret:?}");
3325        assert!(
3326            !rendered.contains("hunter2"),
3327            "secret text leaked: {rendered}"
3328        );
3329        assert!(rendered.contains("<redacted>"));
3330        assert!(rendered.contains("Password"));
3331
3332        let plain = ImeState {
3333            content_type: ImeContentType::Normal,
3334            ..secret
3335        };
3336        assert!(format!("{plain:?}").contains("hunter2"));
3337    }
3338
3339    #[test]
3340    fn key_and_ime_events_are_focus_routed_with_zero_position() {
3341        let key = InputEvent::Key(KeyEvent {
3342            key: Key::Named(NamedKey::Enter),
3343            modifiers: Modifiers::default(),
3344            repeat: false,
3345        });
3346        assert!(key.is_focus_routed());
3347        assert_eq!(key.position(), Point::ZERO);
3348        // translated is identity for focus-routed events.
3349        assert_eq!(key.translated(Vec2::new(5.0, 5.0)), key);
3350
3351        let ime = InputEvent::Ime(ImeEvent::Commit("x".to_string()));
3352        assert!(ime.is_focus_routed());
3353        assert!(!down(1.0, 1.0).is_focus_routed());
3354    }
3355
3356    #[test]
3357    fn pending_result_flush_peek_observes_the_mark_without_draining_it() {
3358        // The peek is what a mobile shell reads while gathering its frame-gate
3359        // inputs, BEFORE deciding whether the frame runs — so it must be
3360        // non-destructive: draining stays `RenderRoot::rebuild`'s job on a frame
3361        // that actually runs. A peek that consumed the mark would leave the
3362        // rebuild with nothing to flush, which is worse than never peeking.
3363        //
3364        // Thread-affine like mark/take, and libtest gives each test its own
3365        // thread, so this needs no cross-test lock — but drain first anyway so
3366        // it never inherits a mark from earlier work on this thread.
3367        let _ = take_pending_result_flush();
3368        assert!(!has_pending_result_flush(), "starts clear");
3369
3370        mark_pending_result_flush();
3371        assert!(has_pending_result_flush(), "the peek observes the mark");
3372        // Repeated peeks are idempotent — the mark survives every one of them.
3373        assert!(has_pending_result_flush());
3374        assert!(has_pending_result_flush());
3375
3376        // Only the drain clears it, and the drain still reports the mark it took.
3377        assert!(take_pending_result_flush(), "the drain still sees the mark");
3378        assert!(
3379            !has_pending_result_flush(),
3380            "the drain is what clears it, not the peek"
3381        );
3382    }
3383
3384    #[test]
3385    fn a_hover_mark_belongs_to_the_root_whose_link_it_names() {
3386        // Two roots on one thread hold colliding epoch integers by construction
3387        // (every root's counter starts at 1 and advances per hover pass), so the
3388        // published link and the mark are both qualified by the root's identity.
3389        // Simulated here with two ids rather than two `RenderRoot`s: this is the
3390        // channel's own contract, and the pods on either side of it only ever
3391        // reach it through these four functions.
3392        const ROOT_A: u64 = 11;
3393        const ROOT_B: u64 = 22;
3394        const EPOCH: u64 = 7;
3395
3396        set_live_hover_link(ROOT_B, EPOCH);
3397        assert!(
3398            live_hover_link_is(ROOT_B, EPOCH),
3399            "the publishing root's pod recognizes its own live link"
3400        );
3401        assert!(
3402            !live_hover_link_is(ROOT_A, EPOCH),
3403            "the same epoch integer under another root is not this link"
3404        );
3405        assert!(
3406            !live_hover_link_is(ROOT_B, 0),
3407            "epoch 0 is 'no link' and matches nothing"
3408        );
3409
3410        // A mark raised for one root is not the other's to consume: draining the
3411        // wrong one must neither report nor clear it.
3412        mark_hover_orphaned(ROOT_A);
3413        assert!(
3414            !take_hover_orphaned(ROOT_B),
3415            "a root does not end its hover on another root's severance"
3416        );
3417        assert!(
3418            take_hover_orphaned(ROOT_A),
3419            "and the mark is still standing for the root that owns it"
3420        );
3421        assert!(
3422            !take_hover_orphaned(ROOT_A),
3423            "the drain is destructive for the matching root"
3424        );
3425
3426        // Leave the thread-local at rest for anything else on this thread.
3427        set_live_hover_link(0, 0);
3428    }
3429
3430    #[test]
3431    fn a_cursor_request_is_one_slot_the_last_writer_owns() {
3432        // Thread-affine like the two flags above, and libtest gives each test its
3433        // own thread — but clear first anyway so nothing earlier on this thread
3434        // leaks in (which is exactly what `RenderRoot::event` does per pass).
3435        clear_cursor_request();
3436        assert_eq!(take_cursor_request(), None, "absence means Default");
3437
3438        let mut count = 0u32;
3439        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3440        ctx.set_cursor(CursorIcon::Text);
3441        // A second widget on the same routed path speaks later and therefore wins;
3442        // there is no per-pod recording to merge, only this one slot.
3443        {
3444            let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, false, false);
3445            child.set_cursor(CursorIcon::Pointer);
3446        }
3447        assert_eq!(
3448            take_cursor_request(),
3449            Some(CursorIcon::Pointer),
3450            "the last set_cursor of the pass is the resolved one"
3451        );
3452        assert_eq!(
3453            take_cursor_request(),
3454            None,
3455            "the drain is destructive — one request per pass"
3456        );
3457    }
3458
3459    #[test]
3460    fn a_nested_request_pass_resolves_its_own_and_hands_the_slot_back() {
3461        // The reentrancy guard on the pass-scoped slot. `RenderRoot::event`
3462        // forbids re-entering itself, so this shape is not reachable today —
3463        // which is the point: the failure it would produce (an inner dispatch
3464        // silently eating the request the outer pass had already collected, or
3465        // draining one the outer pass was still owed) is invisible, so the
3466        // bracket enforces the scoping rather than the convention doing it.
3467        let outer = RequestPass::enter();
3468        let mut count = 0u32;
3469        {
3470            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3471            ctx.set_cursor(CursorIcon::Grab);
3472        }
3473        // A nested pass starts from absence, like any other — and draining it
3474        // ends it, handing the enclosing pass's request straight back.
3475        assert_eq!(
3476            RequestPass::enter().take().cursor,
3477            None,
3478            "a nested pass starts from absence, like any other"
3479        );
3480        {
3481            let inner = RequestPass::enter();
3482            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3483            ctx.set_cursor(CursorIcon::Text);
3484            drop(ctx);
3485            assert_eq!(
3486                inner.take().cursor,
3487                Some(CursorIcon::Text),
3488                "and resolves exactly what was asked inside it"
3489            );
3490        }
3491        // `take` consumes the guard, so the outermost pass ends here: the slot is
3492        // cleared rather than restored, and a `set_cursor` made outside any pass
3493        // (a reconciler's synthesized `Cancel`) still cannot leak into the next.
3494        assert_eq!(
3495            outer.take().cursor,
3496            Some(CursorIcon::Grab),
3497            "the enclosing pass's request survived the nested one"
3498        );
3499        {
3500            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3501            ctx.set_cursor(CursorIcon::NotAllowed);
3502        }
3503        let next = RequestPass::enter();
3504        assert_eq!(
3505            next.take().cursor,
3506            None,
3507            "a request made between passes belongs to no pass"
3508        );
3509    }
3510
3511    #[test]
3512    fn the_default_cursor_is_the_platform_arrow() {
3513        // `Default::default()` is what an absent request resolves to at the root,
3514        // so the derive must land on the arrow and not on some named shape.
3515        assert_eq!(CursorIcon::default(), CursorIcon::Default);
3516    }
3517
3518    #[test]
3519    fn a_nested_request_pass_hands_the_clipboard_slots_back_too() {
3520        // The cursor's reentrancy proof above, for the two channels sharing its
3521        // bracket: one flag guards all three slots, so a nested pass must return
3522        // an enclosing pass's *undrained copy* and *unanswered paste request*
3523        // exactly as it returns its cursor.
3524        let outer = RequestPass::enter();
3525        let mut count = 0u32;
3526        {
3527            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3528            ctx.write_clipboard("outer".to_string());
3529            ctx.request_paste();
3530        }
3531        {
3532            let inner = RequestPass::enter();
3533            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3534            ctx.write_clipboard("inner".to_string());
3535            drop(ctx);
3536            let resolved = inner.take();
3537            assert_eq!(
3538                resolved.clipboard_write.as_deref(),
3539                Some("inner"),
3540                "a nested pass resolves exactly what was written inside it"
3541            );
3542            assert!(
3543                !resolved.paste_request,
3544                "and starts from absence rather than inheriting the enclosing ask"
3545            );
3546        }
3547        let resolved = outer.take();
3548        assert_eq!(
3549            resolved.clipboard_write.as_deref(),
3550            Some("outer"),
3551            "the enclosing pass's write survived the nested one"
3552        );
3553        assert!(resolved.paste_request, "and so did its paste request");
3554
3555        // Outside any pass now: a stray write belongs to nobody.
3556        {
3557            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3558            ctx.write_clipboard("stray".to_string());
3559            ctx.request_paste();
3560        }
3561        let next = RequestPass::enter().take();
3562        assert_eq!(next.clipboard_write, None);
3563        assert!(!next.paste_request);
3564    }
3565
3566    #[test]
3567    fn the_last_clipboard_write_of_a_pass_wins() {
3568        let pass = RequestPass::enter();
3569        let mut count = 0u32;
3570        {
3571            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3572            ctx.write_clipboard("container".to_string());
3573            ctx.write_clipboard("leaf".to_string());
3574        }
3575        assert_eq!(
3576            pass.take().clipboard_write.as_deref(),
3577            Some("leaf"),
3578            "one write is resolved per pass, and the last caller is it"
3579        );
3580    }
3581
3582    #[test]
3583    fn a_paste_request_is_idempotent_within_a_pass() {
3584        let pass = RequestPass::enter();
3585        let mut count = 0u32;
3586        {
3587            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3588            ctx.request_paste();
3589            ctx.request_paste();
3590        }
3591        // Data-free: two asks owe one clipboard read, and the flag cannot
3592        // represent anything else.
3593        assert!(pass.take().paste_request);
3594    }
3595
3596    #[test]
3597    fn an_edit_command_is_focus_routed_with_zero_position() {
3598        let copy = InputEvent::EditCommand(EditCommand::Copy);
3599        assert!(copy.is_focus_routed(), "a clipboard verb follows the focus");
3600        assert!(!copy.is_broadcast(), "and is not a broadcast");
3601        assert_eq!(copy.position(), Point::ZERO);
3602        assert_eq!(
3603            copy.translated(Vec2::new(10.0, 20.0)),
3604            copy,
3605            "a positionless event is returned unchanged by a container's translate"
3606        );
3607    }
3608
3609    /// An overlay event standing in for one routed into a floated surface.
3610    fn overlay(kind: OverlayEventKind) -> InputEvent {
3611        InputEvent::Overlay(OverlayEvent {
3612            key: OverlayKey::next(),
3613            kind,
3614        })
3615    }
3616
3617    #[test]
3618    fn overlay_is_a_broadcast_and_housekeeping_is_the_only_other_one() {
3619        let routed = overlay(OverlayEventKind::Pointer(PointerEvent {
3620            phase: PointerPhase::Down,
3621            position: Point::new(120.0, 80.0),
3622            button: PointerButton::Primary,
3623        }));
3624        assert!(
3625            routed.is_broadcast(),
3626            "an overlay event reaches its owner by broadcast, wherever the owner sits"
3627        );
3628        assert!(
3629            !routed.is_focus_routed(),
3630            "and not down the focus chain — the surface's owner need not be focused"
3631        );
3632        assert!(InputEvent::Housekeeping.is_broadcast());
3633
3634        // ...and nothing else is. Spelled as an exhaustive walk rather than three
3635        // spot checks, so a variant added later has to state its own answer here.
3636        for event in [
3637            InputEvent::Pointer(PointerEvent {
3638                phase: PointerPhase::Down,
3639                position: Point::ZERO,
3640                button: PointerButton::Primary,
3641            }),
3642            InputEvent::Scroll {
3643                position: Point::ZERO,
3644                delta: ScrollDelta::Lines(0.0, 1.0),
3645            },
3646            InputEvent::Key(KeyEvent {
3647                key: Key::Named(NamedKey::Enter),
3648                modifiers: Modifiers::default(),
3649                repeat: false,
3650            }),
3651            InputEvent::Ime(ImeEvent::Enabled),
3652            InputEvent::EditCommand(EditCommand::Copy),
3653        ] {
3654            assert!(
3655                !event.is_broadcast(),
3656                "only Housekeeping and Overlay broadcast, but {event:?} claims to"
3657            );
3658        }
3659    }
3660
3661    #[test]
3662    fn an_overlay_event_carries_no_local_position_and_is_never_translated() {
3663        let routed = overlay(OverlayEventKind::Pointer(PointerEvent {
3664            phase: PointerPhase::Move,
3665            position: Point::new(120.0, 80.0),
3666            button: PointerButton::Primary,
3667        }));
3668        assert_eq!(
3669            routed.position(),
3670            Point::ZERO,
3671            "a broadcast is never hit-tested, so it reports no position to hit-test on"
3672        );
3673        // The payload's own position is WINDOW space, and the container chain the
3674        // broadcast travels describes where the *owner* sits — not where the
3675        // floated surface does — so translating it would corrupt it.
3676        assert_eq!(
3677            routed.translated(Vec2::new(-10.0, -20.0)),
3678            routed,
3679            "the container chain must not shift a window-space payload"
3680        );
3681        let scrolled = overlay(OverlayEventKind::Scroll {
3682            position: Point::new(120.0, 80.0),
3683            delta: ScrollDelta::Pixels(0.0, 12.0),
3684        });
3685        assert_eq!(scrolled.translated(Vec2::new(5.0, 5.0)), scrolled);
3686        let outside = overlay(OverlayEventKind::OutsideDown);
3687        assert_eq!(outside.position(), Point::ZERO);
3688        assert_eq!(outside.translated(Vec2::new(5.0, 5.0)), outside);
3689    }
3690
3691    #[test]
3692    fn edit_commands_drain_in_dispatch_order_within_one_pass() {
3693        let pass = RequestPass::enter();
3694        let mut count = 0u32;
3695        let taken = {
3696            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3697            // The toolbar's "cut" answered as two verbs: order is meaning.
3698            ctx.dispatch_edit_command(EditCommand::Copy);
3699            ctx.dispatch_edit_command(EditCommand::SelectAll);
3700            ctx.dispatch_edit_command(EditCommand::Cut);
3701            ctx.take_edit_commands()
3702        };
3703        assert_eq!(
3704            taken,
3705            vec![EditCommand::Copy, EditCommand::SelectAll, EditCommand::Cut],
3706            "a FIFO, not a last-writer-wins slot"
3707        );
3708
3709        // The drain empties the queue, so a second owner forwarding in the same
3710        // pass cannot re-apply the first owner's verbs.
3711        let mut second = 0u32;
3712        let mut ctx = EventCtx::new(&mut second, Point::ZERO, Size::ZERO);
3713        assert!(ctx.take_edit_commands().is_empty());
3714        drop(ctx);
3715        drop(pass.take());
3716    }
3717
3718    #[test]
3719    fn a_leaked_edit_command_is_cleared_with_the_pass_and_never_reaches_the_next() {
3720        {
3721            let pass = RequestPass::enter();
3722            let mut count = 0u32;
3723            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3724            // Dispatched, and nobody drains it: a wiring bug in the dispatching
3725            // widget. The pass resolving is what reports (debug builds) and clears
3726            // it — a `Cut` surviving into a later pass would apply to whatever is
3727            // selected by then, which is how text gets destroyed silently.
3728            ctx.dispatch_edit_command(EditCommand::Cut);
3729            drop(ctx);
3730            drop(pass.take());
3731        }
3732
3733        let next = RequestPass::enter();
3734        let mut count = 0u32;
3735        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3736        assert!(
3737            ctx.take_edit_commands().is_empty(),
3738            "a leaked command must not survive into the next pass"
3739        );
3740        drop(ctx);
3741        drop(next.take());
3742    }
3743
3744    #[test]
3745    fn a_nested_pass_hands_the_enclosing_passs_edit_commands_back() {
3746        let outer = RequestPass::enter();
3747        let mut count = 0u32;
3748        {
3749            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3750            ctx.dispatch_edit_command(EditCommand::Copy);
3751        }
3752        {
3753            // A nested dispatch (the overlay pre-pass re-entering `RenderRoot::event`
3754            // is the shipped case) starts from an empty queue and must not eat the
3755            // enclosing pass's undrained command.
3756            let inner = RequestPass::enter();
3757            let mut inner_state = 0u32;
3758            let mut ctx = EventCtx::new(&mut inner_state, Point::ZERO, Size::ZERO);
3759            assert!(ctx.take_edit_commands().is_empty());
3760            ctx.dispatch_edit_command(EditCommand::SelectAll);
3761            assert_eq!(ctx.take_edit_commands(), vec![EditCommand::SelectAll]);
3762            drop(ctx);
3763            drop(inner.take());
3764        }
3765        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3766        assert_eq!(
3767            ctx.take_edit_commands(),
3768            vec![EditCommand::Copy],
3769            "the enclosing pass's queue is handed back intact"
3770        );
3771        drop(ctx);
3772        drop(outer.take());
3773    }
3774
3775    #[test]
3776    fn debug_redacts_a_pasted_payload() {
3777        // The clipboard has no content-type hint to key a decision off (see the
3778        // `Debug` impl), so the payload is redacted unconditionally.
3779        let rendered = format!("{:?}", EditCommand::Paste("hunter2".to_string()));
3780        assert!(
3781            !rendered.contains("hunter2"),
3782            "paste payload leaked: {rendered}"
3783        );
3784        assert!(rendered.contains("<redacted>"));
3785        assert!(
3786            rendered.contains("Paste"),
3787            "the verb still prints: {rendered}"
3788        );
3789        // No length either — that leaks too.
3790        assert!(!rendered.contains('7'));
3791        assert_eq!(format!("{:?}", EditCommand::SelectAll), "SelectAll");
3792    }
3793
3794    #[test]
3795    fn a_pointer_contact_is_positioned_and_translated_like_a_pointer() {
3796        let contact = InputEvent::PointerContact {
3797            pointer_id: PointerId::touch(2),
3798            event: PointerEvent {
3799                phase: PointerPhase::Move,
3800                position: Point::new(30.0, 40.0),
3801                button: PointerButton::Primary,
3802            },
3803        };
3804        assert_eq!(contact.position(), Point::new(30.0, 40.0));
3805        assert_eq!(
3806            contact.translated(Vec2::new(-10.0, -20.0)),
3807            InputEvent::PointerContact {
3808                pointer_id: PointerId::touch(2),
3809                event: PointerEvent {
3810                    phase: PointerPhase::Move,
3811                    position: Point::new(20.0, 20.0),
3812                    button: PointerButton::Primary,
3813                },
3814            },
3815            "the position shifts; the id and the rest of the event do not"
3816        );
3817        // Hit-tested, like the pointer it wraps: neither class of non-positional
3818        // event.
3819        assert!(!contact.is_focus_routed());
3820        assert!(!contact.is_broadcast());
3821    }
3822
3823    #[test]
3824    fn pointer_ids_name_the_mouse_and_touch_slots() {
3825        assert_eq!(
3826            PointerId::MOUSE,
3827            PointerId {
3828                source: PointerSource::Mouse,
3829                slot: 0
3830            }
3831        );
3832        assert_eq!(
3833            PointerId::touch(3),
3834            PointerId {
3835                source: PointerSource::Touch,
3836                slot: 3
3837            }
3838        );
3839        assert_ne!(PointerId::MOUSE, PointerId::touch(0));
3840    }
3841
3842    #[test]
3843    fn a_context_reports_the_mouse_outside_any_contact_pass() {
3844        let mut count = 0u32;
3845        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3846        assert_eq!(ctx.pointer_id(), PointerId::MOUSE);
3847        // An opt-in with no pass open is still visible to the container that
3848        // reads it, but records nothing a later pass could inherit.
3849        ctx.capture_contacts();
3850        assert!(ctx.is_contact_capture_requested());
3851        drop(ctx);
3852        let pass = ContactPass::enter(PointerId::touch(0), false);
3853        assert!(!pass.contacts_requested());
3854    }
3855
3856    #[test]
3857    fn a_contact_pass_seeds_every_context_and_collects_the_opt_in() {
3858        let mut count = 0u32;
3859        let outer = ContactPass::enter(PointerId::touch(1), true);
3860        assert!(in_secondary_contact_pass());
3861        {
3862            // A fresh context (what a component builds over its own state)
3863            // reports the pass's contact, and so does every child context.
3864            let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
3865            assert_eq!(ctx.pointer_id(), PointerId::touch(1));
3866            let (contacts, id) = {
3867                let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, false, false);
3868                let id = child.pointer_id();
3869                child.capture_contacts();
3870                (child.is_contact_capture_requested(), id)
3871            };
3872            assert_eq!(id, PointerId::touch(1));
3873            ctx.absorb_child(false, false, contacts, false, false, false, false, None);
3874            assert!(ctx.is_contact_capture_requested(), "the opt-in bubbles");
3875        }
3876        assert!(outer.contacts_requested(), "and is recorded in the pass");
3877
3878        // A nested pass scopes its own contact and opt-in, then hands back.
3879        {
3880            let inner = ContactPass::enter(PointerId::MOUSE, false);
3881            assert_eq!(current_pointer_id(), PointerId::MOUSE);
3882            assert!(!in_secondary_contact_pass());
3883            assert!(!inner.contacts_requested());
3884        }
3885        assert_eq!(current_pointer_id(), PointerId::touch(1));
3886        assert!(in_secondary_contact_pass());
3887        assert!(outer.contacts_requested());
3888        drop(outer);
3889        assert_eq!(current_pointer_id(), PointerId::MOUSE);
3890        assert!(!in_secondary_contact_pass());
3891    }
3892}