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