Skip to main content

kui_core/
input.rs

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