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