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