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