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::F(n) => format!("F{n}"),
324        Key::Unknown => String::new(),
325    }
326}
327
328fn decode_chord(text: &str) -> Option<KeyChord> {
329    let mut rest = text;
330    let mut modifiers = Modifiers::default();
331    loop {
332        if let Some(tail) = rest.strip_prefix("Ctrl+") {
333            modifiers.ctrl = true;
334            rest = tail;
335        } else if let Some(tail) = rest.strip_prefix("Shift+") {
336            modifiers.shift = true;
337            rest = tail;
338        } else if let Some(tail) = rest.strip_prefix("Alt+") {
339            modifiers.alt = true;
340            rest = tail;
341        } else if let Some(tail) = rest.strip_prefix("Meta+") {
342            modifiers.meta = true;
343            rest = tail;
344        } else {
345            break;
346        }
347    }
348    decode_key(rest).map(|key| KeyChord::new(key, modifiers))
349}
350
351fn decode_key(text: &str) -> Option<Key> {
352    match text {
353        "Enter" => Some(Key::Enter),
354        "Tab" => Some(Key::Tab),
355        "Backspace" => Some(Key::Backspace),
356        "Delete" => Some(Key::Delete),
357        "Insert" => Some(Key::Insert),
358        "Escape" => Some(Key::Escape),
359        "ArrowLeft" => Some(Key::ArrowLeft),
360        "ArrowRight" => Some(Key::ArrowRight),
361        "ArrowUp" => Some(Key::ArrowUp),
362        "ArrowDown" => Some(Key::ArrowDown),
363        "Home" => Some(Key::Home),
364        "End" => Some(Key::End),
365        "PageUp" => Some(Key::PageUp),
366        "PageDown" => Some(Key::PageDown),
367        "Space" => Some(Key::Space),
368        "ShiftLeft" => Some(Key::ShiftLeft),
369        "ShiftRight" => Some(Key::ShiftRight),
370        _ => {
371            if let Some(n) = text.strip_prefix('F').and_then(|n| n.parse::<u8>().ok())
372                && (1..=12).contains(&n)
373            {
374                return Some(Key::F(n));
375            }
376            let mut chars = text.chars();
377            if let (Some(c), None) = (chars.next(), chars.next()) {
378                Some(Key::Character(c.to_ascii_lowercase()))
379            } else {
380                None
381            }
382        }
383    }
384}
385
386fn decode_pad_button(name: &str) -> Option<GamepadButton> {
387    Some(match name {
388        "South" => GamepadButton::South,
389        "East" => GamepadButton::East,
390        "West" => GamepadButton::West,
391        "North" => GamepadButton::North,
392        "Start" => GamepadButton::Start,
393        "Select" => GamepadButton::Select,
394        "LeftShoulder" => GamepadButton::LeftShoulder,
395        "RightShoulder" => GamepadButton::RightShoulder,
396        "LeftStick" => GamepadButton::LeftStick,
397        "RightStick" => GamepadButton::RightStick,
398        "DPadUp" => GamepadButton::DPadUp,
399        "DPadDown" => GamepadButton::DPadDown,
400        "DPadLeft" => GamepadButton::DPadLeft,
401        "DPadRight" => GamepadButton::DPadRight,
402        _ => return None,
403    })
404}
405
406fn decode_axis(name: &str) -> Option<GamepadAxis> {
407    Some(match name {
408        "LeftStickX" => GamepadAxis::LeftStickX,
409        "LeftStickY" => GamepadAxis::LeftStickY,
410        "RightStickX" => GamepadAxis::RightStickX,
411        "RightStickY" => GamepadAxis::RightStickY,
412        "LeftTrigger" => GamepadAxis::LeftTrigger,
413        "RightTrigger" => GamepadAxis::RightTrigger,
414        _ => return None,
415    })
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    fn chord(c: char) -> KeymapEntry {
423        KeymapEntry::Key(KeyChord::new(Key::Character(c), Modifiers::default()))
424    }
425
426    #[test]
427    fn chord_covers_full_physical_alphabet() {
428        use repose_core::input::PhysicalKey;
429        assert_eq!(
430            chord_for_physical(PhysicalKey::KeyW),
431            Some(KeyChord::new(Key::Character('w'), Modifiers::default()))
432        );
433        assert_eq!(
434            chord_for_physical(PhysicalKey::Digit1),
435            Some(KeyChord::new(Key::Character('1'), Modifiers::default()))
436        );
437        assert_eq!(
438            chord_for_physical(PhysicalKey::Minus),
439            Some(KeyChord::new(Key::Character('-'), Modifiers::default()))
440        );
441        assert_eq!(
442            chord_for_physical(PhysicalKey::Space),
443            Some(KeyChord::new(Key::Space, Modifiers::default()))
444        );
445        assert_eq!(chord_for_physical(PhysicalKey::F5), None);
446        assert_eq!(chord_for_physical(PhysicalKey::Unidentified), None);
447    }
448
449    #[test]
450    fn glyph_covers_letters_digits_punctuation() {
451        use repose_core::input::PhysicalKey;
452        assert_eq!(glyph_for_physical(PhysicalKey::KeyQ), Some('q'));
453        assert_eq!(glyph_for_physical(PhysicalKey::Digit7), Some('7'));
454        assert_eq!(glyph_for_physical(PhysicalKey::Minus), Some('-'));
455        assert_eq!(glyph_for_physical(PhysicalKey::Slash), Some('/'));
456        assert_eq!(glyph_for_physical(PhysicalKey::Space), None);
457        assert_eq!(glyph_for_physical(PhysicalKey::F1), None);
458    }
459
460    #[test]
461    fn keyboard_and_gamepad_sides_stay_separate() {
462        let mut map = Keymap::new();
463        map.set_keyboard("fire", KeymapEntry::Mouse(PointerButton::Primary));
464        map.set_gamepad("fire", KeymapEntry::Pad(GamepadButton::South));
465        assert_eq!(
466            map.active(&"fire", false),
467            KeymapEntry::Mouse(PointerButton::Primary)
468        );
469        assert_eq!(
470            map.active(&"fire", true),
471            KeymapEntry::Pad(GamepadButton::South)
472        );
473    }
474
475    #[test]
476    fn gamepad_falls_back_to_keyboard_when_unbound() {
477        let mut map = Keymap::new();
478        map.set_keyboard("north", chord('w'));
479        assert_eq!(map.active(&"north", true), chord('w'));
480        assert_eq!(map.active(&"missing", true), KeymapEntry::None);
481    }
482
483    #[test]
484    fn capture_overwrites_one_side_only() {
485        let mut map = Keymap::new();
486        map.set_keyboard("fire", KeymapEntry::Mouse(PointerButton::Primary));
487        map.set_gamepad("fire", KeymapEntry::Pad(GamepadButton::South));
488        let capture = map.begin_capture("fire", KeymapDevice::KeyboardMouse);
489        map.resolve_capture(&capture, Some(chord('f')));
490        assert_eq!(map.keyboard(&"fire"), chord('f'));
491        assert_eq!(map.gamepad(&"fire"), KeymapEntry::Pad(GamepadButton::South));
492        let capture = map.begin_capture("fire", KeymapDevice::Gamepad);
493        map.resolve_capture(&capture, None);
494        assert_eq!(map.gamepad(&"fire"), KeymapEntry::None);
495        assert_eq!(map.active(&"fire", true), chord('f'));
496    }
497
498    #[test]
499    fn entries_round_trip_through_strings() {
500        let entries = [
501            KeymapEntry::None,
502            chord('w'),
503            KeymapEntry::Key(KeyChord::new(
504                Key::Space,
505                Modifiers {
506                    shift: true,
507                    ..Modifiers::default()
508                },
509            )),
510            KeymapEntry::Key(KeyChord::new(Key::Space, Modifiers::default())),
511            KeymapEntry::Key(KeyChord::new(Key::Tab, Modifiers::default())),
512            KeymapEntry::Key(KeyChord::new(Key::Enter, Modifiers::default())),
513            KeymapEntry::Key(KeyChord::new(Key::Escape, Modifiers::default())),
514            KeymapEntry::Key(KeyChord::new(Key::ArrowUp, Modifiers::default())),
515            KeymapEntry::Key(KeyChord::new(Key::ArrowDown, Modifiers::default())),
516            KeymapEntry::Key(KeyChord::new(Key::ArrowLeft, Modifiers::default())),
517            KeymapEntry::Key(KeyChord::new(Key::ArrowRight, Modifiers::default())),
518            KeymapEntry::Key(KeyChord::new(Key::ShiftLeft, Modifiers::default())),
519            KeymapEntry::Physical(PhysicalKey::Space),
520            KeymapEntry::Physical(PhysicalKey::Tab),
521            KeymapEntry::Physical(PhysicalKey::ArrowUp),
522            KeymapEntry::Physical(PhysicalKey::KeyW),
523            KeymapEntry::Physical(PhysicalKey::Digit1),
524            KeymapEntry::Physical(PhysicalKey::Minus),
525            KeymapEntry::Mouse(PointerButton::Primary),
526            KeymapEntry::Mouse(PointerButton::Secondary),
527            KeymapEntry::Mouse(PointerButton::Tertiary),
528            KeymapEntry::Pad(GamepadButton::DPadUp),
529            KeymapEntry::Axis {
530                axis: GamepadAxis::LeftTrigger,
531                threshold: 0.5,
532            },
533        ];
534        for entry in entries {
535            let text = encode_keymap_entry(&entry);
536            assert_eq!(decode_keymap_entry(&text), entry, "round trip {text:?}");
537        }
538        assert_eq!(decode_keymap_entry("bogus"), KeymapEntry::None);
539    }
540
541    #[test]
542    fn logical_and_physical_wire_forms_differ() {
543        let logical = KeymapEntry::Key(KeyChord::new(Key::Space, Modifiers::default()));
544        let physical = KeymapEntry::Physical(PhysicalKey::Space);
545        let ltext = encode_keymap_entry(&logical);
546        let ptext = encode_keymap_entry(&physical);
547        assert_ne!(ltext, ptext, "Space must not collide");
548        assert_eq!(decode_keymap_entry(&ltext), logical);
549        assert_eq!(decode_keymap_entry(&ptext), physical);
550        assert_eq!(
551            decode_keymap_entry("Space"),
552            logical,
553            "legacy bare Space stays logical (NT Swap default)"
554        );
555        assert_eq!(
556            decode_keymap_entry("KeyW"),
557            KeymapEntry::Physical(PhysicalKey::KeyW),
558            "legacy bare KeyW stays physical"
559        );
560    }
561}