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    // The keys whose symbol no layout moves, by their X11 names (`XK_` dropped,
209    // in `SCREAMING_CASE`), from `X11/keysymdef.h`. Compare
210    // `ShellEvent::Key`'s `keysym` against these, not its `key_code`, to act on
211    // the key the layout produced: `key_code` is the physical key, which an xkb
212    // remap (`caps:escape`) or a virtual keyboard's own keymap does not change.
213    /// `XK_BackSpace`.
214    pub const BACKSPACE: Self = Self(0xFF08);
215    /// `XK_Tab`.
216    pub const TAB: Self = Self(0xFF09);
217    /// `XK_Return`.
218    pub const RETURN: Self = Self(0xFF0D);
219    /// `XK_Pause`.
220    pub const PAUSE: Self = Self(0xFF13);
221    /// `XK_Scroll_Lock`.
222    pub const SCROLL_LOCK: Self = Self(0xFF14);
223    /// `XK_Escape`.
224    pub const ESCAPE: Self = Self(0xFF1B);
225    /// `XK_Home`.
226    pub const HOME: Self = Self(0xFF50);
227    /// `XK_Left`.
228    pub const LEFT: Self = Self(0xFF51);
229    /// `XK_Up`.
230    pub const UP: Self = Self(0xFF52);
231    /// `XK_Right`.
232    pub const RIGHT: Self = Self(0xFF53);
233    /// `XK_Down`.
234    pub const DOWN: Self = Self(0xFF54);
235    /// `XK_Prior`.
236    pub const PAGE_UP: Self = Self(0xFF55);
237    /// `XK_Next`.
238    pub const PAGE_DOWN: Self = Self(0xFF56);
239    /// `XK_End`.
240    pub const END: Self = Self(0xFF57);
241    /// `XK_Print`.
242    pub const PRINT: Self = Self(0xFF61);
243    /// `XK_Insert`.
244    pub const INSERT: Self = Self(0xFF63);
245    /// `XK_Menu`.
246    pub const MENU: Self = Self(0xFF67);
247    /// `XK_Num_Lock`.
248    pub const NUM_LOCK: Self = Self(0xFF7F);
249    /// `XK_KP_Enter`.
250    pub const KP_ENTER: Self = Self(0xFF8D);
251    /// `XK_KP_Multiply`.
252    pub const KP_MULTIPLY: Self = Self(0xFFAA);
253    /// `XK_KP_Add`.
254    pub const KP_ADD: Self = Self(0xFFAB);
255    /// `XK_KP_Subtract`.
256    pub const KP_SUBTRACT: Self = Self(0xFFAD);
257    /// `XK_KP_Decimal`.
258    pub const KP_DECIMAL: Self = Self(0xFFAE);
259    /// `XK_KP_Divide`.
260    pub const KP_DIVIDE: Self = Self(0xFFAF);
261    /// `XK_KP_0`.
262    pub const KP_0: Self = Self(0xFFB0);
263    /// `XK_KP_1`.
264    pub const KP_1: Self = Self(0xFFB1);
265    /// `XK_KP_2`.
266    pub const KP_2: Self = Self(0xFFB2);
267    /// `XK_KP_3`.
268    pub const KP_3: Self = Self(0xFFB3);
269    /// `XK_KP_4`.
270    pub const KP_4: Self = Self(0xFFB4);
271    /// `XK_KP_5`.
272    pub const KP_5: Self = Self(0xFFB5);
273    /// `XK_KP_6`.
274    pub const KP_6: Self = Self(0xFFB6);
275    /// `XK_KP_7`.
276    pub const KP_7: Self = Self(0xFFB7);
277    /// `XK_KP_8`.
278    pub const KP_8: Self = Self(0xFFB8);
279    /// `XK_KP_9`.
280    pub const KP_9: Self = Self(0xFFB9);
281    /// `XK_F1`.
282    pub const F1: Self = Self(0xFFBE);
283    /// `XK_F2`.
284    pub const F2: Self = Self(0xFFBF);
285    /// `XK_F3`.
286    pub const F3: Self = Self(0xFFC0);
287    /// `XK_F4`.
288    pub const F4: Self = Self(0xFFC1);
289    /// `XK_F5`.
290    pub const F5: Self = Self(0xFFC2);
291    /// `XK_F6`.
292    pub const F6: Self = Self(0xFFC3);
293    /// `XK_F7`.
294    pub const F7: Self = Self(0xFFC4);
295    /// `XK_F8`.
296    pub const F8: Self = Self(0xFFC5);
297    /// `XK_F9`.
298    pub const F9: Self = Self(0xFFC6);
299    /// `XK_F10`.
300    pub const F10: Self = Self(0xFFC7);
301    /// `XK_F11`.
302    pub const F11: Self = Self(0xFFC8);
303    /// `XK_F12`.
304    pub const F12: Self = Self(0xFFC9);
305    /// `XK_Shift_L`.
306    pub const SHIFT_L: Self = Self(0xFFE1);
307    /// `XK_Shift_R`.
308    pub const SHIFT_R: Self = Self(0xFFE2);
309    /// `XK_Control_L`.
310    pub const CONTROL_L: Self = Self(0xFFE3);
311    /// `XK_Control_R`.
312    pub const CONTROL_R: Self = Self(0xFFE4);
313    /// `XK_Caps_Lock`.
314    pub const CAPS_LOCK: Self = Self(0xFFE5);
315    /// `XK_Alt_L`.
316    pub const ALT_L: Self = Self(0xFFE9);
317    /// `XK_Alt_R`.
318    pub const ALT_R: Self = Self(0xFFEA);
319    /// `XK_Super_L`.
320    pub const SUPER_L: Self = Self(0xFFEB);
321    /// `XK_Super_R`.
322    pub const SUPER_R: Self = Self(0xFFEC);
323    /// `XK_Delete`.
324    pub const DELETE: Self = Self(0xFFFF);
325
326    /// Start of the "Unicode keysym" block: `0x0100_0000 | codepoint`.
327    const UNICODE_BASE: u32 = 0x0100_0000;
328
329    /// The character this symbol produces, if it produces one.
330    ///
331    /// Implements the two rules XKB uses: keysyms `0x20..=0x7E` and
332    /// `0xA0..=0xFF` are Latin-1 directly, and everything else that maps to a
333    /// character is `0x0100_0000 | codepoint`. Function keys, modifiers and
334    /// dead keys return `None`.
335    ///
336    /// This deliberately does **not** cover the several hundred legacy
337    /// non-Latin-1 keysyms that predate the Unicode block (Greek, Cyrillic and
338    /// friends in `0x05xx`–`0x07xx`). Text input is delivered as committed
339    /// strings, not reconstructed from keysyms, so the table would exist only
340    /// to be wrong in a way nobody notices; a shortcut label falls back to the
341    /// [`KeyCode`] name.
342    #[must_use]
343    pub const fn to_char(self) -> Option<char> {
344        let codepoint = match self.0 {
345            0x20..=0x7E | 0xA0..=0xFF => self.0,
346            value if value >= Self::UNICODE_BASE => value - Self::UNICODE_BASE,
347            _ => return None,
348        };
349        char::from_u32(codepoint)
350    }
351
352    /// The keysym for a character, in the canonical form XKB produces.
353    #[must_use]
354    pub const fn from_char(character: char) -> Self {
355        let codepoint = character as u32;
356        match codepoint {
357            0x20..=0x7E | 0xA0..=0xFF => Self(codepoint),
358            _ => Self(Self::UNICODE_BASE | codepoint),
359        }
360    }
361
362    /// Whether this is [`Keysym::NONE`].
363    #[inline]
364    #[must_use]
365    pub const fn is_none(self) -> bool {
366        self.0 == Self::NONE.0
367    }
368}
369
370impl fmt::Display for Keysym {
371    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372        match self.to_char() {
373            Some(character) if !character.is_control() => write!(f, "'{character}'"),
374            _ => write!(f, "keysym {:#06x}", self.0),
375        }
376    }
377}
378
379/// A button on a pointing device.
380///
381/// Named by role, not by physical position: `Left` is the primary button even
382/// on a left-handed mouse, because that is the swap the OS already applied and
383/// the game must not apply again.
384#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
385pub enum PointerButton {
386    /// Primary button.
387    Left,
388    /// Secondary (context-menu) button.
389    Right,
390    /// Wheel click.
391    Middle,
392    /// Thumb button that means "back" — `BTN_SIDE`, `XBUTTON1`.
393    Back,
394    /// Thumb button that means "forward" — `BTN_EXTRA`, `XBUTTON2`.
395    Forward,
396    /// Anything else, by platform-assigned index.
397    ///
398    /// Bindable and loggable, never interpreted by the engine.
399    Other(u16),
400}
401
402/// One finger on a touchscreen, for as long as it stays down.
403///
404/// **Unique among the contacts that are down at the same moment, stable from the
405/// [`TouchPhase::Began`] that starts a gesture to the
406/// [`Ended`](TouchPhase::Ended) or [`Cancelled`](TouchPhase::Cancelled) that
407/// finishes it, and free to be reused after that.** Those three sentences are
408/// the whole contract, and the third is the one that bites: a consumer keying
409/// per-gesture state off this — which finger is holding the movement stick —
410/// must drop that state when the contact ends, or the next finger to land
411/// inherits it.
412///
413/// A contact is not a device. Two fingers on one screen are one [`DeviceId`] and
414/// two `ContactId`s, which is exactly why the id could not be folded into the
415/// device: a per-device registry cannot represent a second finger.
416///
417/// The numbering is the platform's (a browser `pointerId`, a `wl_touch` id, a
418/// Win32 pointer id) and is meaningless to compare against a literal or across
419/// windows — like [`Scancode`], it exists to be matched against *other* ids from
420/// the same stream.
421#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
422pub struct ContactId(pub u32);
423
424impl fmt::Display for ContactId {
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        write!(f, "contact {}", self.0)
427    }
428}
429
430/// Where a [`ContactId`] is in its gesture.
431///
432/// A touchscreen has no "hover": a contact exists from the moment it lands until
433/// the moment it goes away, so this is the lifecycle rather than a button state
434/// — which is why touch does not reuse [`ButtonState`].
435#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
436pub enum TouchPhase {
437    /// The finger landed. The first event any [`ContactId`] gets.
438    Began,
439    /// It moved while down.
440    Moved,
441    /// The user lifted it. **The gesture completed**, and a consumer commits
442    /// whatever it meant — a tap fires, a stick centres, a drag lands where it
443    /// was let go.
444    Ended,
445    /// The system took the gesture away: the OS claimed it for an edge swipe,
446    /// palm rejection changed its mind, the window lost focus, the browser
447    /// decided the page was scrolling after all.
448    ///
449    /// **Not an [`Ended`](Self::Ended), and a consumer that treats it as one
450    /// fires actions the user did not ask for.** The position it carries is the
451    /// last one the platform knew, not a place anyone chose, so the answer is to
452    /// undo the interaction rather than complete it — the drag goes back, the
453    /// tap does not fire, the stick centres without the character having lunged.
454    /// What both share is that the contact is over: the id is gone either way,
455    /// and a consumer that ignores this one is left holding a finger that will
456    /// never move again.
457    Cancelled,
458}
459
460impl TouchPhase {
461    /// Whether this phase ends the contact — [`Ended`](Self::Ended) or
462    /// [`Cancelled`](Self::Cancelled).
463    ///
464    /// The test anything tracking live contacts writes; it is the *reason* they
465    /// differ that the two variants exist for, not the fact that they both stop.
466    #[inline]
467    #[must_use]
468    pub const fn ends_contact(self) -> bool {
469        matches!(self, Self::Ended | Self::Cancelled)
470    }
471
472    /// A short, stable name for logs and test assertions.
473    #[must_use]
474    pub const fn as_str(self) -> &'static str {
475        match self {
476            Self::Began => "Began",
477            Self::Moved => "Moved",
478            Self::Ended => "Ended",
479            Self::Cancelled => "Cancelled",
480        }
481    }
482}
483
484impl fmt::Display for TouchPhase {
485    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486        f.write_str(self.as_str())
487    }
488}
489
490/// How far a scroll event moved, in the units the device reported.
491///
492/// The two variants are not interchangeable and must not be collapsed by the
493/// shell: a notched wheel genuinely reports discrete clicks, a touchpad
494/// genuinely reports pixels, and the conversion factor between them is a
495/// *policy* decision (how many pixels is one line?) that belongs to the action
496/// layer and the UI, where it can be configured, not to a backend.
497#[derive(Clone, Copy, Debug, PartialEq)]
498pub enum ScrollDelta {
499    /// Discrete detents — `y` positive scrolls the content up (away from the
500    /// user), matching every platform's convention for a wheel.
501    Lines {
502        /// Horizontal detents.
503        x: f32,
504        /// Vertical detents.
505        y: f32,
506    },
507    /// Continuous motion in physical pixels, from a touchpad or high-resolution
508    /// wheel. Same sign convention as [`ScrollDelta::Lines`].
509    Pixels {
510        /// Horizontal pixels.
511        x: f64,
512        /// Vertical pixels.
513        y: f64,
514    },
515}
516
517impl ScrollDelta {
518    /// Whether this delta is exactly zero in both axes.
519    ///
520    /// Wayland's `wl_pointer.axis_stop` and X11's button-4/5 emulation both
521    /// produce zero-length scrolls; consumers usually want to drop them.
522    #[must_use]
523    pub fn is_zero(self) -> bool {
524        match self {
525            Self::Lines { x, y } => x == 0.0 && y == 0.0,
526            Self::Pixels { x, y } => x == 0.0 && y == 0.0,
527        }
528    }
529}
530
531/// Generates [`KeyCode`] plus its name table from one list.
532///
533/// Two derived tables (`as_str`, `from_name`) and a hundred-odd variants are
534/// exactly the kind of thing that rots when maintained by hand — one added key
535/// with a forgotten name arm is a binding that serializes and never loads back.
536macro_rules! key_codes {
537    ($($variant:ident => $name:literal),+ $(,)?) => {
538        /// A key identified by physical position, independent of keyboard
539        /// layout.
540        ///
541        /// Variant names follow the W3C UI Events `code` values, which name
542        /// positions after the US-QWERTY legends: [`KeyCode::KeyW`] is the key
543        /// *where W is on a US keyboard*, which on AZERTY is the key labelled
544        /// Z. That is precisely the property gameplay bindings want — WASD
545        /// stays a square under the left hand everywhere — and precisely the
546        /// property a rebind menu must *not* display, which is what
547        /// [`Keysym`](crate::input::Keysym) is for.
548        ///
549        /// # Non-exhaustive on purpose
550        ///
551        /// Unlike [`SurfaceTarget`](crate::SurfaceTarget), which is exhaustive
552        /// so that a new platform breaks every backend loudly, this enum is
553        /// `#[non_exhaustive]`: consumers match a handful of keys and fall
554        /// through on the rest, so adding a key must not be a breaking change.
555        /// A key nobody named is a key nobody bound, which degrades to "the
556        /// binding does not fire" — not to a wrong action.
557        #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
558        #[non_exhaustive]
559        pub enum KeyCode {
560            $(
561                #[doc = concat!("The `", $name, "` key.")]
562                $variant,
563            )+
564        }
565
566        impl KeyCode {
567            /// Every key this engine has a name for, in declaration order.
568            ///
569            /// Used by the rebind UI to enumerate choices and by tests to prove
570            /// the name table round-trips.
571            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
572
573            /// The stable name of this key, as written in binding assets and
574            /// profiles.
575            ///
576            /// Stable across releases: it is a serialization format, not a
577            /// display string. A rebind menu shows the *keysym*, not this.
578            #[must_use]
579            pub const fn as_str(self) -> &'static str {
580                match self {
581                    $(Self::$variant => $name,)+
582                }
583            }
584
585            /// Parses a name produced by [`KeyCode::as_str`].
586            ///
587            /// Not a [`FromStr`](core::str::FromStr) impl: the only possible
588            /// failure is "no such key", and an error type carrying that one
589            /// fact would be ceremony. Bindings for unknown names are kept as
590            /// [`Scancode`](crate::input::Scancode) by the profile loader
591            /// rather than dropped.
592            #[must_use]
593            pub fn from_name(name: &str) -> Option<Self> {
594                match name {
595                    $($name => Some(Self::$variant),)+
596                    _ => None,
597                }
598            }
599        }
600    };
601}
602
603key_codes! {
604    // Alphanumeric block.
605    KeyA => "KeyA", KeyB => "KeyB", KeyC => "KeyC", KeyD => "KeyD", KeyE => "KeyE",
606    KeyF => "KeyF", KeyG => "KeyG", KeyH => "KeyH", KeyI => "KeyI", KeyJ => "KeyJ",
607    KeyK => "KeyK", KeyL => "KeyL", KeyM => "KeyM", KeyN => "KeyN", KeyO => "KeyO",
608    KeyP => "KeyP", KeyQ => "KeyQ", KeyR => "KeyR", KeyS => "KeyS", KeyT => "KeyT",
609    KeyU => "KeyU", KeyV => "KeyV", KeyW => "KeyW", KeyX => "KeyX", KeyY => "KeyY",
610    KeyZ => "KeyZ",
611    Digit0 => "Digit0", Digit1 => "Digit1", Digit2 => "Digit2", Digit3 => "Digit3",
612    Digit4 => "Digit4", Digit5 => "Digit5", Digit6 => "Digit6", Digit7 => "Digit7",
613    Digit8 => "Digit8", Digit9 => "Digit9",
614
615    // Punctuation, in US-QWERTY positions.
616    Backquote => "Backquote", Minus => "Minus", Equal => "Equal",
617    BracketLeft => "BracketLeft", BracketRight => "BracketRight", Backslash => "Backslash",
618    Semicolon => "Semicolon", Quote => "Quote", Comma => "Comma", Period => "Period",
619    Slash => "Slash",
620
621    // Keys that only exist on some physical layouts. Present because a binding
622    // made on an ISO or JIS keyboard has to survive a round trip through a
623    // profile written on an ANSI one.
624    IntlBackslash => "IntlBackslash", IntlRo => "IntlRo", IntlYen => "IntlYen",
625
626    // Editing and whitespace.
627    Escape => "Escape", Tab => "Tab", CapsLock => "CapsLock", Space => "Space",
628    Backspace => "Backspace", Enter => "Enter", Delete => "Delete", Insert => "Insert",
629
630    // Modifiers, left and right distinguished.
631    ShiftLeft => "ShiftLeft", ShiftRight => "ShiftRight",
632    ControlLeft => "ControlLeft", ControlRight => "ControlRight",
633    AltLeft => "AltLeft", AltRight => "AltRight",
634    SuperLeft => "SuperLeft", SuperRight => "SuperRight",
635    ContextMenu => "ContextMenu",
636
637    // Navigation.
638    Home => "Home", End => "End", PageUp => "PageUp", PageDown => "PageDown",
639    ArrowUp => "ArrowUp", ArrowDown => "ArrowDown",
640    ArrowLeft => "ArrowLeft", ArrowRight => "ArrowRight",
641
642    // Function row.
643    F1 => "F1", F2 => "F2", F3 => "F3", F4 => "F4", F5 => "F5", F6 => "F6",
644    F7 => "F7", F8 => "F8", F9 => "F9", F10 => "F10", F11 => "F11", F12 => "F12",
645
646    // System keys.
647    PrintScreen => "PrintScreen", ScrollLock => "ScrollLock", Pause => "Pause",
648
649    // Numpad.
650    NumLock => "NumLock",
651    Numpad0 => "Numpad0", Numpad1 => "Numpad1", Numpad2 => "Numpad2",
652    Numpad3 => "Numpad3", Numpad4 => "Numpad4", Numpad5 => "Numpad5",
653    Numpad6 => "Numpad6", Numpad7 => "Numpad7", Numpad8 => "Numpad8",
654    Numpad9 => "Numpad9",
655    NumpadDivide => "NumpadDivide", NumpadMultiply => "NumpadMultiply",
656    NumpadSubtract => "NumpadSubtract", NumpadAdd => "NumpadAdd",
657    NumpadEnter => "NumpadEnter", NumpadDecimal => "NumpadDecimal",
658}
659
660impl fmt::Display for KeyCode {
661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662        f.write_str(self.as_str())
663    }
664}
665
666/// Generates [`CursorIcon`], [`CursorIcon::ALL`] and both directions of its
667/// keyword table from one list, so a shape cannot be added without its CSS
668/// keyword or left out of the list a parser searches.
669macro_rules! cursor_icons {
670    ($($(#[$meta:meta])* $variant:ident => $name:literal),+ $(,)?) => {
671        /// A standard cursor shape.
672        ///
673        /// Names rather than pixels, because every platform already has themed,
674        /// correctly scaled, user-configured cursors; `crcbl_shell::cursor`
675        /// explains the rest.
676        ///
677        /// **Exhaustive on purpose.** Every shell backend maps each shape to its
678        /// own stock cursor, and a shape added later must fail to compile in
679        /// each of them rather than fall to an arrow through a wildcard arm.
680        /// That makes adding a shape a breaking change, which is the price.
681        #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
682        pub enum CursorIcon {
683            $($(#[$meta])* $variant,)+
684        }
685
686        impl CursorIcon {
687            /// Every shape, in declaration order.
688            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
689
690            /// The CSS/`cursor-shape-v1` keyword for this shape.
691            ///
692            /// Backends map from this rather than from the variant name, because
693            /// the keyword is the string X11 cursor themes and the browser both
694            /// index by.
695            #[must_use]
696            pub const fn as_css_name(self) -> &'static str {
697                match self {
698                    $(Self::$variant => $name,)+
699                }
700            }
701
702            /// The shape a CSS `cursor` keyword names, matched as CSS matches
703            /// keywords: ignoring ASCII case. `None` for a keyword with no shape
704            /// here, `auto` and `none` among them, which are not shapes.
705            #[must_use]
706            pub fn from_css_name(name: &str) -> Option<Self> {
707                Self::ALL
708                    .iter()
709                    .copied()
710                    .find(|shape| shape.as_css_name().eq_ignore_ascii_case(name))
711            }
712        }
713    };
714}
715
716cursor_icons! {
717    /// The platform's normal arrow.
718    #[default]
719    Default => "default",
720    /// The "this is clickable" hand.
721    Pointer => "pointer",
722    /// I-beam, for text.
723    Text => "text",
724    /// Crosshair, for precise placement.
725    Crosshair => "crosshair",
726    /// Something is happening and input is blocked.
727    Wait => "wait",
728    /// Something is happening but input still works.
729    Progress => "progress",
730    /// This action is not allowed here.
731    NotAllowed => "not-allowed",
732    /// The thing under the pointer can be picked up.
733    Grab => "grab",
734    /// The thing under the pointer is being dragged.
735    Grabbing => "grabbing",
736    /// Move in any direction.
737    Move => "move",
738    /// Resize vertically.
739    ResizeNorthSouth => "ns-resize",
740    /// Resize horizontally.
741    ResizeEastWest => "ew-resize",
742    /// Resize along the ↗↙ diagonal.
743    ResizeNorthEastSouthWest => "nesw-resize",
744    /// Resize along the ↖↘ diagonal.
745    ResizeNorthWestSouthEast => "nwse-resize",
746    /// Split pane, vertical divider.
747    ResizeColumn => "col-resize",
748    /// Split pane, horizontal divider.
749    ResizeRow => "row-resize",
750}
751
752/// What a text field taking typing is for, as an input method needs to know
753/// it.
754///
755/// Here rather than in `crcbl-shell` for the reason [`CursorIcon`] is: a UI
756/// names it for its field, and a UI crate depends on this one, not on the
757/// shell.
758///
759/// **Exhaustive on purpose**, as [`CursorIcon`] is: a purpose added later
760/// must fail to compile in every backend's mapping rather than fall to
761/// [`Normal`](Self::Normal) through a wildcard arm, because a password read
762/// as ordinary text is one an input method may learn and suggest back.
763#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
764pub enum TextPurpose {
765    /// Ordinary text: the input method may complete, correct and remember.
766    #[default]
767    Normal,
768    /// A password: hidden as typed, and nothing typed may be stored, learned
769    /// or offered back as a suggestion.
770    Password,
771    /// A PIN: digits, hidden, and not stored, as a password.
772    Pin,
773}
774
775impl TextPurpose {
776    /// Whether what is typed must not be stored, learned or suggested.
777    #[must_use]
778    pub const fn is_sensitive(self) -> bool {
779        match self {
780            Self::Normal => false,
781            Self::Password | Self::Pin => true,
782        }
783    }
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    #[test]
791    fn cursor_keywords_are_unique_css_shaped_and_parse_back() {
792        let shapes = CursorIcon::ALL;
793        let mut names: Vec<&str> = shapes.iter().map(|shape| shape.as_css_name()).collect();
794        let count = names.len();
795        names.sort_unstable();
796        names.dedup();
797        assert_eq!(names.len(), count, "cursor keywords must be unique");
798        assert!(
799            names
800                .iter()
801                .all(|name| name.chars().all(|c| c.is_ascii_lowercase() || c == '-')),
802            "keywords are CSS identifiers"
803        );
804        assert_eq!(CursorIcon::default().as_css_name(), "default");
805        for &shape in shapes {
806            assert_eq!(CursorIcon::from_css_name(shape.as_css_name()), Some(shape));
807        }
808        assert_eq!(
809            CursorIcon::from_css_name("Not-Allowed"),
810            Some(CursorIcon::NotAllowed),
811            "keywords ignore ASCII case"
812        );
813        for not_a_shape in ["auto", "none", "", "pointer ", "hand"] {
814            assert_eq!(
815                CursorIcon::from_css_name(not_a_shape),
816                None,
817                "{not_a_shape:?}"
818            );
819        }
820    }
821
822    #[test]
823    fn key_names_round_trip_and_are_unique() {
824        let mut names: Vec<&'static str> = KeyCode::ALL.iter().map(|key| key.as_str()).collect();
825        let count = names.len();
826        assert!(count > 100, "the table covers a full keyboard, got {count}");
827        names.sort_unstable();
828        names.dedup();
829        assert_eq!(names.len(), count, "key names must be unique");
830
831        for key in KeyCode::ALL {
832            assert_eq!(KeyCode::from_name(key.as_str()), Some(*key));
833            assert_eq!(key.to_string(), key.as_str());
834        }
835        assert_eq!(KeyCode::from_name("KeyÆ"), None);
836    }
837
838    #[test]
839    fn wasd_is_a_physical_square_not_a_layout_one() {
840        // The whole point of the type: these are positions, so a binding stored
841        // as a name resolves to the same position on any layout.
842        for name in ["KeyW", "KeyA", "KeyS", "KeyD"] {
843            assert!(KeyCode::from_name(name).is_some(), "{name}");
844        }
845    }
846
847    #[test]
848    fn keysyms_map_both_ways_over_the_xkb_rules() {
849        // Latin-1 range: the keysym *is* the codepoint.
850        assert_eq!(Keysym::from_char('a'), Keysym(0x61));
851        assert_eq!(Keysym(0x61).to_char(), Some('a'));
852        assert_eq!(Keysym::from_char('ä'), Keysym(0xE4));
853        assert_eq!(Keysym(0xE4).to_char(), Some('ä'));
854
855        // Everything else lives in the Unicode block.
856        let euro = Keysym::from_char('€');
857        assert_eq!(euro, Keysym(0x0100_20AC));
858        assert_eq!(euro.to_char(), Some('€'));
859
860        // Function keys and modifiers are not characters.
861        const XKB_KEY_F1: Keysym = Keysym(0xFFBE);
862        assert_eq!(XKB_KEY_F1.to_char(), None);
863        assert!(Keysym::NONE.is_none());
864        assert_eq!(Keysym::NONE.to_char(), None);
865
866        assert_eq!(Keysym::from_char('q').to_string(), "'q'");
867        assert_eq!(XKB_KEY_F1.to_string(), "keysym 0xffbe");
868    }
869
870    #[test]
871    fn every_printable_ascii_round_trips_through_a_keysym() {
872        for codepoint in 0x20u32..=0x7E {
873            let character = char::from_u32(codepoint).expect("ascii");
874            assert_eq!(Keysym::from_char(character).to_char(), Some(character));
875        }
876    }
877
878    #[test]
879    fn chord_modifiers_exclude_latched_state() {
880        let held = Modifiers::CTRL | Modifiers::SHIFT | Modifiers::CAPS_LOCK | Modifiers::NUM_LOCK;
881        assert_eq!(held.chord(), Modifiers::CTRL | Modifiers::SHIFT);
882        assert!(!Modifiers::CHORD.contains(Modifiers::ALT_GR));
883        assert_eq!(Modifiers::default(), Modifiers::empty());
884    }
885
886    #[test]
887    fn button_state_is_one_type_for_every_kind_of_button() {
888        assert!(ButtonState::from_pressed(true).is_pressed());
889        assert_eq!(ButtonState::from_pressed(false), ButtonState::Released);
890    }
891
892    #[test]
893    fn scroll_units_stay_distinct() {
894        let lines = ScrollDelta::Lines { x: 0.0, y: -1.0 };
895        let pixels = ScrollDelta::Pixels { x: 0.0, y: -13.0 };
896        assert_ne!(
897            format!("{lines:?}"),
898            format!("{pixels:?}"),
899            "a wheel detent and 13 px must not be the same value"
900        );
901        assert!(ScrollDelta::Lines { x: 0.0, y: 0.0 }.is_zero());
902        assert!(ScrollDelta::Pixels { x: 0.0, y: 0.0 }.is_zero());
903        assert!(!lines.is_zero());
904        assert!(!pixels.is_zero());
905    }
906
907    #[test]
908    fn device_ids_are_displayable_and_default_to_unknown() {
909        assert_eq!(DeviceId::default(), DeviceId::UNKNOWN);
910        assert_eq!(DeviceId::UNKNOWN.to_string(), "device ?");
911        assert_eq!(DeviceId(4).to_string(), "device 4");
912        assert_eq!(Scancode(30).to_string(), "scancode 30");
913    }
914}