Skip to main content

presentar_core/
shortcut.rs

1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2//! Keyboard shortcut management system.
3//!
4//! This module provides:
5//! - Keyboard shortcut registration and handling
6//! - Modifier key support (Ctrl, Alt, Shift, Meta)
7//! - Context-aware shortcuts (global, focused widget, etc.)
8//! - Shortcut conflict detection
9
10use crate::event::Key;
11use crate::widget::WidgetId;
12use std::collections::HashMap;
13
14/// Modifier keys for keyboard shortcuts.
15#[derive(
16    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
17)]
18pub struct Modifiers {
19    /// Control key (Cmd on Mac).
20    pub ctrl: bool,
21    /// Alt key (Option on Mac).
22    pub alt: bool,
23    /// Shift key.
24    pub shift: bool,
25    /// Meta key (Windows key, Cmd on Mac).
26    pub meta: bool,
27}
28
29impl Modifiers {
30    /// No modifiers.
31    pub const NONE: Self = Self {
32        ctrl: false,
33        alt: false,
34        shift: false,
35        meta: false,
36    };
37
38    /// Ctrl only.
39    pub const CTRL: Self = Self {
40        ctrl: true,
41        alt: false,
42        shift: false,
43        meta: false,
44    };
45
46    /// Alt only.
47    pub const ALT: Self = Self {
48        ctrl: false,
49        alt: true,
50        shift: false,
51        meta: false,
52    };
53
54    /// Shift only.
55    pub const SHIFT: Self = Self {
56        ctrl: false,
57        alt: false,
58        shift: true,
59        meta: false,
60    };
61
62    /// Meta only.
63    pub const META: Self = Self {
64        ctrl: false,
65        alt: false,
66        shift: false,
67        meta: true,
68    };
69
70    /// Ctrl+Shift.
71    pub const CTRL_SHIFT: Self = Self {
72        ctrl: true,
73        alt: false,
74        shift: true,
75        meta: false,
76    };
77
78    /// Ctrl+Alt.
79    pub const CTRL_ALT: Self = Self {
80        ctrl: true,
81        alt: true,
82        shift: false,
83        meta: false,
84    };
85
86    /// Create custom modifiers.
87    pub const fn new(ctrl: bool, alt: bool, shift: bool, meta: bool) -> Self {
88        Self {
89            ctrl,
90            alt,
91            shift,
92            meta,
93        }
94    }
95
96    /// Check if any modifier is pressed.
97    pub const fn any(&self) -> bool {
98        self.ctrl || self.alt || self.shift || self.meta
99    }
100
101    /// Check if no modifier is pressed.
102    pub const fn none(&self) -> bool {
103        !self.any()
104    }
105
106    /// Get a display string for the modifiers.
107    pub fn display(&self) -> String {
108        let mut parts = Vec::new();
109        if self.ctrl {
110            parts.push("Ctrl");
111        }
112        if self.alt {
113            parts.push("Alt");
114        }
115        if self.shift {
116            parts.push("Shift");
117        }
118        if self.meta {
119            parts.push("Meta");
120        }
121        parts.join("+")
122    }
123}
124
125/// A keyboard shortcut (key + modifiers).
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
127pub struct Shortcut {
128    /// The key.
129    pub key: Key,
130    /// Modifier keys.
131    pub modifiers: Modifiers,
132}
133
134impl Shortcut {
135    /// Create a new shortcut.
136    pub const fn new(key: Key, modifiers: Modifiers) -> Self {
137        Self { key, modifiers }
138    }
139
140    /// Create a shortcut with no modifiers.
141    pub const fn key(key: Key) -> Self {
142        Self::new(key, Modifiers::NONE)
143    }
144
145    /// Create a shortcut with Ctrl modifier.
146    pub const fn ctrl(key: Key) -> Self {
147        Self::new(key, Modifiers::CTRL)
148    }
149
150    /// Create a shortcut with Alt modifier.
151    pub const fn alt(key: Key) -> Self {
152        Self::new(key, Modifiers::ALT)
153    }
154
155    /// Create a shortcut with Shift modifier.
156    pub const fn shift(key: Key) -> Self {
157        Self::new(key, Modifiers::SHIFT)
158    }
159
160    /// Create a shortcut with Ctrl+Shift modifiers.
161    pub const fn ctrl_shift(key: Key) -> Self {
162        Self::new(key, Modifiers::CTRL_SHIFT)
163    }
164
165    /// Get a display string for the shortcut.
166    pub fn display(&self) -> String {
167        let key_name = format!("{:?}", self.key);
168        if self.modifiers.none() {
169            key_name
170        } else {
171            format!("{}+{}", self.modifiers.display(), key_name)
172        }
173    }
174
175    /// Common shortcuts
176    pub const COPY: Self = Self::ctrl(Key::C);
177    pub const CUT: Self = Self::ctrl(Key::X);
178    pub const PASTE: Self = Self::ctrl(Key::V);
179    pub const UNDO: Self = Self::ctrl(Key::Z);
180    pub const REDO: Self = Self::ctrl_shift(Key::Z);
181    pub const SAVE: Self = Self::ctrl(Key::S);
182    pub const SELECT_ALL: Self = Self::ctrl(Key::A);
183    pub const FIND: Self = Self::ctrl(Key::F);
184    pub const ESCAPE: Self = Self::key(Key::Escape);
185    pub const ENTER: Self = Self::key(Key::Enter);
186    pub const TAB: Self = Self::key(Key::Tab);
187    pub const DELETE: Self = Self::key(Key::Delete);
188    pub const BACKSPACE: Self = Self::key(Key::Backspace);
189}
190
191/// Unique ID for a shortcut binding.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
193pub struct ShortcutId(pub u64);
194
195impl ShortcutId {
196    /// Create a new shortcut ID.
197    pub const fn new(id: u64) -> Self {
198        Self(id)
199    }
200}
201
202/// Context in which a shortcut is active.
203#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
204pub enum ShortcutContext {
205    /// Global - active everywhere.
206    #[default]
207    Global,
208    /// Only when a specific widget has focus.
209    Widget(WidgetId),
210    /// Only when a widget type has focus.
211    WidgetType(String),
212    /// Custom context identified by name.
213    Custom(String),
214}
215
216/// Priority for shortcut resolution when multiple match.
217#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
218pub enum ShortcutPriority {
219    /// Low priority - checked last.
220    Low = 0,
221    /// Normal priority.
222    #[default]
223    Normal = 1,
224    /// High priority - checked first.
225    High = 2,
226}
227
228/// Callback for shortcut handling.
229pub type ShortcutHandler = Box<dyn FnMut() -> bool + Send>;
230
231/// Registration for a shortcut binding.
232struct ShortcutBinding {
233    #[allow(dead_code)]
234    id: ShortcutId,
235    shortcut: Shortcut,
236    context: ShortcutContext,
237    priority: ShortcutPriority,
238    description: String,
239    enabled: bool,
240}
241
242/// Manager for keyboard shortcuts.
243pub struct ShortcutManager {
244    /// Next binding ID.
245    next_id: u64,
246    /// Registered bindings.
247    bindings: HashMap<ShortcutId, ShortcutBinding>,
248    /// Handlers (separate for mutability).
249    handlers: HashMap<ShortcutId, ShortcutHandler>,
250    /// Index by shortcut for fast lookup.
251    by_shortcut: HashMap<Shortcut, Vec<ShortcutId>>,
252    /// Current active contexts.
253    active_contexts: Vec<ShortcutContext>,
254    /// Current modifier state.
255    modifiers: Modifiers,
256}
257
258impl ShortcutManager {
259    /// Create a new shortcut manager.
260    pub fn new() -> Self {
261        Self {
262            next_id: 0,
263            bindings: HashMap::new(),
264            handlers: HashMap::new(),
265            by_shortcut: HashMap::new(),
266            active_contexts: vec![ShortcutContext::Global],
267            modifiers: Modifiers::NONE,
268        }
269    }
270
271    /// Register a shortcut.
272    pub fn register(&mut self, shortcut: Shortcut, handler: ShortcutHandler) -> ShortcutId {
273        self.register_with_options(
274            shortcut,
275            handler,
276            ShortcutContext::Global,
277            ShortcutPriority::Normal,
278            "",
279        )
280    }
281
282    /// Register a shortcut with full options.
283    pub fn register_with_options(
284        &mut self,
285        shortcut: Shortcut,
286        handler: ShortcutHandler,
287        context: ShortcutContext,
288        priority: ShortcutPriority,
289        description: &str,
290    ) -> ShortcutId {
291        let id = ShortcutId::new(self.next_id);
292        self.next_id += 1;
293
294        let binding = ShortcutBinding {
295            id,
296            shortcut,
297            context,
298            priority,
299            description: description.to_string(),
300            enabled: true,
301        };
302
303        self.bindings.insert(id, binding);
304        self.handlers.insert(id, handler);
305
306        self.by_shortcut.entry(shortcut).or_default().push(id);
307
308        id
309    }
310
311    /// Unregister a shortcut.
312    pub fn unregister(&mut self, id: ShortcutId) -> bool {
313        if let Some(binding) = self.bindings.remove(&id) {
314            self.handlers.remove(&id);
315
316            if let Some(ids) = self.by_shortcut.get_mut(&binding.shortcut) {
317                ids.retain(|&i| i != id);
318            }
319
320            true
321        } else {
322            false
323        }
324    }
325
326    /// Enable or disable a shortcut.
327    pub fn set_enabled(&mut self, id: ShortcutId, enabled: bool) {
328        if let Some(binding) = self.bindings.get_mut(&id) {
329            binding.enabled = enabled;
330        }
331    }
332
333    /// Check if a shortcut is enabled.
334    pub fn is_enabled(&self, id: ShortcutId) -> bool {
335        self.bindings.get(&id).is_some_and(|b| b.enabled)
336    }
337
338    /// Set the current modifier state.
339    pub fn set_modifiers(&mut self, modifiers: Modifiers) {
340        self.modifiers = modifiers;
341    }
342
343    /// Get the current modifier state.
344    pub fn modifiers(&self) -> Modifiers {
345        self.modifiers
346    }
347
348    /// Push an active context.
349    pub fn push_context(&mut self, context: ShortcutContext) {
350        self.active_contexts.push(context);
351    }
352
353    /// Pop the most recent context.
354    pub fn pop_context(&mut self) -> Option<ShortcutContext> {
355        if self.active_contexts.len() > 1 {
356            self.active_contexts.pop()
357        } else {
358            None
359        }
360    }
361
362    /// Set the focused widget context.
363    pub fn set_focused_widget(&mut self, widget_id: Option<WidgetId>) {
364        // Remove any existing widget context
365        self.active_contexts
366            .retain(|c| !matches!(c, ShortcutContext::Widget(_)));
367
368        if let Some(id) = widget_id {
369            self.active_contexts.push(ShortcutContext::Widget(id));
370        }
371    }
372
373    /// Handle a key press and trigger matching shortcuts.
374    /// Returns true if a shortcut was triggered.
375    pub fn handle_key(&mut self, key: Key) -> bool {
376        let shortcut = Shortcut::new(key, self.modifiers);
377        self.trigger(shortcut)
378    }
379
380    /// Trigger a shortcut directly.
381    pub fn trigger(&mut self, shortcut: Shortcut) -> bool {
382        let binding_ids = match self.by_shortcut.get(&shortcut) {
383            Some(ids) => ids.clone(),
384            None => return false,
385        };
386
387        // Collect matching bindings with their priorities
388        let mut matches: Vec<(ShortcutId, ShortcutPriority)> = binding_ids
389            .iter()
390            .filter_map(|&id| {
391                let binding = self.bindings.get(&id)?;
392                if !binding.enabled {
393                    return None;
394                }
395                if self.is_context_active(&binding.context) {
396                    Some((id, binding.priority))
397                } else {
398                    None
399                }
400            })
401            .collect();
402
403        // Sort by priority (highest first)
404        matches.sort_by(|a, b| b.1.cmp(&a.1));
405
406        // Try handlers in priority order
407        for (id, _) in matches {
408            if let Some(handler) = self.handlers.get_mut(&id) {
409                if handler() {
410                    return true;
411                }
412            }
413        }
414
415        false
416    }
417
418    /// Check if a context is currently active.
419    fn is_context_active(&self, context: &ShortcutContext) -> bool {
420        match context {
421            ShortcutContext::Global => true,
422            other => self.active_contexts.contains(other),
423        }
424    }
425
426    /// Get all registered shortcuts.
427    pub fn shortcuts(&self) -> impl Iterator<Item = (&Shortcut, &str)> {
428        self.bindings
429            .values()
430            .map(|b| (&b.shortcut, b.description.as_str()))
431    }
432
433    /// Get binding count.
434    pub fn binding_count(&self) -> usize {
435        self.bindings.len()
436    }
437
438    /// Check for shortcut conflicts (same shortcut, same context).
439    pub fn find_conflicts(&self) -> Vec<(Shortcut, Vec<ShortcutId>)> {
440        let mut conflicts = Vec::new();
441
442        for (shortcut, ids) in &self.by_shortcut {
443            if ids.len() < 2 {
444                continue;
445            }
446
447            // Group by context
448            let mut by_context: HashMap<&ShortcutContext, Vec<ShortcutId>> = HashMap::new();
449            for &id in ids {
450                if let Some(binding) = self.bindings.get(&id) {
451                    by_context.entry(&binding.context).or_default().push(id);
452                }
453            }
454
455            // Find contexts with multiple bindings
456            for (_, context_ids) in by_context {
457                if context_ids.len() > 1 {
458                    conflicts.push((*shortcut, context_ids));
459                }
460            }
461        }
462
463        conflicts
464    }
465
466    /// Clear all shortcuts.
467    pub fn clear(&mut self) {
468        self.bindings.clear();
469        self.handlers.clear();
470        self.by_shortcut.clear();
471    }
472
473    /// Get the description for a shortcut binding.
474    pub fn description(&self, id: ShortcutId) -> Option<&str> {
475        self.bindings.get(&id).map(|b| b.description.as_str())
476    }
477}
478
479impl Default for ShortcutManager {
480    fn default() -> Self {
481        Self::new()
482    }
483}
484
485impl std::fmt::Debug for ShortcutManager {
486    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
487        f.debug_struct("ShortcutManager")
488            .field("binding_count", &self.bindings.len())
489            .field("active_contexts", &self.active_contexts)
490            .field("modifiers", &self.modifiers)
491            .finish()
492    }
493}
494
495/// Builder for creating shortcuts with fluent API.
496#[derive(Debug, Clone)]
497pub struct ShortcutBuilder {
498    key: Key,
499    modifiers: Modifiers,
500    context: ShortcutContext,
501    priority: ShortcutPriority,
502    description: String,
503}
504
505impl ShortcutBuilder {
506    /// Create a new builder.
507    pub fn new(key: Key) -> Self {
508        Self {
509            key,
510            modifiers: Modifiers::NONE,
511            context: ShortcutContext::Global,
512            priority: ShortcutPriority::Normal,
513            description: String::new(),
514        }
515    }
516
517    /// Add Ctrl modifier.
518    pub fn ctrl(mut self) -> Self {
519        self.modifiers.ctrl = true;
520        self
521    }
522
523    /// Add Alt modifier.
524    pub fn alt(mut self) -> Self {
525        self.modifiers.alt = true;
526        self
527    }
528
529    /// Add Shift modifier.
530    pub fn shift(mut self) -> Self {
531        self.modifiers.shift = true;
532        self
533    }
534
535    /// Add Meta modifier.
536    pub fn meta(mut self) -> Self {
537        self.modifiers.meta = true;
538        self
539    }
540
541    /// Set context.
542    pub fn context(mut self, context: ShortcutContext) -> Self {
543        self.context = context;
544        self
545    }
546
547    /// Set context to a specific widget.
548    pub fn for_widget(mut self, widget_id: WidgetId) -> Self {
549        self.context = ShortcutContext::Widget(widget_id);
550        self
551    }
552
553    /// Set priority.
554    pub fn priority(mut self, priority: ShortcutPriority) -> Self {
555        self.priority = priority;
556        self
557    }
558
559    /// Set description.
560    pub fn description(mut self, desc: &str) -> Self {
561        self.description = desc.to_string();
562        self
563    }
564
565    /// Build the shortcut.
566    pub fn build(self) -> Shortcut {
567        Shortcut::new(self.key, self.modifiers)
568    }
569
570    /// Register with a manager.
571    pub fn register(self, manager: &mut ShortcutManager, handler: ShortcutHandler) -> ShortcutId {
572        let shortcut = Shortcut::new(self.key, self.modifiers);
573        manager.register_with_options(
574            shortcut,
575            handler,
576            self.context,
577            self.priority,
578            &self.description,
579        )
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
587    use std::sync::Arc;
588
589    // Modifiers tests
590    #[allow(clippy::assertions_on_constants)]
591    #[test]
592    fn test_modifiers_constants() {
593        assert!(!Modifiers::NONE.any());
594        assert!(Modifiers::NONE.none());
595
596        assert!(Modifiers::CTRL.ctrl);
597        assert!(!Modifiers::CTRL.alt);
598
599        assert!(Modifiers::CTRL_SHIFT.ctrl);
600        assert!(Modifiers::CTRL_SHIFT.shift);
601    }
602
603    #[test]
604    fn test_modifiers_new() {
605        let mods = Modifiers::new(true, true, false, false);
606        assert!(mods.ctrl);
607        assert!(mods.alt);
608        assert!(!mods.shift);
609        assert!(!mods.meta);
610    }
611
612    #[test]
613    fn test_modifiers_display() {
614        assert_eq!(Modifiers::NONE.display(), "");
615        assert_eq!(Modifiers::CTRL.display(), "Ctrl");
616        assert_eq!(Modifiers::CTRL_SHIFT.display(), "Ctrl+Shift");
617    }
618
619    // Shortcut tests
620    #[test]
621    fn test_shortcut_new() {
622        let shortcut = Shortcut::new(Key::A, Modifiers::CTRL);
623        assert_eq!(shortcut.key, Key::A);
624        assert!(shortcut.modifiers.ctrl);
625    }
626
627    #[test]
628    fn test_shortcut_constructors() {
629        let key_only = Shortcut::key(Key::Escape);
630        assert!(key_only.modifiers.none());
631
632        let ctrl = Shortcut::ctrl(Key::S);
633        assert!(ctrl.modifiers.ctrl);
634
635        let alt = Shortcut::alt(Key::F4);
636        assert!(alt.modifiers.alt);
637
638        let shift = Shortcut::shift(Key::Tab);
639        assert!(shift.modifiers.shift);
640
641        let ctrl_shift = Shortcut::ctrl_shift(Key::Z);
642        assert!(ctrl_shift.modifiers.ctrl);
643        assert!(ctrl_shift.modifiers.shift);
644    }
645
646    #[test]
647    fn test_shortcut_display() {
648        assert_eq!(Shortcut::key(Key::A).display(), "A");
649        assert_eq!(Shortcut::ctrl(Key::S).display(), "Ctrl+S");
650        assert_eq!(Shortcut::ctrl_shift(Key::Z).display(), "Ctrl+Shift+Z");
651    }
652
653    #[test]
654    fn test_shortcut_constants() {
655        assert_eq!(Shortcut::COPY, Shortcut::ctrl(Key::C));
656        assert_eq!(Shortcut::UNDO, Shortcut::ctrl(Key::Z));
657        assert_eq!(Shortcut::REDO, Shortcut::ctrl_shift(Key::Z));
658    }
659
660    #[test]
661    fn test_shortcut_equality() {
662        let s1 = Shortcut::ctrl(Key::S);
663        let s2 = Shortcut::ctrl(Key::S);
664        let s3 = Shortcut::ctrl(Key::A);
665
666        assert_eq!(s1, s2);
667        assert_ne!(s1, s3);
668    }
669
670    // ShortcutId tests
671    #[test]
672    fn test_shortcut_id() {
673        let id1 = ShortcutId::new(1);
674        let id2 = ShortcutId::new(1);
675        let id3 = ShortcutId::new(2);
676
677        assert_eq!(id1, id2);
678        assert_ne!(id1, id3);
679    }
680
681    // ShortcutContext tests
682    #[test]
683    fn test_shortcut_context_default() {
684        assert_eq!(ShortcutContext::default(), ShortcutContext::Global);
685    }
686
687    // ShortcutPriority tests
688    #[test]
689    fn test_shortcut_priority_ordering() {
690        assert!(ShortcutPriority::High > ShortcutPriority::Normal);
691        assert!(ShortcutPriority::Normal > ShortcutPriority::Low);
692    }
693
694    // ShortcutManager tests
695    #[test]
696    fn test_manager_new() {
697        let manager = ShortcutManager::new();
698        assert_eq!(manager.binding_count(), 0);
699    }
700
701    #[test]
702    fn test_manager_register() {
703        let mut manager = ShortcutManager::new();
704
705        let id = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
706        assert_eq!(manager.binding_count(), 1);
707        assert!(manager.is_enabled(id));
708    }
709
710    #[test]
711    fn test_manager_unregister() {
712        let mut manager = ShortcutManager::new();
713
714        let id = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
715        assert_eq!(manager.binding_count(), 1);
716
717        let removed = manager.unregister(id);
718        assert!(removed);
719        assert_eq!(manager.binding_count(), 0);
720    }
721
722    #[test]
723    fn test_manager_set_enabled() {
724        let mut manager = ShortcutManager::new();
725
726        let id = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
727        assert!(manager.is_enabled(id));
728
729        manager.set_enabled(id, false);
730        assert!(!manager.is_enabled(id));
731
732        manager.set_enabled(id, true);
733        assert!(manager.is_enabled(id));
734    }
735
736    #[test]
737    fn test_manager_handle_key() {
738        let triggered = Arc::new(AtomicBool::new(false));
739        let triggered_clone = triggered.clone();
740
741        let mut manager = ShortcutManager::new();
742        manager.register(
743            Shortcut::ctrl(Key::S),
744            Box::new(move || {
745                triggered_clone.store(true, Ordering::SeqCst);
746                true
747            }),
748        );
749
750        // Without Ctrl, should not trigger
751        manager.set_modifiers(Modifiers::NONE);
752        let result = manager.handle_key(Key::S);
753        assert!(!result);
754        assert!(!triggered.load(Ordering::SeqCst));
755
756        // With Ctrl, should trigger
757        manager.set_modifiers(Modifiers::CTRL);
758        let result = manager.handle_key(Key::S);
759        assert!(result);
760        assert!(triggered.load(Ordering::SeqCst));
761    }
762
763    #[test]
764    fn test_manager_trigger() {
765        let counter = Arc::new(AtomicUsize::new(0));
766        let counter_clone = counter.clone();
767
768        let mut manager = ShortcutManager::new();
769        manager.register(
770            Shortcut::ctrl(Key::C),
771            Box::new(move || {
772                counter_clone.fetch_add(1, Ordering::SeqCst);
773                true
774            }),
775        );
776
777        manager.trigger(Shortcut::ctrl(Key::C));
778        assert_eq!(counter.load(Ordering::SeqCst), 1);
779
780        manager.trigger(Shortcut::ctrl(Key::C));
781        assert_eq!(counter.load(Ordering::SeqCst), 2);
782    }
783
784    #[test]
785    fn test_manager_disabled_shortcut_not_triggered() {
786        let triggered = Arc::new(AtomicBool::new(false));
787        let triggered_clone = triggered.clone();
788
789        let mut manager = ShortcutManager::new();
790        let id = manager.register(
791            Shortcut::ctrl(Key::S),
792            Box::new(move || {
793                triggered_clone.store(true, Ordering::SeqCst);
794                true
795            }),
796        );
797
798        manager.set_enabled(id, false);
799        manager.set_modifiers(Modifiers::CTRL);
800
801        let result = manager.handle_key(Key::S);
802        assert!(!result);
803        assert!(!triggered.load(Ordering::SeqCst));
804    }
805
806    #[test]
807    fn test_manager_context() {
808        let triggered = Arc::new(AtomicBool::new(false));
809        let triggered_clone = triggered.clone();
810
811        let mut manager = ShortcutManager::new();
812        manager.register_with_options(
813            Shortcut::ctrl(Key::S),
814            Box::new(move || {
815                triggered_clone.store(true, Ordering::SeqCst);
816                true
817            }),
818            ShortcutContext::Widget(WidgetId::new(1)),
819            ShortcutPriority::Normal,
820            "",
821        );
822
823        // Without widget context, should not trigger
824        manager.set_modifiers(Modifiers::CTRL);
825        let result = manager.handle_key(Key::S);
826        assert!(!result);
827
828        // With widget context, should trigger
829        manager.set_focused_widget(Some(WidgetId::new(1)));
830        let result = manager.handle_key(Key::S);
831        assert!(result);
832        assert!(triggered.load(Ordering::SeqCst));
833    }
834
835    #[test]
836    fn test_manager_priority() {
837        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
838        let order1 = order.clone();
839        let order2 = order.clone();
840
841        let mut manager = ShortcutManager::new();
842
843        manager.register_with_options(
844            Shortcut::ctrl(Key::S),
845            Box::new(move || {
846                order1.lock().unwrap().push("low");
847                false // Don't consume, let next handler run
848            }),
849            ShortcutContext::Global,
850            ShortcutPriority::Low,
851            "",
852        );
853
854        manager.register_with_options(
855            Shortcut::ctrl(Key::S),
856            Box::new(move || {
857                order2.lock().unwrap().push("high");
858                false
859            }),
860            ShortcutContext::Global,
861            ShortcutPriority::High,
862            "",
863        );
864
865        manager.trigger(Shortcut::ctrl(Key::S));
866
867        let order_vec = order.lock().unwrap();
868        assert_eq!(*order_vec, vec!["high", "low"]);
869    }
870
871    #[test]
872    fn test_manager_handler_consumes() {
873        let counter = Arc::new(AtomicUsize::new(0));
874        let c1 = counter.clone();
875        let c2 = counter.clone();
876
877        let mut manager = ShortcutManager::new();
878
879        manager.register_with_options(
880            Shortcut::ctrl(Key::S),
881            Box::new(move || {
882                c1.fetch_add(1, Ordering::SeqCst);
883                true // Consume the event
884            }),
885            ShortcutContext::Global,
886            ShortcutPriority::High,
887            "",
888        );
889
890        manager.register_with_options(
891            Shortcut::ctrl(Key::S),
892            Box::new(move || {
893                c2.fetch_add(1, Ordering::SeqCst);
894                true
895            }),
896            ShortcutContext::Global,
897            ShortcutPriority::Low,
898            "",
899        );
900
901        manager.trigger(Shortcut::ctrl(Key::S));
902
903        // Only high priority handler should have run
904        assert_eq!(counter.load(Ordering::SeqCst), 1);
905    }
906
907    #[test]
908    fn test_manager_push_pop_context() {
909        let mut manager = ShortcutManager::new();
910
911        manager.push_context(ShortcutContext::Custom("editor".to_string()));
912        manager.push_context(ShortcutContext::Custom("modal".to_string()));
913
914        let popped = manager.pop_context();
915        assert_eq!(popped, Some(ShortcutContext::Custom("modal".to_string())));
916
917        let popped = manager.pop_context();
918        assert_eq!(popped, Some(ShortcutContext::Custom("editor".to_string())));
919
920        // Can't pop Global context
921        let popped = manager.pop_context();
922        assert!(popped.is_none());
923    }
924
925    #[test]
926    fn test_manager_find_conflicts() {
927        let mut manager = ShortcutManager::new();
928
929        let id1 = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
930        let id2 = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
931        manager.register(Shortcut::ctrl(Key::A), Box::new(|| true)); // No conflict
932
933        let conflicts = manager.find_conflicts();
934        assert_eq!(conflicts.len(), 1);
935        assert!(conflicts[0].1.contains(&id1));
936        assert!(conflicts[0].1.contains(&id2));
937    }
938
939    #[test]
940    fn test_manager_shortcuts() {
941        let mut manager = ShortcutManager::new();
942
943        manager.register_with_options(
944            Shortcut::ctrl(Key::S),
945            Box::new(|| true),
946            ShortcutContext::Global,
947            ShortcutPriority::Normal,
948            "Save",
949        );
950
951        manager.register_with_options(
952            Shortcut::ctrl(Key::O),
953            Box::new(|| true),
954            ShortcutContext::Global,
955            ShortcutPriority::Normal,
956            "Open",
957        );
958
959        assert_eq!(manager.shortcuts().count(), 2);
960    }
961
962    #[test]
963    fn test_manager_clear() {
964        let mut manager = ShortcutManager::new();
965        manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
966        manager.register(Shortcut::ctrl(Key::O), Box::new(|| true));
967
968        manager.clear();
969        assert_eq!(manager.binding_count(), 0);
970    }
971
972    #[test]
973    fn test_manager_description() {
974        let mut manager = ShortcutManager::new();
975
976        let id = manager.register_with_options(
977            Shortcut::ctrl(Key::S),
978            Box::new(|| true),
979            ShortcutContext::Global,
980            ShortcutPriority::Normal,
981            "Save document",
982        );
983
984        assert_eq!(manager.description(id), Some("Save document"));
985    }
986
987    // ShortcutBuilder tests
988    #[test]
989    fn test_builder() {
990        let shortcut = ShortcutBuilder::new(Key::S).ctrl().shift().build();
991
992        assert_eq!(shortcut.key, Key::S);
993        assert!(shortcut.modifiers.ctrl);
994        assert!(shortcut.modifiers.shift);
995        assert!(!shortcut.modifiers.alt);
996    }
997
998    #[test]
999    fn test_builder_register() {
1000        let mut manager = ShortcutManager::new();
1001
1002        let id = ShortcutBuilder::new(Key::S)
1003            .ctrl()
1004            .description("Save")
1005            .register(&mut manager, Box::new(|| true));
1006
1007        assert!(manager.is_enabled(id));
1008        assert_eq!(manager.description(id), Some("Save"));
1009    }
1010
1011    #[test]
1012    fn test_builder_for_widget() {
1013        let builder = ShortcutBuilder::new(Key::Enter).for_widget(WidgetId::new(42));
1014
1015        assert_eq!(builder.context, ShortcutContext::Widget(WidgetId::new(42)));
1016    }
1017}