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, 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub enum KeymapDevice {
47 KeyboardMouse,
48 Gamepad,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Hash)]
54pub struct KeymapCapture<A> {
55 pub action: A,
56 pub device: KeymapDevice,
57}
58
59#[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 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 pub fn begin_capture(&self, action: A, device: KeymapDevice) -> KeymapCapture<A> {
104 KeymapCapture { action, device }
105 }
106
107 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#[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 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 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 if let Some(chord) = decode_chord(text) {
212 return KeymapEntry::Key(chord);
213 }
214 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
223pub 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
250pub 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::ControlLeft => "ControlLeft".to_string(),
324 Key::ControlRight => "ControlRight".to_string(),
325 Key::AltLeft => "AltLeft".to_string(),
326 Key::AltRight => "AltRight".to_string(),
327 Key::MetaLeft => "MetaLeft".to_string(),
328 Key::MetaRight => "MetaRight".to_string(),
329 Key::CapsLock => "CapsLock".to_string(),
330 Key::NumLock => "NumLock".to_string(),
331 Key::ScrollLock => "ScrollLock".to_string(),
332 Key::Pause => "Pause".to_string(),
333 Key::PrintScreen => "PrintScreen".to_string(),
334 Key::ContextMenu => "ContextMenu".to_string(),
335 Key::NumpadEnter => "NumpadEnter".to_string(),
336 Key::MediaPlay => "MediaPlay".to_string(),
337 Key::MediaPause => "MediaPause".to_string(),
338 Key::MediaPlayPause => "MediaPlayPause".to_string(),
339 Key::MediaStop => "MediaStop".to_string(),
340 Key::MediaNext => "MediaNext".to_string(),
341 Key::MediaPrevious => "MediaPrevious".to_string(),
342 Key::VolumeUp => "VolumeUp".to_string(),
343 Key::VolumeDown => "VolumeDown".to_string(),
344 Key::VolumeMute => "VolumeMute".to_string(),
345 Key::Numpad(c) => format!("Numpad{}", *c as u32),
346 Key::F(n) => format!("F{n}"),
347 Key::Unknown => String::new(),
348 }
349}
350
351fn decode_chord(text: &str) -> Option<KeyChord> {
352 let mut rest = text;
353 let mut modifiers = Modifiers::default();
354 loop {
355 if let Some(tail) = rest.strip_prefix("Ctrl+") {
356 modifiers.ctrl = true;
357 rest = tail;
358 } else if let Some(tail) = rest.strip_prefix("Shift+") {
359 modifiers.shift = true;
360 rest = tail;
361 } else if let Some(tail) = rest.strip_prefix("Alt+") {
362 modifiers.alt = true;
363 rest = tail;
364 } else if let Some(tail) = rest.strip_prefix("Meta+") {
365 modifiers.meta = true;
366 rest = tail;
367 } else {
368 break;
369 }
370 }
371 decode_key(rest).map(|key| KeyChord::new(key, modifiers))
372}
373
374fn decode_key(text: &str) -> Option<Key> {
375 match text {
376 "Enter" => Some(Key::Enter),
377 "Tab" => Some(Key::Tab),
378 "Backspace" => Some(Key::Backspace),
379 "Delete" => Some(Key::Delete),
380 "Insert" => Some(Key::Insert),
381 "Escape" => Some(Key::Escape),
382 "ArrowLeft" => Some(Key::ArrowLeft),
383 "ArrowRight" => Some(Key::ArrowRight),
384 "ArrowUp" => Some(Key::ArrowUp),
385 "ArrowDown" => Some(Key::ArrowDown),
386 "Home" => Some(Key::Home),
387 "End" => Some(Key::End),
388 "PageUp" => Some(Key::PageUp),
389 "PageDown" => Some(Key::PageDown),
390 "Space" => Some(Key::Space),
391 "ShiftLeft" => Some(Key::ShiftLeft),
392 "ShiftRight" => Some(Key::ShiftRight),
393 "ControlLeft" => Some(Key::ControlLeft),
394 "ControlRight" => Some(Key::ControlRight),
395 "AltLeft" => Some(Key::AltLeft),
396 "AltRight" => Some(Key::AltRight),
397 "MetaLeft" => Some(Key::MetaLeft),
398 "MetaRight" => Some(Key::MetaRight),
399 "CapsLock" => Some(Key::CapsLock),
400 "NumLock" => Some(Key::NumLock),
401 "ScrollLock" => Some(Key::ScrollLock),
402 "Pause" => Some(Key::Pause),
403 "PrintScreen" => Some(Key::PrintScreen),
404 "ContextMenu" => Some(Key::ContextMenu),
405 "NumpadEnter" => Some(Key::NumpadEnter),
406 "MediaPlay" => Some(Key::MediaPlay),
407 "MediaPause" => Some(Key::MediaPause),
408 "MediaPlayPause" => Some(Key::MediaPlayPause),
409 "MediaStop" => Some(Key::MediaStop),
410 "MediaNext" => Some(Key::MediaNext),
411 "MediaPrevious" => Some(Key::MediaPrevious),
412 "VolumeUp" => Some(Key::VolumeUp),
413 "VolumeDown" => Some(Key::VolumeDown),
414 "VolumeMute" => Some(Key::VolumeMute),
415 _ => {
416 if let Some(digit) = text
417 .strip_prefix("Numpad")
418 .and_then(|d| d.parse::<u32>().ok())
419 && let Some(c) = char::from_u32(digit)
420 {
421 return Some(Key::Numpad(c));
422 }
423 if let Some(n) = text.strip_prefix('F').and_then(|n| n.parse::<u8>().ok())
424 && (1..=12).contains(&n)
425 {
426 return Some(Key::F(n));
427 }
428 let mut chars = text.chars();
429 if let (Some(c), None) = (chars.next(), chars.next()) {
430 Some(Key::Character(c.to_ascii_lowercase()))
431 } else {
432 None
433 }
434 }
435 }
436}
437
438fn decode_pad_button(name: &str) -> Option<GamepadButton> {
439 Some(match name {
440 "South" => GamepadButton::South,
441 "East" => GamepadButton::East,
442 "West" => GamepadButton::West,
443 "North" => GamepadButton::North,
444 "Start" => GamepadButton::Start,
445 "Select" => GamepadButton::Select,
446 "LeftShoulder" => GamepadButton::LeftShoulder,
447 "RightShoulder" => GamepadButton::RightShoulder,
448 "LeftStick" => GamepadButton::LeftStick,
449 "RightStick" => GamepadButton::RightStick,
450 "DPadUp" => GamepadButton::DPadUp,
451 "DPadDown" => GamepadButton::DPadDown,
452 "DPadLeft" => GamepadButton::DPadLeft,
453 "DPadRight" => GamepadButton::DPadRight,
454 _ => return None,
455 })
456}
457
458fn decode_axis(name: &str) -> Option<GamepadAxis> {
459 Some(match name {
460 "LeftStickX" => GamepadAxis::LeftStickX,
461 "LeftStickY" => GamepadAxis::LeftStickY,
462 "RightStickX" => GamepadAxis::RightStickX,
463 "RightStickY" => GamepadAxis::RightStickY,
464 "LeftTrigger" => GamepadAxis::LeftTrigger,
465 "RightTrigger" => GamepadAxis::RightTrigger,
466 _ => return None,
467 })
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 fn chord(c: char) -> KeymapEntry {
475 KeymapEntry::Key(KeyChord::new(Key::Character(c), Modifiers::default()))
476 }
477
478 #[test]
479 fn chord_covers_full_physical_alphabet() {
480 use repose_core::input::PhysicalKey;
481 assert_eq!(
482 chord_for_physical(PhysicalKey::KeyW),
483 Some(KeyChord::new(Key::Character('w'), Modifiers::default()))
484 );
485 assert_eq!(
486 chord_for_physical(PhysicalKey::Digit1),
487 Some(KeyChord::new(Key::Character('1'), Modifiers::default()))
488 );
489 assert_eq!(
490 chord_for_physical(PhysicalKey::Minus),
491 Some(KeyChord::new(Key::Character('-'), Modifiers::default()))
492 );
493 assert_eq!(
494 chord_for_physical(PhysicalKey::Space),
495 Some(KeyChord::new(Key::Space, Modifiers::default()))
496 );
497 assert_eq!(chord_for_physical(PhysicalKey::F5), None);
498 assert_eq!(chord_for_physical(PhysicalKey::Unidentified), None);
499 }
500
501 #[test]
502 fn glyph_covers_letters_digits_punctuation() {
503 use repose_core::input::PhysicalKey;
504 assert_eq!(glyph_for_physical(PhysicalKey::KeyQ), Some('q'));
505 assert_eq!(glyph_for_physical(PhysicalKey::Digit7), Some('7'));
506 assert_eq!(glyph_for_physical(PhysicalKey::Minus), Some('-'));
507 assert_eq!(glyph_for_physical(PhysicalKey::Slash), Some('/'));
508 assert_eq!(glyph_for_physical(PhysicalKey::Space), None);
509 assert_eq!(glyph_for_physical(PhysicalKey::F1), None);
510 }
511
512 #[test]
513 fn keyboard_and_gamepad_sides_stay_separate() {
514 let mut map = Keymap::new();
515 map.set_keyboard("fire", KeymapEntry::Mouse(PointerButton::Primary));
516 map.set_gamepad("fire", KeymapEntry::Pad(GamepadButton::South));
517 assert_eq!(
518 map.active(&"fire", false),
519 KeymapEntry::Mouse(PointerButton::Primary)
520 );
521 assert_eq!(
522 map.active(&"fire", true),
523 KeymapEntry::Pad(GamepadButton::South)
524 );
525 }
526
527 #[test]
528 fn gamepad_falls_back_to_keyboard_when_unbound() {
529 let mut map = Keymap::new();
530 map.set_keyboard("north", chord('w'));
531 assert_eq!(map.active(&"north", true), chord('w'));
532 assert_eq!(map.active(&"missing", true), KeymapEntry::None);
533 }
534
535 #[test]
536 fn capture_overwrites_one_side_only() {
537 let mut map = Keymap::new();
538 map.set_keyboard("fire", KeymapEntry::Mouse(PointerButton::Primary));
539 map.set_gamepad("fire", KeymapEntry::Pad(GamepadButton::South));
540 let capture = map.begin_capture("fire", KeymapDevice::KeyboardMouse);
541 map.resolve_capture(&capture, Some(chord('f')));
542 assert_eq!(map.keyboard(&"fire"), chord('f'));
543 assert_eq!(map.gamepad(&"fire"), KeymapEntry::Pad(GamepadButton::South));
544 let capture = map.begin_capture("fire", KeymapDevice::Gamepad);
545 map.resolve_capture(&capture, None);
546 assert_eq!(map.gamepad(&"fire"), KeymapEntry::None);
547 assert_eq!(map.active(&"fire", true), chord('f'));
548 }
549
550 #[test]
551 fn entries_round_trip_through_strings() {
552 let entries = [
553 KeymapEntry::None,
554 chord('w'),
555 KeymapEntry::Key(KeyChord::new(
556 Key::Space,
557 Modifiers {
558 shift: true,
559 ..Modifiers::default()
560 },
561 )),
562 KeymapEntry::Key(KeyChord::new(Key::Space, Modifiers::default())),
563 KeymapEntry::Key(KeyChord::new(Key::Tab, Modifiers::default())),
564 KeymapEntry::Key(KeyChord::new(Key::Enter, Modifiers::default())),
565 KeymapEntry::Key(KeyChord::new(Key::Escape, Modifiers::default())),
566 KeymapEntry::Key(KeyChord::new(Key::ArrowUp, Modifiers::default())),
567 KeymapEntry::Key(KeyChord::new(Key::ArrowDown, Modifiers::default())),
568 KeymapEntry::Key(KeyChord::new(Key::ArrowLeft, Modifiers::default())),
569 KeymapEntry::Key(KeyChord::new(Key::ArrowRight, Modifiers::default())),
570 KeymapEntry::Key(KeyChord::new(Key::ShiftLeft, Modifiers::default())),
571 KeymapEntry::Physical(PhysicalKey::Space),
572 KeymapEntry::Physical(PhysicalKey::Tab),
573 KeymapEntry::Physical(PhysicalKey::ArrowUp),
574 KeymapEntry::Physical(PhysicalKey::KeyW),
575 KeymapEntry::Physical(PhysicalKey::Digit1),
576 KeymapEntry::Physical(PhysicalKey::Minus),
577 KeymapEntry::Mouse(PointerButton::Primary),
578 KeymapEntry::Mouse(PointerButton::Secondary),
579 KeymapEntry::Mouse(PointerButton::Tertiary),
580 KeymapEntry::Pad(GamepadButton::DPadUp),
581 KeymapEntry::Axis {
582 axis: GamepadAxis::LeftTrigger,
583 threshold: 0.5,
584 },
585 ];
586 for entry in entries {
587 let text = encode_keymap_entry(&entry);
588 assert_eq!(decode_keymap_entry(&text), entry, "round trip {text:?}");
589 }
590 assert_eq!(decode_keymap_entry("bogus"), KeymapEntry::None);
591 }
592
593 #[test]
594 fn every_key_variant_round_trips() {
595 let keys = [
596 Key::ControlLeft,
597 Key::ControlRight,
598 Key::AltLeft,
599 Key::AltRight,
600 Key::MetaLeft,
601 Key::MetaRight,
602 Key::CapsLock,
603 Key::NumLock,
604 Key::ScrollLock,
605 Key::Pause,
606 Key::PrintScreen,
607 Key::ContextMenu,
608 Key::NumpadEnter,
609 Key::Numpad('5'),
610 Key::Numpad('.'),
611 Key::MediaPlay,
612 Key::MediaPause,
613 Key::MediaPlayPause,
614 Key::MediaStop,
615 Key::MediaNext,
616 Key::MediaPrevious,
617 Key::VolumeUp,
618 Key::VolumeDown,
619 Key::VolumeMute,
620 ];
621 for key in keys {
622 let entry = KeymapEntry::Key(KeyChord::new(key.clone(), Modifiers::default()));
623 let text = encode_keymap_entry(&entry);
624 assert_eq!(decode_keymap_entry(&text), entry, "round trip {text:?}");
625 }
626 }
627
628 #[test]
629 fn logical_and_physical_wire_forms_differ() {
630 let logical = KeymapEntry::Key(KeyChord::new(Key::Space, Modifiers::default()));
631 let physical = KeymapEntry::Physical(PhysicalKey::Space);
632 let ltext = encode_keymap_entry(&logical);
633 let ptext = encode_keymap_entry(&physical);
634 assert_ne!(ltext, ptext, "Space must not collide");
635 assert_eq!(decode_keymap_entry(<ext), logical);
636 assert_eq!(decode_keymap_entry(&ptext), physical);
637 assert_eq!(
638 decode_keymap_entry("Space"),
639 logical,
640 "legacy bare Space stays logical (NT Swap default)"
641 );
642 assert_eq!(
643 decode_keymap_entry("KeyW"),
644 KeymapEntry::Physical(PhysicalKey::KeyW),
645 "legacy bare KeyW stays physical"
646 );
647 }
648}