Skip to main content

crcbl_core/
input.rs

1//! Input vocabulary: the names every layer of the engine uses for keys,
2//! buttons, axes and the devices they came from.
3//!
4//! # Why this is in `crcbl-core` and not in `crcbl-shell`
5//!
6//! The shell-boundary rule in `docs/notes/backends.md` puts "input event
7//! normalisation into engine types" in `crcbl-core::input`, and the input
8//! design (`docs/notes/simulation.md`, _What the deleted 19-input plan left
9//! behind_) explains why: the action layer (`crcbl-input`, P2), the profile
10//! store (topic 14, where user rebinds are serialized), the UI (glyph hints),
11//! and the editor all have to *name* a key. If [`KeyCode`] lived in the
12//! windowing crate, the save-game crate would depend on a windowing library to
13//! spell `Space`, and a headless `crcbl sim` replay would link a shell it never
14//! opens.
15//!
16//! The split is therefore:
17//!
18//! ```text
19//! crcbl-core::input   vocabulary — KeyCode, Keysym, PointerButton, Modifiers,
20//!                                  ButtonState, ScrollDelta, DeviceId,
21//!                                  ContactId, TouchPhase, CursorIcon
22//! crcbl-shell         events     — ShellEvent, which names windows and is only
23//!                                  ever produced by a shell backend
24//! ```
25//!
26//! Nothing here knows what a window is, and nothing here allocates.
27//!
28//! # Physical versus logical: three coordinates for one keystroke
29//!
30//! A key press carries three independent facts, and conflating any two of them
31//! is the classic input bug:
32//!
33//! | Fact | Type | Depends on layout? | Used for |
34//! | --- | --- | --- | --- |
35//! | Which key was pressed | [`KeyCode`] | no | gameplay bindings ("W moves forward" stays under the W key on AZERTY) |
36//! | What the OS calls that key | [`Scancode`] | no | round-tripping keys we have no name for |
37//! | What symbol it produces | [`Keysym`] | **yes** | rebind menus, shortcut display, layout-sensitive shortcuts |
38//!
39//! And a fourth, which is not a keystroke at all: **text**. Composed text
40//! arrives as a separate commit string (`ShellEvent::TextCommit`) because IME
41//! input has no one-to-one relationship with key presses — one keystroke can
42//! commit five characters, five keystrokes can commit one, and a dead key
43//! commits nothing.
44//!
45//! # `Keysym` is the X11/XKB namespace
46//!
47//! [`Keysym`] holds an X11 keysym value, which is what `xkbcommon` produces.
48//! This is a decision, not an accident: two of the five planned shell backends
49//! (Wayland and X11 — the only two that exist at P0) produce these natively,
50//! and the other three need a translation table whatever namespace we pick.
51//! Inventing an engine-specific symbol enum would mean *three* tables instead of
52//! two plus a well-documented, stable, externally-specified numbering that
53//! already covers every keyboard symbol anyone has ever shipped.
54
55use core::fmt;
56
57bitflags::bitflags! {
58    /// Modifier keys held when an input event was produced.
59    ///
60    /// Latched state (Caps Lock, Num Lock) is included alongside held state
61    /// because a rebind UI and a shortcut matcher both need it, and asking the
62    /// shell for it later — after the event has been queued — would race with
63    /// the user letting go.
64    ///
65    /// Left and right modifiers are *not* distinguished here. The distinction
66    /// survives in [`KeyCode`] ([`ShiftLeft`](KeyCode::ShiftLeft) versus
67    /// [`ShiftRight`](KeyCode::ShiftRight)), which is where a game that wants to
68    /// bind them separately should look; a shortcut matcher never wants it, and
69    /// duplicating the axis here would mean every `contains(SHIFT)` check had to
70    /// remember to test two bits.
71    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
72    pub struct Modifiers: u32 {
73        /// Either Shift key is held.
74        const SHIFT = 1 << 0;
75        /// Either Control key is held.
76        const CTRL = 1 << 1;
77        /// Either Alt key is held (Option on macOS).
78        const ALT = 1 << 2;
79        /// Either Super key is held — Windows key, Command on macOS, Meta on
80        /// X11.
81        const SUPER = 1 << 3;
82        /// Caps Lock is latched on.
83        const CAPS_LOCK = 1 << 4;
84        /// Num Lock is latched on.
85        const NUM_LOCK = 1 << 5;
86        /// AltGr / ISO Level-3 Shift is held.
87        ///
88        /// Separate from [`ALT`](Self::ALT) because on many European layouts
89        /// AltGr produces characters, and a shortcut bound to `Alt+E` must not
90        /// fire when the user types `€`.
91        const ALT_GR = 1 << 6;
92    }
93}
94
95impl Modifiers {
96    /// The modifiers a shortcut matcher cares about: Shift, Ctrl, Alt, Super.
97    ///
98    /// Masks off latched state, which is never part of a chord.
99    pub const CHORD: Self = Self::SHIFT
100        .union(Self::CTRL)
101        .union(Self::ALT)
102        .union(Self::SUPER);
103
104    /// Just the chord bits of `self`.
105    #[inline]
106    #[must_use]
107    pub const fn chord(self) -> Self {
108        self.intersection(Self::CHORD)
109    }
110}
111
112/// Which physical device an event came from.
113///
114/// The input design requires a per-device id on raw events from day one,
115/// because local-multiplayer device-to-player assignment is post-MVP but is
116/// impossible to retrofit into an event stream that never said which keyboard
117/// pressed the key. Ids are unique for the lifetime of the process and are not
118/// reused when a device is unplugged.
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub struct DeviceId(pub u32);
121
122impl DeviceId {
123    /// The device that events with no attributable source carry.
124    ///
125    /// Backends that cannot tell two keyboards apart (X11 core protocol without
126    /// XInput2, and the browser) use this for everything, and so do synthesized
127    /// events from a replay or a test script.
128    pub const UNKNOWN: Self = Self(0);
129}
130
131impl fmt::Display for DeviceId {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        if *self == Self::UNKNOWN {
134            f.write_str("device ?")
135        } else {
136            write!(f, "device {}", self.0)
137        }
138    }
139}
140
141/// Whether a two-state input element went down or came up.
142///
143/// One type for keyboard keys, pointer buttons and gamepad buttons: they are
144/// the same fact, and a separate `KeyState`/`MouseState` pair only creates
145/// conversion noise at the action layer, which treats all three identically.
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
147pub enum ButtonState {
148    /// The element went down.
149    Pressed,
150    /// The element came up.
151    Released,
152}
153
154impl ButtonState {
155    /// Whether this is [`ButtonState::Pressed`].
156    #[inline]
157    #[must_use]
158    pub const fn is_pressed(self) -> bool {
159        matches!(self, Self::Pressed)
160    }
161
162    /// [`Pressed`](Self::Pressed) for `true`, [`Released`](Self::Released) for
163    /// `false`.
164    #[inline]
165    #[must_use]
166    pub const fn from_pressed(pressed: bool) -> Self {
167        if pressed {
168            Self::Pressed
169        } else {
170            Self::Released
171        }
172    }
173}
174
175/// The raw, platform-assigned code for a physical key.
176///
177/// Opaque on purpose: its numbering is whatever the window system uses (Linux
178/// evdev codes offset by 8 on both Wayland and X11, `MapVirtualKey` scan codes
179/// on Win32, `kVK_*` on macOS). It exists so a key the engine has no
180/// [`KeyCode`] name for can still be bound, logged and round-tripped through a
181/// profile rather than being silently dropped.
182///
183/// Never compare a `Scancode` against a literal in engine code — that is
184/// platform sniffing with extra steps. Compare [`KeyCode`].
185#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
186pub struct Scancode(pub u32);
187
188impl fmt::Display for Scancode {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "scancode {}", self.0)
191    }
192}
193
194/// A layout-mapped key symbol, in the X11/XKB keysym numbering.
195///
196/// See the [module docs](self) for why that numbering. The values a game
197/// actually wants are usually reachable through [`Keysym::to_char`]; the raw
198/// number matters only for named function keys, which live above
199/// `0xFF00`.
200#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
201pub struct Keysym(pub u32);
202
203impl Keysym {
204    /// No symbol — a key that produces nothing in the current layout, or a
205    /// backend that could not map one. `XKB_KEY_NoSymbol`.
206    pub const NONE: Self = Self(0);
207
208    /// Start of the "Unicode keysym" block: `0x0100_0000 | codepoint`.
209    const UNICODE_BASE: u32 = 0x0100_0000;
210
211    /// The character this symbol produces, if it produces one.
212    ///
213    /// Implements the two rules XKB uses: keysyms `0x20..=0x7E` and
214    /// `0xA0..=0xFF` are Latin-1 directly, and everything else that maps to a
215    /// character is `0x0100_0000 | codepoint`. Function keys, modifiers and
216    /// dead keys return `None`.
217    ///
218    /// This deliberately does **not** cover the several hundred legacy
219    /// non-Latin-1 keysyms that predate the Unicode block (Greek, Cyrillic and
220    /// friends in `0x05xx`–`0x07xx`). Text input is delivered as committed
221    /// strings, not reconstructed from keysyms, so the table would exist only
222    /// to be wrong in a way nobody notices; a shortcut label falls back to the
223    /// [`KeyCode`] name.
224    #[must_use]
225    pub const fn to_char(self) -> Option<char> {
226        let codepoint = match self.0 {
227            0x20..=0x7E | 0xA0..=0xFF => self.0,
228            value if value >= Self::UNICODE_BASE => value - Self::UNICODE_BASE,
229            _ => return None,
230        };
231        char::from_u32(codepoint)
232    }
233
234    /// The keysym for a character, in the canonical form XKB produces.
235    #[must_use]
236    pub const fn from_char(character: char) -> Self {
237        let codepoint = character as u32;
238        match codepoint {
239            0x20..=0x7E | 0xA0..=0xFF => Self(codepoint),
240            _ => Self(Self::UNICODE_BASE | codepoint),
241        }
242    }
243
244    /// Whether this is [`Keysym::NONE`].
245    #[inline]
246    #[must_use]
247    pub const fn is_none(self) -> bool {
248        self.0 == Self::NONE.0
249    }
250}
251
252impl fmt::Display for Keysym {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        match self.to_char() {
255            Some(character) if !character.is_control() => write!(f, "'{character}'"),
256            _ => write!(f, "keysym {:#06x}", self.0),
257        }
258    }
259}
260
261/// A button on a pointing device.
262///
263/// Named by role, not by physical position: `Left` is the primary button even
264/// on a left-handed mouse, because that is the swap the OS already applied and
265/// the game must not apply again.
266#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
267pub enum PointerButton {
268    /// Primary button.
269    Left,
270    /// Secondary (context-menu) button.
271    Right,
272    /// Wheel click.
273    Middle,
274    /// Thumb button that means "back" — `BTN_SIDE`, `XBUTTON1`.
275    Back,
276    /// Thumb button that means "forward" — `BTN_EXTRA`, `XBUTTON2`.
277    Forward,
278    /// Anything else, by platform-assigned index.
279    ///
280    /// Bindable and loggable, never interpreted by the engine.
281    Other(u16),
282}
283
284/// One finger on a touchscreen, for as long as it stays down.
285///
286/// **Unique among the contacts that are down at the same moment, stable from the
287/// [`TouchPhase::Began`] that starts a gesture to the
288/// [`Ended`](TouchPhase::Ended) or [`Cancelled`](TouchPhase::Cancelled) that
289/// finishes it, and free to be reused after that.** Those three sentences are
290/// the whole contract, and the third is the one that bites: a consumer keying
291/// per-gesture state off this — which finger is holding the movement stick —
292/// must drop that state when the contact ends, or the next finger to land
293/// inherits it.
294///
295/// A contact is not a device. Two fingers on one screen are one [`DeviceId`] and
296/// two `ContactId`s, which is exactly why the id could not be folded into the
297/// device: a per-device registry cannot represent a second finger.
298///
299/// The numbering is the platform's (a browser `pointerId`, a `wl_touch` id, a
300/// Win32 pointer id) and is meaningless to compare against a literal or across
301/// windows — like [`Scancode`], it exists to be matched against *other* ids from
302/// the same stream.
303#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
304pub struct ContactId(pub u32);
305
306impl fmt::Display for ContactId {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        write!(f, "contact {}", self.0)
309    }
310}
311
312/// Where a [`ContactId`] is in its gesture.
313///
314/// A touchscreen has no "hover": a contact exists from the moment it lands until
315/// the moment it goes away, so this is the lifecycle rather than a button state
316/// — which is why touch does not reuse [`ButtonState`].
317#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
318pub enum TouchPhase {
319    /// The finger landed. The first event any [`ContactId`] gets.
320    Began,
321    /// It moved while down.
322    Moved,
323    /// The user lifted it. **The gesture completed**, and a consumer commits
324    /// whatever it meant — a tap fires, a stick centres, a drag lands where it
325    /// was let go.
326    Ended,
327    /// The system took the gesture away: the OS claimed it for an edge swipe,
328    /// palm rejection changed its mind, the window lost focus, the browser
329    /// decided the page was scrolling after all.
330    ///
331    /// **Not an [`Ended`](Self::Ended), and a consumer that treats it as one
332    /// fires actions the user did not ask for.** The position it carries is the
333    /// last one the platform knew, not a place anyone chose, so the answer is to
334    /// undo the interaction rather than complete it — the drag goes back, the
335    /// tap does not fire, the stick centres without the character having lunged.
336    /// What both share is that the contact is over: the id is gone either way,
337    /// and a consumer that ignores this one is left holding a finger that will
338    /// never move again.
339    Cancelled,
340}
341
342impl TouchPhase {
343    /// Whether this phase ends the contact — [`Ended`](Self::Ended) or
344    /// [`Cancelled`](Self::Cancelled).
345    ///
346    /// The test anything tracking live contacts writes; it is the *reason* they
347    /// differ that the two variants exist for, not the fact that they both stop.
348    #[inline]
349    #[must_use]
350    pub const fn ends_contact(self) -> bool {
351        matches!(self, Self::Ended | Self::Cancelled)
352    }
353
354    /// A short, stable name for logs and test assertions.
355    #[must_use]
356    pub const fn as_str(self) -> &'static str {
357        match self {
358            Self::Began => "Began",
359            Self::Moved => "Moved",
360            Self::Ended => "Ended",
361            Self::Cancelled => "Cancelled",
362        }
363    }
364}
365
366impl fmt::Display for TouchPhase {
367    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368        f.write_str(self.as_str())
369    }
370}
371
372/// How far a scroll event moved, in the units the device reported.
373///
374/// The two variants are not interchangeable and must not be collapsed by the
375/// shell: a notched wheel genuinely reports discrete clicks, a touchpad
376/// genuinely reports pixels, and the conversion factor between them is a
377/// *policy* decision (how many pixels is one line?) that belongs to the action
378/// layer and the UI, where it can be configured, not to a backend.
379#[derive(Clone, Copy, Debug, PartialEq)]
380pub enum ScrollDelta {
381    /// Discrete detents — `y` positive scrolls the content up (away from the
382    /// user), matching every platform's convention for a wheel.
383    Lines {
384        /// Horizontal detents.
385        x: f32,
386        /// Vertical detents.
387        y: f32,
388    },
389    /// Continuous motion in physical pixels, from a touchpad or high-resolution
390    /// wheel. Same sign convention as [`ScrollDelta::Lines`].
391    Pixels {
392        /// Horizontal pixels.
393        x: f64,
394        /// Vertical pixels.
395        y: f64,
396    },
397}
398
399impl ScrollDelta {
400    /// Whether this delta is exactly zero in both axes.
401    ///
402    /// Wayland's `wl_pointer.axis_stop` and X11's button-4/5 emulation both
403    /// produce zero-length scrolls; consumers usually want to drop them.
404    #[must_use]
405    pub fn is_zero(self) -> bool {
406        match self {
407            Self::Lines { x, y } => x == 0.0 && y == 0.0,
408            Self::Pixels { x, y } => x == 0.0 && y == 0.0,
409        }
410    }
411}
412
413/// Generates [`KeyCode`] plus its name table from one list.
414///
415/// Two derived tables (`as_str`, `from_name`) and a hundred-odd variants are
416/// exactly the kind of thing that rots when maintained by hand — one added key
417/// with a forgotten name arm is a binding that serializes and never loads back.
418macro_rules! key_codes {
419    ($($variant:ident => $name:literal),+ $(,)?) => {
420        /// A key identified by physical position, independent of keyboard
421        /// layout.
422        ///
423        /// Variant names follow the W3C UI Events `code` values, which name
424        /// positions after the US-QWERTY legends: [`KeyCode::KeyW`] is the key
425        /// *where W is on a US keyboard*, which on AZERTY is the key labelled
426        /// Z. That is precisely the property gameplay bindings want — WASD
427        /// stays a square under the left hand everywhere — and precisely the
428        /// property a rebind menu must *not* display, which is what
429        /// [`Keysym`](crate::input::Keysym) is for.
430        ///
431        /// # Non-exhaustive on purpose
432        ///
433        /// Unlike [`SurfaceTarget`](crate::SurfaceTarget), which is exhaustive
434        /// so that a new platform breaks every backend loudly, this enum is
435        /// `#[non_exhaustive]`: consumers match a handful of keys and fall
436        /// through on the rest, so adding a key must not be a breaking change.
437        /// A key nobody named is a key nobody bound, which degrades to "the
438        /// binding does not fire" — not to a wrong action.
439        #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
440        #[non_exhaustive]
441        pub enum KeyCode {
442            $(
443                #[doc = concat!("The `", $name, "` key.")]
444                $variant,
445            )+
446        }
447
448        impl KeyCode {
449            /// Every key this engine has a name for, in declaration order.
450            ///
451            /// Used by the rebind UI to enumerate choices and by tests to prove
452            /// the name table round-trips.
453            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
454
455            /// The stable name of this key, as written in binding assets and
456            /// profiles.
457            ///
458            /// Stable across releases: it is a serialization format, not a
459            /// display string. A rebind menu shows the *keysym*, not this.
460            #[must_use]
461            pub const fn as_str(self) -> &'static str {
462                match self {
463                    $(Self::$variant => $name,)+
464                }
465            }
466
467            /// Parses a name produced by [`KeyCode::as_str`].
468            ///
469            /// Not a [`FromStr`](core::str::FromStr) impl: the only possible
470            /// failure is "no such key", and an error type carrying that one
471            /// fact would be ceremony. Bindings for unknown names are kept as
472            /// [`Scancode`](crate::input::Scancode) by the profile loader
473            /// rather than dropped.
474            #[must_use]
475            pub fn from_name(name: &str) -> Option<Self> {
476                match name {
477                    $($name => Some(Self::$variant),)+
478                    _ => None,
479                }
480            }
481        }
482    };
483}
484
485key_codes! {
486    // Alphanumeric block.
487    KeyA => "KeyA", KeyB => "KeyB", KeyC => "KeyC", KeyD => "KeyD", KeyE => "KeyE",
488    KeyF => "KeyF", KeyG => "KeyG", KeyH => "KeyH", KeyI => "KeyI", KeyJ => "KeyJ",
489    KeyK => "KeyK", KeyL => "KeyL", KeyM => "KeyM", KeyN => "KeyN", KeyO => "KeyO",
490    KeyP => "KeyP", KeyQ => "KeyQ", KeyR => "KeyR", KeyS => "KeyS", KeyT => "KeyT",
491    KeyU => "KeyU", KeyV => "KeyV", KeyW => "KeyW", KeyX => "KeyX", KeyY => "KeyY",
492    KeyZ => "KeyZ",
493    Digit0 => "Digit0", Digit1 => "Digit1", Digit2 => "Digit2", Digit3 => "Digit3",
494    Digit4 => "Digit4", Digit5 => "Digit5", Digit6 => "Digit6", Digit7 => "Digit7",
495    Digit8 => "Digit8", Digit9 => "Digit9",
496
497    // Punctuation, in US-QWERTY positions.
498    Backquote => "Backquote", Minus => "Minus", Equal => "Equal",
499    BracketLeft => "BracketLeft", BracketRight => "BracketRight", Backslash => "Backslash",
500    Semicolon => "Semicolon", Quote => "Quote", Comma => "Comma", Period => "Period",
501    Slash => "Slash",
502
503    // Keys that only exist on some physical layouts. Present because a binding
504    // made on an ISO or JIS keyboard has to survive a round trip through a
505    // profile written on an ANSI one.
506    IntlBackslash => "IntlBackslash", IntlRo => "IntlRo", IntlYen => "IntlYen",
507
508    // Editing and whitespace.
509    Escape => "Escape", Tab => "Tab", CapsLock => "CapsLock", Space => "Space",
510    Backspace => "Backspace", Enter => "Enter", Delete => "Delete", Insert => "Insert",
511
512    // Modifiers, left and right distinguished.
513    ShiftLeft => "ShiftLeft", ShiftRight => "ShiftRight",
514    ControlLeft => "ControlLeft", ControlRight => "ControlRight",
515    AltLeft => "AltLeft", AltRight => "AltRight",
516    SuperLeft => "SuperLeft", SuperRight => "SuperRight",
517    ContextMenu => "ContextMenu",
518
519    // Navigation.
520    Home => "Home", End => "End", PageUp => "PageUp", PageDown => "PageDown",
521    ArrowUp => "ArrowUp", ArrowDown => "ArrowDown",
522    ArrowLeft => "ArrowLeft", ArrowRight => "ArrowRight",
523
524    // Function row.
525    F1 => "F1", F2 => "F2", F3 => "F3", F4 => "F4", F5 => "F5", F6 => "F6",
526    F7 => "F7", F8 => "F8", F9 => "F9", F10 => "F10", F11 => "F11", F12 => "F12",
527
528    // System keys.
529    PrintScreen => "PrintScreen", ScrollLock => "ScrollLock", Pause => "Pause",
530
531    // Numpad.
532    NumLock => "NumLock",
533    Numpad0 => "Numpad0", Numpad1 => "Numpad1", Numpad2 => "Numpad2",
534    Numpad3 => "Numpad3", Numpad4 => "Numpad4", Numpad5 => "Numpad5",
535    Numpad6 => "Numpad6", Numpad7 => "Numpad7", Numpad8 => "Numpad8",
536    Numpad9 => "Numpad9",
537    NumpadDivide => "NumpadDivide", NumpadMultiply => "NumpadMultiply",
538    NumpadSubtract => "NumpadSubtract", NumpadAdd => "NumpadAdd",
539    NumpadEnter => "NumpadEnter", NumpadDecimal => "NumpadDecimal",
540}
541
542impl fmt::Display for KeyCode {
543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544        f.write_str(self.as_str())
545    }
546}
547
548/// Generates [`CursorIcon`], [`CursorIcon::ALL`] and both directions of its
549/// keyword table from one list, so a shape cannot be added without its CSS
550/// keyword or left out of the list a parser searches.
551macro_rules! cursor_icons {
552    ($($(#[$meta:meta])* $variant:ident => $name:literal),+ $(,)?) => {
553        /// A standard cursor shape.
554        ///
555        /// Names rather than pixels, because every platform already has themed,
556        /// correctly scaled, user-configured cursors; `crcbl_shell::cursor`
557        /// explains the rest.
558        ///
559        /// **Exhaustive on purpose.** Every shell backend maps each shape to its
560        /// own stock cursor, and a shape added later must fail to compile in
561        /// each of them rather than fall to an arrow through a wildcard arm.
562        /// That makes adding a shape a breaking change, which is the price.
563        #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
564        pub enum CursorIcon {
565            $($(#[$meta])* $variant,)+
566        }
567
568        impl CursorIcon {
569            /// Every shape, in declaration order.
570            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
571
572            /// The CSS/`cursor-shape-v1` keyword for this shape.
573            ///
574            /// Backends map from this rather than from the variant name, because
575            /// the keyword is the string X11 cursor themes and the browser both
576            /// index by.
577            #[must_use]
578            pub const fn as_css_name(self) -> &'static str {
579                match self {
580                    $(Self::$variant => $name,)+
581                }
582            }
583
584            /// The shape a CSS `cursor` keyword names, matched as CSS matches
585            /// keywords: ignoring ASCII case. `None` for a keyword with no shape
586            /// here, `auto` and `none` among them, which are not shapes.
587            #[must_use]
588            pub fn from_css_name(name: &str) -> Option<Self> {
589                Self::ALL
590                    .iter()
591                    .copied()
592                    .find(|shape| shape.as_css_name().eq_ignore_ascii_case(name))
593            }
594        }
595    };
596}
597
598cursor_icons! {
599    /// The platform's normal arrow.
600    #[default]
601    Default => "default",
602    /// The "this is clickable" hand.
603    Pointer => "pointer",
604    /// I-beam, for text.
605    Text => "text",
606    /// Crosshair, for precise placement.
607    Crosshair => "crosshair",
608    /// Something is happening and input is blocked.
609    Wait => "wait",
610    /// Something is happening but input still works.
611    Progress => "progress",
612    /// This action is not allowed here.
613    NotAllowed => "not-allowed",
614    /// The thing under the pointer can be picked up.
615    Grab => "grab",
616    /// The thing under the pointer is being dragged.
617    Grabbing => "grabbing",
618    /// Move in any direction.
619    Move => "move",
620    /// Resize vertically.
621    ResizeNorthSouth => "ns-resize",
622    /// Resize horizontally.
623    ResizeEastWest => "ew-resize",
624    /// Resize along the ↗↙ diagonal.
625    ResizeNorthEastSouthWest => "nesw-resize",
626    /// Resize along the ↖↘ diagonal.
627    ResizeNorthWestSouthEast => "nwse-resize",
628    /// Split pane, vertical divider.
629    ResizeColumn => "col-resize",
630    /// Split pane, horizontal divider.
631    ResizeRow => "row-resize",
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    #[test]
639    fn cursor_keywords_are_unique_css_shaped_and_parse_back() {
640        let shapes = CursorIcon::ALL;
641        let mut names: Vec<&str> = shapes.iter().map(|shape| shape.as_css_name()).collect();
642        let count = names.len();
643        names.sort_unstable();
644        names.dedup();
645        assert_eq!(names.len(), count, "cursor keywords must be unique");
646        assert!(
647            names
648                .iter()
649                .all(|name| name.chars().all(|c| c.is_ascii_lowercase() || c == '-')),
650            "keywords are CSS identifiers"
651        );
652        assert_eq!(CursorIcon::default().as_css_name(), "default");
653        for &shape in shapes {
654            assert_eq!(CursorIcon::from_css_name(shape.as_css_name()), Some(shape));
655        }
656        assert_eq!(
657            CursorIcon::from_css_name("Not-Allowed"),
658            Some(CursorIcon::NotAllowed),
659            "keywords ignore ASCII case"
660        );
661        for not_a_shape in ["auto", "none", "", "pointer ", "hand"] {
662            assert_eq!(
663                CursorIcon::from_css_name(not_a_shape),
664                None,
665                "{not_a_shape:?}"
666            );
667        }
668    }
669
670    #[test]
671    fn key_names_round_trip_and_are_unique() {
672        let mut names: Vec<&'static str> = KeyCode::ALL.iter().map(|key| key.as_str()).collect();
673        let count = names.len();
674        assert!(count > 100, "the table covers a full keyboard, got {count}");
675        names.sort_unstable();
676        names.dedup();
677        assert_eq!(names.len(), count, "key names must be unique");
678
679        for key in KeyCode::ALL {
680            assert_eq!(KeyCode::from_name(key.as_str()), Some(*key));
681            assert_eq!(key.to_string(), key.as_str());
682        }
683        assert_eq!(KeyCode::from_name("KeyÆ"), None);
684    }
685
686    #[test]
687    fn wasd_is_a_physical_square_not_a_layout_one() {
688        // The whole point of the type: these are positions, so a binding stored
689        // as a name resolves to the same position on any layout.
690        for name in ["KeyW", "KeyA", "KeyS", "KeyD"] {
691            assert!(KeyCode::from_name(name).is_some(), "{name}");
692        }
693    }
694
695    #[test]
696    fn keysyms_map_both_ways_over_the_xkb_rules() {
697        // Latin-1 range: the keysym *is* the codepoint.
698        assert_eq!(Keysym::from_char('a'), Keysym(0x61));
699        assert_eq!(Keysym(0x61).to_char(), Some('a'));
700        assert_eq!(Keysym::from_char('ä'), Keysym(0xE4));
701        assert_eq!(Keysym(0xE4).to_char(), Some('ä'));
702
703        // Everything else lives in the Unicode block.
704        let euro = Keysym::from_char('€');
705        assert_eq!(euro, Keysym(0x0100_20AC));
706        assert_eq!(euro.to_char(), Some('€'));
707
708        // Function keys and modifiers are not characters.
709        const XKB_KEY_F1: Keysym = Keysym(0xFFBE);
710        assert_eq!(XKB_KEY_F1.to_char(), None);
711        assert!(Keysym::NONE.is_none());
712        assert_eq!(Keysym::NONE.to_char(), None);
713
714        assert_eq!(Keysym::from_char('q').to_string(), "'q'");
715        assert_eq!(XKB_KEY_F1.to_string(), "keysym 0xffbe");
716    }
717
718    #[test]
719    fn every_printable_ascii_round_trips_through_a_keysym() {
720        for codepoint in 0x20u32..=0x7E {
721            let character = char::from_u32(codepoint).expect("ascii");
722            assert_eq!(Keysym::from_char(character).to_char(), Some(character));
723        }
724    }
725
726    #[test]
727    fn chord_modifiers_exclude_latched_state() {
728        let held = Modifiers::CTRL | Modifiers::SHIFT | Modifiers::CAPS_LOCK | Modifiers::NUM_LOCK;
729        assert_eq!(held.chord(), Modifiers::CTRL | Modifiers::SHIFT);
730        assert!(!Modifiers::CHORD.contains(Modifiers::ALT_GR));
731        assert_eq!(Modifiers::default(), Modifiers::empty());
732    }
733
734    #[test]
735    fn button_state_is_one_type_for_every_kind_of_button() {
736        assert!(ButtonState::from_pressed(true).is_pressed());
737        assert_eq!(ButtonState::from_pressed(false), ButtonState::Released);
738    }
739
740    #[test]
741    fn scroll_units_stay_distinct() {
742        let lines = ScrollDelta::Lines { x: 0.0, y: -1.0 };
743        let pixels = ScrollDelta::Pixels { x: 0.0, y: -13.0 };
744        assert_ne!(
745            format!("{lines:?}"),
746            format!("{pixels:?}"),
747            "a wheel detent and 13 px must not be the same value"
748        );
749        assert!(ScrollDelta::Lines { x: 0.0, y: 0.0 }.is_zero());
750        assert!(ScrollDelta::Pixels { x: 0.0, y: 0.0 }.is_zero());
751        assert!(!lines.is_zero());
752        assert!(!pixels.is_zero());
753    }
754
755    #[test]
756    fn device_ids_are_displayable_and_default_to_unknown() {
757        assert_eq!(DeviceId::default(), DeviceId::UNKNOWN);
758        assert_eq!(DeviceId::UNKNOWN.to_string(), "device ?");
759        assert_eq!(DeviceId(4).to_string(), "device 4");
760        assert_eq!(Scancode(30).to_string(), "scancode 30");
761    }
762}