Skip to main content

astrelis_ui/middleware/
keybind.rs

1//! Keybind registry for middleware shortcuts.
2//!
3//! Provides a system for registering and matching keyboard shortcuts
4//! that can trigger middleware actions.
5
6use astrelis_winit::event::KeyCode;
7use bitflags::bitflags;
8
9bitflags! {
10    /// Keyboard modifier flags.
11    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12    pub struct Modifiers: u8 {
13        /// No modifiers pressed.
14        const NONE = 0;
15        /// Shift key is pressed.
16        const SHIFT = 1 << 0;
17        /// Control key is pressed (Cmd on macOS).
18        const CTRL = 1 << 1;
19        /// Alt key is pressed (Option on macOS).
20        const ALT = 1 << 2;
21        /// Super/Meta key (Windows key, Cmd on macOS).
22        const SUPER = 1 << 3;
23    }
24}
25
26impl Modifiers {
27    /// Create modifiers from individual key states.
28    pub fn from_keys(shift: bool, ctrl: bool, alt: bool, super_key: bool) -> Self {
29        let mut mods = Modifiers::NONE;
30        if shift {
31            mods |= Modifiers::SHIFT;
32        }
33        if ctrl {
34            mods |= Modifiers::CTRL;
35        }
36        if alt {
37            mods |= Modifiers::ALT;
38        }
39        if super_key {
40            mods |= Modifiers::SUPER;
41        }
42        mods
43    }
44}
45
46/// A keyboard shortcut definition.
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct Keybind {
49    /// The key that triggers this keybind.
50    pub key: KeyCode,
51    /// Required modifier keys.
52    pub modifiers: Modifiers,
53    /// Human-readable description of what this keybind does.
54    pub description: &'static str,
55}
56
57impl Keybind {
58    /// Create a new keybind.
59    pub fn new(key: KeyCode, modifiers: Modifiers, description: &'static str) -> Self {
60        Self {
61            key,
62            modifiers,
63            description,
64        }
65    }
66
67    /// Create a keybind with no modifiers.
68    pub fn key(key: KeyCode, description: &'static str) -> Self {
69        Self::new(key, Modifiers::NONE, description)
70    }
71
72    /// Create a keybind with Ctrl modifier.
73    pub fn ctrl(key: KeyCode, description: &'static str) -> Self {
74        Self::new(key, Modifiers::CTRL, description)
75    }
76
77    /// Create a keybind with Shift modifier.
78    pub fn shift(key: KeyCode, description: &'static str) -> Self {
79        Self::new(key, Modifiers::SHIFT, description)
80    }
81
82    /// Create a keybind with Ctrl+Shift modifiers.
83    pub fn ctrl_shift(key: KeyCode, description: &'static str) -> Self {
84        Self::new(key, Modifiers::CTRL | Modifiers::SHIFT, description)
85    }
86
87    /// Check if this keybind matches the given key and modifiers.
88    pub fn matches(&self, key: KeyCode, modifiers: Modifiers) -> bool {
89        self.key == key && self.modifiers == modifiers
90    }
91
92    /// Format this keybind as a human-readable string.
93    pub fn to_string_short(&self) -> String {
94        let mut parts = Vec::new();
95
96        if self.modifiers.contains(Modifiers::CTRL) {
97            #[cfg(target_os = "macos")]
98            parts.push("⌘");
99            #[cfg(not(target_os = "macos"))]
100            parts.push("Ctrl");
101        }
102        if self.modifiers.contains(Modifiers::ALT) {
103            #[cfg(target_os = "macos")]
104            parts.push("⌥");
105            #[cfg(not(target_os = "macos"))]
106            parts.push("Alt");
107        }
108        if self.modifiers.contains(Modifiers::SHIFT) {
109            #[cfg(target_os = "macos")]
110            parts.push("⇧");
111            #[cfg(not(target_os = "macos"))]
112            parts.push("Shift");
113        }
114        if self.modifiers.contains(Modifiers::SUPER) {
115            #[cfg(target_os = "macos")]
116            parts.push("⌘");
117            #[cfg(not(target_os = "macos"))]
118            parts.push("Win");
119        }
120
121        parts.push(key_code_name(self.key));
122
123        parts.join("+")
124    }
125}
126
127/// Registry of keybinds for middlewares.
128#[derive(Debug, Default)]
129pub struct KeybindRegistry {
130    /// Registered keybinds: (middleware_name, keybind, priority)
131    keybinds: Vec<(&'static str, Keybind, i32)>,
132}
133
134impl KeybindRegistry {
135    /// Create a new empty keybind registry.
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Register a keybind for a middleware.
141    ///
142    /// Priority determines which middleware handles conflicts (higher wins).
143    pub fn register(&mut self, middleware: &'static str, keybind: Keybind, priority: i32) {
144        self.keybinds.push((middleware, keybind, priority));
145        // Sort by priority descending so higher priority keybinds are checked first
146        self.keybinds.sort_by(|a, b| b.2.cmp(&a.2));
147    }
148
149    /// Unregister all keybinds for a middleware.
150    pub fn unregister(&mut self, middleware: &str) {
151        self.keybinds.retain(|(name, _, _)| *name != middleware);
152    }
153
154    /// Find all keybinds that match the given key and modifiers.
155    ///
156    /// Returns matches in priority order (highest first).
157    pub fn find_matches(&self, key: KeyCode, modifiers: Modifiers) -> Vec<(&str, &Keybind)> {
158        self.keybinds
159            .iter()
160            .filter(|(_, keybind, _)| keybind.matches(key, modifiers))
161            .map(|(name, keybind, _)| (*name, keybind))
162            .collect()
163    }
164
165    /// Get all registered keybinds for a middleware.
166    pub fn get_keybinds(&self, middleware: &'static str) -> Vec<&Keybind> {
167        self.keybinds
168            .iter()
169            .filter(|(name, _, _)| *name == middleware)
170            .map(|(_, keybind, _)| keybind)
171            .collect()
172    }
173
174    /// Get all registered keybinds.
175    pub fn all_keybinds(&self) -> impl Iterator<Item = (&'static str, &Keybind)> {
176        self.keybinds
177            .iter()
178            .map(|(name, keybind, _)| (*name, keybind))
179    }
180
181    /// Clear all registered keybinds.
182    pub fn clear(&mut self) {
183        self.keybinds.clear();
184    }
185}
186
187/// Get a human-readable name for a key code.
188fn key_code_name(key: KeyCode) -> &'static str {
189    match key {
190        KeyCode::Escape => "Esc",
191        KeyCode::F1 => "F1",
192        KeyCode::F2 => "F2",
193        KeyCode::F3 => "F3",
194        KeyCode::F4 => "F4",
195        KeyCode::F5 => "F5",
196        KeyCode::F6 => "F6",
197        KeyCode::F7 => "F7",
198        KeyCode::F8 => "F8",
199        KeyCode::F9 => "F9",
200        KeyCode::F10 => "F10",
201        KeyCode::F11 => "F11",
202        KeyCode::F12 => "F12",
203        KeyCode::Backquote => "`",
204        KeyCode::Digit1 => "1",
205        KeyCode::Digit2 => "2",
206        KeyCode::Digit3 => "3",
207        KeyCode::Digit4 => "4",
208        KeyCode::Digit5 => "5",
209        KeyCode::Digit6 => "6",
210        KeyCode::Digit7 => "7",
211        KeyCode::Digit8 => "8",
212        KeyCode::Digit9 => "9",
213        KeyCode::Digit0 => "0",
214        KeyCode::Minus => "-",
215        KeyCode::Equal => "=",
216        KeyCode::Backspace => "Backspace",
217        KeyCode::Tab => "Tab",
218        KeyCode::KeyQ => "Q",
219        KeyCode::KeyW => "W",
220        KeyCode::KeyE => "E",
221        KeyCode::KeyR => "R",
222        KeyCode::KeyT => "T",
223        KeyCode::KeyY => "Y",
224        KeyCode::KeyU => "U",
225        KeyCode::KeyI => "I",
226        KeyCode::KeyO => "O",
227        KeyCode::KeyP => "P",
228        KeyCode::BracketLeft => "[",
229        KeyCode::BracketRight => "]",
230        KeyCode::Backslash => "\\",
231        KeyCode::KeyA => "A",
232        KeyCode::KeyS => "S",
233        KeyCode::KeyD => "D",
234        KeyCode::KeyF => "F",
235        KeyCode::KeyG => "G",
236        KeyCode::KeyH => "H",
237        KeyCode::KeyJ => "J",
238        KeyCode::KeyK => "K",
239        KeyCode::KeyL => "L",
240        KeyCode::Semicolon => ";",
241        KeyCode::Quote => "'",
242        KeyCode::Enter => "Enter",
243        KeyCode::KeyZ => "Z",
244        KeyCode::KeyX => "X",
245        KeyCode::KeyC => "C",
246        KeyCode::KeyV => "V",
247        KeyCode::KeyB => "B",
248        KeyCode::KeyN => "N",
249        KeyCode::KeyM => "M",
250        KeyCode::Comma => ",",
251        KeyCode::Period => ".",
252        KeyCode::Slash => "/",
253        KeyCode::Space => "Space",
254        KeyCode::ArrowUp => "↑",
255        KeyCode::ArrowDown => "↓",
256        KeyCode::ArrowLeft => "←",
257        KeyCode::ArrowRight => "→",
258        KeyCode::Home => "Home",
259        KeyCode::End => "End",
260        KeyCode::PageUp => "PgUp",
261        KeyCode::PageDown => "PgDn",
262        KeyCode::Insert => "Ins",
263        KeyCode::Delete => "Del",
264        _ => "?",
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_keybind_creation() {
274        let kb = Keybind::key(KeyCode::F12, "Toggle inspector");
275        assert_eq!(kb.key, KeyCode::F12);
276        assert_eq!(kb.modifiers, Modifiers::NONE);
277        assert_eq!(kb.description, "Toggle inspector");
278    }
279
280    #[test]
281    fn test_keybind_with_modifiers() {
282        let kb = Keybind::ctrl_shift(KeyCode::KeyI, "Open inspector");
283        assert!(kb.modifiers.contains(Modifiers::CTRL));
284        assert!(kb.modifiers.contains(Modifiers::SHIFT));
285        assert!(!kb.modifiers.contains(Modifiers::ALT));
286    }
287
288    #[test]
289    fn test_keybind_matching() {
290        let kb = Keybind::ctrl(KeyCode::KeyS, "Save");
291
292        assert!(kb.matches(KeyCode::KeyS, Modifiers::CTRL));
293        assert!(!kb.matches(KeyCode::KeyS, Modifiers::NONE));
294        assert!(!kb.matches(KeyCode::KeyS, Modifiers::CTRL | Modifiers::SHIFT));
295        assert!(!kb.matches(KeyCode::KeyA, Modifiers::CTRL));
296    }
297
298    #[test]
299    fn test_registry_operations() {
300        let mut registry = KeybindRegistry::new();
301
302        registry.register("inspector", Keybind::key(KeyCode::F12, "Toggle"), 100);
303        registry.register("inspector", Keybind::key(KeyCode::F5, "Freeze"), 100);
304        registry.register("profiler", Keybind::key(KeyCode::F11, "Profile"), 50);
305
306        // Find matches
307        let matches = registry.find_matches(KeyCode::F12, Modifiers::NONE);
308        assert_eq!(matches.len(), 1);
309        assert_eq!(matches[0].0, "inspector");
310
311        // Get keybinds for middleware
312        let inspector_binds = registry.get_keybinds("inspector");
313        assert_eq!(inspector_binds.len(), 2);
314
315        // Unregister
316        registry.unregister("inspector");
317        assert!(registry.get_keybinds("inspector").is_empty());
318        assert_eq!(registry.get_keybinds("profiler").len(), 1);
319    }
320
321    #[test]
322    fn test_registry_priority_ordering() {
323        let mut registry = KeybindRegistry::new();
324
325        // Register same key with different priorities
326        registry.register("low", Keybind::key(KeyCode::F1, "Low priority"), 10);
327        registry.register("high", Keybind::key(KeyCode::F1, "High priority"), 100);
328        registry.register("medium", Keybind::key(KeyCode::F1, "Medium priority"), 50);
329
330        let matches = registry.find_matches(KeyCode::F1, Modifiers::NONE);
331        assert_eq!(matches.len(), 3);
332        assert_eq!(matches[0].0, "high");
333        assert_eq!(matches[1].0, "medium");
334        assert_eq!(matches[2].0, "low");
335    }
336
337    #[test]
338    fn test_keybind_to_string() {
339        let simple = Keybind::key(KeyCode::F12, "Test");
340        assert_eq!(simple.to_string_short(), "F12");
341
342        let _with_ctrl = Keybind::ctrl(KeyCode::KeyS, "Save");
343        #[cfg(not(target_os = "macos"))]
344        assert_eq!(_with_ctrl.to_string_short(), "Ctrl+S");
345
346        let _with_shift = Keybind::ctrl_shift(KeyCode::KeyZ, "Redo");
347        #[cfg(not(target_os = "macos"))]
348        assert_eq!(_with_shift.to_string_short(), "Ctrl+Shift+Z");
349    }
350
351    #[test]
352    fn test_modifiers_bitflags() {
353        let mods = Modifiers::CTRL | Modifiers::SHIFT;
354        assert!(mods.contains(Modifiers::CTRL));
355        assert!(mods.contains(Modifiers::SHIFT));
356        assert!(!mods.contains(Modifiers::ALT));
357    }
358}