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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
48pub enum KeymapDevice {
49 KeyboardMouse,
50 Gamepad,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, Hash)]
56pub struct KeymapCapture<A> {
57 pub action: A,
58 pub device: KeymapDevice,
59}
60
61#[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 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 pub fn begin_capture(&self, action: A, device: KeymapDevice) -> KeymapCapture<A> {
106 KeymapCapture { action, device }
107 }
108
109 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#[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 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 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 if let Some(chord) = decode_chord(text) {
214 return KeymapEntry::Key(chord);
215 }
216 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
225pub 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
252pub 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(<ext), 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}