Skip to main content

repame_input/
keymap.rs

1use std::collections::HashMap;
2use std::hash::Hash;
3
4use repose_core::input::{GamepadAxis, GamepadButton, Key, Modifiers, PhysicalKey, PointerButton};
5use repose_core::shortcuts::KeyChord;
6
7/// One side of a remappable control: what the keyboard/mouse entry or
8/// the gamepad entry currently holds.
9#[derive(Clone, Debug, Default, PartialEq)]
10pub enum KeymapEntry {
11    #[default]
12    None,
13    Key(KeyChord),
14    Physical(PhysicalKey),
15    Mouse(PointerButton),
16    Pad(GamepadButton),
17    Axis {
18        axis: GamepadAxis,
19        threshold: f32,
20    },
21}
22
23impl std::hash::Hash for KeymapEntry {
24    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
25        std::mem::discriminant(self).hash(state);
26        match self {
27            KeymapEntry::None => {}
28            KeymapEntry::Key(chord) => chord.hash(state),
29            KeymapEntry::Physical(key) => key.hash(state),
30            KeymapEntry::Mouse(button) => {
31                (*button as u8).hash(state);
32            }
33            KeymapEntry::Pad(button) => button.hash(state),
34            KeymapEntry::Axis { axis, threshold } => {
35                axis.hash(state);
36                let t = if *threshold == 0.0 { 0.0 } else { *threshold };
37                t.to_bits().hash(state);
38            }
39        }
40    }
41}
42
43/// next gamepad button when a pad drove the gesture, else the next
44/// keyboard key / mouse button (`Other_10:374-404`).
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub enum KeymapDevice {
47    KeyboardMouse,
48    Gamepad,
49}
50
51/// Pending rebind gesture: which action, which side, armed by the
52/// options screen and resolved by the next pressed input.
53#[derive(Clone, Debug, PartialEq, Eq, Hash)]
54pub struct KeymapCapture<A> {
55    pub action: A,
56    pub device: KeymapDevice,
57}
58
59/// Per-action remappable bindings: a keyboard/mouse entry plus a
60/// (`Key[$ key][0]` keyboard, `Key[$ key][1]` gamepad).
61#[derive(Clone, Debug, Default)]
62pub struct Keymap<A> {
63    keyboard: HashMap<A, KeymapEntry>,
64    gamepad: HashMap<A, KeymapEntry>,
65}
66
67impl<A: Clone + Eq + Hash> Keymap<A> {
68    pub fn new() -> Self {
69        Self {
70            keyboard: HashMap::new(),
71            gamepad: HashMap::new(),
72        }
73    }
74
75    pub fn set_keyboard(&mut self, action: A, entry: KeymapEntry) {
76        self.keyboard.insert(action, entry);
77    }
78
79    pub fn set_gamepad(&mut self, action: A, entry: KeymapEntry) {
80        self.gamepad.insert(action, entry);
81    }
82
83    pub fn keyboard(&self, action: &A) -> KeymapEntry {
84        self.keyboard.get(action).cloned().unwrap_or_default()
85    }
86
87    pub fn gamepad(&self, action: &A) -> KeymapEntry {
88        self.gamepad.get(action).cloned().unwrap_or_default()
89    }
90
91    /// Active entry for an action: gamepad side when a pad drives
92    pub fn active(&self, action: &A, gamepad: bool) -> KeymapEntry {
93        if gamepad {
94            let entry = self.gamepad(action);
95            if entry != KeymapEntry::None {
96                return entry;
97            }
98        }
99        self.keyboard(action)
100    }
101
102    /// Begin a rebind gesture for `action` on `device`
103    pub fn begin_capture(&self, action: A, device: KeymapDevice) -> KeymapCapture<A> {
104        KeymapCapture { action, device }
105    }
106
107    /// Resolve a capture with the next pressed input. `None` clears
108    /// mouse button, or pad button/axis overwrites that side
109    pub fn resolve_capture(&mut self, capture: &KeymapCapture<A>, pressed: Option<KeymapEntry>) {
110        let entry = pressed.unwrap_or(KeymapEntry::None);
111        match capture.device {
112            KeymapDevice::KeyboardMouse => {
113                self.keyboard.insert(capture.action.clone(), entry);
114            }
115            KeymapDevice::Gamepad => {
116                self.gamepad.insert(capture.action.clone(), entry);
117            }
118        }
119    }
120
121    pub fn actions(&self) -> impl Iterator<Item = &A> {
122        self.keyboard.keys().chain(self.gamepad.keys())
123    }
124
125    pub fn to_action_map(&self) -> super::map::ActionMap<A> {
126        let mut out = super::map::ActionMap::new();
127        for action in self.actions() {
128            for entry in [self.keyboard(action), self.gamepad(action)] {
129                if let Some(binding) = super::binding::Binding::from_entry(&entry) {
130                    out.bind(action.clone(), binding);
131                }
132            }
133        }
134        out
135    }
136
137    pub fn to_action_map_contexts(&self, gameplay: &[A], menu: &[A]) -> super::map::ActionMap<A> {
138        let mut out = self.to_action_map();
139        for action in gameplay {
140            out.in_context("gameplay", action.clone());
141        }
142        for action in menu {
143            out.in_context("menu", action.clone());
144        }
145        out
146    }
147}
148
149/// Serializable row: one action name plus both entries as strings.
150/// Games map `action` back to their enum; the strings stay readable
151/// in the save file (`KeyW`, `Space`, `MouseLeft`, `PadSouth`, ...).
152#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
153pub struct KeymapRow {
154    pub action: String,
155    pub keyboard: String,
156    pub gamepad: String,
157}
158
159pub fn encode_keymap_entry(entry: &KeymapEntry) -> String {
160    match entry {
161        KeymapEntry::None => String::new(),
162        KeymapEntry::Key(chord) => encode_chord(chord),
163        // Explicit prefix: bare physical names ("Space", "Tab", arrows,
164        // ...) collide with logical chord spellings, so physical
165        // positions never serialize bare.
166        KeymapEntry::Physical(key) => format!("phys:{}", key.name()),
167        KeymapEntry::Mouse(PointerButton::Primary) => "MouseLeft".to_string(),
168        KeymapEntry::Mouse(PointerButton::Secondary) => "MouseRight".to_string(),
169        KeymapEntry::Mouse(PointerButton::Tertiary) => "MouseMiddle".to_string(),
170        KeymapEntry::Pad(button) => format!("Pad{button:?}"),
171        KeymapEntry::Axis { axis, threshold } => format!("Axis{axis:?}@{threshold}"),
172    }
173}
174
175pub fn decode_keymap_entry(text: &str) -> KeymapEntry {
176    let text = text.trim();
177    if text.is_empty() {
178        return KeymapEntry::None;
179    }
180    match text {
181        "MouseLeft" => return KeymapEntry::Mouse(PointerButton::Primary),
182        "MouseRight" => return KeymapEntry::Mouse(PointerButton::Secondary),
183        "MouseMiddle" => return KeymapEntry::Mouse(PointerButton::Tertiary),
184        _ => {}
185    }
186    if let Some(name) = text.strip_prefix("Pad")
187        && let Some(button) = decode_pad_button(name)
188    {
189        return KeymapEntry::Pad(button);
190    }
191    if let Some(rest) = text.strip_prefix("Axis")
192        && let Some((axis_name, threshold)) = rest.split_once('@')
193        && let Some(axis) = decode_axis(axis_name)
194        && let Ok(threshold) = threshold.parse::<f32>()
195    {
196        return KeymapEntry::Axis { axis, threshold };
197    }
198    // Physical positions carry an explicit prefix (see the encoder).
199    if let Some(name) = text.strip_prefix("phys:") {
200        let name = name.trim();
201        let physical = PhysicalKey::from_name(name);
202        if !matches!(physical, PhysicalKey::Unidentified) || name == "Unidentified" {
203            return KeymapEntry::Physical(physical);
204        }
205        return KeymapEntry::None;
206    }
207    // Logical chords next: letters, digits, modifiers, and the named
208    // keys ("Space", "Tab", arrows, ...). This also claims legacy bare
209    // saves of those names as logical — the defaults are logical, so a
210    // bare "Space" must stay the NT Swap binding, never flip physical.
211    if let Some(chord) = decode_chord(text) {
212        return KeymapEntry::Key(chord);
213    }
214    // Legacy bare physical positions ("KeyW", "Digit1", "Minus", ...):
215    // no logical spelling collides with them, so they stay physical.
216    let physical = PhysicalKey::from_name(text);
217    if !matches!(physical, PhysicalKey::Unidentified) || text == "Unidentified" {
218        return KeymapEntry::Physical(physical);
219    }
220    KeymapEntry::None
221}
222
223/// REMAP capture helper: every physical key position maps to a
224/// plain (no-modifier) [`KeyChord`] so ANY key is capturable.
225pub fn chord_for_physical(key: PhysicalKey) -> Option<KeyChord> {
226    use repose_core::input::Key;
227    let chord_key = match key {
228        PhysicalKey::Space => Key::Space,
229        PhysicalKey::Tab => Key::Tab,
230        PhysicalKey::ShiftLeft => Key::ShiftLeft,
231        PhysicalKey::ShiftRight => Key::ShiftRight,
232        PhysicalKey::ArrowUp => Key::ArrowUp,
233        PhysicalKey::ArrowDown => Key::ArrowDown,
234        PhysicalKey::ArrowLeft => Key::ArrowLeft,
235        PhysicalKey::ArrowRight => Key::ArrowRight,
236        PhysicalKey::Enter => Key::Enter,
237        PhysicalKey::Escape => Key::Escape,
238        PhysicalKey::Backspace => Key::Backspace,
239        PhysicalKey::Delete => Key::Delete,
240        PhysicalKey::Insert => Key::Insert,
241        PhysicalKey::Home => Key::Home,
242        PhysicalKey::End => Key::End,
243        PhysicalKey::PageUp => Key::PageUp,
244        PhysicalKey::PageDown => Key::PageDown,
245        _ => Key::Character(glyph_for_physical(key)?),
246    };
247    Some(KeyChord::new(chord_key, Modifiers::default()))
248}
249
250/// Physical-position -> glyph table.
251pub fn glyph_for_physical(key: PhysicalKey) -> Option<char> {
252    let name = key.name();
253    if let Some(tail) = name
254        .strip_prefix("Key")
255        .or_else(|| name.strip_prefix("Digit"))
256    {
257        let mut chars = tail.chars();
258        match (chars.next(), chars.next()) {
259            (Some(c), None) => return Some(c.to_ascii_lowercase()),
260            _ => return None,
261        }
262    }
263    Some(match key {
264        PhysicalKey::Backquote => '`',
265        PhysicalKey::Minus => '-',
266        PhysicalKey::Equal => '=',
267        PhysicalKey::BracketLeft => '[',
268        PhysicalKey::BracketRight => ']',
269        PhysicalKey::Backslash => '\\',
270        PhysicalKey::Semicolon => ';',
271        PhysicalKey::Quote => '\'',
272        PhysicalKey::Comma => ',',
273        PhysicalKey::Period => '.',
274        PhysicalKey::Slash => '/',
275        _ => return None,
276    })
277}
278
279fn encode_chord(chord: &KeyChord) -> String {
280    let mut out = String::new();
281    if chord.modifiers.ctrl {
282        out.push_str("Ctrl+");
283    }
284    if chord.modifiers.shift {
285        out.push_str("Shift+");
286    }
287    if chord.modifiers.alt {
288        out.push_str("Alt+");
289    }
290    if chord.modifiers.meta {
291        out.push_str("Meta+");
292    }
293    out.push_str(&encode_key(&chord.key));
294    out
295}
296
297fn encode_key(key: &Key) -> String {
298    match key {
299        Key::Character(c) => {
300            if *c == ' ' {
301                "Space".to_string()
302            } else {
303                c.to_ascii_uppercase().to_string()
304            }
305        }
306        Key::Enter => "Enter".to_string(),
307        Key::Tab => "Tab".to_string(),
308        Key::Backspace => "Backspace".to_string(),
309        Key::Delete => "Delete".to_string(),
310        Key::Insert => "Insert".to_string(),
311        Key::Escape => "Escape".to_string(),
312        Key::ArrowLeft => "ArrowLeft".to_string(),
313        Key::ArrowRight => "ArrowRight".to_string(),
314        Key::ArrowUp => "ArrowUp".to_string(),
315        Key::ArrowDown => "ArrowDown".to_string(),
316        Key::Home => "Home".to_string(),
317        Key::End => "End".to_string(),
318        Key::PageUp => "PageUp".to_string(),
319        Key::PageDown => "PageDown".to_string(),
320        Key::Space => "Space".to_string(),
321        Key::ShiftLeft => "ShiftLeft".to_string(),
322        Key::ShiftRight => "ShiftRight".to_string(),
323        Key::ControlLeft => "ControlLeft".to_string(),
324        Key::ControlRight => "ControlRight".to_string(),
325        Key::AltLeft => "AltLeft".to_string(),
326        Key::AltRight => "AltRight".to_string(),
327        Key::MetaLeft => "MetaLeft".to_string(),
328        Key::MetaRight => "MetaRight".to_string(),
329        Key::CapsLock => "CapsLock".to_string(),
330        Key::NumLock => "NumLock".to_string(),
331        Key::ScrollLock => "ScrollLock".to_string(),
332        Key::Pause => "Pause".to_string(),
333        Key::PrintScreen => "PrintScreen".to_string(),
334        Key::ContextMenu => "ContextMenu".to_string(),
335        Key::NumpadEnter => "NumpadEnter".to_string(),
336        Key::MediaPlay => "MediaPlay".to_string(),
337        Key::MediaPause => "MediaPause".to_string(),
338        Key::MediaPlayPause => "MediaPlayPause".to_string(),
339        Key::MediaStop => "MediaStop".to_string(),
340        Key::MediaNext => "MediaNext".to_string(),
341        Key::MediaPrevious => "MediaPrevious".to_string(),
342        Key::VolumeUp => "VolumeUp".to_string(),
343        Key::VolumeDown => "VolumeDown".to_string(),
344        Key::VolumeMute => "VolumeMute".to_string(),
345        Key::Numpad(c) => format!("Numpad{}", *c as u32),
346        Key::F(n) => format!("F{n}"),
347        Key::Unknown => String::new(),
348    }
349}
350
351fn decode_chord(text: &str) -> Option<KeyChord> {
352    let mut rest = text;
353    let mut modifiers = Modifiers::default();
354    loop {
355        if let Some(tail) = rest.strip_prefix("Ctrl+") {
356            modifiers.ctrl = true;
357            rest = tail;
358        } else if let Some(tail) = rest.strip_prefix("Shift+") {
359            modifiers.shift = true;
360            rest = tail;
361        } else if let Some(tail) = rest.strip_prefix("Alt+") {
362            modifiers.alt = true;
363            rest = tail;
364        } else if let Some(tail) = rest.strip_prefix("Meta+") {
365            modifiers.meta = true;
366            rest = tail;
367        } else {
368            break;
369        }
370    }
371    decode_key(rest).map(|key| KeyChord::new(key, modifiers))
372}
373
374fn decode_key(text: &str) -> Option<Key> {
375    match text {
376        "Enter" => Some(Key::Enter),
377        "Tab" => Some(Key::Tab),
378        "Backspace" => Some(Key::Backspace),
379        "Delete" => Some(Key::Delete),
380        "Insert" => Some(Key::Insert),
381        "Escape" => Some(Key::Escape),
382        "ArrowLeft" => Some(Key::ArrowLeft),
383        "ArrowRight" => Some(Key::ArrowRight),
384        "ArrowUp" => Some(Key::ArrowUp),
385        "ArrowDown" => Some(Key::ArrowDown),
386        "Home" => Some(Key::Home),
387        "End" => Some(Key::End),
388        "PageUp" => Some(Key::PageUp),
389        "PageDown" => Some(Key::PageDown),
390        "Space" => Some(Key::Space),
391        "ShiftLeft" => Some(Key::ShiftLeft),
392        "ShiftRight" => Some(Key::ShiftRight),
393        "ControlLeft" => Some(Key::ControlLeft),
394        "ControlRight" => Some(Key::ControlRight),
395        "AltLeft" => Some(Key::AltLeft),
396        "AltRight" => Some(Key::AltRight),
397        "MetaLeft" => Some(Key::MetaLeft),
398        "MetaRight" => Some(Key::MetaRight),
399        "CapsLock" => Some(Key::CapsLock),
400        "NumLock" => Some(Key::NumLock),
401        "ScrollLock" => Some(Key::ScrollLock),
402        "Pause" => Some(Key::Pause),
403        "PrintScreen" => Some(Key::PrintScreen),
404        "ContextMenu" => Some(Key::ContextMenu),
405        "NumpadEnter" => Some(Key::NumpadEnter),
406        "MediaPlay" => Some(Key::MediaPlay),
407        "MediaPause" => Some(Key::MediaPause),
408        "MediaPlayPause" => Some(Key::MediaPlayPause),
409        "MediaStop" => Some(Key::MediaStop),
410        "MediaNext" => Some(Key::MediaNext),
411        "MediaPrevious" => Some(Key::MediaPrevious),
412        "VolumeUp" => Some(Key::VolumeUp),
413        "VolumeDown" => Some(Key::VolumeDown),
414        "VolumeMute" => Some(Key::VolumeMute),
415        _ => {
416            if let Some(digit) = text
417                .strip_prefix("Numpad")
418                .and_then(|d| d.parse::<u32>().ok())
419                && let Some(c) = char::from_u32(digit)
420            {
421                return Some(Key::Numpad(c));
422            }
423            if let Some(n) = text.strip_prefix('F').and_then(|n| n.parse::<u8>().ok())
424                && (1..=12).contains(&n)
425            {
426                return Some(Key::F(n));
427            }
428            let mut chars = text.chars();
429            if let (Some(c), None) = (chars.next(), chars.next()) {
430                Some(Key::Character(c.to_ascii_lowercase()))
431            } else {
432                None
433            }
434        }
435    }
436}
437
438fn decode_pad_button(name: &str) -> Option<GamepadButton> {
439    Some(match name {
440        "South" => GamepadButton::South,
441        "East" => GamepadButton::East,
442        "West" => GamepadButton::West,
443        "North" => GamepadButton::North,
444        "Start" => GamepadButton::Start,
445        "Select" => GamepadButton::Select,
446        "LeftShoulder" => GamepadButton::LeftShoulder,
447        "RightShoulder" => GamepadButton::RightShoulder,
448        "LeftStick" => GamepadButton::LeftStick,
449        "RightStick" => GamepadButton::RightStick,
450        "DPadUp" => GamepadButton::DPadUp,
451        "DPadDown" => GamepadButton::DPadDown,
452        "DPadLeft" => GamepadButton::DPadLeft,
453        "DPadRight" => GamepadButton::DPadRight,
454        _ => return None,
455    })
456}
457
458fn decode_axis(name: &str) -> Option<GamepadAxis> {
459    Some(match name {
460        "LeftStickX" => GamepadAxis::LeftStickX,
461        "LeftStickY" => GamepadAxis::LeftStickY,
462        "RightStickX" => GamepadAxis::RightStickX,
463        "RightStickY" => GamepadAxis::RightStickY,
464        "LeftTrigger" => GamepadAxis::LeftTrigger,
465        "RightTrigger" => GamepadAxis::RightTrigger,
466        _ => return None,
467    })
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    fn chord(c: char) -> KeymapEntry {
475        KeymapEntry::Key(KeyChord::new(Key::Character(c), Modifiers::default()))
476    }
477
478    #[test]
479    fn chord_covers_full_physical_alphabet() {
480        use repose_core::input::PhysicalKey;
481        assert_eq!(
482            chord_for_physical(PhysicalKey::KeyW),
483            Some(KeyChord::new(Key::Character('w'), Modifiers::default()))
484        );
485        assert_eq!(
486            chord_for_physical(PhysicalKey::Digit1),
487            Some(KeyChord::new(Key::Character('1'), Modifiers::default()))
488        );
489        assert_eq!(
490            chord_for_physical(PhysicalKey::Minus),
491            Some(KeyChord::new(Key::Character('-'), Modifiers::default()))
492        );
493        assert_eq!(
494            chord_for_physical(PhysicalKey::Space),
495            Some(KeyChord::new(Key::Space, Modifiers::default()))
496        );
497        assert_eq!(chord_for_physical(PhysicalKey::F5), None);
498        assert_eq!(chord_for_physical(PhysicalKey::Unidentified), None);
499    }
500
501    #[test]
502    fn glyph_covers_letters_digits_punctuation() {
503        use repose_core::input::PhysicalKey;
504        assert_eq!(glyph_for_physical(PhysicalKey::KeyQ), Some('q'));
505        assert_eq!(glyph_for_physical(PhysicalKey::Digit7), Some('7'));
506        assert_eq!(glyph_for_physical(PhysicalKey::Minus), Some('-'));
507        assert_eq!(glyph_for_physical(PhysicalKey::Slash), Some('/'));
508        assert_eq!(glyph_for_physical(PhysicalKey::Space), None);
509        assert_eq!(glyph_for_physical(PhysicalKey::F1), None);
510    }
511
512    #[test]
513    fn keyboard_and_gamepad_sides_stay_separate() {
514        let mut map = Keymap::new();
515        map.set_keyboard("fire", KeymapEntry::Mouse(PointerButton::Primary));
516        map.set_gamepad("fire", KeymapEntry::Pad(GamepadButton::South));
517        assert_eq!(
518            map.active(&"fire", false),
519            KeymapEntry::Mouse(PointerButton::Primary)
520        );
521        assert_eq!(
522            map.active(&"fire", true),
523            KeymapEntry::Pad(GamepadButton::South)
524        );
525    }
526
527    #[test]
528    fn gamepad_falls_back_to_keyboard_when_unbound() {
529        let mut map = Keymap::new();
530        map.set_keyboard("north", chord('w'));
531        assert_eq!(map.active(&"north", true), chord('w'));
532        assert_eq!(map.active(&"missing", true), KeymapEntry::None);
533    }
534
535    #[test]
536    fn capture_overwrites_one_side_only() {
537        let mut map = Keymap::new();
538        map.set_keyboard("fire", KeymapEntry::Mouse(PointerButton::Primary));
539        map.set_gamepad("fire", KeymapEntry::Pad(GamepadButton::South));
540        let capture = map.begin_capture("fire", KeymapDevice::KeyboardMouse);
541        map.resolve_capture(&capture, Some(chord('f')));
542        assert_eq!(map.keyboard(&"fire"), chord('f'));
543        assert_eq!(map.gamepad(&"fire"), KeymapEntry::Pad(GamepadButton::South));
544        let capture = map.begin_capture("fire", KeymapDevice::Gamepad);
545        map.resolve_capture(&capture, None);
546        assert_eq!(map.gamepad(&"fire"), KeymapEntry::None);
547        assert_eq!(map.active(&"fire", true), chord('f'));
548    }
549
550    #[test]
551    fn entries_round_trip_through_strings() {
552        let entries = [
553            KeymapEntry::None,
554            chord('w'),
555            KeymapEntry::Key(KeyChord::new(
556                Key::Space,
557                Modifiers {
558                    shift: true,
559                    ..Modifiers::default()
560                },
561            )),
562            KeymapEntry::Key(KeyChord::new(Key::Space, Modifiers::default())),
563            KeymapEntry::Key(KeyChord::new(Key::Tab, Modifiers::default())),
564            KeymapEntry::Key(KeyChord::new(Key::Enter, Modifiers::default())),
565            KeymapEntry::Key(KeyChord::new(Key::Escape, Modifiers::default())),
566            KeymapEntry::Key(KeyChord::new(Key::ArrowUp, Modifiers::default())),
567            KeymapEntry::Key(KeyChord::new(Key::ArrowDown, Modifiers::default())),
568            KeymapEntry::Key(KeyChord::new(Key::ArrowLeft, Modifiers::default())),
569            KeymapEntry::Key(KeyChord::new(Key::ArrowRight, Modifiers::default())),
570            KeymapEntry::Key(KeyChord::new(Key::ShiftLeft, Modifiers::default())),
571            KeymapEntry::Physical(PhysicalKey::Space),
572            KeymapEntry::Physical(PhysicalKey::Tab),
573            KeymapEntry::Physical(PhysicalKey::ArrowUp),
574            KeymapEntry::Physical(PhysicalKey::KeyW),
575            KeymapEntry::Physical(PhysicalKey::Digit1),
576            KeymapEntry::Physical(PhysicalKey::Minus),
577            KeymapEntry::Mouse(PointerButton::Primary),
578            KeymapEntry::Mouse(PointerButton::Secondary),
579            KeymapEntry::Mouse(PointerButton::Tertiary),
580            KeymapEntry::Pad(GamepadButton::DPadUp),
581            KeymapEntry::Axis {
582                axis: GamepadAxis::LeftTrigger,
583                threshold: 0.5,
584            },
585        ];
586        for entry in entries {
587            let text = encode_keymap_entry(&entry);
588            assert_eq!(decode_keymap_entry(&text), entry, "round trip {text:?}");
589        }
590        assert_eq!(decode_keymap_entry("bogus"), KeymapEntry::None);
591    }
592
593    #[test]
594    fn every_key_variant_round_trips() {
595        let keys = [
596            Key::ControlLeft,
597            Key::ControlRight,
598            Key::AltLeft,
599            Key::AltRight,
600            Key::MetaLeft,
601            Key::MetaRight,
602            Key::CapsLock,
603            Key::NumLock,
604            Key::ScrollLock,
605            Key::Pause,
606            Key::PrintScreen,
607            Key::ContextMenu,
608            Key::NumpadEnter,
609            Key::Numpad('5'),
610            Key::Numpad('.'),
611            Key::MediaPlay,
612            Key::MediaPause,
613            Key::MediaPlayPause,
614            Key::MediaStop,
615            Key::MediaNext,
616            Key::MediaPrevious,
617            Key::VolumeUp,
618            Key::VolumeDown,
619            Key::VolumeMute,
620        ];
621        for key in keys {
622            let entry = KeymapEntry::Key(KeyChord::new(key.clone(), Modifiers::default()));
623            let text = encode_keymap_entry(&entry);
624            assert_eq!(decode_keymap_entry(&text), entry, "round trip {text:?}");
625        }
626    }
627
628    #[test]
629    fn logical_and_physical_wire_forms_differ() {
630        let logical = KeymapEntry::Key(KeyChord::new(Key::Space, Modifiers::default()));
631        let physical = KeymapEntry::Physical(PhysicalKey::Space);
632        let ltext = encode_keymap_entry(&logical);
633        let ptext = encode_keymap_entry(&physical);
634        assert_ne!(ltext, ptext, "Space must not collide");
635        assert_eq!(decode_keymap_entry(&ltext), logical);
636        assert_eq!(decode_keymap_entry(&ptext), physical);
637        assert_eq!(
638            decode_keymap_entry("Space"),
639            logical,
640            "legacy bare Space stays logical (NT Swap default)"
641        );
642        assert_eq!(
643            decode_keymap_entry("KeyW"),
644            KeymapEntry::Physical(PhysicalKey::KeyW),
645            "legacy bare KeyW stays physical"
646        );
647    }
648}