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