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