Skip to main content

azul_core/
physical_key.rs

1//! Scancode -> [`PhysicalKey`] tables, one per platform convention.
2//!
3//! `ScanCode` already carried the physical key as a raw `u32` on every
4//! backend, so nothing new has to be plumbed to fill
5//! `KeyboardState.current_physical_key` — what was missing is the tables that
6//! turn a platform-specific number into a name an application can match on.
7//!
8//! Three conventions cover every backend the engine has:
9//!
10//! * **evdev** (`from_evdev`) — Wayland's `wl_keyboard.key` IS an evdev code,
11//!   X11 keycodes are `evdev + 8`, and Android's `KeyEvent` scan code is evdev
12//!   too. One table serves all three.
13//! * **PS/2 set 1** (`from_windows_scancode`) — what `WM_KEYDOWN`'s `lParam`
14//!   carries, where the `E0` prefix (bit 24 of `lParam`) is what separates
15//!   NumpadEnter from Enter, ControlRight from ControlLeft, and the arrow
16//!   cluster from the numpad.
17//! * **Carbon virtual keycodes** (`from_macos_keycode`) — `NSEvent.keyCode`.
18//!   These are POSITIONAL despite the name; `kVK_ANSI_A` is the position, not
19//!   the letter.
20//!
21//! Every table returns [`PhysicalKey::Unidentified`] for a code it does not
22//! name, which is a value the enum carries precisely so that an unknown key is
23//! reported honestly rather than guessed at or dropped.
24
25use crate::window::PhysicalKey;
26
27impl PhysicalKey {
28    /// Linux evdev keycode -> position.
29    ///
30    /// Used directly by Wayland and Android. X11 callers must pass
31    /// `keycode - 8`; see [`Self::from_x11_keycode`].
32    #[must_use]
33    pub const fn from_evdev(code: u32) -> Self {
34        use PhysicalKey::*;
35        match code {
36            1 => Escape,
37            2 => Digit1, 3 => Digit2, 4 => Digit3, 5 => Digit4, 6 => Digit5,
38            7 => Digit6, 8 => Digit7, 9 => Digit8, 10 => Digit9, 11 => Digit0,
39            12 => Minus, 13 => Equal, 14 => Backspace, 15 => Tab,
40            16 => KeyQ, 17 => KeyW, 18 => KeyE, 19 => KeyR, 20 => KeyT,
41            21 => KeyY, 22 => KeyU, 23 => KeyI, 24 => KeyO, 25 => KeyP,
42            26 => BracketLeft, 27 => BracketRight, 28 => Enter, 29 => ControlLeft,
43            30 => KeyA, 31 => KeyS, 32 => KeyD, 33 => KeyF, 34 => KeyG,
44            35 => KeyH, 36 => KeyJ, 37 => KeyK, 38 => KeyL,
45            39 => Semicolon, 40 => Quote, 41 => Backquote, 42 => ShiftLeft, 43 => Backslash,
46            44 => KeyZ, 45 => KeyX, 46 => KeyC, 47 => KeyV, 48 => KeyB,
47            49 => KeyN, 50 => KeyM, 51 => Comma, 52 => Period, 53 => Slash,
48            54 => ShiftRight, 55 => NumpadMultiply, 56 => AltLeft, 57 => Space, 58 => CapsLock,
49            59 => F1, 60 => F2, 61 => F3, 62 => F4, 63 => F5,
50            64 => F6, 65 => F7, 66 => F8, 67 => F9, 68 => F10,
51            69 => NumLock, 70 => ScrollLock,
52            71 => Numpad7, 72 => Numpad8, 73 => Numpad9, 74 => NumpadSubtract,
53            75 => Numpad4, 76 => Numpad5, 77 => Numpad6, 78 => NumpadAdd,
54            79 => Numpad1, 80 => Numpad2, 81 => Numpad3, 82 => Numpad0, 83 => NumpadDecimal,
55            // 86 is KEY_102ND, the extra key ISO boards have next to left
56            // shift that ANSI boards do not.
57            86 => IntlBackslash, 87 => F11, 88 => F12,
58            89 => IntlRo,
59            92 => Convert, 93 => KanaMode, 94 => NonConvert, 95 => NumpadComma,
60            96 => NumpadEnter, 97 => ControlRight, 98 => NumpadDivide,
61            99 => PrintScreen, 100 => AltRight,
62            102 => Home, 103 => ArrowUp, 104 => PageUp, 105 => ArrowLeft,
63            106 => ArrowRight, 107 => End, 108 => ArrowDown, 109 => PageDown,
64            110 => Insert, 111 => Delete,
65            117 => NumpadEqual, 119 => Pause,
66            121 => NumpadComma, 122 => Lang1, 123 => Lang2, 124 => IntlYen,
67            125 => MetaLeft, 126 => MetaRight, 127 => ContextMenu,
68            183 => F13, 184 => F14, 185 => F15, 186 => F16, 187 => F17, 188 => F18,
69            189 => F19, 190 => F20, 191 => F21, 192 => F22, 193 => F23, 194 => F24,
70            _ => Unidentified,
71        }
72    }
73
74    /// X11 keycode -> position.
75    ///
76    /// XKB keycodes are `evdev + 8` by protocol, so this is the evdev table
77    /// with the offset removed. A keycode below 8 cannot be an evdev key and
78    /// is reported as unidentified rather than wrapping into a wrong one.
79    #[must_use]
80    pub const fn from_x11_keycode(keycode: u32) -> Self {
81        if keycode < 8 {
82            return PhysicalKey::Unidentified;
83        }
84        Self::from_evdev(keycode - 8)
85    }
86
87    /// PS/2 set-1 scancode -> position, as `WM_KEYDOWN` reports it.
88    ///
89    /// `extended` is `lParam` bit 24 (the `E0` prefix). It is not optional
90    /// detail: without it Enter and NumpadEnter, ControlLeft and ControlRight,
91    /// and the whole arrow cluster versus the numpad are the SAME scancode.
92    #[must_use]
93    pub const fn from_windows_scancode(scancode: u32, extended: bool) -> Self {
94        use PhysicalKey::*;
95        if extended {
96            return match scancode {
97                0x1C => NumpadEnter,
98                0x1D => ControlRight,
99                0x35 => NumpadDivide,
100                0x37 => PrintScreen,
101                0x38 => AltRight,
102                0x45 => Pause,
103                0x47 => Home,
104                0x48 => ArrowUp,
105                0x49 => PageUp,
106                0x4B => ArrowLeft,
107                0x4D => ArrowRight,
108                0x4F => End,
109                0x50 => ArrowDown,
110                0x51 => PageDown,
111                0x52 => Insert,
112                0x53 => Delete,
113                0x5B => MetaLeft,
114                0x5C => MetaRight,
115                0x5D => ContextMenu,
116                _ => Unidentified,
117            };
118        }
119        match scancode {
120            0x01 => Escape,
121            0x02 => Digit1, 0x03 => Digit2, 0x04 => Digit3, 0x05 => Digit4, 0x06 => Digit5,
122            0x07 => Digit6, 0x08 => Digit7, 0x09 => Digit8, 0x0A => Digit9, 0x0B => Digit0,
123            0x0C => Minus, 0x0D => Equal, 0x0E => Backspace, 0x0F => Tab,
124            0x10 => KeyQ, 0x11 => KeyW, 0x12 => KeyE, 0x13 => KeyR, 0x14 => KeyT,
125            0x15 => KeyY, 0x16 => KeyU, 0x17 => KeyI, 0x18 => KeyO, 0x19 => KeyP,
126            0x1A => BracketLeft, 0x1B => BracketRight, 0x1C => Enter, 0x1D => ControlLeft,
127            0x1E => KeyA, 0x1F => KeyS, 0x20 => KeyD, 0x21 => KeyF, 0x22 => KeyG,
128            0x23 => KeyH, 0x24 => KeyJ, 0x25 => KeyK, 0x26 => KeyL,
129            0x27 => Semicolon, 0x28 => Quote, 0x29 => Backquote, 0x2A => ShiftLeft,
130            0x2B => Backslash,
131            0x2C => KeyZ, 0x2D => KeyX, 0x2E => KeyC, 0x2F => KeyV, 0x30 => KeyB,
132            0x31 => KeyN, 0x32 => KeyM, 0x33 => Comma, 0x34 => Period, 0x35 => Slash,
133            0x36 => ShiftRight, 0x37 => NumpadMultiply, 0x38 => AltLeft,
134            0x39 => Space, 0x3A => CapsLock,
135            0x3B => F1, 0x3C => F2, 0x3D => F3, 0x3E => F4, 0x3F => F5,
136            0x40 => F6, 0x41 => F7, 0x42 => F8, 0x43 => F9, 0x44 => F10,
137            0x45 => NumLock, 0x46 => ScrollLock,
138            0x47 => Numpad7, 0x48 => Numpad8, 0x49 => Numpad9, 0x4A => NumpadSubtract,
139            0x4B => Numpad4, 0x4C => Numpad5, 0x4D => Numpad6, 0x4E => NumpadAdd,
140            0x4F => Numpad1, 0x50 => Numpad2, 0x51 => Numpad3, 0x52 => Numpad0,
141            0x53 => NumpadDecimal,
142            0x56 => IntlBackslash, 0x57 => F11, 0x58 => F12, 0x59 => NumpadEqual,
143            0x64 => F13, 0x65 => F14, 0x66 => F15, 0x67 => F16, 0x68 => F17,
144            0x69 => F18, 0x6A => F19, 0x6B => F20, 0x6C => F21, 0x6D => F22,
145            0x6E => F23, 0x76 => F24,
146            0x70 => KanaMode, 0x73 => IntlRo, 0x79 => Convert, 0x7B => NonConvert,
147            0x7D => IntlYen,
148            _ => Unidentified,
149        }
150    }
151
152    /// Carbon virtual keycode (`NSEvent.keyCode`) -> position.
153    ///
154    /// Cross-checked entry by entry against
155    /// `macos_keycode_to_virtual_key`, the table this codebase already
156    /// trusts for the LOGICAL key; the two agree on every code both name.
157    #[must_use]
158    pub const fn from_macos_keycode(keycode: u16) -> Self {
159        use PhysicalKey::*;
160        match keycode {
161            0x00 => KeyA, 0x01 => KeyS, 0x02 => KeyD, 0x03 => KeyF, 0x04 => KeyH,
162            0x05 => KeyG, 0x06 => KeyZ, 0x07 => KeyX, 0x08 => KeyC, 0x09 => KeyV,
163            // kVK_ISO_Section — the extra ISO key, absent on ANSI boards.
164            0x0A => IntlBackslash,
165            0x0B => KeyB, 0x0C => KeyQ, 0x0D => KeyW, 0x0E => KeyE, 0x0F => KeyR,
166            0x10 => KeyY, 0x11 => KeyT,
167            0x12 => Digit1, 0x13 => Digit2, 0x14 => Digit3, 0x15 => Digit4,
168            0x16 => Digit6, 0x17 => Digit5, 0x18 => Equal, 0x19 => Digit9,
169            0x1A => Digit7, 0x1B => Minus, 0x1C => Digit8, 0x1D => Digit0,
170            0x1E => BracketRight, 0x1F => KeyO, 0x20 => KeyU, 0x21 => BracketLeft,
171            0x22 => KeyI, 0x23 => KeyP, 0x24 => Enter, 0x25 => KeyL, 0x26 => KeyJ,
172            0x27 => Quote, 0x28 => KeyK, 0x29 => Semicolon, 0x2A => Backslash,
173            0x2B => Comma, 0x2C => Slash, 0x2D => KeyN, 0x2E => KeyM, 0x2F => Period,
174            0x30 => Tab, 0x31 => Space, 0x32 => Backquote, 0x33 => Backspace,
175            0x35 => Escape,
176            0x36 => MetaRight, 0x37 => MetaLeft, 0x38 => ShiftLeft, 0x39 => CapsLock,
177            0x3A => AltLeft, 0x3B => ControlLeft, 0x3C => ShiftRight, 0x3D => AltRight,
178            0x3E => ControlRight,
179            // 0x3F is kVK_Function (the `fn` key), which has no W3C `code`.
180            0x40 => F17, 0x41 => NumpadDecimal, 0x43 => NumpadMultiply, 0x45 => NumpadAdd,
181            // kVK_ANSI_KeypadClear sits where NumLock does on a PC board.
182            0x47 => NumLock,
183            0x4B => NumpadDivide, 0x4C => NumpadEnter, 0x4E => NumpadSubtract,
184            0x4F => F18, 0x50 => F19, 0x51 => NumpadEqual,
185            0x52 => Numpad0, 0x53 => Numpad1, 0x54 => Numpad2, 0x55 => Numpad3,
186            0x56 => Numpad4, 0x57 => Numpad5, 0x58 => Numpad6, 0x59 => Numpad7,
187            0x5A => F20, 0x5B => Numpad8, 0x5C => Numpad9,
188            0x5D => IntlYen, 0x5E => IntlRo, 0x5F => NumpadComma,
189            0x60 => F5, 0x61 => F6, 0x62 => F7, 0x63 => F3, 0x64 => F8, 0x65 => F9,
190            0x66 => Lang2, 0x67 => F11, 0x68 => Lang1, 0x69 => F13, 0x6A => F16,
191            0x6B => F14, 0x6D => F10, 0x6E => ContextMenu, 0x6F => F12,
192            0x71 => F15, 0x72 => Insert, 0x73 => Home, 0x74 => PageUp, 0x75 => Delete,
193            0x76 => F4, 0x77 => End, 0x78 => F2, 0x79 => PageDown, 0x7A => F1,
194            0x7B => ArrowLeft, 0x7C => ArrowRight, 0x7D => ArrowDown, 0x7E => ArrowUp,
195            _ => Unidentified,
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    /// The same physical key must get the same name from every platform's
205    /// table — that is the entire point of the enum, and a table typo would
206    /// otherwise only show up as a wrong binding on one OS.
207    #[test]
208    fn the_same_position_gets_the_same_name_on_every_platform() {
209        // (name, evdev, windows set-1, macOS Carbon)
210        let cases: &[(PhysicalKey, u32, u32, u16)] = &[
211            (PhysicalKey::KeyA, 30, 0x1E, 0x00),
212            (PhysicalKey::KeyW, 17, 0x11, 0x0D),
213            (PhysicalKey::KeyZ, 44, 0x2C, 0x06),
214            (PhysicalKey::Digit1, 2, 0x02, 0x12),
215            (PhysicalKey::Digit0, 11, 0x0B, 0x1D),
216            (PhysicalKey::Space, 57, 0x39, 0x31),
217            (PhysicalKey::Enter, 28, 0x1C, 0x24),
218            (PhysicalKey::Tab, 15, 0x0F, 0x30),
219            (PhysicalKey::Escape, 1, 0x01, 0x35),
220            (PhysicalKey::Backspace, 14, 0x0E, 0x33),
221            (PhysicalKey::ShiftLeft, 42, 0x2A, 0x38),
222            (PhysicalKey::ShiftRight, 54, 0x36, 0x3C),
223            (PhysicalKey::CapsLock, 58, 0x3A, 0x39),
224            (PhysicalKey::F1, 59, 0x3B, 0x7A),
225            (PhysicalKey::F12, 88, 0x58, 0x6F),
226            (PhysicalKey::Numpad0, 82, 0x52, 0x52),
227            (PhysicalKey::NumpadAdd, 78, 0x4E, 0x45),
228            (PhysicalKey::Comma, 51, 0x33, 0x2B),
229            (PhysicalKey::Slash, 53, 0x35, 0x2C),
230            (PhysicalKey::Backquote, 41, 0x29, 0x32),
231            (PhysicalKey::IntlBackslash, 86, 0x56, 0x0A),
232        ];
233        for (want, evdev, win, mac) in cases {
234            assert_eq!(PhysicalKey::from_evdev(*evdev), *want, "evdev {evdev}");
235            assert_eq!(
236                PhysicalKey::from_windows_scancode(*win, false),
237                *want,
238                "windows {win:#04X}",
239            );
240            assert_eq!(PhysicalKey::from_macos_keycode(*mac), *want, "macos {mac:#04X}");
241            // X11 is the evdev table shifted by the protocol's +8.
242            assert_eq!(PhysicalKey::from_x11_keycode(evdev + 8), *want, "x11 {}", evdev + 8);
243        }
244    }
245
246    /// Without the `E0` prefix these pairs are the SAME scancode, so dropping
247    /// `extended` would silently merge them.
248    #[test]
249    fn the_windows_extended_bit_separates_the_duplicated_scancodes() {
250        let pairs: &[(u32, PhysicalKey, PhysicalKey)] = &[
251            (0x1C, PhysicalKey::Enter, PhysicalKey::NumpadEnter),
252            (0x1D, PhysicalKey::ControlLeft, PhysicalKey::ControlRight),
253            (0x35, PhysicalKey::Slash, PhysicalKey::NumpadDivide),
254            (0x38, PhysicalKey::AltLeft, PhysicalKey::AltRight),
255            (0x45, PhysicalKey::NumLock, PhysicalKey::Pause),
256            (0x47, PhysicalKey::Numpad7, PhysicalKey::Home),
257            (0x48, PhysicalKey::Numpad8, PhysicalKey::ArrowUp),
258            (0x4B, PhysicalKey::Numpad4, PhysicalKey::ArrowLeft),
259            (0x4D, PhysicalKey::Numpad6, PhysicalKey::ArrowRight),
260            (0x50, PhysicalKey::Numpad2, PhysicalKey::ArrowDown),
261            (0x52, PhysicalKey::Numpad0, PhysicalKey::Insert),
262            (0x53, PhysicalKey::NumpadDecimal, PhysicalKey::Delete),
263        ];
264        for (sc, plain, ext) in pairs {
265            assert_eq!(PhysicalKey::from_windows_scancode(*sc, false), *plain, "{sc:#04X}");
266            assert_eq!(PhysicalKey::from_windows_scancode(*sc, true), *ext, "E0 {sc:#04X}");
267            assert_ne!(plain, ext);
268        }
269    }
270
271    /// An unknown code is reported as unidentified, never guessed at and never
272    /// silently turned into a neighbouring key.
273    #[test]
274    fn an_unnamed_code_is_unidentified() {
275        assert_eq!(PhysicalKey::from_evdev(0), PhysicalKey::Unidentified);
276        assert_eq!(PhysicalKey::from_evdev(9999), PhysicalKey::Unidentified);
277        assert_eq!(PhysicalKey::from_windows_scancode(0xFE, false), PhysicalKey::Unidentified);
278        assert_eq!(PhysicalKey::from_windows_scancode(0x02, true), PhysicalKey::Unidentified);
279        assert_eq!(PhysicalKey::from_macos_keycode(0xFF), PhysicalKey::Unidentified);
280        // kVK_Function has no W3C `code` and must not be invented.
281        assert_eq!(PhysicalKey::from_macos_keycode(0x3F), PhysicalKey::Unidentified);
282    }
283
284    /// X11 keycodes are `evdev + 8`; below that they cannot be evdev keys and
285    /// must not wrap into a wrong one.
286    #[test]
287    fn an_x11_keycode_below_the_offset_cannot_wrap() {
288        for kc in 0..8 {
289            assert_eq!(PhysicalKey::from_x11_keycode(kc), PhysicalKey::Unidentified, "{kc}");
290        }
291        assert_eq!(PhysicalKey::from_x11_keycode(8), PhysicalKey::Unidentified); // evdev 0
292        assert_eq!(PhysicalKey::from_x11_keycode(9), PhysicalKey::Escape); // evdev 1
293    }
294
295    /// Left and right modifiers are distinct positions — the reason
296    /// `PhysicalKey` splits them where `KeyModifiers` deliberately does not.
297    #[test]
298    fn left_and_right_modifiers_are_distinct_positions() {
299        assert_ne!(PhysicalKey::from_evdev(29), PhysicalKey::from_evdev(97));
300        assert_eq!(PhysicalKey::from_evdev(29), PhysicalKey::ControlLeft);
301        assert_eq!(PhysicalKey::from_evdev(97), PhysicalKey::ControlRight);
302        assert_eq!(PhysicalKey::from_macos_keycode(0x37), PhysicalKey::MetaLeft);
303        assert_eq!(PhysicalKey::from_macos_keycode(0x36), PhysicalKey::MetaRight);
304    }
305}