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