Skip to main content

repame_input/
keymap.rs

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