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