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}