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}