Skip to main content

kui_core/
input.rs

1//! Input handling and event production. Hit regions come from the previous
2//! frame's layout (the standard immediate-mode trade); events leave as plain
3//! data tagged with the origin that declared them, so the runner can route to
4//! the host app or an extension without knowing what either looks like.
5
6use crate::cursor::CursorShape;
7use crate::geom::{Rect, Vec2};
8use crate::key::Key;
9use crate::tree::OriginId;
10use crate::value::Value;
11use crate::window::{WindowCommand, WindowId, WindowRole};
12
13#[derive(Clone, Debug, PartialEq)]
14pub enum InputEvent {
15    /// Logical coordinates.
16    CursorMoved(Vec2),
17    CursorLeft,
18    /// A button press. `clicks` is driver-measured multi-click state
19    /// (1 = single, 2 = double, 3+ = triple) — the core is clock-free, so
20    /// click timing lives with whoever owns the event loop. Only the
21    /// primary button presses, drags and clicks; see [`MouseButton`].
22    MouseDown {
23        button: MouseButton,
24        clicks: u8,
25    },
26    /// The release of `button`. A non-primary release resolves nothing:
27    /// the primary button is the one that can be holding a press.
28    MouseUp {
29        button: MouseButton,
30    },
31    /// Wheel/trackpad delta in logical px (positive y = scroll up), a
32    /// scroll gesture of its own: the same as
33    /// [`InputEvent::ScrollGesture`] with `begins: true`. What a driver
34    /// that cannot tell one gesture from the next sends, and what every
35    /// door taking a bare delta (`kui_input_scroll`, Node's `scroll`)
36    /// feeds.
37    Scroll(Vec2),
38    /// A wheel or trackpad delta that is part of a scroll *gesture*
39    /// (backlog F107): a swipe and its momentum, or a wheel spun without
40    /// a pause. `begins` is true on a gesture's first event. The target
41    /// is chosen then, per axis — the innermost scroller under the
42    /// pointer that can still move that way, a scroller at its limit
43    /// passing the gesture to the one around it unless it says
44    /// `overscroll: contain`, an `on_scroll` node taking the axes its
45    /// `scroll_axes` names — and the rest of the gesture goes on to that
46    /// target (it is *latched*) wherever the pointer or the content
47    /// under it has gone since, until the next `begins`. An axis the
48    /// gesture had not moved on picks its target the first time it
49    /// does; a target whose node is gone, or behind a modal, is picked
50    /// again. Where gestures begin and end is the driver's to say — the
51    /// core is clock-free: the native runner begins one after a 200 ms
52    /// pause, on a switch between a wheel's notches and a trackpad's
53    /// pixels, and for a wheel on a pointer move.
54    ScrollGesture {
55        delta: Vec2,
56        begins: bool,
57    },
58    /// Committed text (typing, paste). Routed to the focused editor; with
59    /// none, a printable character presses or searches the focused
60    /// control. Never delivered to an `onKey` sink: the raw press already
61    /// reached it as a `key` event carrying `text`, and a sink hearing
62    /// both would type every character twice.
63    Text(String),
64    /// Text an IME committed at the end of a composition (backlog C17).
65    /// Routed like `Text` to a focused editor; otherwise delivered to the
66    /// focused sink as `{kind:"text", text, tag}` — the one committed text
67    /// the platform never reports as a key press with `text`, so it is the
68    /// one a sink has to be told about. Drivers send `Ime::Commit` here and
69    /// keep typing on `Text`.
70    Commit(String),
71    /// The clipboard's answer to a paste the app asked for
72    /// (`Core::request_paste`, a menu's Paste), with what the pasteboard
73    /// said about it (backlog F84). Routed exactly as [`InputEvent::Commit`]
74    /// is — a focused editor takes it as typing, a focused sink hears
75    /// `{kind:"text", text, tag}` — and the sink's event gains
76    /// `concealed: true` and `transient: true` for the markers that are
77    /// set, and nothing for those that are not.
78    ///
79    /// A variant of its own rather than two fields on `Commit`, so every
80    /// match on a commit still compiles and a driver that answers with a
81    /// bare `Commit` (an older C or Node host) is still an answer: both
82    /// clear the one-ask gate (backlog AR34), and a `Commit` is a paste
83    /// whose pasteboard marked nothing.
84    Paste {
85        text: String,
86        marks: ClipboardMarks,
87    },
88    /// In-progress IME composition (text and the caret byte range inside
89    /// it), inserted inline at the focused editor's caret as an uncommitted
90    /// marked range: following text shifts and the paragraph rewraps.
91    /// Empty text cancels it; the commit arrives separately as `Commit`.
92    /// With no editor focused it goes to the focused sink as
93    /// `{kind:"preedit", text, cursor: [start, end] | null, tag}`, an
94    /// empty `text` meaning the composition ended without a commit.
95    Preedit(String, Option<(usize, usize)>),
96    /// Navigation/editing key. Routed to the focused editor.
97    Key(EditKey, Mods),
98    /// A full key press, routed to whatever holds key focus (see
99    /// `Core::set_key_focus`). Apps that own their own text model take
100    /// keys through this instead of the editor path.
101    KeyDown(KeyPress),
102    /// The release of a key, routed the way [`InputEvent::KeyDown`] is —
103    /// so a held-key interaction (WASD, press-and-hold to preview, a key
104    /// that arms a mode) is a pair of events, not a guess about timing.
105    /// Only a key whose press was delivered produces one: a release the
106    /// focused sink never saw the press of is dropped, and focus moving
107    /// away while a key is held synthesizes the release first (see
108    /// `Core::release_held_keys`). The core clears `text` and `repeat` on
109    /// the way out — a release inserts nothing and never repeats.
110    KeyUp(KeyPress),
111    /// A request from assistive technology (see [`crate::access`]):
112    /// activate, focus, set an editor's text, scroll. Resolved in the core
113    /// the way the pointer or keyboard equivalent would be, so the app
114    /// sees the same events either way.
115    Access(crate::access::AccessRequest),
116    /// The physical modifier state changed. Reaches the host as a
117    /// `{kind="modifiers", shift, ctrl, alt, super}` event on the root (an
118    /// Elm-style app keeps it in its model and lets the view react — a
119    /// Cmd-held drag overlay, a hint bar) and is queryable while building
120    /// a frame (`Ui::modifiers`).
121    Modifiers(KeyMods),
122    /// A force click at a point in logical viewport coordinates: the
123    /// press deepened past the second stage of a Force Touch trackpad
124    /// (`docs/adr/0017-selection-as-a-scope.md`, decision 6).
125    ///
126    /// Routed like the secondary press — the topmost node under the point,
127    /// no focus moved, no caret placed, no click — because it arrives
128    /// *during* an ordinary press that is still running, and the click
129    /// that press produces still happens afterwards. Over text it selects
130    /// the word and asks the host to look it up; anywhere else it reaches
131    /// a node declaring `on_force_click`.
132    ///
133    /// macOS-only in practice: no other platform winit supports reports
134    /// pressure at all, and there a user can switch it off.
135    ForceClick(Vec2),
136    /// Files dragged in from the OS are over the window at `at` (logical
137    /// viewport coordinates) — entering and moving alike: the core tells
138    /// the two apart by whether the zone under the point changed, and a
139    /// change is the old zone's `leave` then the new one's `enter`
140    /// (`docs/adr/0031-a-drop-zone-is-a-row-and-the-files-are-an-event.md`,
141    /// decision 4). `paths` are the OS paths as the driver reported them.
142    /// A repeat at the same point emits nothing.
143    DragFiles {
144        paths: Vec<String>,
145        at: Vec2,
146    },
147    /// The dragged files were released at `at`: the zone there hears
148    /// `{kind="drop", phase="drop"}` and nothing hears a `leave`; with no
149    /// zone there, nothing is emitted and whatever was lit hears its
150    /// `leave`.
151    DropFiles {
152        paths: Vec<String>,
153        at: Vec2,
154    },
155    /// The dragged files left the window, or the OS ended the drag
156    /// elsewhere: the lit zone hears its `leave`.
157    DragCancel,
158    /// A file dialog's answer (backlog C51): the paths the user picked,
159    /// none for a dialog cancelled. Whoever asked with
160    /// `Core::request_files` hears `{kind:"files", paths, tag}`; with no
161    /// ask outstanding it is dropped.
162    Files(Vec<String>),
163}
164
165/// What the pasteboard said about the text a paste brought back (backlog
166/// F84): the markers password managers set on a copied secret, after the
167/// convention at nspasteboard.org that 1Password, Bitwarden, KeePassXC and
168/// the macOS clipboard managers follow. Read by the driver, which owns the
169/// clipboard, and handed over with the text as [`InputEvent::Paste`].
170///
171/// Where the runner reads each:
172///
173/// - `concealed`: the macOS pasteboard type `org.nspasteboard.ConcealedType`;
174///   on Windows the registered format
175///   `ExcludeClipboardContentFromMonitorProcessing` being present.
176/// - `transient`: `org.nspasteboard.TransientType`; on Windows the format
177///   `CanIncludeInClipboardHistory` holding 0.
178///
179/// The runner does not read them on Linux yet — KDE's
180/// `x-kde-passwordManagerHint: secret` is a MIME type arboard writes but
181/// cannot list — so there both stay false.
182#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
183pub struct ClipboardMarks {
184    /// The text is a secret: do not show it, log it or keep it anywhere.
185    pub concealed: bool,
186    /// The text is on the clipboard for a moment: do not keep it in a
187    /// history.
188    pub transient: bool,
189}
190
191impl ClipboardMarks {
192    /// Both markers: what a password manager puts on a secret it copies,
193    /// and what `Core::set_clipboard_secret` writes.
194    pub const SECRET: Self = Self {
195        concealed: true,
196        transient: true,
197    };
198
199    /// Neither marker set.
200    pub fn is_empty(self) -> bool {
201        !self.concealed && !self.transient
202    }
203
204    /// As bits, the C ABI's spelling: `KUI_PASTE_CONCEALED` 1,
205    /// `KUI_PASTE_TRANSIENT` 2.
206    pub fn bits(self) -> u32 {
207        self.concealed as u32 | (self.transient as u32) << 1
208    }
209
210    /// From [`ClipboardMarks::bits`]; unknown bits are ignored.
211    pub fn from_bits(bits: u32) -> Self {
212        Self {
213            concealed: bits & 1 != 0,
214            transient: bits & 2 != 0,
215        }
216    }
217}
218
219/// Which button a press came from — driver-facing rather than shaped after
220/// any one windowing library, so every driver maps its own vocabulary onto
221/// this one.
222///
223/// Only [`MouseButton::Primary`] drives the pointer model: it presses,
224/// drags, places the caret and produces `on_click`. A
225/// [`MouseButton::Secondary`] press asks the node under it for a context
226/// menu (`NodeSpec::on_context_menu`) and touches nothing else — not
227/// focus, not the caret, not a scrollbar thumb — because a right-click on
228/// a selection has to leave that selection alone. Every non-primary
229/// button, the secondary one included, reaches a node that claims it with
230/// `NodeSpec::on_button` (backlog F105): its press, the motion while it is
231/// held and its release, captured by that node; a claimed secondary press
232/// is that node's instead of a context menu. A non-primary press moves no
233/// focus, caret, selection or scrollbar either way.
234#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
235pub enum MouseButton {
236    /// The button that clicks and drags. The OS has already applied a
237    /// left-handed swap, so this is not necessarily the left one.
238    #[default]
239    Primary,
240    /// The context-menu button.
241    Secondary,
242    Middle,
243    /// A button this vocabulary does not name (back, forward, thumb
244    /// buttons), by driver index.
245    Other(u8),
246}
247
248impl MouseButton {
249    /// The number bindings pass buttons as: 0 primary, 1 secondary,
250    /// 2 middle, `3 + n` for `Other(n)`.
251    pub fn code(self) -> u32 {
252        match self {
253            MouseButton::Primary => 0,
254            MouseButton::Secondary => 1,
255            MouseButton::Middle => 2,
256            MouseButton::Other(n) => 3 + n as u32,
257        }
258    }
259
260    /// Inverse of [`MouseButton::code`]; anything past the named three is
261    /// an `Other`, saturating rather than wrapping.
262    pub fn from_code(code: u32) -> Self {
263        match code {
264            0 => MouseButton::Primary,
265            1 => MouseButton::Secondary,
266            2 => MouseButton::Middle,
267            n => MouseButton::Other((n - 3).min(u8::MAX as u32) as u8),
268        }
269    }
270
271    /// The three buttons that have a name, in code order. `Other` has no
272    /// name: a binding that needs one takes a [`MouseButton::code`].
273    pub const NAMED: [MouseButton; 3] = [
274        MouseButton::Primary,
275        MouseButton::Secondary,
276        MouseButton::Middle,
277    ];
278
279    /// The wire name of a named button (`"primary"`, `"secondary"`,
280    /// `"middle"`); `None` for an `Other`.
281    pub fn name(self) -> Option<&'static str> {
282        match self {
283            MouseButton::Primary => Some("primary"),
284            MouseButton::Secondary => Some("secondary"),
285            MouseButton::Middle => Some("middle"),
286            MouseButton::Other(_) => None,
287        }
288    }
289
290    /// The three named buttons by name, for bindings that spell them as
291    /// strings: the inverse of [`Self::name`].
292    pub fn from_name(name: &str) -> Option<Self> {
293        Self::NAMED.into_iter().find(|b| b.name() == Some(name))
294    }
295
296    /// The value a `button` event carries for this button (backlog
297    /// F105): its name for a named one, its [`MouseButton::code`] for an
298    /// `Other`.
299    pub fn to_value(self) -> Value {
300        match self.name() {
301            Some(name) => Value::str(name),
302            None => Value::Int(self.code() as i64),
303        }
304    }
305}
306
307/// Which of the non-primary buttons a node's `on_button` claims (backlog
308/// F105): [`Buttons::SECONDARY`], [`Buttons::MIDDLE`] and
309/// [`Buttons::OTHER`] (every button past the named three), or-ed together.
310/// A node declaring `on_button` claims [`Buttons::ALL`] unless it says
311/// otherwise. The primary button is never in it: that one presses, drags
312/// and clicks for every node.
313///
314/// The C ABI's spelling is the same bits (`KuiSpec.buttons`: 1 secondary,
315/// 2 middle, 4 other), a zeroed field meaning all three; the schema's is
316/// the names, `"secondary middle"`.
317#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
318pub struct Buttons(u8);
319
320impl Buttons {
321    /// Claims nothing: an `on_button` that hears no press.
322    pub const NONE: Self = Self(0);
323    /// The context-menu button. Claimed, its press is the owner's
324    /// `button` event instead of a `contextmenu` event or the stock menu.
325    pub const SECONDARY: Self = Self(1);
326    pub const MIDDLE: Self = Self(2);
327    /// Every button past the named three (back, forward, thumb buttons).
328    pub const OTHER: Self = Self(4);
329    pub const ALL: Self = Self(7);
330
331    /// Whether `button` is in the set. The primary button never is.
332    pub fn contains(self, button: MouseButton) -> bool {
333        let bit = match button {
334            MouseButton::Primary => return false,
335            MouseButton::Secondary => Self::SECONDARY,
336            MouseButton::Middle => Self::MIDDLE,
337            MouseButton::Other(_) => Self::OTHER,
338        };
339        self.0 & bit.0 != 0
340    }
341
342    /// As bits, the C ABI's spelling: 1 secondary, 2 middle, 4 other.
343    pub const fn bits(self) -> u32 {
344        self.0 as u32
345    }
346
347    /// From [`Buttons::bits`]; unknown bits are ignored. Zero is
348    /// [`Buttons::NONE`] here: it is the C binding that reads a zeroed
349    /// field as all three, being a field the host never set.
350    pub fn from_bits(bits: u32) -> Self {
351        Self((bits & Self::ALL.0 as u32) as u8)
352    }
353
354    /// From the schema's spelling: names separated by spaces or commas —
355    /// `"middle"`, `"secondary middle"`, `"secondary, middle, other"`. A
356    /// word that is none of the three is skipped, so a string of none of
357    /// them claims nothing: a typo never takes the secondary button away
358    /// from a context menu.
359    pub fn parse(names: &str) -> Self {
360        names.split(|c: char| c == ',' || c.is_whitespace()).fold(
361            Self::NONE,
362            |set, name| match name {
363                "secondary" => set | Self::SECONDARY,
364                "middle" => set | Self::MIDDLE,
365                "other" => set | Self::OTHER,
366                _ => set,
367            },
368        )
369    }
370}
371
372impl Default for Buttons {
373    fn default() -> Self {
374        Self::ALL
375    }
376}
377
378impl std::ops::BitOr for Buttons {
379    type Output = Self;
380    fn bitor(self, rhs: Self) -> Self {
381        Self(self.0 | rhs.0)
382    }
383}
384
385impl std::ops::BitOrAssign for Buttons {
386    fn bitor_assign(&mut self, rhs: Self) {
387        self.0 |= rhs.0;
388    }
389}
390
391impl InputEvent {
392    /// A primary-button press — the spelling drivers and tests want when
393    /// they only ever send one button.
394    pub fn mouse_down(clicks: u8) -> Self {
395        InputEvent::MouseDown {
396            button: MouseButton::Primary,
397            clicks,
398        }
399    }
400
401    /// A primary-button release.
402    pub fn mouse_up() -> Self {
403        InputEvent::MouseUp {
404            button: MouseButton::Primary,
405        }
406    }
407}
408
409/// Editing keys, decoupled from any windowing library's key codes.
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum EditKey {
412    Left,
413    Right,
414    Up,
415    Down,
416    Home,
417    End,
418    PageUp,
419    PageDown,
420    Backspace,
421    Delete,
422    Enter,
423    Tab,
424    SelectAll,
425    /// Undo/redo of the edit widget's own history (drivers map the platform
426    /// chords; hosts with their own text model never see these — they take
427    /// the raw chord through `KeyDown`).
428    Undo,
429    Redo,
430    Escape,
431}
432
433impl EditKey {
434    /// Every editing key, in declaration order — the list a binding's
435    /// name table and a generated type union are checked against, so a
436    /// key added here reaches C, Node and TypeScript or fails a build
437    /// (ADR 0020, decision 9, the same way `Role::ALL` did).
438    pub const ALL: [EditKey; 16] = [
439        EditKey::Left,
440        EditKey::Right,
441        EditKey::Up,
442        EditKey::Down,
443        EditKey::Home,
444        EditKey::End,
445        EditKey::PageUp,
446        EditKey::PageDown,
447        EditKey::Backspace,
448        EditKey::Delete,
449        EditKey::Enter,
450        EditKey::Tab,
451        EditKey::SelectAll,
452        EditKey::Undo,
453        EditKey::Redo,
454        EditKey::Escape,
455    ];
456
457    /// The wire name a binding spells the key as (`"pageup"`,
458    /// `"selectall"`: lower case, no separator).
459    pub fn name(self) -> &'static str {
460        match self {
461            EditKey::Left => "left",
462            EditKey::Right => "right",
463            EditKey::Up => "up",
464            EditKey::Down => "down",
465            EditKey::Home => "home",
466            EditKey::End => "end",
467            EditKey::PageUp => "pageup",
468            EditKey::PageDown => "pagedown",
469            EditKey::Backspace => "backspace",
470            EditKey::Delete => "delete",
471            EditKey::Enter => "enter",
472            EditKey::Tab => "tab",
473            EditKey::SelectAll => "selectall",
474            EditKey::Undo => "undo",
475            EditKey::Redo => "redo",
476            EditKey::Escape => "escape",
477        }
478    }
479
480    /// The key a wire name spells: the inverse of [`Self::name`].
481    pub fn from_name(name: &str) -> Option<EditKey> {
482        Self::ALL.into_iter().find(|k| k.name() == name)
483    }
484}
485
486/// Modifier state for editing keys. `word` is Alt/Option (word-wise motion),
487/// `doc` is the platform primary modifier (line/document-wise motion).
488#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
489pub struct Mods {
490    pub shift: bool,
491    pub word: bool,
492    pub doc: bool,
493}
494
495impl Mods {
496    /// No modifier held: what the `with_*` steps start from —
497    /// `Mods::NONE.with_shift().with_word()`.
498    pub const NONE: Mods = Mods {
499        shift: false,
500        word: false,
501        doc: false,
502    };
503
504    pub const fn with_shift(mut self) -> Self {
505        self.shift = true;
506        self
507    }
508
509    pub const fn with_word(mut self) -> Self {
510        self.word = true;
511        self
512    }
513
514    pub const fn with_doc(mut self) -> Self {
515        self.doc = true;
516        self
517    }
518}
519
520/// A physical key press: the full keyboard, decoupled from any windowing
521/// library. [`EditKey`] is the input widget's closed navigation vocabulary;
522/// this is what apps that own their own text model bind against — an editor
523/// with modal keymaps, a game, a scripted panel.
524#[derive(Clone, Copy, Debug, PartialEq, Eq)]
525pub enum KeyCode {
526    /// A character-producing key, as the active layout produced it — `W`
527    /// and `$` arrive as themselves (shift already applied), which is what
528    /// keymaps bind against.
529    ///
530    /// A layout that produces something outside ASCII does not reach here:
531    /// the driver substitutes the US-QWERTY key at that position, as Shift
532    /// prints it (`J`, `:`; unshifted under Alt), so a keymap written in
533    /// Latin keeps working on a Cyrillic, Greek, Hebrew or Arabic layout
534    /// instead of matching nothing at all. `text` is still the layout's
535    /// own character. See [`KeyPress::from_layout`], [`KeyPress::physical`],
536    /// and `docs/adr/0002` decision 11 for why the layout still wins
537    /// whenever it speaks ASCII.
538    Char(char),
539    /// Function key: `F(1)` .. `F(35)`.
540    F(u8),
541    Left,
542    Right,
543    Up,
544    Down,
545    Home,
546    End,
547    PageUp,
548    PageDown,
549    Backspace,
550    Delete,
551    Enter,
552    Tab,
553    Escape,
554    Space,
555    Insert,
556    /// Print Screen / SysRq.
557    PrintScreen,
558    /// Pause / Break.
559    Pause,
560    /// The context-menu key (the one beside the right-hand Ctrl).
561    Menu,
562    /// The keypad's middle key with Num Lock off (X11's `KP_Begin`), and
563    /// Clear where a keyboard has one.
564    Clear,
565    /// The modifier keys themselves, which side in [`KeyPress::location`].
566    /// Heard only by a sink that asked for them
567    /// ([`crate::NodeSpec::modifier_keys`]): to every other sink a
568    /// modifier is only ever held, in [`KeyMods`], and a Shift pressed
569    /// between two keys of a sequence must not read as a key between
570    /// them.
571    Shift,
572    Ctrl,
573    Alt,
574    /// Command on a Mac, the Windows key, Super.
575    Super,
576    /// The lock keys, as keys; what they lock is [`KeyPress::locks`].
577    /// Modifier keys as far as delivery goes (see [`KeyCode::Shift`]).
578    CapsLock,
579    NumLock,
580    ScrollLock,
581    MediaPlay,
582    MediaPause,
583    MediaPlayPause,
584    MediaStop,
585    MediaNext,
586    MediaPrev,
587    MediaRecord,
588    MediaFastForward,
589    MediaRewind,
590    VolumeUp,
591    VolumeDown,
592    VolumeMute,
593    /// A key this vocabulary doesn't name; `KeyPress::text` may still carry
594    /// what it would insert.
595    Unknown,
596}
597
598/// Every named key with its payload name, in one table so
599/// [`KeyCode::name`] and [`KeyCode::from_name`] cannot drift apart —
600/// all but `Char` and `F`, which are spelled by rule.
601const NAMED_KEYS: [(KeyCode, &str); 38] = [
602    (KeyCode::Left, "left"),
603    (KeyCode::Right, "right"),
604    (KeyCode::Up, "up"),
605    (KeyCode::Down, "down"),
606    (KeyCode::Home, "home"),
607    (KeyCode::End, "end"),
608    (KeyCode::PageUp, "pageup"),
609    (KeyCode::PageDown, "pagedown"),
610    (KeyCode::Backspace, "backspace"),
611    (KeyCode::Delete, "delete"),
612    (KeyCode::Enter, "enter"),
613    (KeyCode::Tab, "tab"),
614    (KeyCode::Escape, "escape"),
615    (KeyCode::Space, "space"),
616    (KeyCode::Insert, "insert"),
617    (KeyCode::PrintScreen, "printscreen"),
618    (KeyCode::Pause, "pause"),
619    (KeyCode::Menu, "menu"),
620    (KeyCode::Clear, "clear"),
621    (KeyCode::Shift, "shift"),
622    (KeyCode::Ctrl, "ctrl"),
623    (KeyCode::Alt, "alt"),
624    (KeyCode::Super, "super"),
625    (KeyCode::CapsLock, "capslock"),
626    (KeyCode::NumLock, "numlock"),
627    (KeyCode::ScrollLock, "scrolllock"),
628    (KeyCode::MediaPlay, "mediaplay"),
629    (KeyCode::MediaPause, "mediapause"),
630    (KeyCode::MediaPlayPause, "mediaplaypause"),
631    (KeyCode::MediaStop, "mediastop"),
632    (KeyCode::MediaNext, "medianext"),
633    (KeyCode::MediaPrev, "mediaprev"),
634    (KeyCode::MediaRecord, "mediarecord"),
635    (KeyCode::MediaFastForward, "mediafastforward"),
636    (KeyCode::MediaRewind, "mediarewind"),
637    (KeyCode::VolumeUp, "volumeup"),
638    (KeyCode::VolumeDown, "volumedown"),
639    (KeyCode::VolumeMute, "volumemute"),
640];
641
642impl KeyCode {
643    /// Stable lowercase name for the data payload: `"a"`, `"f5"`, `"pageup"`.
644    /// Bindings in C and Lua match on these.
645    pub fn name(self) -> String {
646        match self {
647            KeyCode::Char(c) => c.to_string(),
648            KeyCode::F(n) => format!("f{n}"),
649            KeyCode::Unknown => "unknown".into(),
650            named => NAMED_KEYS
651                .iter()
652                .find(|(k, _)| *k == named)
653                .map_or("unknown", |(_, n)| n)
654                .into(),
655        }
656    }
657
658    /// Every key this vocabulary names but `Char` and `F`, with its
659    /// payload name — for a binding that lists them (the generated key
660    /// name types) and a test that walks them.
661    pub fn named() -> &'static [(KeyCode, &'static str)] {
662        &NAMED_KEYS
663    }
664
665    /// Whether this is a modifier or lock key — heard only by a sink
666    /// that asked for them ([`crate::NodeSpec::modifier_keys`]).
667    pub fn is_modifier(self) -> bool {
668        matches!(
669            self,
670            KeyCode::Shift
671                | KeyCode::Ctrl
672                | KeyCode::Alt
673                | KeyCode::Super
674                | KeyCode::CapsLock
675                | KeyCode::NumLock
676                | KeyCode::ScrollLock
677        )
678    }
679
680    /// The inverse of [`KeyCode::name`]: the name a binding spells a key
681    /// with. A single character is that character (already
682    /// layout-resolved, so `"W"` and `"$"` arrive as themselves), `"f1"`
683    /// .. `"f35"` a function key, and the rest are the names above.
684    /// `None` for a name this vocabulary does not know — every binding
685    /// that takes keys as strings parses them here, so they cannot drift
686    /// apart.
687    pub fn from_name(s: &str) -> Option<KeyCode> {
688        let mut chars = s.chars();
689        if let (Some(c), None) = (chars.next(), chars.next()) {
690            return Some(KeyCode::Char(c));
691        }
692        if let Some(n) = s.strip_prefix('f').and_then(|n| n.parse::<u8>().ok())
693            && (1..=35).contains(&n)
694        {
695            return Some(KeyCode::F(n));
696        }
697        if s == "unknown" {
698            return Some(KeyCode::Unknown);
699        }
700        NAMED_KEYS.iter().find(|(_, n)| *n == s).map(|(k, _)| *k)
701    }
702
703    /// What a press of this key types, for a door whose host did not say:
704    /// the character itself, a space for Space, nothing for any other
705    /// named key or under Ctrl, Alt or Super — the rule the winit runner
706    /// reads off the layout's own key.
707    ///
708    /// Asked of the key *as the layout named it*, never of the code
709    /// [`KeyPress::from_layout`] resolved: on a Russian layout ⇧ on the
710    /// key printed `;` binds as `:` and types `Ж`, and asking the stand-in
711    /// typed the `:` into an editor (backlog RG28).
712    pub fn typed(self, mods: KeyMods) -> Option<String> {
713        if mods.ctrl || mods.alt || mods.super_key {
714            return None;
715        }
716        match self {
717            KeyCode::Char(c) => Some(c.to_string()),
718            KeyCode::Space => Some(" ".to_string()),
719            _ => None,
720        }
721    }
722}
723
724/// Which half of a key's life an event reports. Both halves arrive as one
725/// `{kind="key"}` payload — the way a drag's three phases and a hover's
726/// two do — so an app binds one handler and matches `phase`.
727#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
728pub enum KeyPhase {
729    #[default]
730    Down,
731    Up,
732}
733
734impl KeyPhase {
735    /// The payload spelling: `"down"` / `"up"`.
736    pub fn name(self) -> &'static str {
737        match self {
738            KeyPhase::Down => "down",
739            KeyPhase::Up => "up",
740        }
741    }
742}
743
744/// Where on the keyboard a key sits, for the keys that have twins: the
745/// left or right Shift, Ctrl, Alt or Super, and the keypad's digits,
746/// operators, Enter and (with Num Lock off) arrows beside the main
747/// block's. Everything else is `Standard`. `code` stays what the key is
748/// — the keypad's `1` is `Char('1')`, its Enter is `Enter` — so a keymap
749/// that does not care reads nothing new, and one that does (a terminal
750/// speaking kitty's keyboard protocol, a game) reads this.
751#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
752pub enum KeyLocation {
753    #[default]
754    Standard,
755    Left,
756    Right,
757    Numpad,
758}
759
760impl KeyLocation {
761    /// The payload spelling: `"standard"`, `"left"`, `"right"`, `"numpad"`.
762    pub fn name(self) -> &'static str {
763        match self {
764            KeyLocation::Standard => "standard",
765            KeyLocation::Left => "left",
766            KeyLocation::Right => "right",
767            KeyLocation::Numpad => "numpad",
768        }
769    }
770
771    pub fn from_name(s: &str) -> Option<Self> {
772        Some(match s {
773            "standard" => KeyLocation::Standard,
774            "left" => KeyLocation::Left,
775            "right" => KeyLocation::Right,
776            "numpad" => KeyLocation::Numpad,
777            _ => return None,
778        })
779    }
780
781    /// The C door's spelling, two bits above the modifiers in the same
782    /// word (`KUI_KLOC_*`): 0 standard, 1 left, 2 right, 3 numpad, at
783    /// [`KeyLocation::SHIFT`].
784    pub const SHIFT: u32 = 8;
785    pub const MASK: u32 = 3 << Self::SHIFT;
786
787    pub fn bits(self) -> u32 {
788        (match self {
789            KeyLocation::Standard => 0,
790            KeyLocation::Left => 1,
791            KeyLocation::Right => 2,
792            KeyLocation::Numpad => 3,
793        }) << Self::SHIFT
794    }
795
796    pub fn from_bits(bits: u32) -> Self {
797        match (bits & Self::MASK) >> Self::SHIFT {
798            1 => KeyLocation::Left,
799            2 => KeyLocation::Right,
800            3 => KeyLocation::Numpad,
801            _ => KeyLocation::Standard,
802        }
803    }
804}
805
806/// Which Option keys act as Alt on macOS (backlog F113) — what a frame
807/// declares with [`crate::Ui::option_as_alt`]. On a Mac, Option composes:
808/// ⌥m types "µ", and ⌥u, ⌥e, ⌥i, ⌥n and ⌥` are *dead keys* that start
809/// an accent and wait for the next key, so the press never arrives as a
810/// key at all and a keymap that binds `<A-u>` never hears it. An Option
811/// key named here is Alt instead: it composes nothing, types nothing, and
812/// every key under it arrives as a chord of the key the layout prints
813/// unmodified — what a terminal's "Option as Meta" and an editor's Alt
814/// bindings want. `None`, the default, is the Mac's own behaviour; one
815/// side leaves the other composing, so a user keeps `ü` on the right
816/// Option while the left one is Alt. Other platforms have no such
817/// composition on Alt and read nothing here.
818#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
819pub enum OptionAsAlt {
820    #[default]
821    None,
822    Left,
823    Right,
824    Both,
825}
826
827impl OptionAsAlt {
828    /// Every value, in the order of the C door's numbers.
829    pub const ALL: [OptionAsAlt; 4] = [
830        OptionAsAlt::None,
831        OptionAsAlt::Left,
832        OptionAsAlt::Right,
833        OptionAsAlt::Both,
834    ];
835
836    /// The prop's spelling: `"none"`, `"left"`, `"right"`, `"both"`.
837    pub fn name(self) -> &'static str {
838        match self {
839            OptionAsAlt::None => "none",
840            OptionAsAlt::Left => "left",
841            OptionAsAlt::Right => "right",
842            OptionAsAlt::Both => "both",
843        }
844    }
845
846    pub fn from_name(s: &str) -> Option<Self> {
847        Self::ALL.into_iter().find(|v| v.name() == s)
848    }
849
850    /// The C door's number (`KUI_OPTION_AS_ALT_*`) and the binary IR's: 0
851    /// none, 1 left, 2 right, 3 both.
852    pub fn index(self) -> u32 {
853        self as u32
854    }
855
856    /// The value at `index`; `None` past the four, so a door can refuse
857    /// what it does not know rather than guess.
858    pub fn from_index(index: u32) -> Option<Self> {
859        Self::ALL.get(index as usize).copied()
860    }
861
862    /// Whether an Option key at `location` is Alt under this setting.
863    pub fn covers(self, location: KeyLocation) -> bool {
864        match self {
865            OptionAsAlt::None => false,
866            OptionAsAlt::Left => location == KeyLocation::Left,
867            OptionAsAlt::Right => location == KeyLocation::Right,
868            OptionAsAlt::Both => matches!(location, KeyLocation::Left | KeyLocation::Right),
869        }
870    }
871}
872
873/// What the lock keys hold at a press: Caps Lock and Num Lock on or off.
874/// Not a modifier held — [`KeyMods`] is only what is down, which
875/// accelerators and chords compare exactly — but state a press was made
876/// under, which a terminal speaking kitty's keyboard protocol reports
877/// and a keypad reading needs (its `1` is an End with Num Lock off).
878#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
879pub struct KeyLocks {
880    pub caps: bool,
881    pub num: bool,
882}
883
884impl KeyLocks {
885    /// The C door's spelling, beside the modifiers in the same word:
886    /// `KUI_KLOCK_CAPS`, `KUI_KLOCK_NUM`.
887    pub const CAPS: u32 = 1 << 4;
888    pub const NUM: u32 = 1 << 5;
889
890    pub fn bits(self) -> u32 {
891        (if self.caps { Self::CAPS } else { 0 }) | (if self.num { Self::NUM } else { 0 })
892    }
893
894    pub fn from_bits(bits: u32) -> Self {
895        KeyLocks {
896            caps: bits & Self::CAPS != 0,
897            num: bits & Self::NUM != 0,
898        }
899    }
900}
901
902/// Which alphabet the layout a press was typed on writes, as the platform
903/// answers it: what decides whose ASCII a keymap matches (backlog F115).
904///
905/// A Latin layout's ASCII is the label on the key — AZERTY's `&` on the
906/// key US-QWERTY prints 1, German's `-` on its `/` — and a keymap matches
907/// it. A non-Latin layout's ASCII is incidental: macOS's Russian puts `]`
908/// on the key US-QWERTY prints `` ` ``, `"` on ⇧2 and `:` on ⇧5, Windows'
909/// Russian `.` on `/`, and the user reaching for `` ` `` there means the
910/// key, as the letters beside it mean theirs. So on a non-Latin layout
911/// every key reads as US-QWERTY prints it, punctuation and digits
912/// included — macOS's own rule for a ⌘ shortcut, which it resolves
913/// through the ASCII-capable layout whenever the current one is not.
914///
915/// `Latin` is also what a driver says when it cannot ask: each key is
916/// then judged by itself, and only one the layout put no ASCII on falls
917/// back (see [`KeyPress::from_layout`]).
918#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
919pub enum LayoutScript {
920    #[default]
921    Latin,
922    NonLatin,
923}
924
925impl LayoutScript {
926    /// The C door's spelling, beside the modifiers and the locks in the
927    /// same word: `KUI_KLAYOUT_NONLATIN`.
928    pub const NON_LATIN: u32 = 1 << 6;
929
930    pub fn from_bits(bits: u32) -> Self {
931        if bits & Self::NON_LATIN != 0 {
932            LayoutScript::NonLatin
933        } else {
934            LayoutScript::Latin
935        }
936    }
937
938    /// The Node door's spelling: `"latin"`, `"nonLatin"`.
939    pub fn from_name(s: &str) -> Option<Self> {
940        match s {
941            "latin" => Some(LayoutScript::Latin),
942            "nonLatin" | "non_latin" => Some(LayoutScript::NonLatin),
943            _ => None,
944        }
945    }
946}
947
948/// Physical modifier state. Unlike [`Mods`] — which abstracts platform
949/// conventions for the input widget (`word`, `doc`) — nothing here is
950/// normalized: an app binding `Ctrl-w` needs to know it was Control and not
951/// Command.
952#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
953pub struct KeyMods {
954    pub shift: bool,
955    pub ctrl: bool,
956    pub alt: bool,
957    /// Windows key / Command / Super.
958    pub super_key: bool,
959}
960
961impl KeyMods {
962    /// Bit 0 Shift, 1 Ctrl, 2 Alt, 3 Super — the order the fields are
963    /// declared in, and the C header's `KUI_KMOD_*` (pinned there). What
964    /// a wire that carries the state as one integer spells it as: the
965    /// conformance corpus's `modifiers` step, the C input door.
966    pub const SHIFT: u32 = 1 << 0;
967    pub const CTRL: u32 = 1 << 1;
968    pub const ALT: u32 = 1 << 2;
969    pub const SUPER: u32 = 1 << 3;
970
971    /// No modifier held: what the `with_*` steps start from —
972    /// `KeyMods::NONE.with_shift().with_ctrl()`.
973    pub const NONE: KeyMods = KeyMods {
974        shift: false,
975        ctrl: false,
976        alt: false,
977        super_key: false,
978    };
979
980    pub const fn with_shift(mut self) -> Self {
981        self.shift = true;
982        self
983    }
984
985    pub const fn with_ctrl(mut self) -> Self {
986        self.ctrl = true;
987        self
988    }
989
990    pub const fn with_alt(mut self) -> Self {
991        self.alt = true;
992        self
993    }
994
995    pub const fn with_super(mut self) -> Self {
996        self.super_key = true;
997        self
998    }
999
1000    /// The platform primary shortcut modifier held: Command on macOS,
1001    /// Control elsewhere — the one [`Self::primary`] reads.
1002    pub const fn with_primary(self) -> Self {
1003        if cfg!(target_os = "macos") {
1004            self.with_super()
1005        } else {
1006            self.with_ctrl()
1007        }
1008    }
1009
1010    pub fn from_bits(bits: u32) -> Self {
1011        Self {
1012            shift: bits & Self::SHIFT != 0,
1013            ctrl: bits & Self::CTRL != 0,
1014            alt: bits & Self::ALT != 0,
1015            super_key: bits & Self::SUPER != 0,
1016        }
1017    }
1018
1019    pub fn bits(self) -> u32 {
1020        let bit = |on: bool, b: u32| if on { b } else { 0 };
1021        bit(self.shift, Self::SHIFT)
1022            | bit(self.ctrl, Self::CTRL)
1023            | bit(self.alt, Self::ALT)
1024            | bit(self.super_key, Self::SUPER)
1025    }
1026
1027    pub fn any(self) -> bool {
1028        self.shift || self.ctrl || self.alt || self.super_key
1029    }
1030
1031    /// The platform primary shortcut modifier: Command on macOS, Control
1032    /// elsewhere.
1033    pub fn primary(self) -> bool {
1034        if cfg!(target_os = "macos") {
1035            self.super_key
1036        } else {
1037            self.ctrl
1038        }
1039    }
1040
1041    /// The payload form of a modifier change:
1042    /// `{kind="modifiers", shift=, ctrl=, alt=, super=}`.
1043    pub fn to_value(self) -> Value {
1044        Value::map([
1045            ("kind", Value::str("modifiers")),
1046            ("shift", Value::Bool(self.shift)),
1047            ("ctrl", Value::Bool(self.ctrl)),
1048            ("alt", Value::Bool(self.alt)),
1049            ("super", Value::Bool(self.super_key)),
1050        ])
1051    }
1052}
1053
1054/// What US-QWERTY prints on a key under Shift: the upper-case letter, the
1055/// symbol above a digit, the pair on a punctuation key. Anything else —
1056/// already shifted, or not a US key at all — is itself.
1057fn us_shifted(c: char) -> char {
1058    match c {
1059        'a'..='z' => c.to_ascii_uppercase(),
1060        '1' => '!',
1061        '2' => '@',
1062        '3' => '#',
1063        '4' => '$',
1064        '5' => '%',
1065        '6' => '^',
1066        '7' => '&',
1067        '8' => '*',
1068        '9' => '(',
1069        '0' => ')',
1070        '`' => '~',
1071        '-' => '_',
1072        '=' => '+',
1073        '[' => '{',
1074        ']' => '}',
1075        '\\' => '|',
1076        ';' => ':',
1077        '\'' => '"',
1078        ',' => '<',
1079        '.' => '>',
1080        '/' => '?',
1081        other => other,
1082    }
1083}
1084
1085/// One key press, delivered to whatever holds key focus. Carries both the
1086/// binding view (`code` + `mods`) and the typing view (`text`), so an app can
1087/// serve a modal keymap and an insert mode from the same event.
1088#[derive(Clone, Debug, PartialEq, Eq)]
1089pub struct KeyPress {
1090    pub code: KeyCode,
1091    /// Where the key *is*, independent of the layout: the US-QWERTY key at
1092    /// that position, in the same vocabulary as `code`. The key left of B
1093    /// is `Char('v')` on every layout on earth, so a chord map written
1094    /// against this one binds a shape rather than a character — what a
1095    /// game's WASD wants, and what a keymap wants when it would rather be
1096    /// wrong about the label than wrong about the finger.
1097    ///
1098    /// `code` is usually the better default; see its note. `Unknown` when
1099    /// the platform reports a position this vocabulary cannot name.
1100    pub physical: KeyCode,
1101    pub mods: KeyMods,
1102    /// What this press would insert, if anything — already resolved through
1103    /// the keyboard layout. `None` for pure navigation and chords.
1104    pub text: Option<String>,
1105    /// Set when the press came from OS key repeat.
1106    pub repeat: bool,
1107    /// Which of a key's twins this is: the left or right modifier, the
1108    /// keypad's digit or the main block's (see [`KeyLocation`]).
1109    pub location: KeyLocation,
1110    /// Caps Lock and Num Lock as the press left them: a lock key's own
1111    /// press reports the state it turned the lock to, on every platform
1112    /// (backlog RG96).
1113    pub locks: KeyLocks,
1114}
1115
1116impl KeyPress {
1117    /// A press whose position is its own code — what a layout that agrees
1118    /// with US-QWERTY produces, and the sane reading of an injected press:
1119    /// naming a key is saying which key was pressed. The one fold: an
1120    /// ASCII letter's position is its lower-case letter, since a window
1121    /// reports `physical` from a table that never sees Shift (`Z` beside
1122    /// `code: "Z"` for ⇧Z would be a pair no window ever sends; backlog
1123    /// F65). A `physical` a caller spells is delivered as spelled — this
1124    /// is only the default, which was already a guess.
1125    pub fn new(code: KeyCode, mods: KeyMods) -> Self {
1126        let physical = match code {
1127            KeyCode::Char(c) if c.is_ascii_uppercase() => KeyCode::Char(c.to_ascii_lowercase()),
1128            other => other,
1129        };
1130        Self {
1131            code,
1132            physical,
1133            mods,
1134            text: None,
1135            repeat: false,
1136            location: KeyLocation::Standard,
1137            locks: KeyLocks::default(),
1138        }
1139    }
1140
1141    /// Says which of a key's twins this is (`Numpad` for the keypad's,
1142    /// `Left` / `Right` for a modifier's).
1143    pub fn with_location(mut self, location: KeyLocation) -> Self {
1144        self.location = location;
1145        self
1146    }
1147
1148    /// Says what the lock keys held at the press.
1149    pub fn with_locks(mut self, locks: KeyLocks) -> Self {
1150        self.locks = locks;
1151        self
1152    }
1153
1154    /// Says which physical key produced this press, when the layout put a
1155    /// different code on it (`⌥v` on Dvorak: code `v`, physical `.`).
1156    pub fn with_physical(mut self, physical: KeyCode) -> Self {
1157        self.physical = physical;
1158        self
1159    }
1160
1161    /// The press a driver builds from the two things the OS tells it: what
1162    /// the active layout put on the key, and which key it was. Every driver
1163    /// resolves `code` the same way because they all come through here.
1164    ///
1165    /// The layout wins while it speaks ASCII, so a chord lands on the key
1166    /// the user can *see* — Dvorak's `⌥v` on the key printed V, AZERTY's
1167    /// `⌘a` on the one printed A, QWERTZ's `⌘z` on the one printed Z. A
1168    /// layout that produces anything else (Cyrillic, Greek, Hebrew, Arabic)
1169    /// would make every Latin keymap in every app match nothing at all, so
1170    /// the US-QWERTY letter at that position stands in; this is the rule
1171    /// browsers use to keep `⌘C` copying on a Russian layout. A layout key
1172    /// this vocabulary cannot name falls back the same way.
1173    ///
1174    /// The stand-in is what US-QWERTY would have produced for the *same
1175    /// press*, Shift included: a window reports `physical` from a table
1176    /// that never sees Shift (backlog F65), so ⇧ on the key printed J is
1177    /// `J`, not `j`, and ⇧ on the key printed `;` is `:` — the key a vim
1178    /// hand on a Russian layout reaches for, and gets `;` from otherwise.
1179    ///
1180    /// Except under Alt, where the stand-in is the unshifted position.
1181    /// What a layout puts on an ⌥ key is a composed character (macOS US
1182    /// ⌥⇧J is `Ô`), so a driver resolving a chord reads the key with
1183    /// every modifier stripped — the winit runner's `j` for ⌥⇧J, on a
1184    /// US layout and a Russian one alike — and a host that passes the
1185    /// composed character lands here instead. Folding Shift here too is
1186    /// what makes the two agree; `mods` still says Shift was held.
1187    ///
1188    /// Caps Lock is not read here: it is [`KeyPress::locks`], set after,
1189    /// so the stand-in follows Shift alone and a Caps-Locked non-Latin
1190    /// key stands in as the lower-case letter, where US-QWERTY would
1191    /// print the upper-case one.
1192    ///
1193    /// `physical` is reported either way, for a keymap that would rather
1194    /// bind the finger than the label. See `docs/adr/0002` decision 11.
1195    ///
1196    /// This judges each key by itself, which is all a driver that cannot
1197    /// ask about the layout can do; one that can says so through
1198    /// [`KeyPress::from_layout_in`].
1199    pub fn from_layout(layout: KeyCode, physical: KeyCode, mods: KeyMods) -> Self {
1200        Self::from_layout_in(layout, physical, mods, LayoutScript::Latin)
1201    }
1202
1203    /// [`KeyPress::from_layout`] on a layout whose alphabet the driver
1204    /// knows. On a [`LayoutScript::NonLatin`] one the US-QWERTY key stands
1205    /// in for every character the layout put where US-QWERTY has another,
1206    /// ASCII or not, so macOS Russian's `]` on the key printed `` ` `` is
1207    /// `` ` ``, its `"` on ⇧2 is `@`, and Windows Russian's `.` on the key
1208    /// printed `/` is `/` (backlog F115). A key that already is its
1209    /// position's character — a digit, the keypad's — keeps it, and a key
1210    /// at a position this vocabulary cannot name (ISO's extra key) keeps
1211    /// the layout's, there being nothing to stand in.
1212    pub fn from_layout_in(
1213        layout: KeyCode,
1214        physical: KeyCode,
1215        mods: KeyMods,
1216        script: LayoutScript,
1217    ) -> Self {
1218        let stand_in = || match (mods.shift && !mods.alt, physical) {
1219            (true, KeyCode::Char(c)) => KeyCode::Char(us_shifted(c)),
1220            _ => physical,
1221        };
1222        let code = match layout {
1223            KeyCode::Char(c) if !c.is_ascii() => stand_in(),
1224            KeyCode::Unknown => stand_in(),
1225            KeyCode::Char(c)
1226                if script == LayoutScript::NonLatin
1227                    && matches!(physical, KeyCode::Char(p) if p != c) =>
1228            {
1229                stand_in()
1230            }
1231            named_or_ascii => named_or_ascii,
1232        };
1233        Self {
1234            code,
1235            physical,
1236            mods,
1237            text: None,
1238            repeat: false,
1239            location: KeyLocation::Standard,
1240            locks: KeyLocks::default(),
1241        }
1242    }
1243
1244    pub fn with_text(mut self, text: impl Into<String>) -> Self {
1245        self.text = Some(text.into());
1246        self
1247    }
1248
1249    /// Whether `other` is a press or release of the same key as this one
1250    /// — how a release is matched to the press it lets go of, and a repeat
1251    /// to the press it repeats. By position when the platform reported
1252    /// one, because `code` moves under a held key: hold `w`, press Shift,
1253    /// and the OS repeat arrives as `W`, which by `code` would be a second
1254    /// key held, with the first stuck down until focus moved (AR9). A
1255    /// press whose position the vocabulary could not name is matched on
1256    /// `code`, which is all it has. And by [`KeyPress::location`] too:
1257    /// the keypad's `1` and the main block's share a position's name,
1258    /// as the two Shifts do, and are two keys.
1259    pub fn same_key(&self, other: &KeyPress) -> bool {
1260        self.location == other.location
1261            && if self.physical != KeyCode::Unknown && other.physical != KeyCode::Unknown {
1262                self.physical == other.physical
1263            } else {
1264                self.code == other.code
1265            }
1266    }
1267
1268    /// The **second** event a real key press produces, after its
1269    /// [`InputEvent::KeyDown`] — the other half of what a window does with
1270    /// one key going down, and the one table that says which key is which.
1271    ///
1272    /// A press is two channels, and every driver drives both, in this
1273    /// order. `KeyDown` goes to whatever holds key focus, so an app that
1274    /// owns its keyboard hears the raw key; this is what the *core* is
1275    /// asked to do with the same key — Escape dismisses a modal, Tab walks
1276    /// the focus ring, the arrows nudge a focused slider, Space presses a
1277    /// focused control, a printable character reaches the focused editor.
1278    /// A test that sent only `KeyDown` got the first channel and none of
1279    /// the second, which is why `key_down("escape")` left a modal open
1280    /// (backlog F6); [`crate::Core::press`] is the pair.
1281    ///
1282    /// `None` for a key this vocabulary does not name — a function key,
1283    /// Insert — and for every chord, which carries no `text` because it
1284    /// inserts nothing. The press still stands on the sink channel.
1285    pub fn edit_event(&self) -> Option<InputEvent> {
1286        let key = match self.code {
1287            KeyCode::Left => EditKey::Left,
1288            KeyCode::Right => EditKey::Right,
1289            KeyCode::Up => EditKey::Up,
1290            KeyCode::Down => EditKey::Down,
1291            KeyCode::Home => EditKey::Home,
1292            KeyCode::End => EditKey::End,
1293            KeyCode::PageUp => EditKey::PageUp,
1294            KeyCode::PageDown => EditKey::PageDown,
1295            KeyCode::Backspace => EditKey::Backspace,
1296            KeyCode::Delete => EditKey::Delete,
1297            KeyCode::Enter => EditKey::Enter,
1298            KeyCode::Tab => EditKey::Tab,
1299            KeyCode::Escape => EditKey::Escape,
1300            // Space is the text channel rather than an `EditKey`: it
1301            // inserts into a focused editor and presses a focused control
1302            // (`docs/adr/0002`). Under Shift it is still a space; under
1303            // any other modifier it is a chord like every other chord —
1304            // an IME toggle, an Emacs mark — and inserts nothing (AR10;
1305            // before that it said " " whatever was held, so Ctrl+Space
1306            // typed a space into an editor and clicked a control).
1307            KeyCode::Space => {
1308                let m = self.mods;
1309                return (!m.ctrl && !m.alt && !m.super_key)
1310                    .then(|| InputEvent::Text(" ".to_string()));
1311            }
1312            // Anything else inserts whatever it inserts. A driver leaves
1313            // `text` unset for a chord, so this is where one stops.
1314            _ => {
1315                let text = self.text.as_deref()?;
1316                return text
1317                    .chars()
1318                    .any(|c| !c.is_control())
1319                    .then(|| InputEvent::Text(text.to_string()));
1320            }
1321        };
1322        // `word` is Alt and `doc` the platform primary, which is the whole
1323        // of what the editing vocabulary normalizes (see [`Mods`]).
1324        Some(InputEvent::Key(
1325            key,
1326            Mods {
1327                shift: self.mods.shift,
1328                word: self.mods.alt,
1329                doc: self.mods.primary(),
1330            },
1331        ))
1332    }
1333
1334    /// Strips a press down to what a release reports: nothing is inserted
1335    /// on the way up, and a release never comes from key repeat.
1336    pub fn released(mut self) -> Self {
1337        self.text = None;
1338        self.repeat = false;
1339        self
1340    }
1341
1342    /// The payload form crossing into events, C, and Lua:
1343    /// `{kind="key", phase="down"|"up", code="w", physical="w", shift=,
1344    /// ctrl=, alt=, super=, text=, repeat=, location="standard",
1345    /// caps_lock=, num_lock=}`.
1346    pub fn to_value(&self, phase: KeyPhase) -> Value {
1347        Value::map([
1348            ("kind", Value::str("key")),
1349            ("phase", Value::str(phase.name())),
1350            ("code", Value::Str(self.code.name())),
1351            ("physical", Value::Str(self.physical.name())),
1352            ("shift", Value::Bool(self.mods.shift)),
1353            ("ctrl", Value::Bool(self.mods.ctrl)),
1354            ("alt", Value::Bool(self.mods.alt)),
1355            ("super", Value::Bool(self.mods.super_key)),
1356            (
1357                "text",
1358                match &self.text {
1359                    Some(t) => Value::Str(t.clone()),
1360                    None => Value::Null,
1361                },
1362            ),
1363            ("repeat", Value::Bool(self.repeat)),
1364            ("location", Value::str(self.location.name())),
1365            ("caps_lock", Value::Bool(self.locks.caps)),
1366            ("num_lock", Value::Bool(self.locks.num)),
1367        ])
1368    }
1369}
1370
1371/// An event produced by the UI, ready for routing.
1372#[derive(Clone, Debug, PartialEq)]
1373pub struct UiEvent {
1374    pub origin: OriginId,
1375    /// Which window the event came from — a *new* field and not a second
1376    /// reading of `origin`, which says which frontend drew the node and
1377    /// answers `HOST` for a window an extension also draws into.
1378    ///
1379    /// Most producers cannot fill it in: a hit test and the edit buffer
1380    /// know nothing about windows. They leave it [`WindowId::MAIN`] and the
1381    /// core stamps its own `env.window.id` over it as the event leaves
1382    /// (`Core::handle_input`, `Core::take_pending_events`) — one core is
1383    /// one window, so that is the whole answer. The audio store is the
1384    /// exception: its mounts are per window, so a `sound` event carries
1385    /// the window that declared the node and the stamp leaves it alone. A
1386    /// driver that builds an event itself stamps it itself.
1387    pub window: WindowId,
1388    pub key: Key,
1389    pub payload: Value,
1390    /// The slot whose fill drew the node — its key, the one `begin_slot`
1391    /// returned and `key_of(full_name)` answers — or `None` for a node the
1392    /// host drew itself. What `origin` cannot say: one extension fills
1393    /// many slots (a Lua host with a view per pane), and an event routed
1394    /// by pane needs the slot, not the extension. Stamped by the core on
1395    /// the way out like `window`, from the fill ranges the last frame
1396    /// recorded (`Tree::fills`); a producer leaves it `None` (backlog K2).
1397    pub slot: Option<Key>,
1398}
1399
1400impl UiEvent {
1401    /// An event as a producer builds it: the window is left [`WindowId::MAIN`]
1402    /// and the slot `None` for the core to stamp on the way out (see
1403    /// [`UiEvent::window`], [`UiEvent::slot`]).
1404    pub fn on(origin: OriginId, key: Key, payload: Value) -> Self {
1405        Self {
1406            origin,
1407            window: WindowId::MAIN,
1408            key,
1409            payload,
1410            slot: None,
1411        }
1412    }
1413
1414    /// Merges the node's tag into a map payload. A `Null` tag declares the
1415    /// behaviour and names nothing, so it is the one value left out — the
1416    /// rule every row with a tag reads by, stated once.
1417    /// The payload's `kind`: what a core event says it is — `"drag"`,
1418    /// `"key"`, `"scroll"` — or the `kind` of an app's own map tag. None
1419    /// for a payload that is not a map or has no string `kind`.
1420    #[inline]
1421    pub fn kind(&self) -> Option<&str> {
1422        self.payload.get_str("kind")
1423    }
1424
1425    pub fn tagged(mut self, tag: Option<&Value>) -> Self {
1426        if let Some(tag) = tag
1427            && *tag != Value::Null
1428            && let Value::Map(entries) = &mut self.payload
1429        {
1430            entries.push(("tag".to_string(), tag.clone()));
1431        }
1432        self
1433    }
1434}
1435
1436/// The node whose `on_context_menu` a secondary press on a region opens:
1437/// the region's own node or an ancestor's (see `HitRegion::context_menu`).
1438#[derive(Clone, Debug, PartialEq)]
1439pub struct MenuOwner {
1440    pub key: Key,
1441    pub origin: OriginId,
1442    pub tag: Value,
1443}
1444
1445/// The zone files dragged over a region land on: the region's own node
1446/// or an ancestor's (see `HitRegion::drop`, ADR 0031, decision 2). The
1447/// same three fields as [`MenuOwner`], resolved by the same walk.
1448pub type DropOwner = MenuOwner;
1449
1450/// The node a non-primary button's press went to and whose capture it is
1451/// until the release (backlog F105): the nearest node at or above the
1452/// region pressed whose `on_button` claims that button. The same three
1453/// fields as [`MenuOwner`], resolved by the core at the press rather than
1454/// carried on every region — a middle press is one event in a session,
1455/// and a tag on `HitRegion` would be a clone on every region of every
1456/// frame (C15).
1457pub type ButtonOwner = MenuOwner;
1458
1459/// The shape inside a region's rect that a point has to be in to hit it
1460/// (`docs/adr/0026-hit-testing-by-shape.md`). The rect is always tested
1461/// first, so a shape is evaluated only for the few regions under the
1462/// pointer. Inline on the region rather than behind an index: the ADR
1463/// priced the twenty bytes at +7% on a 10k-region frame and measured
1464/// nothing, so the simpler shape won. Points for a stroke or a fill live
1465/// in the interaction's own list, relative to the region's top-left in
1466/// logical px, copied at emission because the frame's stores do not
1467/// outlive the frame and a press does.
1468#[derive(Clone, Copy, Debug, PartialEq)]
1469pub enum HitShape {
1470    /// The whole rect — every box, and what every region was before.
1471    Rect,
1472    /// A box with rounded corners: a point in a corner's square but past
1473    /// its arc misses. Radii clockwise from the top-left, logical px,
1474    /// as the node's `radius` row.
1475    Rounded([f32; 4]),
1476    /// A round-capped stroke through `len` points from `first`, `width`
1477    /// wide: a point within half the width of any piece hits. A hairline
1478    /// is hard to hit, so the grab is at least [`MIN_STROKE_GRAB`] wide.
1479    Segments { first: u32, len: u32, width: f32 },
1480    /// A filled outline through `len` points from `first`: a point inside
1481    /// by the even-odd rule hits — the same rule the stock polygon paints
1482    /// by, so the hit is the fill exactly, a self-intersecting outline's
1483    /// unfilled overlaps included.
1484    Polygon { first: u32, len: u32 },
1485}
1486
1487/// The narrowest a stroke's hit target gets, logical px, whatever its
1488/// drawn width: a 1 px connector is a 4 px target, the way a 1 px splitter
1489/// handle is wider than its line everywhere.
1490pub const MIN_STROKE_GRAB: f32 = 4.0;
1491
1492#[derive(Clone, Debug)]
1493pub struct HitRegion {
1494    pub key: Key,
1495    pub origin: OriginId,
1496    /// Logical coordinates.
1497    pub rect: Rect,
1498    /// Ancestor clip; a point must be inside both to hit.
1499    pub clip: Rect,
1500    /// The shape inside `rect` a point must also be in, when there is one.
1501    pub shape: HitShape,
1502    /// Click payload; None for hover-only regions (hoverable, edits) — a
1503    /// click on those emits no `UiEvent`.
1504    pub payload: Option<Value>,
1505    /// Drag tag when the node declared `on_drag`: pressing it starts a
1506    /// pointer-captured drag, and cursor motion until release emits
1507    /// `{kind="drag", phase, x, y, dx, dy, parent, tag}` events on this node.
1508    pub drag: Option<Value>,
1509    /// The node's parent rect (logical) — carried into drag payloads so
1510    /// handlers can turn absolute positions into fractions of the container
1511    /// (a splitter's ratio) without any geometry query API.
1512    pub parent_rect: Rect,
1513    /// Content-box origin of an editable text node; None for plain hits.
1514    pub edit_origin: Option<Vec2>,
1515    /// The selection scope this node is inside, when it is inside one
1516    /// (`docs/adr/0017-selection-as-a-scope.md`): a press here starts a
1517    /// drag-select over the scope's text. A region that also carries a
1518    /// click payload is a control first — a press on a button inside a
1519    /// selectable card clicks it — so this is read only where nothing
1520    /// else claims the press.
1521    pub select_scope: Option<Key>,
1522    /// Key-sink tag when the node declared `on_key`: clicking it takes
1523    /// key focus, and key presses then arrive on it carrying this tag.
1524    pub key_sink: Option<Value>,
1525    /// The sink declared `key_up`: releases reach it too. Without it a
1526    /// release is dropped at routing, and the sink hears presses only.
1527    pub key_up: bool,
1528    /// The context menu a secondary press here opens: the node's own
1529    /// `on_context_menu`, or the nearest enclosing one — a container
1530    /// offering a menu for everything inside it is the common case, and
1531    /// a press on a child that declared none is unclaimed in the sense
1532    /// ADR 0011 gave keys, so it reaches the enclosing menu the way an
1533    /// unclaimed key reaches the enclosing sink (backlog T1). Resolved at
1534    /// emission, where the tree is; the walk stops at the modal boundary
1535    /// and skips a disabled node's own. The press emits
1536    /// `{kind="contextmenu", x, y, tag}` on the *owner*, not on this node.
1537    /// None when nothing encloses this region offers one, and the press
1538    /// is swallowed here.
1539    pub context_menu: Option<MenuOwner>,
1540    /// The drop zone this region belongs to — its own `on_drop` or the
1541    /// nearest enclosing declaration's — resolved at emission (ADR 0031,
1542    /// decision 2). None where no zone encloses it: files dragged over
1543    /// such a region look past it to the topmost zone beneath.
1544    pub drop: Option<DropOwner>,
1545    /// A press on this node moves keyboard focus to it (an editor, a
1546    /// sink, a control, a `focusable` node — never a disabled one).
1547    pub focusable: bool,
1548    /// Window-chrome role: interactions become `WindowCommand`s, not events.
1549    pub window: Option<WindowRole>,
1550    /// Hover tag when the node declared `on_hover`: the pointer entering or
1551    /// leaving emits `{kind="hover", phase="enter"|"leave", tag}` on it.
1552    pub hover: Option<Value>,
1553    /// Hover group id (`NodeSpec::hover_group`): hovering or pressing any
1554    /// member lights up every member.
1555    pub group: Option<u64>,
1556    /// Sounds the node declared (`NodeSpec::click_sound` / `hover_sound`):
1557    /// a click / the pointer entering queues them as sound requests the
1558    /// core turns into audio commands.
1559    pub click_sound: Option<crate::resources::SoundId>,
1560    pub hover_sound: Option<crate::resources::SoundId>,
1561    /// Pointer shape declared by the node (`NodeSpec::cursor`). None = the
1562    /// I-beam over text, the arrow otherwise (`Interaction::implied_shape`).
1563    pub cursor: Option<CursorShape>,
1564    /// A slider's track when the node declared `on_change` (ADR 0034,
1565    /// decision 4): a press here proposes the value under the pointer and
1566    /// captures the pointer until release, each new value a `change`
1567    /// event. Boxed: nearly every region has none.
1568    pub slider: Option<Box<crate::slider::SliderTrack>>,
1569}
1570
1571/// An OS file drag over a zone (ADR 0031): what `dropBg` reads and what
1572/// the next `DragFiles` compares against.
1573#[derive(Clone, Debug)]
1574struct DropHover {
1575    owner: DropOwner,
1576    /// Where the last `DragFiles` put the pointer: a repeat at the same
1577    /// point is not a `move`.
1578    last: Vec2,
1579    /// The `leave`, built at `enter` with the paths of that moment.
1580    leave: UiEvent,
1581}
1582
1583/// The points a frame's stroke and fill shapes index, built beside its
1584/// regions.
1585#[derive(Clone, Debug, Default)]
1586pub struct HitShapes {
1587    pub points: Vec<Vec2>,
1588}
1589
1590impl HitShapes {
1591    /// Adds a stroke's points and returns the shape over them.
1592    pub fn segments(&mut self, points: &[Vec2], width: f32) -> HitShape {
1593        let first = self.points.len() as u32;
1594        self.points.extend_from_slice(points);
1595        HitShape::Segments {
1596            first,
1597            len: points.len() as u32,
1598            width,
1599        }
1600    }
1601
1602    /// Adds a fill's points and returns the shape over them.
1603    pub fn polygon(&mut self, points: &[Vec2]) -> HitShape {
1604        let first = self.points.len() as u32;
1605        self.points.extend_from_slice(points);
1606        HitShape::Polygon {
1607            first,
1608            len: points.len() as u32,
1609        }
1610    }
1611}
1612
1613/// Whether `p` (relative to the box's top-left) is inside a `w`×`h` box
1614/// with the given corner radii: in the box, and not in a corner's square
1615/// past its arc. Radii are clamped to the half extents as the shader
1616/// clamps them, so an oversized radius is the pill it draws as.
1617pub fn in_rounded_rect(p: Vec2, w: f32, h: f32, radii: [f32; 4]) -> bool {
1618    let cap = (w * 0.5).min(h * 0.5).max(0.0);
1619    // Corner centres clockwise from the top-left, each with its radius.
1620    let corners = [
1621        (radii[0].min(cap), radii[0].min(cap), radii[0].min(cap)),
1622        (w - radii[1].min(cap), radii[1].min(cap), radii[1].min(cap)),
1623        (
1624            w - radii[2].min(cap),
1625            h - radii[2].min(cap),
1626            radii[2].min(cap),
1627        ),
1628        (radii[3].min(cap), h - radii[3].min(cap), radii[3].min(cap)),
1629    ];
1630    for (i, &(cx, cy, r)) in corners.iter().enumerate() {
1631        if r <= 0.0 {
1632            continue;
1633        }
1634        // Past the centre toward the corner on both axes: in the square.
1635        let in_square = match i {
1636            0 => p.x < cx && p.y < cy,
1637            1 => p.x > cx && p.y < cy,
1638            2 => p.x > cx && p.y > cy,
1639            _ => p.x < cx && p.y > cy,
1640        };
1641        if in_square && (p.x - cx).powi(2) + (p.y - cy).powi(2) > r * r {
1642            return false;
1643        }
1644    }
1645    true
1646}
1647
1648/// Distance from `p` to the segment `a`–`b`.
1649pub fn segment_distance(p: Vec2, a: Vec2, b: Vec2) -> f32 {
1650    let (ex, ey) = (b.x - a.x, b.y - a.y);
1651    let (wx, wy) = (p.x - a.x, p.y - a.y);
1652    let ee = ex * ex + ey * ey;
1653    let t = if ee > 0.0 {
1654        ((wx * ex + wy * ey) / ee).clamp(0.0, 1.0)
1655    } else {
1656        0.0
1657    };
1658    let (dx, dy) = (wx - ex * t, wy - ey * t);
1659    (dx * dx + dy * dy).sqrt()
1660}
1661
1662/// Whether `p` is inside the outline through `pts` by the even-odd rule
1663/// (the crossing test). A point on an edge counts as inside on one side
1664/// and outside on the other, which is what every hit test of a shared
1665/// edge between two wedges wants: exactly one of them.
1666pub fn in_polygon(p: Vec2, pts: &[Vec2]) -> bool {
1667    let n = pts.len();
1668    if n < 3 {
1669        return false;
1670    }
1671    let mut inside = false;
1672    let mut j = n - 1;
1673    for i in 0..n {
1674        let (a, b) = (pts[i], pts[j]);
1675        if (a.y > p.y) != (b.y > p.y) {
1676            let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
1677            if p.x < x {
1678                inside = !inside;
1679            }
1680        }
1681        j = i;
1682    }
1683    inside
1684}
1685
1686/// A scroll container's on-screen area, for wheel routing — or an
1687/// `on_scroll` node's, which takes the wheel the same way and turns it
1688/// into an event instead of an offset (ADR 0029, decision 4).
1689#[derive(Clone, Copy, Debug)]
1690pub struct ScrollRegion {
1691    pub key: Key,
1692    /// The scroller's index in the frame's tree: what its bars are
1693    /// emitted from, at the end of its layer.
1694    pub(crate) node: u32,
1695    pub rect: Rect,
1696    pub clip: Rect,
1697    /// Outside the frame's modal scope: the bar still draws, the wheel
1698    /// and the thumb do nothing (see `docs/adr/0003-modal-surfaces.md`).
1699    pub inert: bool,
1700    /// The node declared `on_scroll`: the wheel over it is an event on
1701    /// it, no bars are drawn and no offset is kept. A region that is
1702    /// both — a scroller that also declared the row — is the handler's:
1703    /// the app asked to hear the wheel, and hearing it *and* having the
1704    /// content move under it would be two answers to one notch.
1705    pub handler: bool,
1706    /// The axes a gesture may take here: a container's `scroll_x` /
1707    /// `scroll_y`, a handler's `scroll_axes`. Carried from the frame that
1708    /// drew the region, with `contain` and `parent`, so the wheel never
1709    /// reads the tree by `node` — a tree a build under way may have
1710    /// cleared or refilled (the alpha.22 regression pass).
1711    pub(crate) takes_x: bool,
1712    pub(crate) takes_y: bool,
1713    /// The axes it scrolls as a container (`scroll_x` / `scroll_y`): a
1714    /// handler that is one too is answered on them by its room, as a
1715    /// container is (backlog F118).
1716    pub(crate) scrolls_x: bool,
1717    pub(crate) scrolls_y: bool,
1718    /// `overscroll: contain`: a gesture starting here stays here.
1719    pub(crate) contain: bool,
1720    /// The index in the frame's region list of the nearest scroll region
1721    /// around this one in the tree, [`crate::tree::NIL`] for none: where
1722    /// a gesture this one passes goes next (ADR 0038's "the scroller
1723    /// around it"), whatever else is painted under the pointer.
1724    pub(crate) parent: u32,
1725}
1726
1727#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1728pub enum ScrollAxis {
1729    X,
1730    Y,
1731}
1732
1733/// One scrollbar drawn this frame (logical coordinates), for thumb dragging
1734/// and track jumps. Rebuilt by `finish_frame` alongside the indicator quads.
1735#[derive(Clone, Copy, Debug)]
1736pub struct ScrollbarRegion {
1737    pub key: Key,
1738    pub axis: ScrollAxis,
1739    /// The thumb as drawn.
1740    pub thumb: Rect,
1741    /// The full track strip (the grabbable gutter).
1742    pub track: Rect,
1743    /// Thumb length along the axis.
1744    pub bar_len: f32,
1745    /// The container's max scroll offset on this axis.
1746    pub max: f32,
1747    /// Behind a modal: drawn, but not grabbable.
1748    pub inert: bool,
1749    /// The hit list's length when the bar was painted: every region below
1750    /// this index is under the bar, every one at or above it is in a layer
1751    /// over it (ADR 0023, decision 4).
1752    pub(crate) above: u32,
1753}
1754
1755/// What a press at a point lands on, in paint order: the topmost hit
1756/// region, unless a scrollbar painted over it is there too.
1757pub(crate) enum Target<'a> {
1758    Bar(ScrollbarRegion),
1759    Hit(&'a HitRegion),
1760}
1761
1762impl ScrollbarRegion {
1763    /// Offset for a cursor position, given where inside the thumb it grabbed.
1764    pub(crate) fn offset_for(&self, p: Vec2, grab: f32) -> f32 {
1765        let (pos, track_start, track_len) = match self.axis {
1766            ScrollAxis::X => (p.x, self.track.x, self.track.w),
1767            ScrollAxis::Y => (p.y, self.track.y, self.track.h),
1768        };
1769        let range = (track_len - self.bar_len).max(1.0);
1770        ((pos - track_start - grab) / range).clamp(0.0, 1.0) * self.max
1771    }
1772}
1773
1774/// An in-flight pointer-captured drag on an `on_drag` node.
1775#[derive(Clone, Debug)]
1776struct DragState {
1777    key: Key,
1778    origin: OriginId,
1779    tag: Value,
1780    parent_rect: Rect,
1781    /// Where the press landed. Every `dx`/`dy` the drag reports is the
1782    /// displacement from here — `start` is zero, a `move` is where the
1783    /// pointer is now, `end` is the whole distance — so a handler commits
1784    /// from any phase without summing anything, and the slop below drops
1785    /// nothing from the total (backlog F2).
1786    press: Vec2,
1787    /// Where the pointer was last seen: the `end` position of a drag
1788    /// released while the cursor was outside the window.
1789    last: Vec2,
1790    /// Whether motion left the click slop; suppresses the click on
1791    /// release so a node can carry both `on_click` and `on_drag`. Once
1792    /// set it stays set — a drag that wanders back is still a drag.
1793    moved: bool,
1794}
1795
1796/// How far from the press point a pointer may wander before the press
1797/// stops counting as a click and the drag starts reporting `move`s.
1798/// Measured from the press, not per event, so a slow pointer that never
1799/// covers 3 px between two events still gets there.
1800const DRAG_SLOP: f32 = 3.0;
1801
1802impl DragState {
1803    /// The displacement `p` is from the press point.
1804    fn displacement(&self, p: Vec2) -> Vec2 {
1805        Vec2::new(p.x - self.press.x, p.y - self.press.y)
1806    }
1807}
1808
1809/// A non-primary button held on the node that claimed it (backlog F105):
1810/// its motion and its release go to `owner` wherever the pointer is.
1811#[derive(Clone, Debug)]
1812struct ButtonCapture {
1813    button: MouseButton,
1814    owner: ButtonOwner,
1815    /// Where the pointer was last seen: a repeat at the same point is not
1816    /// a `move`, and a release with the cursor outside the window happens
1817    /// here.
1818    last: Vec2,
1819}
1820
1821#[derive(Default)]
1822pub struct Interaction {
1823    /// In paint order: later entries are on top.
1824    pub(crate) hits: Vec<HitRegion>,
1825    /// The points the stroke and fill shapes index, rebuilt with the
1826    /// hits; empty on a frame of plain boxes.
1827    shape_points: Vec<Vec2>,
1828    /// In paint order: later entries are on top (innermost last).
1829    pub(crate) scroll_regions: Vec<ScrollRegion>,
1830    /// This frame's scrollbars, topmost last (they draw over content).
1831    pub(crate) scrollbars: Vec<ScrollbarRegion>,
1832    /// Scrollbar thumb being dragged: which bar, and the grab point inside
1833    /// the thumb (axis-local). Offset math happens in `Core::handle_input`
1834    /// (it needs the `ScrollStore`).
1835    pub(crate) scrollbar_drag: Option<(Key, ScrollAxis, f32)>,
1836    /// Window intents produced by chrome nodes; drained by the driver via
1837    /// `Core::take_window_commands`.
1838    pub(crate) window_commands: Vec<WindowCommand>,
1839    /// The window this core draws, for the commands chrome nodes issue —
1840    /// a hit region has no window, so the core writes it here from
1841    /// `env.window.id` before routing each input.
1842    pub(crate) window: WindowId,
1843    /// Sounds nodes asked for (`click_sound` on click, `hover_sound` on
1844    /// enter); the core turns them into play commands (`take_sound_requests`).
1845    pub(crate) sound_requests: Vec<crate::resources::SoundId>,
1846    /// Pointer-captured drag on an `on_drag` node.
1847    drag: Option<DragState>,
1848    /// The non-primary buttons held on the node that claimed each with
1849    /// `on_button` (backlog F105), one capture per button, in press order.
1850    /// Empty — and unallocated — in an app that declares none.
1851    held_buttons: Vec<ButtonCapture>,
1852    /// Pointer-captured slide on a slider that declared `on_change`: the
1853    /// node, its track, and the last value proposed, so a move that lands
1854    /// on the same step proposes nothing.
1855    slide: Option<(Key, OriginId, Box<crate::slider::SliderTrack>, f64)>,
1856    /// The last primary press's driver-measured click count (1 for a
1857    /// single, 2 for a double, …): what the `clicks` a press or drag
1858    /// inside a key sink carries reads (backlog C34).
1859    press_clicks: u8,
1860    /// Last reported physical modifier state.
1861    modifiers: KeyMods,
1862    cursor: Option<Vec2>,
1863    hovered: Option<Key>,
1864    pressed: Option<Key>,
1865    /// Hover group of the hovered / pressed region, for group styling.
1866    hovered_group: Option<u64>,
1867    pressed_group: Option<u64>,
1868    /// The `on_hover` leave event for the hovered node, prepared on enter.
1869    hovered_leave: Option<UiEvent>,
1870    /// The zone files dragged in from the OS are over, with the `leave`
1871    /// prepared at `enter` — the region may be gone from the next
1872    /// frame's hits (ADR 0031, decision 2).
1873    drop: Option<DropHover>,
1874    /// Hover enter/leave events raised outside `handle` — a new frame's hit
1875    /// regions changing what sits under a still cursor. Drained by the next
1876    /// `handle` or by `take_pending`.
1877    pending: Vec<UiEvent>,
1878}
1879
1880impl Interaction {
1881    pub fn set_hits(&mut self, hits: Vec<HitRegion>) {
1882        self.set_hits_shaped(hits, HitShapes::default());
1883    }
1884
1885    /// `set_hits` with the points the regions' shapes index (ADR 0026).
1886    pub fn set_hits_shaped(&mut self, hits: Vec<HitRegion>, shapes: HitShapes) {
1887        self.hits = hits;
1888        self.shape_points = shapes.points;
1889        let mut out = std::mem::take(&mut self.pending);
1890        // The pointer is where it was: whatever changed under it is the
1891        // content (backlog DX20).
1892        self.refresh_hover(&mut out, "content");
1893        self.pending = out;
1894    }
1895
1896    /// Events produced outside `handle` (see `pending`); drivers take them
1897    /// after finishing a frame so a hover change under a still cursor is
1898    /// not delayed until the next input.
1899    pub fn take_pending(&mut self) -> Vec<UiEvent> {
1900        std::mem::take(&mut self.pending)
1901    }
1902
1903    /// Drains the sounds nodes asked for since the last drain.
1904    pub(crate) fn take_sound_requests(&mut self) -> Vec<crate::resources::SoundId> {
1905        std::mem::take(&mut self.sound_requests)
1906    }
1907
1908    /// Hands back the previous frame's hit buffer (cleared) so emission can
1909    /// refill it without reallocating.
1910    pub fn take_hit_buffer(&mut self) -> Vec<HitRegion> {
1911        let mut hits = std::mem::take(&mut self.hits);
1912        hits.clear();
1913        hits
1914    }
1915
1916    /// The previous frame's point list (cleared), on the same terms.
1917    pub fn take_shape_buffer(&mut self) -> HitShapes {
1918        let mut shapes = HitShapes {
1919            points: std::mem::take(&mut self.shape_points),
1920        };
1921        shapes.points.clear();
1922        shapes
1923    }
1924
1925    /// Whether `p` is in region `h`: inside its rect and its clip, and
1926    /// inside its shape when it has one. The rect test is what every
1927    /// region pays; the shape is paid by the few under the pointer.
1928    #[inline]
1929    fn contains(&self, h: &HitRegion, p: Vec2) -> bool {
1930        if !(h.rect.contains(p) && h.clip.contains(p)) {
1931            return false;
1932        }
1933        if h.shape == HitShape::Rect {
1934            return true;
1935        }
1936        let local = Vec2::new(p.x - h.rect.x, p.y - h.rect.y);
1937        match h.shape {
1938            HitShape::Rect => true,
1939            HitShape::Rounded(radii) => in_rounded_rect(local, h.rect.w, h.rect.h, radii),
1940            // A shape whose points are not here — a region installed
1941            // through `set_hits` without its shapes, or one from another
1942            // frame — misses rather than panics in the input path.
1943            HitShape::Segments { first, len, width } => {
1944                let Some(pts) = self
1945                    .shape_points
1946                    .get(first as usize..(first + len) as usize)
1947                else {
1948                    return false;
1949                };
1950                let half = (width * 0.5).max(MIN_STROKE_GRAB * 0.5);
1951                pts.windows(2)
1952                    .any(|w| segment_distance(local, w[0], w[1]) <= half)
1953            }
1954            HitShape::Polygon { first, len } => {
1955                let Some(pts) = self
1956                    .shape_points
1957                    .get(first as usize..(first + len) as usize)
1958                else {
1959                    return false;
1960                };
1961                in_polygon(local, pts)
1962            }
1963        }
1964    }
1965
1966    pub fn cursor(&self) -> Option<Vec2> {
1967        self.cursor
1968    }
1969
1970    /// Physical modifier state as of the last `InputEvent::Modifiers`.
1971    pub fn modifiers(&self) -> KeyMods {
1972        self.modifiers
1973    }
1974
1975    /// This frame's hit regions in paint order (topmost last) — for hosts
1976    /// that mirror chrome regions into OS-level hit testing (e.g. answering
1977    /// Windows' WM_NCHITTEST so snap layouts and native caption behavior
1978    /// work over custom-drawn controls).
1979    pub fn hits(&self) -> &[HitRegion] {
1980        &self.hits
1981    }
1982
1983    pub(crate) fn hit_at(&self, p: Vec2) -> Option<&HitRegion> {
1984        self.hits.iter().rev().find(|h| self.contains(h, p))
1985    }
1986
1987    /// Every scroll region under the cursor — containers and `on_scroll`
1988    /// handlers — topmost by paint order first: what a notch walks when
1989    /// the innermost scroller moves on one axis only.
1990    pub(crate) fn scroll_regions_at(&self) -> impl Iterator<Item = &ScrollRegion> {
1991        let p = self.cursor;
1992        self.scroll_regions.iter().rev().filter(move |r| {
1993            p.is_some_and(|p| !r.inert && r.rect.contains(p) && r.clip.contains(p))
1994        })
1995    }
1996
1997    /// The content origin the editor `key` was drawn at this frame — what
1998    /// a caret drag places against. Read off the frame rather than kept
1999    /// from the press: a scroller nudged under a held drag moves the
2000    /// origin, and a caret placed against the press's origin would land
2001    /// the nudge off (ADR 0029, decision 2).
2002    pub(crate) fn edit_origin_of(&self, key: Key) -> Option<Vec2> {
2003        self.hits
2004            .iter()
2005            .rev()
2006            .find(|h| h.key == key && h.edit_origin.is_some())
2007            .and_then(|h| h.edit_origin)
2008    }
2009
2010    /// Re-resolves the hovered region under the cursor, emitting `on_hover`
2011    /// leave/enter events when the hovered node changes. `by` is what
2012    /// moved: `"pointer"` for the cursor, `"content"` for a frame that put
2013    /// something else under a still one — a list scrolled by the wheel or
2014    /// the keyboard, a row that grew (backlog DX20).
2015    fn refresh_hover(&mut self, out: &mut Vec<UiEvent>, by: &'static str) {
2016        let before = self.hovered;
2017        // Through `target_at`, like the press and the cursor shape (ADR
2018        // 0023, decision 4): over a bar painted above the node, nothing
2019        // is hovered — the node beneath used to light its `hover_bg` and
2020        // fire `enter` while the press would have grabbed the thumb
2021        // (backlog AR31).
2022        let idx = self.cursor.and_then(|p| match self.target_at(p)? {
2023            Target::Bar(_) => None,
2024            Target::Hit(h) => self.hits.iter().rposition(|x| std::ptr::eq(x, h)),
2025        });
2026        let (hovered, group) = match idx {
2027            Some(i) => (Some(self.hits[i].key), self.hits[i].group),
2028            None => (None, None),
2029        };
2030        self.hovered = hovered;
2031        self.hovered_group = group;
2032        if before == hovered {
2033            return;
2034        }
2035        // The old region may be gone from a new frame's hits, so the leave
2036        // event was prepared when the node was entered.
2037        out.extend(self.hovered_leave.take().map(|ev| Self::moved_by(ev, by)));
2038        if let Some(i) = idx {
2039            out.extend(Self::hover_event(&self.hits[i], "enter").map(|ev| Self::moved_by(ev, by)));
2040            self.hovered_leave = Self::hover_event(&self.hits[i], "leave");
2041            if let Some(sound) = self.hits[i].hover_sound {
2042                self.sound_requests.push(sound);
2043            }
2044        }
2045    }
2046
2047    /// A hover event's `by`, set as it goes out: a `leave` is built when
2048    /// its node is entered, before anyone knows what will move.
2049    fn moved_by(mut ev: UiEvent, by: &'static str) -> UiEvent {
2050        if let Value::Map(entries) = &mut ev.payload {
2051            // After `phase`, before the tag: the order the payload reads in.
2052            let at = entries
2053                .iter()
2054                .position(|(k, _)| k == "tag")
2055                .unwrap_or(entries.len());
2056            entries.insert(at, ("by".to_string(), Value::str(by)));
2057        }
2058        ev
2059    }
2060
2061    fn hover_event(region: &HitRegion, phase: &str) -> Option<UiEvent> {
2062        let tag = region.hover.as_ref()?;
2063        let payload = Value::map([("kind", Value::str("hover")), ("phase", Value::str(phase))]);
2064        Some(UiEvent::on(region.origin, region.key, payload).tagged(Some(tag)))
2065    }
2066
2067    fn context_menu_event(region: &HitRegion, p: Vec2) -> Option<UiEvent> {
2068        let owner = region.context_menu.as_ref()?;
2069        let payload = Value::map([
2070            ("kind", Value::str("contextmenu")),
2071            ("x", Value::Float(p.x as f64)),
2072            ("y", Value::Float(p.y as f64)),
2073        ]);
2074        Some(UiEvent::on(owner.origin, owner.key, payload).tagged(Some(&owner.tag)))
2075    }
2076
2077    /// What is under `p`, by the paint order and nothing else: the topmost
2078    /// hit region there, or the topmost scrollbar there if it was painted
2079    /// over that region — a bar wins the content of its own scroller and
2080    /// loses to a float over it (ADR 0023, decision 4). The press and the
2081    /// cursor shape both ask this, so they cannot disagree. A bar behind a
2082    /// modal is drawn and not a target.
2083    pub(crate) fn target_at(&self, p: Vec2) -> Option<Target<'_>> {
2084        let hit = self.hits.iter().rposition(|h| self.contains(h, p));
2085        let bar = self
2086            .scrollbars
2087            .iter()
2088            .rev()
2089            .find(|b| !b.inert && b.track.contains(p));
2090        match (bar, hit) {
2091            (Some(b), Some(h)) if (h as u32) < b.above => Some(Target::Bar(*b)),
2092            (Some(b), None) => Some(Target::Bar(*b)),
2093            (_, Some(h)) => Some(Target::Hit(&self.hits[h])),
2094            (None, None) => None,
2095        }
2096    }
2097
2098    /// Whether this bar is being thumb-dragged (for active styling).
2099    pub fn is_scrollbar_dragging(&self, key: Key, axis: ScrollAxis) -> bool {
2100        matches!(self.scrollbar_drag, Some((k, a, _)) if k == key && a == axis)
2101    }
2102
2103    fn drag_event(state: &DragState, phase: &str, p: Vec2, d: Vec2) -> UiEvent {
2104        let pr = state.parent_rect;
2105        let payload = Value::map([
2106            ("kind", Value::str("drag")),
2107            ("phase", Value::str(phase)),
2108            ("x", Value::Float(p.x as f64)),
2109            ("y", Value::Float(p.y as f64)),
2110            ("dx", Value::Float(d.x as f64)),
2111            ("dy", Value::Float(d.y as f64)),
2112            (
2113                "parent",
2114                Value::map([
2115                    ("x", Value::Float(pr.x as f64)),
2116                    ("y", Value::Float(pr.y as f64)),
2117                    ("w", Value::Float(pr.w as f64)),
2118                    ("h", Value::Float(pr.h as f64)),
2119                ]),
2120            ),
2121        ]);
2122        UiEvent::on(state.origin, state.key, payload).tagged(Some(&state.tag))
2123    }
2124
2125    /// `{kind="button", phase, button, x, y, clicks?, tag}` on the owner
2126    /// (backlog F105); `clicks` on the press only.
2127    fn button_event(
2128        owner: &ButtonOwner,
2129        button: MouseButton,
2130        phase: &str,
2131        p: Vec2,
2132        clicks: Option<u8>,
2133    ) -> UiEvent {
2134        let mut fields = vec![
2135            ("kind", Value::str("button")),
2136            ("phase", Value::str(phase)),
2137            ("button", button.to_value()),
2138            ("x", Value::Float(p.x as f64)),
2139            ("y", Value::Float(p.y as f64)),
2140        ];
2141        if let Some(clicks) = clicks {
2142            fields.push(("clicks", Value::Int(clicks as i64)));
2143        }
2144        UiEvent::on(owner.origin, owner.key, Value::map(fields)).tagged(Some(&owner.tag))
2145    }
2146
2147    /// A non-primary press the core found an `on_button` owner for
2148    /// (backlog F105): the owner hears `press`, and the button is captured
2149    /// by it — every move while it is held and its release go to the same
2150    /// node wherever the pointer is. A second press of a button already
2151    /// held (its release lost to another window) starts over: the old
2152    /// owner hears its capture end in a `release` first, since a capture
2153    /// never ends without one. Nothing else happens: no pressed state, no
2154    /// focus, no context menu. Returns how many pointer-made events it
2155    /// pushed, as `handle` does.
2156    pub(crate) fn press_button(
2157        &mut self,
2158        button: MouseButton,
2159        clicks: u8,
2160        owner: ButtonOwner,
2161        out: &mut Vec<UiEvent>,
2162    ) -> usize {
2163        out.append(&mut self.pending);
2164        let Some(p) = self.cursor else {
2165            return 0;
2166        };
2167        let mut n = 0;
2168        if let Some(i) = self.held_buttons.iter().position(|h| h.button == button) {
2169            let held = self.held_buttons.remove(i);
2170            out.push(Self::button_event(&held.owner, button, "release", p, None));
2171            n += 1;
2172        }
2173        out.push(Self::button_event(&owner, button, "press", p, Some(clicks)));
2174        self.held_buttons.push(ButtonCapture {
2175            button,
2176            owner,
2177            last: p,
2178        });
2179        n + 1
2180    }
2181
2182    /// Lets go of every held button, each owner hearing its `release`
2183    /// where the pointer was last seen (backlog F105): the window lost the
2184    /// keyboard, and the real releases will happen where this window
2185    /// never hears them — as a held key gets its synthetic up. Returns
2186    /// how many events it pushed, for the core's `attach_pointer`.
2187    pub(crate) fn release_buttons(&mut self, out: &mut Vec<UiEvent>) -> usize {
2188        let n = self.held_buttons.len();
2189        for held in std::mem::take(&mut self.held_buttons) {
2190            let p = self.cursor.unwrap_or(held.last);
2191            out.push(Self::button_event(
2192                &held.owner,
2193                held.button,
2194                "release",
2195                p,
2196                None,
2197            ));
2198        }
2199        n
2200    }
2201
2202    /// Lets go of the primary button's hold without a click (backlog
2203    /// RG75): an `on_drag` node hears its drag `end` and a slider its
2204    /// slide's `end` where the pointer was last seen, and the press is
2205    /// forgotten. The window lost the keyboard, and the release will
2206    /// happen where it never hears it — a click nobody finished must not
2207    /// fire. Returns how many events it pushed, for `attach_pointer`.
2208    pub(crate) fn release_primary(&mut self, out: &mut Vec<UiEvent>) -> usize {
2209        let mut n = 0;
2210        if let Some(drag) = self.drag.take() {
2211            let p = self.cursor.unwrap_or(drag.last);
2212            out.push(Self::drag_event(&drag, "end", p, drag.displacement(p)));
2213            n += 1;
2214        }
2215        if let Some((key, origin, track, last)) = self.slide.take() {
2216            let v = self.cursor.map_or(last, |p| track.value_at(p));
2217            out.push(crate::slider::change_event(
2218                origin, key, v, "end", &track.tag,
2219            ));
2220            n += 1;
2221        }
2222        self.pressed = None;
2223        self.pressed_group = None;
2224        n
2225    }
2226
2227    /// Lets go of every held button whose owner `alive` says is gone from
2228    /// the frame (backlog F105): nothing is left to hear its release.
2229    pub(crate) fn drop_gone_buttons(&mut self, alive: impl Fn(Key) -> bool) {
2230        if !self.held_buttons.is_empty() {
2231            self.held_buttons.retain(|h| alive(h.owner.key));
2232        }
2233    }
2234
2235    /// The node holding `button`'s capture, if a claimed press of it is
2236    /// held (backlog F105).
2237    pub fn button_owner(&self, button: MouseButton) -> Option<Key> {
2238        self.held_buttons
2239            .iter()
2240            .find(|h| h.button == button)
2241            .map(|h| h.owner.key)
2242    }
2243
2244    /// Returns how many of the events at the end of `out` a press made —
2245    /// a drag in any phase, a click on the release — as against the
2246    /// hover, context-menu and modifier events it also raises. That is
2247    /// the mark `Core::attach_pointer` reads to give a click or drag its
2248    /// `cell` and `line` / `byte` / `clicks`: said here, where the event
2249    /// is built, rather than guessed afterwards from its payload's
2250    /// `kind` (AR11). Every arm pushes its pointer-made events last.
2251    pub fn handle(&mut self, ev: InputEvent, out: &mut Vec<UiEvent>) -> usize {
2252        out.append(&mut self.pending);
2253        let mut pointer_made = 0;
2254        match ev {
2255            InputEvent::CursorMoved(p) => {
2256                self.cursor = Some(p);
2257                self.refresh_hover(out, "pointer");
2258                if let Some((key, origin, track, last)) = &mut self.slide {
2259                    let v = track.value_at(p);
2260                    if v != *last {
2261                        *last = v;
2262                        out.push(crate::slider::change_event(
2263                            *origin, *key, v, "move", &track.tag,
2264                        ));
2265                        pointer_made += 1;
2266                    }
2267                }
2268                if let Some(drag) = &mut self.drag
2269                    && p != drag.last
2270                {
2271                    drag.last = p;
2272                    let d = drag.displacement(p);
2273                    if d.x.abs() + d.y.abs() > DRAG_SLOP {
2274                        drag.moved = true;
2275                    }
2276                    if drag.moved {
2277                        out.push(Self::drag_event(drag, "move", p, d));
2278                        pointer_made += 1;
2279                    }
2280                }
2281                // Every held button's owner hears the motion, with no
2282                // slop: a terminal reports a drag of one cell (F105).
2283                for held in &mut self.held_buttons {
2284                    if p != held.last {
2285                        held.last = p;
2286                        out.push(Self::button_event(&held.owner, held.button, "move", p, None));
2287                        pointer_made += 1;
2288                    }
2289                }
2290            }
2291            InputEvent::CursorLeft => {
2292                self.cursor = None;
2293                self.refresh_hover(out, "pointer");
2294            }
2295            InputEvent::MouseDown { button, .. } if button != MouseButton::Primary => {
2296                // Nothing but the primary button presses: no pressed
2297                // state, so a release cannot become a click, and a drag
2298                // already in flight keeps its capture. A secondary press
2299                // asks whatever is under the pointer for a context menu.
2300                // A press an `on_button` node claimed never gets here: the
2301                // core resolves it and calls `press_button` instead.
2302                if button == MouseButton::Secondary
2303                    && let Some(p) = self.cursor
2304                    && let Some(ev) = self.hit_at(p).and_then(|h| Self::context_menu_event(h, p))
2305                {
2306                    out.push(ev);
2307                }
2308            }
2309            InputEvent::MouseDown { clicks, .. } => {
2310                self.pressed = self.hovered;
2311                self.pressed_group = self.hovered_group;
2312                self.press_clicks = clicks;
2313                if let Some(h) = self.cursor.and_then(|p| self.hit_at(p)) {
2314                    if h.window == Some(WindowRole::Drag) {
2315                        // The OS drag steals subsequent mouse events, so don't
2316                        // leave a press pending.
2317                        self.pressed = None;
2318                        self.window_commands
2319                            .push(WindowCommand::StartDrag(self.window));
2320                    } else if let Some(track) = &h.slider {
2321                        let p = self.cursor.unwrap();
2322                        let v = track.value_at(p);
2323                        out.push(crate::slider::change_event(
2324                            h.origin, h.key, v, "move", &track.tag,
2325                        ));
2326                        pointer_made += 1;
2327                        self.slide = Some((h.key, h.origin, track.clone(), v));
2328                    } else if let Some(tag) = &h.drag {
2329                        let p = self.cursor.unwrap();
2330                        let state = DragState {
2331                            key: h.key,
2332                            origin: h.origin,
2333                            tag: tag.clone(),
2334                            parent_rect: h.parent_rect,
2335                            press: p,
2336                            last: p,
2337                            moved: false,
2338                        };
2339                        out.push(Self::drag_event(&state, "start", p, Vec2::ZERO));
2340                        pointer_made += 1;
2341                        self.drag = Some(state);
2342                    }
2343                }
2344            }
2345            InputEvent::Modifiers(m) => {
2346                if m != self.modifiers {
2347                    self.modifiers = m;
2348                    out.push(UiEvent {
2349                        origin: OriginId::HOST,
2350                        window: WindowId::MAIN,
2351                        key: Key::ROOT,
2352                        payload: m.to_value(),
2353                        slot: None,
2354                    });
2355                }
2356            }
2357            // Routed by the core (they need the retained stores).
2358            InputEvent::Scroll(_)
2359            | InputEvent::ScrollGesture { .. }
2360            | InputEvent::Text(_)
2361            | InputEvent::Commit(_)
2362            | InputEvent::Paste { .. }
2363            | InputEvent::Preedit(..)
2364            | InputEvent::Key(..)
2365            | InputEvent::KeyDown(_)
2366            | InputEvent::KeyUp(_)
2367            | InputEvent::Access(_)
2368            // A force click needs the text and selection stores, and the
2369            // node it lands on it finds by hit test the way a secondary
2370            // press does.
2371            | InputEvent::ForceClick(_) => {}
2372            InputEvent::DragFiles { paths, at } => self.drag_files(&paths, at, out),
2373            InputEvent::DropFiles { paths, at } => self.drop_files(&paths, at, out),
2374            InputEvent::DragCancel => self.drag_cancel(out),
2375            // The core's, answered before the pointer is asked.
2376            InputEvent::Files(_) => {}
2377            // A non-primary release resolves no click; it ends the capture
2378            // its press began, if an `on_button` node claimed that press.
2379            InputEvent::MouseUp { button } if button != MouseButton::Primary => {
2380                if let Some(i) = self.held_buttons.iter().position(|h| h.button == button) {
2381                    let held = self.held_buttons.remove(i);
2382                    let p = self.cursor.unwrap_or(held.last);
2383                    out.push(Self::button_event(&held.owner, button, "release", p, None));
2384                    pointer_made += 1;
2385                }
2386            }
2387            InputEvent::MouseUp { .. } => {
2388                let dragged = self.drag.take().inspect(|drag| {
2389                    let p = self.cursor.unwrap_or(drag.last);
2390                    out.push(Self::drag_event(drag, "end", p, drag.displacement(p)));
2391                    pointer_made += 1;
2392                });
2393                // A slide ends where the pointer let go: the value to
2394                // commit, proposed again whether or not it moved.
2395                let slid = self.slide.take().inspect(|(key, origin, track, last)| {
2396                    let v = self.cursor.map_or(*last, |p| track.value_at(p));
2397                    out.push(crate::slider::change_event(
2398                        *origin, *key, v, "end", &track.tag,
2399                    ));
2400                    pointer_made += 1;
2401                });
2402                // A press that actually dragged is not a click, and a
2403                // press on a slider's track is the slide, never a click.
2404                let click_ok = !dragged.is_some_and(|d| d.moved) && slid.is_none();
2405                if click_ok
2406                    && let (Some(pressed), Some(hovered)) = (self.pressed, self.hovered)
2407                    && pressed == hovered
2408                    && let Some(region) = self.hits.iter().rev().find(|h| h.key == pressed)
2409                {
2410                    if let Some(sound) = region.click_sound {
2411                        self.sound_requests.push(sound);
2412                    }
2413                    match (region.window, &region.payload) {
2414                        (Some(WindowRole::Button(b)), _) => {
2415                            self.window_commands.push(b.command(self.window))
2416                        }
2417                        (Some(WindowRole::Drag), _) | (None, None) => {}
2418                        (None, Some(payload)) => {
2419                            out.push(UiEvent {
2420                                origin: region.origin,
2421                                window: WindowId::MAIN,
2422                                key: region.key,
2423                                payload: payload.clone(),
2424                                slot: None,
2425                            });
2426                            pointer_made += 1;
2427                        }
2428                    }
2429                }
2430                self.pressed = None;
2431                self.pressed_group = None;
2432            }
2433        }
2434        pointer_made
2435    }
2436
2437    pub fn is_hovered(&self, key: Key) -> bool {
2438        self.hovered == Some(key)
2439    }
2440
2441    /// Whether files dragged in from the OS are over `key` (ADR 0031):
2442    /// what `drop_bg` reads when the node opens.
2443    pub fn is_drop_target(&self, key: Key) -> bool {
2444        self.drop.as_ref().is_some_and(|d| d.owner.key == key)
2445    }
2446
2447    /// The zone the dragged files are over, if any — what a driver
2448    /// answers the OS with (a copy cursor over a zone, not-allowed
2449    /// elsewhere) and what a test reads to say a zone was found.
2450    pub fn drop_target(&self) -> Option<Key> {
2451        self.drop.as_ref().map(|d| d.owner.key)
2452    }
2453
2454    /// The topmost zone under `p` (ADR 0031, decision 2): the topmost
2455    /// region there whose resolved `drop` is some. A region resolving to
2456    /// no zone — an overlay the app showed on `enter` — is looked past.
2457    fn zone_at(&self, p: Vec2) -> Option<&DropOwner> {
2458        self.hits
2459            .iter()
2460            .rev()
2461            .find(|h| h.drop.is_some() && self.contains(h, p))
2462            .and_then(|h| h.drop.as_ref())
2463    }
2464
2465    fn drop_event(owner: &DropOwner, phase: &str, paths: &[String], at: Option<Vec2>) -> UiEvent {
2466        let mut fields = vec![
2467            ("kind", Value::str("drop")),
2468            ("phase", Value::str(phase)),
2469            (
2470                "paths",
2471                Value::list(
2472                    paths
2473                        .iter()
2474                        .map(|p| Value::str(p.as_str()))
2475                        .collect::<Vec<_>>(),
2476                ),
2477            ),
2478        ];
2479        if let Some(p) = at {
2480            fields.push(("x", Value::Float(p.x as f64)));
2481            fields.push(("y", Value::Float(p.y as f64)));
2482        }
2483        UiEvent::on(owner.origin, owner.key, Value::map(fields)).tagged(Some(&owner.tag))
2484    }
2485
2486    fn drag_files(&mut self, paths: &[String], at: Vec2, out: &mut Vec<UiEvent>) {
2487        let zone = self.zone_at(at).cloned();
2488        if let (Some(cur), Some(z)) = (&mut self.drop, &zone)
2489            && cur.owner.key == z.key
2490            && cur.owner.origin == z.origin
2491        {
2492            if cur.last != at {
2493                cur.last = at;
2494                out.push(Self::drop_event(z, "move", paths, Some(at)));
2495            }
2496            return;
2497        }
2498        out.extend(self.drop.take().map(|d| d.leave));
2499        if let Some(owner) = zone {
2500            out.push(Self::drop_event(&owner, "enter", paths, Some(at)));
2501            let leave = Self::drop_event(&owner, "leave", paths, None);
2502            self.drop = Some(DropHover {
2503                owner,
2504                last: at,
2505                leave,
2506            });
2507        }
2508    }
2509
2510    fn drop_files(&mut self, paths: &[String], at: Vec2, out: &mut Vec<UiEvent>) {
2511        let zone = self.zone_at(at).cloned();
2512        // The lit zone is not the one under the point (a headless drive
2513        // that never sent `DragFiles`, a frame that moved the zone): it
2514        // hears its leave first. The zone that takes the drop hears no
2515        // leave — the drop ends the hover (decision 1).
2516        if let Some(cur) = self.drop.take()
2517            && !zone
2518                .as_ref()
2519                .is_some_and(|z| z.key == cur.owner.key && z.origin == cur.owner.origin)
2520        {
2521            out.push(cur.leave);
2522        }
2523        if let Some(owner) = zone {
2524            out.push(Self::drop_event(&owner, "drop", paths, Some(at)));
2525        }
2526    }
2527
2528    fn drag_cancel(&mut self, out: &mut Vec<UiEvent>) {
2529        out.extend(self.drop.take().map(|d| d.leave));
2530    }
2531
2532    pub fn is_pressed(&self, key: Key) -> bool {
2533        self.pressed == Some(key) && (self.hovered == Some(key) || self.drag_captured(key))
2534    }
2535
2536    /// Whether a pointer-captured drag is running on `key`. The press is
2537    /// stuck to that node until release, so it stays pressed even when the
2538    /// cursor wanders off it (hover itself keeps following the cursor, so
2539    /// drop targets under the drag still light up).
2540    fn drag_captured(&self, key: Key) -> bool {
2541        self.drag.as_ref().is_some_and(|d| d.key == key)
2542    }
2543
2544    /// The hovered node, if any (its key from the last finished frame).
2545    pub fn hovered(&self) -> Option<Key> {
2546        self.hovered
2547    }
2548
2549    /// The node a press is held on, if any.
2550    /// The click count the last primary press carried; see
2551    /// `press_clicks`. Zero after a click nothing pressed for — Enter,
2552    /// Space, an assistive-technology `click` — so a payload attached
2553    /// from the pointer's position does not describe a press that never
2554    /// happened.
2555    pub(crate) fn press_clicks(&self) -> u8 {
2556        self.press_clicks
2557    }
2558
2559    /// A click is being made without a press (`Core::click_node`): the
2560    /// count the last press carried no longer describes it.
2561    pub(crate) fn note_synthetic_click(&mut self) {
2562        self.press_clicks = 0;
2563    }
2564
2565    pub fn pressed_key(&self) -> Option<Key> {
2566        self.pressed
2567    }
2568
2569    /// The pointer shape for where the pointer is now (see
2570    /// [`crate::cursor`]): what the topmost region under it — the same
2571    /// region a click would go to — declared with `cursor`, the I-beam
2572    /// over text, and the arrow otherwise. A clickable or draggable node
2573    /// that declared nothing is the arrow: a hand or a grab is the view's
2574    /// to say.
2575    pub fn cursor_shape(&self) -> CursorShape {
2576        // A captured drag owns the pointer: the shape stays the dragged
2577        // node's however far the cursor wanders off it.
2578        if let Some(drag) = &self.drag {
2579            return self
2580                .hits
2581                .iter()
2582                .rev()
2583                .find(|h| h.key == drag.key)
2584                .and_then(|h| h.cursor)
2585                .unwrap_or(CursorShape::Default);
2586        }
2587        // A bar that would take the press takes the shape too — an
2588        // overlay bar across an editor is not an I-beam — and a float
2589        // over the bar keeps its own.
2590        if self.scrollbar_drag.is_some() {
2591            return CursorShape::Default;
2592        }
2593        let Some(p) = self.cursor else {
2594            return CursorShape::Default;
2595        };
2596        match self.target_at(p) {
2597            Some(Target::Hit(region)) => {
2598                region.cursor.unwrap_or_else(|| Self::implied_shape(region))
2599            }
2600            Some(Target::Bar(_)) | None => CursorShape::Default,
2601        }
2602    }
2603
2604    /// The shape a region takes when it declares none: the I-beam over
2605    /// text that can be edited or selected — the one shape every desktop
2606    /// derives, because the words themselves are what says they can be
2607    /// taken — and the arrow over everything else. Nothing here reads
2608    /// `payload`, `drag` or `focusable`: a hand over a button and a grab
2609    /// over a handle are declared, and the stock button declares its own.
2610    fn implied_shape(region: &HitRegion) -> CursorShape {
2611        match region {
2612            // Window chrome is the platform's: every desktop points at a
2613            // titlebar and its buttons with the plain arrow.
2614            _ if region.window.is_some() => CursorShape::Default,
2615            _ if region.edit_origin.is_some() => CursorShape::Text,
2616            _ if region.select_scope.is_some() => CursorShape::Text,
2617            _ => CursorShape::Default,
2618        }
2619    }
2620
2621    /// Whether any member of hover group `group` is hovered.
2622    pub fn is_group_hovered(&self, group: u64) -> bool {
2623        self.hovered_group == Some(group)
2624    }
2625
2626    /// Whether the press started on a member of `group` and the pointer is
2627    /// still over one (the group analogue of `is_pressed`).
2628    pub fn is_group_pressed(&self, group: u64) -> bool {
2629        self.pressed_group == Some(group)
2630            && (self.hovered_group == Some(group) || self.drag.is_some())
2631    }
2632}
2633
2634#[cfg(test)]
2635mod tests {
2636    use super::*;
2637
2638    fn region(key: Key, origin: u16, x: f32, y: f32, w: f32, h: f32, tag: &str) -> HitRegion {
2639        HitRegion {
2640            key,
2641            origin: OriginId(origin),
2642            rect: Rect::new(x, y, w, h),
2643            clip: Rect::new(-1e9, -1e9, 2e9, 2e9),
2644            shape: HitShape::Rect,
2645            payload: Some(Value::str(tag)),
2646            drag: None,
2647            parent_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
2648            edit_origin: None,
2649            select_scope: None,
2650            key_sink: None,
2651            key_up: false,
2652            context_menu: None,
2653            drop: None,
2654            focusable: true,
2655            window: None,
2656            hover: None,
2657            group: None,
2658            click_sound: None,
2659            hover_sound: None,
2660            cursor: None,
2661            slider: None,
2662        }
2663    }
2664
2665    fn drive(interaction: &mut Interaction, events: &[InputEvent]) -> Vec<UiEvent> {
2666        let mut out = Vec::new();
2667        for ev in events {
2668            interaction.handle(ev.clone(), &mut out);
2669        }
2670        out
2671    }
2672
2673    #[test]
2674    fn click_inside_produces_event() {
2675        let mut it = Interaction::default();
2676        let k = Key::ROOT.str("btn");
2677        it.set_hits(vec![region(k, 0, 10.0, 10.0, 100.0, 30.0, "go")]);
2678        let evs = drive(
2679            &mut it,
2680            &[
2681                InputEvent::CursorMoved(Vec2::new(50.0, 20.0)),
2682                InputEvent::mouse_down(1),
2683                InputEvent::mouse_up(),
2684            ],
2685        );
2686        assert_eq!(evs.len(), 1);
2687        assert_eq!(evs[0].key, k);
2688        assert_eq!(evs[0].payload.as_str(), Some("go"));
2689    }
2690
2691    #[test]
2692    fn press_then_drag_away_does_not_click() {
2693        let mut it = Interaction::default();
2694        let k = Key::ROOT.str("btn");
2695        it.set_hits(vec![region(k, 0, 0.0, 0.0, 50.0, 50.0, "go")]);
2696        let evs = drive(
2697            &mut it,
2698            &[
2699                InputEvent::CursorMoved(Vec2::new(10.0, 10.0)),
2700                InputEvent::mouse_down(1),
2701                InputEvent::CursorMoved(Vec2::new(500.0, 500.0)),
2702                InputEvent::mouse_up(),
2703            ],
2704        );
2705        assert!(evs.is_empty());
2706    }
2707
2708    #[test]
2709    fn topmost_region_wins_on_overlap() {
2710        let mut it = Interaction::default();
2711        let bottom = Key::ROOT.str("bottom");
2712        let top = Key::ROOT.str("top");
2713        it.set_hits(vec![
2714            region(bottom, 0, 0.0, 0.0, 100.0, 100.0, "bottom"),
2715            region(top, 0, 25.0, 25.0, 50.0, 50.0, "top"),
2716        ]);
2717        let evs = drive(
2718            &mut it,
2719            &[
2720                InputEvent::CursorMoved(Vec2::new(50.0, 50.0)),
2721                InputEvent::mouse_down(1),
2722                InputEvent::mouse_up(),
2723            ],
2724        );
2725        assert_eq!(evs.len(), 1);
2726        assert_eq!(evs[0].key, top);
2727    }
2728
2729    #[test]
2730    fn event_carries_declaring_origin() {
2731        let mut it = Interaction::default();
2732        let k = Key::ROOT.str("ext-btn");
2733        it.set_hits(vec![region(k, 3, 0.0, 0.0, 10.0, 10.0, "x")]);
2734        let evs = drive(
2735            &mut it,
2736            &[
2737                InputEvent::CursorMoved(Vec2::new(5.0, 5.0)),
2738                InputEvent::mouse_down(1),
2739                InputEvent::mouse_up(),
2740            ],
2741        );
2742        assert_eq!(evs[0].origin, OriginId(3));
2743    }
2744
2745    #[test]
2746    fn cursor_leave_clears_hover() {
2747        let mut it = Interaction::default();
2748        let k = Key::ROOT.str("btn");
2749        it.set_hits(vec![region(k, 0, 0.0, 0.0, 50.0, 50.0, "x")]);
2750        drive(&mut it, &[InputEvent::CursorMoved(Vec2::new(10.0, 10.0))]);
2751        assert!(it.is_hovered(k));
2752        drive(&mut it, &[InputEvent::CursorLeft]);
2753        assert!(!it.is_hovered(k));
2754        // Click after leaving produces nothing.
2755        let evs = drive(
2756            &mut it,
2757            &[InputEvent::mouse_down(1), InputEvent::mouse_up()],
2758        );
2759        assert!(evs.is_empty());
2760    }
2761
2762    #[test]
2763    fn secondary_press_asks_the_node_under_it_for_a_menu() {
2764        let mut it = Interaction::default();
2765        let k = Key::ROOT.str("panel");
2766        let mut r = region(k, 0, 0.0, 0.0, 100.0, 100.0, "click-me");
2767        r.context_menu = Some(MenuOwner {
2768            key: k,
2769            origin: OriginId::HOST,
2770            tag: Value::str("panel-menu"),
2771        });
2772        it.set_hits(vec![r]);
2773        let evs = drive(
2774            &mut it,
2775            &[
2776                InputEvent::CursorMoved(Vec2::new(40.0, 30.0)),
2777                InputEvent::MouseDown {
2778                    button: MouseButton::Secondary,
2779                    clicks: 1,
2780                },
2781                InputEvent::MouseUp {
2782                    button: MouseButton::Secondary,
2783                },
2784            ],
2785        );
2786        // The menu arrives on the press, with the point to open it at, and
2787        // the release adds nothing — no click, though the node has one.
2788        assert_eq!(evs.len(), 1);
2789        assert_eq!(evs[0].key, k);
2790        assert_eq!(
2791            evs[0].payload.get("kind").unwrap().as_str(),
2792            Some("contextmenu")
2793        );
2794        assert_eq!(evs[0].payload.get("x").unwrap().as_float(), Some(40.0));
2795        assert_eq!(evs[0].payload.get("y").unwrap().as_float(), Some(30.0));
2796        assert_eq!(
2797            evs[0].payload.get("tag").unwrap().as_str(),
2798            Some("panel-menu")
2799        );
2800        assert!(!it.is_pressed(k));
2801    }
2802
2803    /// ADR 0031, decisions 1, 2 and 4: a button inside a zone is the
2804    /// zone, an overlay that is no zone is looked past, a drop ends the
2805    /// hover without a leave, a cancel leaves.
2806    #[test]
2807    fn dragged_files_find_the_topmost_zone_and_look_past_what_is_none() {
2808        let mut it = Interaction::default();
2809        let zone = Key::ROOT.str("zone");
2810        let button = Key::ROOT.str("button");
2811        let overlay = Key::ROOT.str("overlay");
2812        let other = Key::ROOT.str("other");
2813        let owner = |k: Key, tag: &str| {
2814            Some(DropOwner {
2815                key: k,
2816                origin: OriginId::HOST,
2817                tag: Value::str(tag),
2818            })
2819        };
2820        let mut z = region(zone, 0, 0.0, 0.0, 100.0, 100.0, "z");
2821        z.drop = owner(zone, "files");
2822        // The button is inside the zone: its region resolved to the zone.
2823        let mut b = region(button, 0, 10.0, 10.0, 30.0, 30.0, "press");
2824        b.drop = owner(zone, "files");
2825        // The overlay is painted over everything and belongs to no zone.
2826        let o = region(overlay, 0, 0.0, 0.0, 100.0, 100.0, "overlay");
2827        let mut second = region(other, 0, 100.0, 0.0, 100.0, 100.0, "o");
2828        second.drop = owner(other, "other-files");
2829        it.set_hits(vec![z, b, second, o]);
2830        let paths = vec!["/drop/1.txt".to_string()];
2831        let phases = |evs: &[UiEvent]| {
2832            evs.iter()
2833                .map(|e| {
2834                    (
2835                        e.key,
2836                        e.payload
2837                            .get("phase")
2838                            .unwrap()
2839                            .as_str()
2840                            .unwrap()
2841                            .to_string(),
2842                    )
2843                })
2844                .collect::<Vec<_>>()
2845        };
2846        // Over the button, through the overlay: the zone's enter.
2847        let evs = drive(
2848            &mut it,
2849            &[InputEvent::DragFiles {
2850                paths: paths.clone(),
2851                at: Vec2::new(20.0, 20.0),
2852            }],
2853        );
2854        assert_eq!(phases(&evs), vec![(zone, "enter".to_string())]);
2855        assert_eq!(evs[0].payload.get("tag").unwrap().as_str(), Some("files"));
2856        assert_eq!(evs[0].payload.get("x").unwrap().as_float(), Some(20.0));
2857        assert_eq!(it.drop_target(), Some(zone));
2858        assert!(it.is_drop_target(zone));
2859        // The same point again is nothing; a new one is a move.
2860        let evs = drive(
2861            &mut it,
2862            &[
2863                InputEvent::DragFiles {
2864                    paths: paths.clone(),
2865                    at: Vec2::new(20.0, 20.0),
2866                },
2867                InputEvent::DragFiles {
2868                    paths: paths.clone(),
2869                    at: Vec2::new(60.0, 60.0),
2870                },
2871            ],
2872        );
2873        assert_eq!(phases(&evs), vec![(zone, "move".to_string())]);
2874        // Into the other zone: leave, then enter, in that order.
2875        let evs = drive(
2876            &mut it,
2877            &[InputEvent::DragFiles {
2878                paths: paths.clone(),
2879                at: Vec2::new(150.0, 50.0),
2880            }],
2881        );
2882        assert_eq!(
2883            phases(&evs),
2884            vec![(zone, "leave".to_string()), (other, "enter".to_string())]
2885        );
2886        assert!(evs[0].payload.get("x").is_none());
2887        // Dropped there: the drop and nothing after it.
2888        let evs = drive(
2889            &mut it,
2890            &[InputEvent::DropFiles {
2891                paths: paths.clone(),
2892                at: Vec2::new(150.0, 50.0),
2893            }],
2894        );
2895        assert_eq!(phases(&evs), vec![(other, "drop".to_string())]);
2896        assert_eq!(it.drop_target(), None);
2897        // Over the first zone, then out of the window: its leave.
2898        let evs = drive(
2899            &mut it,
2900            &[
2901                InputEvent::DragFiles {
2902                    paths: paths.clone(),
2903                    at: Vec2::new(50.0, 50.0),
2904                },
2905                InputEvent::DragCancel,
2906            ],
2907        );
2908        assert_eq!(
2909            phases(&evs),
2910            vec![(zone, "enter".to_string()), (zone, "leave".to_string())]
2911        );
2912        // A drop off every zone with one lit: the lit one's leave, no drop.
2913        let evs = drive(
2914            &mut it,
2915            &[
2916                InputEvent::DragFiles {
2917                    paths: paths.clone(),
2918                    at: Vec2::new(50.0, 50.0),
2919                },
2920                InputEvent::DropFiles {
2921                    paths: paths.clone(),
2922                    at: Vec2::new(250.0, 50.0),
2923                },
2924            ],
2925        );
2926        assert_eq!(
2927            phases(&evs),
2928            vec![(zone, "enter".to_string()), (zone, "leave".to_string())]
2929        );
2930        assert_eq!(it.drop_target(), None);
2931    }
2932
2933    #[test]
2934    fn secondary_press_on_a_node_without_a_menu_emits_nothing() {
2935        let mut it = Interaction::default();
2936        let k = Key::ROOT.str("btn");
2937        it.set_hits(vec![region(k, 0, 0.0, 0.0, 100.0, 100.0, "go")]);
2938        let evs = drive(
2939            &mut it,
2940            &[
2941                InputEvent::CursorMoved(Vec2::new(40.0, 30.0)),
2942                InputEvent::MouseDown {
2943                    button: MouseButton::Secondary,
2944                    clicks: 1,
2945                },
2946                InputEvent::MouseUp {
2947                    button: MouseButton::Secondary,
2948                },
2949            ],
2950        );
2951        assert!(evs.is_empty());
2952    }
2953
2954    /// A secondary press in the middle of a primary one leaves the press
2955    /// alone: the primary release still clicks.
2956    #[test]
2957    fn secondary_press_does_not_interrupt_a_held_primary() {
2958        let mut it = Interaction::default();
2959        let k = Key::ROOT.str("btn");
2960        it.set_hits(vec![region(k, 0, 0.0, 0.0, 100.0, 100.0, "go")]);
2961        let evs = drive(
2962            &mut it,
2963            &[
2964                InputEvent::CursorMoved(Vec2::new(40.0, 30.0)),
2965                InputEvent::mouse_down(1),
2966                InputEvent::MouseDown {
2967                    button: MouseButton::Secondary,
2968                    clicks: 1,
2969                },
2970                InputEvent::MouseUp {
2971                    button: MouseButton::Secondary,
2972                },
2973            ],
2974        );
2975        assert!(evs.is_empty());
2976        assert!(it.is_pressed(k));
2977        let evs = drive(&mut it, &[InputEvent::mouse_up()]);
2978        assert_eq!(evs.len(), 1);
2979        assert_eq!(evs[0].payload.as_str(), Some("go"));
2980    }
2981
2982    #[test]
2983    fn middle_press_routes_nowhere() {
2984        let mut it = Interaction::default();
2985        let k = Key::ROOT.str("panel");
2986        let mut r = region(k, 0, 0.0, 0.0, 100.0, 100.0, "go");
2987        r.context_menu = Some(MenuOwner {
2988            key: k,
2989            origin: OriginId::HOST,
2990            tag: Value::str("panel-menu"),
2991        });
2992        it.set_hits(vec![r]);
2993        let evs = drive(
2994            &mut it,
2995            &[
2996                InputEvent::CursorMoved(Vec2::new(40.0, 30.0)),
2997                InputEvent::MouseDown {
2998                    button: MouseButton::Middle,
2999                    clicks: 1,
3000                },
3001                InputEvent::MouseUp {
3002                    button: MouseButton::Middle,
3003                },
3004            ],
3005        );
3006        assert!(evs.is_empty());
3007    }
3008
3009    #[test]
3010    fn button_codes_round_trip() {
3011        for b in [
3012            MouseButton::Primary,
3013            MouseButton::Secondary,
3014            MouseButton::Middle,
3015            MouseButton::Other(0),
3016            MouseButton::Other(9),
3017        ] {
3018            assert_eq!(MouseButton::from_code(b.code()), b);
3019        }
3020    }
3021
3022    #[test]
3023    fn new_frame_hits_preserve_hover_state() {
3024        let mut it = Interaction::default();
3025        let k = Key::ROOT.str("btn");
3026        it.set_hits(vec![region(k, 0, 0.0, 0.0, 50.0, 50.0, "x")]);
3027        drive(&mut it, &[InputEvent::CursorMoved(Vec2::new(10.0, 10.0))]);
3028        // Same widget moved: hover follows the rect under the cursor.
3029        it.set_hits(vec![region(k, 0, 100.0, 100.0, 50.0, 50.0, "x")]);
3030        assert!(!it.is_hovered(k));
3031        it.set_hits(vec![region(k, 0, 0.0, 0.0, 50.0, 50.0, "x")]);
3032        assert!(it.is_hovered(k));
3033    }
3034}