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