1use std::collections::BTreeMap;
10
11use winit::keyboard::KeyCode;
12
13use crate::settings::KeymapSettings;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub enum Action {
21 NextPreset,
24 NextPresetInstant,
26 PreviousPreset,
28 ToggleRandomMode,
30 TogglePresetLock,
32 EngineReset,
34 LoadMenu,
36
37 RateUp,
40 RateDown,
42
43 EditMenu,
45 SavePreset,
46 ToggleMonitor,
47 MashUp,
48 CycleShaderLock,
49
50 ShowHelp,
52 ShowSongInfo,
53 ShowFps,
54 ShowPresetName,
55 ShowRating,
56 RereadIni,
57 ToggleAnaglyph3D,
58
59 CycleBeatDetection,
61 ToggleFullscreen,
62 ToggleOptions,
63 Quit,
64
65 SpriteCycle,
67 SpriteRandom,
68 SpritePopRecent,
69 MessageCycle,
70 ClearAllOverlays,
71 ClearMessages,
72
73 WarpAmountDown,
75 WarpAmountUp,
76 RotationCcw,
77 RotationCw,
78 ZoomOut,
79 ZoomIn,
80 WarpSpeedDown,
81 WarpSpeedUp,
82
83 CycleWaveform,
85 WaveScaleDown,
86 WaveScaleUp,
87 WaveOpacityDown,
88 WaveOpacityUp,
89}
90
91impl Action {
92 #[allow(dead_code)] pub fn as_str(self) -> &'static str {
99 match self {
100 Action::NextPreset => "next_preset",
101 Action::NextPresetInstant => "next_preset_instant",
102 Action::PreviousPreset => "previous_preset",
103 Action::ToggleRandomMode => "toggle_random_mode",
104 Action::TogglePresetLock => "toggle_preset_lock",
105 Action::EngineReset => "engine_reset",
106 Action::LoadMenu => "load_menu",
107 Action::RateUp => "rate_up",
108 Action::RateDown => "rate_down",
109 Action::EditMenu => "edit_menu",
110 Action::SavePreset => "save_preset",
111 Action::ToggleMonitor => "toggle_monitor",
112 Action::MashUp => "mash_up",
113 Action::CycleShaderLock => "cycle_shader_lock",
114 Action::ShowHelp => "show_help",
115 Action::ShowSongInfo => "show_song_info",
116 Action::ShowFps => "show_fps",
117 Action::ShowPresetName => "show_preset_name",
118 Action::ShowRating => "show_rating",
119 Action::RereadIni => "reread_ini",
120 Action::ToggleAnaglyph3D => "toggle_anaglyph_3d",
121 Action::CycleBeatDetection => "cycle_beat_detection",
122 Action::ToggleFullscreen => "toggle_fullscreen",
123 Action::ToggleOptions => "toggle_options",
124 Action::Quit => "quit",
125 Action::SpriteCycle => "sprite_cycle",
126 Action::SpriteRandom => "sprite_random",
127 Action::SpritePopRecent => "sprite_pop_recent",
128 Action::MessageCycle => "message_cycle",
129 Action::ClearAllOverlays => "clear_all_overlays",
130 Action::ClearMessages => "clear_messages",
131 Action::WarpAmountDown => "warp_amount_down",
132 Action::WarpAmountUp => "warp_amount_up",
133 Action::RotationCcw => "rotation_ccw",
134 Action::RotationCw => "rotation_cw",
135 Action::ZoomOut => "zoom_out",
136 Action::ZoomIn => "zoom_in",
137 Action::WarpSpeedDown => "warp_speed_down",
138 Action::WarpSpeedUp => "warp_speed_up",
139 Action::CycleWaveform => "cycle_waveform",
140 Action::WaveScaleDown => "wave_scale_down",
141 Action::WaveScaleUp => "wave_scale_up",
142 Action::WaveOpacityDown => "wave_opacity_down",
143 Action::WaveOpacityUp => "wave_opacity_up",
144 }
145 }
146
147 fn from_str(s: &str) -> Option<Action> {
148 Some(match s {
149 "next_preset" => Action::NextPreset,
150 "next_preset_instant" => Action::NextPresetInstant,
151 "previous_preset" => Action::PreviousPreset,
152 "toggle_random_mode" => Action::ToggleRandomMode,
153 "toggle_preset_lock" => Action::TogglePresetLock,
154 "engine_reset" => Action::EngineReset,
155 "load_menu" => Action::LoadMenu,
156 "rate_up" => Action::RateUp,
157 "rate_down" => Action::RateDown,
158 "edit_menu" => Action::EditMenu,
159 "save_preset" => Action::SavePreset,
160 "toggle_monitor" => Action::ToggleMonitor,
161 "mash_up" => Action::MashUp,
162 "cycle_shader_lock" => Action::CycleShaderLock,
163 "show_help" => Action::ShowHelp,
164 "show_song_info" => Action::ShowSongInfo,
165 "show_fps" => Action::ShowFps,
166 "show_preset_name" => Action::ShowPresetName,
167 "show_rating" => Action::ShowRating,
168 "reread_ini" => Action::RereadIni,
169 "toggle_anaglyph_3d" => Action::ToggleAnaglyph3D,
170 "cycle_beat_detection" => Action::CycleBeatDetection,
171 "toggle_fullscreen" => Action::ToggleFullscreen,
172 "toggle_options" => Action::ToggleOptions,
173 "quit" => Action::Quit,
174 "sprite_cycle" => Action::SpriteCycle,
175 "sprite_random" => Action::SpriteRandom,
176 "sprite_pop_recent" => Action::SpritePopRecent,
177 "message_cycle" => Action::MessageCycle,
178 "clear_all_overlays" => Action::ClearAllOverlays,
179 "clear_messages" => Action::ClearMessages,
180 "warp_amount_down" => Action::WarpAmountDown,
181 "warp_amount_up" => Action::WarpAmountUp,
182 "rotation_ccw" => Action::RotationCcw,
183 "rotation_cw" => Action::RotationCw,
184 "zoom_out" => Action::ZoomOut,
185 "zoom_in" => Action::ZoomIn,
186 "warp_speed_down" => Action::WarpSpeedDown,
187 "warp_speed_up" => Action::WarpSpeedUp,
188 "cycle_waveform" => Action::CycleWaveform,
189 "wave_scale_down" => Action::WaveScaleDown,
190 "wave_scale_up" => Action::WaveScaleUp,
191 "wave_opacity_down" => Action::WaveOpacityDown,
192 "wave_opacity_up" => Action::WaveOpacityUp,
193 _ => return None,
194 })
195 }
196}
197
198#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
201pub struct Modifiers {
202 pub ctrl: bool,
203 pub shift: bool,
204 pub alt: bool,
205}
206
207impl Modifiers {
208 pub const NONE: Modifiers = Modifiers {
209 ctrl: false,
210 shift: false,
211 alt: false,
212 };
213 pub const CTRL: Modifiers = Modifiers {
214 ctrl: true,
215 shift: false,
216 alt: false,
217 };
218 pub const SHIFT: Modifiers = Modifiers {
219 ctrl: false,
220 shift: true,
221 alt: false,
222 };
223
224 pub fn from_winit(m: winit::keyboard::ModifiersState) -> Self {
225 Self {
226 ctrl: m.control_key(),
227 shift: m.shift_key(),
228 alt: m.alt_key(),
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
236pub struct Binding {
237 pub key: KeyCode,
238 pub mods: Modifiers,
239}
240
241impl Binding {
242 pub const fn new(key: KeyCode, mods: Modifiers) -> Self {
243 Self { key, mods }
244 }
245
246 pub const fn bare(key: KeyCode) -> Self {
248 Self {
249 key,
250 mods: Modifiers::NONE,
251 }
252 }
253}
254
255#[derive(Debug, Clone)]
259pub struct Keymap {
260 table: BTreeMap<Binding, Action>,
261}
262
263impl Keymap {
264 pub fn default_table() -> Self {
268 let mut t = BTreeMap::new();
269 let mut bind = |b: Binding, a: Action| {
270 t.insert(b, a);
271 };
272
273 bind(Binding::bare(KeyCode::Space), Action::NextPreset);
275 bind(Binding::bare(KeyCode::ArrowRight), Action::NextPreset);
276 bind(Binding::bare(KeyCode::KeyH), Action::NextPresetInstant);
277 bind(Binding::bare(KeyCode::Backspace), Action::PreviousPreset);
278 bind(Binding::bare(KeyCode::ArrowLeft), Action::PreviousPreset);
279 bind(Binding::bare(KeyCode::KeyR), Action::ToggleRandomMode);
280 bind(Binding::bare(KeyCode::ScrollLock), Action::TogglePresetLock);
281 bind(Binding::bare(KeyCode::F5), Action::EngineReset);
282 bind(Binding::bare(KeyCode::KeyL), Action::LoadMenu);
283
284 bind(Binding::bare(KeyCode::Equal), Action::RateUp);
287 bind(Binding::bare(KeyCode::NumpadAdd), Action::RateUp);
288 bind(Binding::bare(KeyCode::Minus), Action::RateDown);
289 bind(Binding::bare(KeyCode::NumpadSubtract), Action::RateDown);
290
291 bind(Binding::bare(KeyCode::KeyM), Action::EditMenu);
293 bind(Binding::bare(KeyCode::KeyS), Action::SavePreset);
294 bind(Binding::bare(KeyCode::KeyN), Action::ToggleMonitor);
295 bind(Binding::bare(KeyCode::KeyA), Action::MashUp);
296 bind(Binding::bare(KeyCode::KeyD), Action::CycleShaderLock);
297
298 bind(Binding::bare(KeyCode::F1), Action::ShowHelp);
300 bind(Binding::bare(KeyCode::F2), Action::ShowSongInfo);
301 bind(Binding::bare(KeyCode::F3), Action::ShowFps);
302 bind(Binding::bare(KeyCode::F4), Action::ShowPresetName);
303 bind(Binding::bare(KeyCode::F6), Action::ShowRating);
304 bind(Binding::bare(KeyCode::F7), Action::RereadIni);
305 bind(Binding::bare(KeyCode::F9), Action::ToggleAnaglyph3D);
306
307 bind(Binding::bare(KeyCode::F8), Action::CycleBeatDetection);
309 bind(Binding::bare(KeyCode::F11), Action::ToggleFullscreen);
310 bind(Binding::bare(KeyCode::Tab), Action::ToggleOptions);
311 bind(Binding::bare(KeyCode::Escape), Action::Quit);
312 bind(Binding::bare(KeyCode::KeyQ), Action::Quit);
313
314 bind(Binding::bare(KeyCode::KeyK), Action::SpriteCycle);
316 bind(
317 Binding::new(KeyCode::KeyK, Modifiers::SHIFT),
318 Action::SpriteRandom,
319 );
320 bind(Binding::bare(KeyCode::Delete), Action::SpritePopRecent);
321 bind(Binding::bare(KeyCode::KeyT), Action::MessageCycle);
322 bind(
323 Binding::new(KeyCode::KeyT, Modifiers::CTRL),
324 Action::ClearAllOverlays,
325 );
326 bind(
327 Binding::new(KeyCode::KeyY, Modifiers::CTRL),
328 Action::ClearMessages,
329 );
330
331 bind(Binding::bare(KeyCode::BracketLeft), Action::WarpAmountDown);
334 bind(Binding::bare(KeyCode::BracketRight), Action::WarpAmountUp);
335 bind(Binding::bare(KeyCode::Comma), Action::RotationCcw);
336 bind(Binding::bare(KeyCode::Period), Action::RotationCw);
337 bind(Binding::bare(KeyCode::KeyO), Action::ZoomOut);
338 bind(
339 Binding::new(KeyCode::KeyO, Modifiers::SHIFT),
340 Action::ZoomIn,
341 );
342 bind(Binding::bare(KeyCode::KeyI), Action::WarpSpeedDown);
343 bind(
344 Binding::new(KeyCode::KeyI, Modifiers::SHIFT),
345 Action::WarpSpeedUp,
346 );
347 bind(Binding::bare(KeyCode::KeyW), Action::CycleWaveform);
348 bind(Binding::bare(KeyCode::KeyJ), Action::WaveScaleDown);
349 bind(
350 Binding::new(KeyCode::KeyJ, Modifiers::SHIFT),
351 Action::WaveScaleUp,
352 );
353 bind(Binding::bare(KeyCode::KeyE), Action::WaveOpacityDown);
354 bind(
355 Binding::new(KeyCode::KeyE, Modifiers::SHIFT),
356 Action::WaveOpacityUp,
357 );
358
359 Self { table: t }
360 }
361
362 pub fn from_settings(s: &KeymapSettings) -> Self {
368 let mut km = Self::default_table();
369 for (action_name, chord) in &s.bindings {
370 let Some(action) = Action::from_str(action_name) else {
371 log::warn!(
372 "keymap: unknown action {:?} in settings — ignored",
373 action_name
374 );
375 continue;
376 };
377 let Some(binding) = parse_chord(chord) else {
378 log::warn!(
379 "keymap: failed to parse chord {:?} for action {:?} — ignored",
380 chord,
381 action_name
382 );
383 continue;
384 };
385 km.table.retain(|_, &mut a| a != action);
386 km.table.insert(binding, action);
387 }
388 km
389 }
390
391 pub fn lookup(&self, binding: Binding) -> Option<Action> {
393 self.table.get(&binding).copied()
394 }
395}
396
397impl Default for Keymap {
398 fn default() -> Self {
399 Self::default_table()
400 }
401}
402
403fn parse_chord(s: &str) -> Option<Binding> {
408 let mut mods = Modifiers::NONE;
409 let mut last: Option<&str> = None;
410 for part in s.split('+').map(str::trim).filter(|p| !p.is_empty()) {
411 match part.to_ascii_lowercase().as_str() {
412 "ctrl" | "control" => mods.ctrl = true,
413 "shift" => mods.shift = true,
414 "alt" | "option" => mods.alt = true,
415 _ => {
416 if last.is_some() {
417 return None; }
419 last = Some(part);
420 }
421 }
422 }
423 let key = parse_keycode(last?)?;
424 Some(Binding { key, mods })
425}
426
427fn parse_keycode(s: &str) -> Option<KeyCode> {
428 let s_upper = s.to_ascii_uppercase();
431 Some(match s_upper.as_str() {
432 "SPACE" => KeyCode::Space,
433 "ENTER" | "RETURN" => KeyCode::Enter,
434 "TAB" => KeyCode::Tab,
435 "ESCAPE" | "ESC" => KeyCode::Escape,
436 "BACKSPACE" => KeyCode::Backspace,
437 "DELETE" | "DEL" => KeyCode::Delete,
438 "INSERT" | "INS" => KeyCode::Insert,
439 "SCROLLLOCK" | "SCROLL_LOCK" | "SCRLK" => KeyCode::ScrollLock,
440 "PAUSE" => KeyCode::Pause,
441 "LEFT" | "ARROWLEFT" => KeyCode::ArrowLeft,
442 "RIGHT" | "ARROWRIGHT" => KeyCode::ArrowRight,
443 "UP" | "ARROWUP" => KeyCode::ArrowUp,
444 "DOWN" | "ARROWDOWN" => KeyCode::ArrowDown,
445 "EQUAL" | "EQUALS" | "PLUS" | "=" => KeyCode::Equal,
446 "MINUS" | "-" => KeyCode::Minus,
447 "BRACKETLEFT" | "LBRACKET" | "[" => KeyCode::BracketLeft,
448 "BRACKETRIGHT" | "RBRACKET" | "]" => KeyCode::BracketRight,
449 "COMMA" | "," => KeyCode::Comma,
450 "PERIOD" | "." => KeyCode::Period,
451 "SEMICOLON" | ";" => KeyCode::Semicolon,
452 "SLASH" | "/" => KeyCode::Slash,
453 "BACKSLASH" | "\\" => KeyCode::Backslash,
454 "F1" => KeyCode::F1,
455 "F2" => KeyCode::F2,
456 "F3" => KeyCode::F3,
457 "F4" => KeyCode::F4,
458 "F5" => KeyCode::F5,
459 "F6" => KeyCode::F6,
460 "F7" => KeyCode::F7,
461 "F8" => KeyCode::F8,
462 "F9" => KeyCode::F9,
463 "F10" => KeyCode::F10,
464 "F11" => KeyCode::F11,
465 "F12" => KeyCode::F12,
466 other => {
467 if other.len() == 1 {
469 let c = other.chars().next().unwrap();
470 if c.is_ascii_alphabetic() {
471 return letter_to_keycode(c.to_ascii_uppercase());
472 }
473 if c.is_ascii_digit() {
474 return digit_to_keycode(c);
475 }
476 }
477 return None;
478 }
479 })
480}
481
482fn letter_to_keycode(c: char) -> Option<KeyCode> {
483 Some(match c {
484 'A' => KeyCode::KeyA,
485 'B' => KeyCode::KeyB,
486 'C' => KeyCode::KeyC,
487 'D' => KeyCode::KeyD,
488 'E' => KeyCode::KeyE,
489 'F' => KeyCode::KeyF,
490 'G' => KeyCode::KeyG,
491 'H' => KeyCode::KeyH,
492 'I' => KeyCode::KeyI,
493 'J' => KeyCode::KeyJ,
494 'K' => KeyCode::KeyK,
495 'L' => KeyCode::KeyL,
496 'M' => KeyCode::KeyM,
497 'N' => KeyCode::KeyN,
498 'O' => KeyCode::KeyO,
499 'P' => KeyCode::KeyP,
500 'Q' => KeyCode::KeyQ,
501 'R' => KeyCode::KeyR,
502 'S' => KeyCode::KeyS,
503 'T' => KeyCode::KeyT,
504 'U' => KeyCode::KeyU,
505 'V' => KeyCode::KeyV,
506 'W' => KeyCode::KeyW,
507 'X' => KeyCode::KeyX,
508 'Y' => KeyCode::KeyY,
509 'Z' => KeyCode::KeyZ,
510 _ => return None,
511 })
512}
513
514fn digit_to_keycode(c: char) -> Option<KeyCode> {
515 Some(match c {
516 '0' => KeyCode::Digit0,
517 '1' => KeyCode::Digit1,
518 '2' => KeyCode::Digit2,
519 '3' => KeyCode::Digit3,
520 '4' => KeyCode::Digit4,
521 '5' => KeyCode::Digit5,
522 '6' => KeyCode::Digit6,
523 '7' => KeyCode::Digit7,
524 '8' => KeyCode::Digit8,
525 '9' => KeyCode::Digit9,
526 _ => return None,
527 })
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn default_table_covers_md2_essentials() {
536 let km = Keymap::default_table();
537 assert_eq!(
538 km.lookup(Binding::bare(KeyCode::Space)),
539 Some(Action::NextPreset)
540 );
541 assert_eq!(
542 km.lookup(Binding::bare(KeyCode::Backspace)),
543 Some(Action::PreviousPreset)
544 );
545 assert_eq!(
546 km.lookup(Binding::bare(KeyCode::ScrollLock)),
547 Some(Action::TogglePresetLock)
548 );
549 assert_eq!(
550 km.lookup(Binding::new(KeyCode::KeyT, Modifiers::CTRL)),
551 Some(Action::ClearAllOverlays)
552 );
553 assert_eq!(
554 km.lookup(Binding::bare(KeyCode::Equal)),
555 Some(Action::RateUp)
556 );
557 }
558
559 #[test]
560 fn shift_variant_distinct_from_bare() {
561 let km = Keymap::default_table();
562 assert_eq!(
563 km.lookup(Binding::bare(KeyCode::KeyK)),
564 Some(Action::SpriteCycle)
565 );
566 assert_eq!(
567 km.lookup(Binding::new(KeyCode::KeyK, Modifiers::SHIFT)),
568 Some(Action::SpriteRandom)
569 );
570 }
571
572 #[test]
573 fn parse_chord_handles_modifiers_and_letters() {
574 let b = parse_chord("Ctrl+Shift+K").unwrap();
575 assert_eq!(b.key, KeyCode::KeyK);
576 assert!(b.mods.ctrl && b.mods.shift && !b.mods.alt);
577 let b = parse_chord("F5").unwrap();
578 assert_eq!(b.key, KeyCode::F5);
579 assert_eq!(b.mods, Modifiers::NONE);
580 assert!(parse_chord("Bogus+Bogus+K").is_none());
581 assert!(parse_chord("Ctrl+Shift").is_none());
582 }
583
584 #[test]
585 fn settings_override_replaces_default_binding() {
586 let mut s = KeymapSettings::default();
587 s.bindings.insert(
588 Action::TogglePresetLock.as_str().to_string(),
589 "Ctrl+L".into(),
590 );
591 let km = Keymap::from_settings(&s);
592 assert_ne!(
595 km.lookup(Binding::bare(KeyCode::ScrollLock)),
596 Some(Action::TogglePresetLock)
597 );
598 assert_eq!(
599 km.lookup(Binding::new(KeyCode::KeyL, Modifiers::CTRL)),
600 Some(Action::TogglePresetLock)
601 );
602 }
603}