Skip to main content

repose_core/
shortcuts.rs

1use crate::Vec2;
2use crate::effects::Dispose;
3use crate::input::{Key, Modifiers, PointerKind};
4use crate::remember_with_key;
5use std::cell::{Cell, RefCell};
6use std::rc::{Rc, Weak};
7
8#[derive(Clone, Debug, PartialEq)]
9pub enum Gesture {
10    SwipeLeft,
11    SwipeRight,
12    /// Center-less pinch kept for back-compat with producers that do not
13    /// track a centroid (none in-tree emit this, handlers must treat it as
14    /// unroutable and return false unless they own the whole surface).
15    Pinch {
16        delta_scale: f32,
17    },
18    PinchWithCenter {
19        delta_scale: f32,
20        center: Vec2,
21    },
22    /// Two-finger rotation (twist). `delta_rotation` is in radians
23    /// (positive = clockwise in screen space, y-down), `center` is the
24    /// gesture centroid in physical px.
25    Rotate {
26        delta_rotation: f32,
27        center: Vec2,
28    },
29    /// 2/3-finger pan (centroid translation). `delta` is in physical px,
30    /// positive = content moves right/down (natural scrolling). `center` is
31    /// the gesture centroid in physical px (Compose `calculateCentroid`).
32    Pan {
33        delta: Vec2,
34        center: Vec2,
35    },
36}
37
38/// Low-level drag-and-drop actions dispatched by the platform.
39/// The framework handles gesture detection (mouse drag vs touch long press)
40/// and the DnD state machine internally.
41#[derive(Clone, Debug, PartialEq)]
42pub enum DragAction {
43    /// Pointer button pressed (mouse down / touch start).
44    Press {
45        position: Vec2,
46        capture_id: u64,
47        kind: PointerKind,
48        modifiers: Modifiers,
49    },
50    /// Pointer moved while button is pressed or touch is active.
51    Move {
52        position: Vec2,
53        modifiers: Modifiers,
54    },
55    /// Pointer released.
56    Release {
57        position: Vec2,
58        modifiers: Modifiers,
59    },
60    /// Drag cancelled (e.g. Escape key).
61    Cancel,
62}
63
64#[derive(Clone, Debug, PartialEq)]
65pub enum Action {
66    Copy,
67    Cut,
68    Paste,
69    SelectAll,
70    Undo,
71    Redo,
72
73    Back,
74    Find,
75    Save,
76
77    FocusNext,
78    FocusPrevious,
79    FocusLeft,
80    FocusRight,
81    FocusUp,
82    FocusDown,
83
84    Gesture(Gesture),
85    Drag(DragAction),
86    Custom(Rc<str>),
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, Hash)]
90pub struct KeyChord {
91    pub key: Key,
92    pub modifiers: Modifiers,
93}
94
95impl KeyChord {
96    pub fn new(key: Key, modifiers: Modifiers) -> Self {
97        Self { key, modifiers }
98    }
99}
100
101#[derive(Clone, Debug)]
102pub struct ShortcutBinding {
103    pub chord: KeyChord,
104    pub action: Action,
105}
106
107#[derive(Clone, Debug, Default)]
108pub struct ShortcutMap {
109    pub bindings: Vec<ShortcutBinding>,
110}
111
112impl ShortcutMap {
113    pub fn new() -> Self {
114        Self {
115            bindings: Vec::new(),
116        }
117    }
118
119    pub fn bind(mut self, key: Key, modifiers: Modifiers, action: Action) -> Self {
120        self.bindings.push(ShortcutBinding {
121            chord: KeyChord::new(key, modifiers),
122            action,
123        });
124        self
125    }
126
127    pub fn bind_action(mut self, action: Action) -> Self {
128        if let Some(chord) = default_chord_for(&action) {
129            self.bindings.push(ShortcutBinding { chord, action });
130        }
131        self
132    }
133
134    pub fn merge(mut self, other: ShortcutMap) -> Self {
135        self.bindings.extend(other.bindings);
136        self
137    }
138
139    pub fn insert(&mut self, key: Key, modifiers: Modifiers, action: Action) {
140        self.bindings.push(ShortcutBinding {
141            chord: KeyChord::new(key, modifiers),
142            action,
143        });
144    }
145
146    pub fn action_for(&self, chord: &KeyChord) -> Option<Action> {
147        self.bindings
148            .iter()
149            .rev()
150            .find(|binding| &binding.chord == chord)
151            .map(|binding| binding.action.clone())
152    }
153}
154
155pub type Handler = Rc<dyn Fn(Action) -> bool>;
156
157#[derive(Clone)]
158pub struct ShortcutState {
159    pub handler: Option<Handler>,
160    pub default_map: ShortcutMap,
161    pub scopes: Vec<ShortcutMap>,
162    runtime_installs: Rc<RefCell<RuntimeShortcutInstalls>>,
163    use_global_fallback: bool,
164    identity: u64,
165}
166
167struct RuntimeShortcutMapEntry {
168    key: String,
169    token: u64,
170    map: ShortcutMap,
171    cleanup: Dispose,
172}
173
174struct RuntimeShortcutHandlerEntry {
175    key: String,
176    token: u64,
177    handler: Handler,
178    cleanup: Dispose,
179}
180
181#[derive(Default)]
182struct RuntimeShortcutInstalls {
183    maps: Vec<RuntimeShortcutMapEntry>,
184    handlers: Vec<RuntimeShortcutHandlerEntry>,
185    next_token: u64,
186}
187
188impl ShortcutState {
189    pub fn new() -> Self {
190        Self {
191            handler: None,
192            default_map: default_map(),
193            scopes: Vec::new(),
194            runtime_installs: Rc::new(RefCell::new(RuntimeShortcutInstalls {
195                next_token: 1,
196                ..RuntimeShortcutInstalls::default()
197            })),
198            use_global_fallback: true,
199            identity: next_shortcut_state_id(),
200        }
201    }
202
203    pub fn without_global_fallback(mut self) -> Self {
204        self.use_global_fallback = false;
205        self
206    }
207
208    fn resolve_local_action(&self, chord: &KeyChord) -> Option<Action> {
209        if chord.key == Key::Unknown {
210            return None;
211        }
212        if let Some(action) = self
213            .scopes
214            .iter()
215            .rev()
216            .find_map(|scope| scope.action_for(chord))
217        {
218            return Some(action);
219        }
220        if let Some(action) = self
221            .runtime_installs
222            .borrow()
223            .maps
224            .iter()
225            .rev()
226            .find_map(|entry| entry.map.action_for(chord))
227        {
228            return Some(action);
229        }
230        self.default_map.action_for(chord)
231    }
232
233    pub fn resolve_action(&self, chord: &KeyChord) -> Option<Action> {
234        self.resolve_local_action(chord).or_else(|| {
235            self.use_global_fallback
236                .then(|| resolve_global_action(chord.clone()))
237                .flatten()
238        })
239    }
240
241    fn handle_local(&self, action: Action) -> bool {
242        let handler = self
243            .runtime_installs
244            .borrow()
245            .handlers
246            .iter()
247            .rev()
248            .map(|entry| entry.handler.clone())
249            .next()
250            .or_else(|| self.handler.clone());
251        handler.is_some_and(|handler| handler(action))
252    }
253
254    pub fn handle(&self, action: Action) -> bool {
255        if self.handle_local(action.clone()) {
256            return true;
257        }
258        self.use_global_fallback && handle_global(action)
259    }
260}
261
262impl Default for ShortcutState {
263    fn default() -> Self {
264        Self::new()
265    }
266}
267
268struct ScopeEntry {
269    token: Option<u64>,
270    map: ShortcutMap,
271}
272
273struct ShortcutScopeStack(Vec<ScopeEntry>);
274
275impl ShortcutScopeStack {
276    #[cfg(test)]
277    fn push(&mut self, map: ShortcutMap) {
278        self.0.push(ScopeEntry { token: None, map });
279    }
280
281    fn push_installed(&mut self, token: u64, map: ShortcutMap) {
282        self.0.push(ScopeEntry {
283            token: Some(token),
284            map,
285        });
286    }
287
288    #[cfg(test)]
289    fn pop(&mut self) -> Option<ShortcutMap> {
290        self.0.pop().map(|entry| entry.map)
291    }
292
293    fn remove(&mut self, token: u64) -> Option<ShortcutMap> {
294        let index = self.0.iter().position(|entry| entry.token == Some(token))?;
295        Some(self.0.remove(index).map)
296    }
297
298    fn update(&mut self, token: u64, map: ShortcutMap) -> Option<ShortcutMap> {
299        let entry = self.0.iter_mut().find(|entry| entry.token == Some(token))?;
300        Some(std::mem::replace(&mut entry.map, map))
301    }
302
303    fn iter_rev(&self) -> impl Iterator<Item = &ShortcutMap> {
304        self.0.iter().rev().map(|entry| &entry.map)
305    }
306}
307
308struct MapInstallState {
309    token: u64,
310    active: bool,
311    cleanup: Option<Dispose>,
312}
313
314struct HandlerInstallState {
315    token: u64,
316    active: bool,
317    cleanup: Option<Dispose>,
318}
319
320thread_local! {
321    static HANDLER: RefCell<Option<Handler>> = RefCell::new(None);
322    static BASE_HANDLER: RefCell<Option<Handler>> = RefCell::new(None);
323    static HANDLER_ENTRIES: RefCell<Vec<(u64, Handler)>> = const { RefCell::new(Vec::new()) };
324    static DEFAULT_MAP: RefCell<ShortcutMap> = RefCell::new(default_map());
325    static SCOPES: RefCell<ShortcutScopeStack> =
326        const { RefCell::new(ShortcutScopeStack(Vec::new())) };
327    static NEXT_INSTALLER_ID: Cell<u64> = const { Cell::new(1) };
328    static NEXT_SHORTCUT_STATE_ID: Cell<u64> = const { Cell::new(1) };
329    static ACTIVE_SHORTCUT_STATE: RefCell<Option<ShortcutState>> = const { RefCell::new(None) };
330}
331
332fn next_installer_id() -> u64 {
333    NEXT_INSTALLER_ID.with(|next| {
334        let id = next.get();
335        next.set(id.wrapping_add(1));
336        id
337    })
338}
339
340fn next_shortcut_state_id() -> u64 {
341    NEXT_SHORTCUT_STATE_ID.with(|next| {
342        let id = next.get();
343        next.set(id.wrapping_add(1));
344        id
345    })
346}
347
348pub fn with_runtime_state<R>(state: &ShortcutState, f: impl FnOnce() -> R) -> R {
349    struct Restore(Option<ShortcutState>);
350    impl Drop for Restore {
351        fn drop(&mut self) {
352            ACTIVE_SHORTCUT_STATE.with(|slot| {
353                *slot.borrow_mut() = self.0.take();
354            });
355        }
356    }
357    let previous = ACTIVE_SHORTCUT_STATE.with(|slot| slot.borrow_mut().replace(state.clone()));
358    let _restore = Restore(previous);
359    f()
360}
361
362fn active_runtime_state() -> Option<ShortcutState> {
363    ACTIVE_SHORTCUT_STATE.with(|slot| slot.borrow().clone())
364}
365
366pub(crate) fn active_runtime_identity() -> Option<u64> {
367    active_runtime_state().map(|state| state.identity)
368}
369
370fn sync_handler() {
371    let handler = HANDLER_ENTRIES
372        .try_with(|entries| {
373            entries
374                .try_borrow()
375                .ok()
376                .and_then(|entries| entries.last().map(|(_, handler)| handler.clone()))
377        })
378        .ok()
379        .flatten()
380        .or_else(|| BASE_HANDLER.with(|base| base.try_borrow().ok().and_then(|base| base.clone())));
381    let old = HANDLER.try_with(|current| {
382        current
383            .try_borrow_mut()
384            .ok()
385            .map(|mut current| std::mem::replace(&mut *current, handler))
386    });
387    if let Ok(Some(old)) = old {
388        drop(old);
389    }
390}
391
392pub fn set(handler: Option<Handler>) {
393    let old = BASE_HANDLER.try_with(|base| {
394        base.try_borrow_mut()
395            .ok()
396            .map(|mut base| std::mem::replace(&mut *base, handler))
397    });
398    if let Ok(Some(old)) = old {
399        drop(old);
400    }
401    sync_handler();
402}
403
404fn handle_global(action: Action) -> bool {
405    let handler = HANDLER
406        .try_with(|current| {
407            current
408                .try_borrow()
409                .ok()
410                .and_then(|current| current.clone())
411        })
412        .ok()
413        .flatten();
414    handler.map(|handler| handler(action)).unwrap_or(false)
415}
416
417pub fn handle(action: Action) -> bool {
418    if let Some(state) = active_runtime_state() {
419        return state.handle(action);
420    }
421    handle_global(action)
422}
423
424fn resolve_global_action(chord: KeyChord) -> Option<Action> {
425    if chord.key == Key::Unknown {
426        return None;
427    }
428    let scoped = SCOPES
429        .try_with(|scopes| {
430            scopes
431                .try_borrow()
432                .ok()
433                .and_then(|scopes| scopes.iter_rev().find_map(|scope| scope.action_for(&chord)))
434        })
435        .ok()
436        .flatten();
437    scoped.or_else(|| {
438        DEFAULT_MAP
439            .try_with(|map| map.try_borrow().ok().and_then(|map| map.action_for(&chord)))
440            .ok()
441            .flatten()
442    })
443}
444
445pub fn resolve_action(chord: KeyChord) -> Option<Action> {
446    if let Some(state) = active_runtime_state() {
447        return state.resolve_action(&chord);
448    }
449    resolve_global_action(chord)
450}
451
452pub fn set_default_map(map: ShortcutMap) {
453    let old = DEFAULT_MAP.try_with(|current| {
454        current
455            .try_borrow_mut()
456            .ok()
457            .map(|mut current| std::mem::replace(&mut *current, map))
458    });
459    if let Ok(Some(old)) = old {
460        drop(old);
461    }
462    crate::request_frame();
463}
464
465fn map_cleanup_key(key: &str) -> String {
466    format!("shortcut-map-cleanup:{key}")
467}
468
469fn handler_cleanup_key(key: &str) -> String {
470    format!("shortcut-handler-cleanup:{key}")
471}
472
473fn owner_disposer(key: String, cleanup: Dispose, owner: String) -> Dispose {
474    Dispose::new(move || {
475        crate::runtime::remove_keyed_disposer_for_owner(&key, &cleanup, &owner);
476    })
477}
478
479fn runtime_owner_disposer(key: String, cleanup: Dispose, owner: String) -> Dispose {
480    Dispose::new(move || {
481        cleanup.run();
482        crate::runtime::remove_keyed_disposer_for_owner(&key, &cleanup, &owner);
483    })
484}
485
486fn register_runtime_cleanup(state: &ShortcutState, key: &str, cleanup: Dispose) -> Dispose {
487    let cleanup_key = format!(
488        "runtime-shortcut-cleanup:{}:{key}",
489        Rc::as_ptr(&state.runtime_installs) as usize
490    );
491    let owner =
492        crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
493    if !crate::runtime::keyed_disposer_has_owner(&cleanup_key, &cleanup, &owner) {
494        crate::runtime::register_keyed_disposer_for_owner(
495            cleanup_key.clone(),
496            cleanup.clone(),
497            owner.clone(),
498        );
499        if let Some(scope) = crate::scope::current_scope() {
500            let registered = cleanup.clone();
501            let scope_key = cleanup_key.clone();
502            let scope_owner = owner.clone();
503            scope.add_disposer(move || {
504                crate::runtime::remove_keyed_disposer_for_owner(
505                    &scope_key,
506                    &registered,
507                    &scope_owner,
508                );
509            });
510        }
511    }
512    runtime_owner_disposer(cleanup_key, cleanup, owner)
513}
514
515fn install_runtime_map(state: &ShortcutState, key: String, map: ShortcutMap) -> Dispose {
516    let mut installs = state.runtime_installs.borrow_mut();
517    if let Some(entry) = installs.maps.iter_mut().find(|entry| entry.key == key) {
518        entry.map = map;
519        return entry.cleanup.clone();
520    }
521    let token = installs.next_token;
522    installs.next_token = installs.next_token.wrapping_add(1);
523    let weak: Weak<RefCell<RuntimeShortcutInstalls>> = Rc::downgrade(&state.runtime_installs);
524    let cleanup_key = key.clone();
525    let cleanup = Dispose::new(move || {
526        let Some(installs) = weak.upgrade() else {
527            return;
528        };
529        let mut installs = installs.borrow_mut();
530        if let Some(index) = installs
531            .maps
532            .iter()
533            .position(|entry| entry.key == cleanup_key && entry.token == token)
534        {
535            installs.maps.remove(index);
536        }
537    });
538    let disposer = register_runtime_cleanup(state, &key, cleanup);
539    installs.maps.push(RuntimeShortcutMapEntry {
540        key,
541        token,
542        map,
543        cleanup: disposer.clone(),
544    });
545    disposer
546}
547
548fn install_runtime_handler(state: &ShortcutState, key: String, handler: Handler) -> Dispose {
549    let mut installs = state.runtime_installs.borrow_mut();
550    if let Some(entry) = installs.handlers.iter_mut().find(|entry| entry.key == key) {
551        entry.handler = handler;
552        return entry.cleanup.clone();
553    }
554    let token = installs.next_token;
555    installs.next_token = installs.next_token.wrapping_add(1);
556    let weak: Weak<RefCell<RuntimeShortcutInstalls>> = Rc::downgrade(&state.runtime_installs);
557    let cleanup_key = key.clone();
558    let cleanup = Dispose::new(move || {
559        let Some(installs) = weak.upgrade() else {
560            return;
561        };
562        let mut installs = installs.borrow_mut();
563        if let Some(index) = installs
564            .handlers
565            .iter()
566            .position(|entry| entry.key == cleanup_key && entry.token == token)
567        {
568            installs.handlers.remove(index);
569        }
570    });
571    let disposer = register_runtime_cleanup(state, &key, cleanup);
572    installs.handlers.push(RuntimeShortcutHandlerEntry {
573        key,
574        token,
575        handler,
576        cleanup: disposer.clone(),
577    });
578    disposer
579}
580
581fn install_shortcut_map_state(key: impl Into<String>, map: ShortcutMap) -> Dispose {
582    let key = key.into();
583    if let Some(state) = active_runtime_state() {
584        return install_runtime_map(&state, key, map);
585    }
586    let state_key = format!("shortcut-map-state:{key}");
587    let state: Rc<RefCell<MapInstallState>> = remember_with_key(state_key, || {
588        RefCell::new(MapInstallState {
589            token: 0,
590            active: false,
591            cleanup: None,
592        })
593    });
594    let (token, cleanup, installed) = {
595        let weak: Weak<RefCell<MapInstallState>> = Rc::downgrade(&state);
596        let mut state = state.borrow_mut();
597        if state.active {
598            (
599                state.token,
600                state.cleanup.clone().expect("active map state"),
601                false,
602            )
603        } else {
604            let token = next_installer_id();
605            let cleanup = Dispose::new(move || {
606                let removed = SCOPES
607                    .try_with(|scopes| {
608                        scopes
609                            .try_borrow_mut()
610                            .ok()
611                            .and_then(|mut scopes| scopes.remove(token))
612                    })
613                    .ok()
614                    .flatten();
615                drop(removed);
616                if let Some(state) = weak.upgrade() {
617                    let old = {
618                        let mut state = state.borrow_mut();
619                        if state.token == token {
620                            state.active = false;
621                            state.cleanup.take()
622                        } else {
623                            None
624                        }
625                    };
626                    drop(old);
627                }
628            });
629            state.token = token;
630            state.active = true;
631            state.cleanup = Some(cleanup.clone());
632            (token, cleanup, true)
633        }
634    };
635    if installed {
636        SCOPES.with(|scopes| scopes.borrow_mut().push_installed(token, map));
637    } else {
638        let old = SCOPES
639            .try_with(|scopes| {
640                scopes
641                    .try_borrow_mut()
642                    .ok()
643                    .and_then(|mut scopes| scopes.update(token, map))
644            })
645            .ok()
646            .flatten();
647        drop(old);
648    }
649    let cleanup_key = map_cleanup_key(&key);
650    let owner =
651        crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
652    if !crate::runtime::keyed_disposer_has_owner(&cleanup_key, &cleanup, &owner) {
653        crate::runtime::register_keyed_disposer_for_owner(
654            cleanup_key.clone(),
655            cleanup.clone(),
656            owner.clone(),
657        );
658        if let Some(scope) = crate::scope::current_scope() {
659            let registered = cleanup.clone();
660            let scope_key = cleanup_key.clone();
661            let scope_owner = owner.clone();
662            scope.add_disposer(move || {
663                crate::runtime::remove_keyed_disposer_for_owner(
664                    &scope_key,
665                    &registered,
666                    &scope_owner,
667                );
668            });
669        }
670    }
671    owner_disposer(cleanup_key, cleanup, owner)
672}
673
674pub fn install_shortcut_map_with_key(key: impl Into<String>, map: ShortcutMap) -> Dispose {
675    install_shortcut_map_state(key, map)
676}
677
678#[allow(non_snake_case)]
679pub fn InstallShortcutMapWithKey(key: impl Into<String>, map: ShortcutMap) -> Dispose {
680    install_shortcut_map_state(key, map)
681}
682
683#[track_caller]
684#[allow(non_snake_case)]
685pub fn InstallShortcutMap(map: ShortcutMap) -> Dispose {
686    let location = std::panic::Location::caller();
687    install_shortcut_map_state(
688        format!(
689            "{}:{}:{}",
690            location.file(),
691            location.line(),
692            location.column()
693        ),
694        map,
695    )
696}
697
698pub fn install_shortcut_handler_state(key: impl Into<String>, handler: Handler) -> Dispose {
699    let key = key.into();
700    if let Some(state) = active_runtime_state() {
701        return install_runtime_handler(&state, key, handler);
702    }
703    let state_key = format!("shortcut-handler-state:{key}");
704    let state: Rc<RefCell<HandlerInstallState>> = remember_with_key(state_key, || {
705        RefCell::new(HandlerInstallState {
706            token: 0,
707            active: false,
708            cleanup: None,
709        })
710    });
711    let (token, cleanup, installed) = {
712        let weak: Weak<RefCell<HandlerInstallState>> = Rc::downgrade(&state);
713        let mut state = state.borrow_mut();
714        if state.active {
715            (
716                state.token,
717                state.cleanup.clone().expect("active handler state"),
718                false,
719            )
720        } else {
721            let token = next_installer_id();
722            let cleanup = Dispose::new(move || {
723                let removed = HANDLER_ENTRIES
724                    .try_with(|entries| {
725                        entries.try_borrow_mut().ok().and_then(|mut entries| {
726                            let index = entries.iter().position(|(id, _)| *id == token)?;
727                            Some(entries.remove(index).1)
728                        })
729                    })
730                    .ok()
731                    .flatten();
732                drop(removed);
733                if let Some(state) = weak.upgrade() {
734                    let old = {
735                        let mut state = state.borrow_mut();
736                        if state.token == token {
737                            state.active = false;
738                            state.cleanup.take()
739                        } else {
740                            None
741                        }
742                    };
743                    drop(old);
744                }
745                sync_handler();
746            });
747            state.token = token;
748            state.active = true;
749            state.cleanup = Some(cleanup.clone());
750            (token, cleanup, true)
751        }
752    };
753    if installed {
754        let old = HANDLER_ENTRIES
755            .try_with(|entries| {
756                entries.try_borrow_mut().ok().map(|mut entries| {
757                    entries.push((token, handler));
758                    None::<Handler>
759                })
760            })
761            .ok()
762            .flatten();
763        drop(old);
764        sync_handler();
765    } else {
766        let old = HANDLER_ENTRIES
767            .try_with(|entries| {
768                entries.try_borrow_mut().ok().and_then(|mut entries| {
769                    let entry = entries.iter_mut().find(|(id, _)| *id == token)?;
770                    let old = entry.1.clone();
771                    entry.1 = handler;
772                    Some(old)
773                })
774            })
775            .ok()
776            .flatten();
777        drop(old);
778        sync_handler();
779    }
780    let cleanup_key = handler_cleanup_key(&key);
781    let owner =
782        crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
783    if !crate::runtime::keyed_disposer_has_owner(&cleanup_key, &cleanup, &owner) {
784        crate::runtime::register_keyed_disposer_for_owner(
785            cleanup_key.clone(),
786            cleanup.clone(),
787            owner.clone(),
788        );
789        if let Some(scope) = crate::scope::current_scope() {
790            let registered = cleanup.clone();
791            let scope_key = cleanup_key.clone();
792            let scope_owner = owner.clone();
793            scope.add_disposer(move || {
794                crate::runtime::remove_keyed_disposer_for_owner(
795                    &scope_key,
796                    &registered,
797                    &scope_owner,
798                );
799            });
800        }
801    }
802    owner_disposer(cleanup_key, cleanup, owner)
803}
804
805pub fn install_shortcut_handler_with_key(key: impl Into<String>, handler: Handler) -> Dispose {
806    install_shortcut_handler_state(key, handler)
807}
808
809#[allow(non_snake_case)]
810pub fn InstallShortcutHandlerWithKey(key: impl Into<String>, handler: Handler) -> Dispose {
811    install_shortcut_handler_state(key, handler)
812}
813
814#[track_caller]
815#[allow(non_snake_case)]
816pub fn InstallShortcutHandler(handler: Handler) -> Dispose {
817    let location = std::panic::Location::caller();
818    install_shortcut_handler_state(
819        format!(
820            "{}:{}:{}",
821            location.file(),
822            location.line(),
823            location.column()
824        ),
825        handler,
826    )
827}
828
829pub fn default_chord_for(action: &Action) -> Option<KeyChord> {
830    // On non-macOS, sets ctrl true
831    let cmd = Modifiers {
832        command: true,
833        ctrl: !cfg!(target_os = "macos"),
834        ..Modifiers::default()
835    };
836    match action {
837        Action::Copy => Some(KeyChord::new(Key::Character('c'), cmd)),
838        Action::Cut => Some(KeyChord::new(Key::Character('x'), cmd)),
839        Action::Paste => Some(KeyChord::new(Key::Character('v'), cmd)),
840        Action::SelectAll => Some(KeyChord::new(Key::Character('a'), cmd)),
841        Action::Undo => Some(KeyChord::new(Key::Character('z'), cmd)),
842        Action::Redo => Some(KeyChord::new(
843            Key::Character('z'),
844            Modifiers {
845                command: true,
846                shift: true,
847                ctrl: !cfg!(target_os = "macos"),
848                ..Modifiers::default()
849            },
850        )),
851        Action::Find => Some(KeyChord::new(Key::Character('f'), cmd)),
852        Action::Save => Some(KeyChord::new(Key::Character('s'), cmd)),
853        Action::FocusNext => Some(KeyChord::new(Key::Tab, Modifiers::default())),
854        Action::FocusPrevious => Some(KeyChord::new(
855            Key::Tab,
856            Modifiers {
857                shift: true,
858                ..Modifiers::default()
859            },
860        )),
861        Action::FocusLeft => Some(KeyChord::new(Key::ArrowLeft, Modifiers::default())),
862        Action::FocusRight => Some(KeyChord::new(Key::ArrowRight, Modifiers::default())),
863        Action::FocusUp => Some(KeyChord::new(Key::ArrowUp, Modifiers::default())),
864        Action::FocusDown => Some(KeyChord::new(Key::ArrowDown, Modifiers::default())),
865        _ => None,
866    }
867}
868
869pub fn default_map() -> ShortcutMap {
870    let mut map = ShortcutMap::new();
871    let actions = vec![
872        Action::Copy,
873        Action::Cut,
874        Action::Paste,
875        Action::SelectAll,
876        Action::Undo,
877        Action::Redo,
878        Action::Find,
879        Action::Save,
880        Action::FocusNext,
881        Action::FocusPrevious,
882        Action::FocusLeft,
883        Action::FocusRight,
884        Action::FocusUp,
885        Action::FocusDown,
886    ];
887    for action in actions {
888        if let Some(chord) = default_chord_for(&action) {
889            map.insert(chord.key, chord.modifiers, action);
890        }
891    }
892    map
893}
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898
899    #[test]
900    fn resolve_action_prefers_scopes() {
901        let mut map = ShortcutMap::new();
902        map.insert(
903            Key::Character('k'),
904            Modifiers::default(),
905            Action::Custom("one".into()),
906        );
907        set_default_map(map);
908
909        let mut scope = ShortcutMap::new();
910        scope.insert(
911            Key::Character('k'),
912            Modifiers::default(),
913            Action::Custom("two".into()),
914        );
915
916        SCOPES.with(|scopes| scopes.borrow_mut().push(scope));
917
918        let chord = KeyChord::new(Key::Character('k'), Modifiers::default());
919        assert_eq!(
920            resolve_action(chord.clone()),
921            Some(Action::Custom("two".into()))
922        );
923
924        SCOPES.with(|scopes| scopes.borrow_mut().pop());
925        assert_eq!(resolve_action(chord), Some(Action::Custom("one".into())));
926    }
927
928    #[test]
929    fn runtime_installers_are_isolated() {
930        let a = ShortcutState::new();
931        let b = ShortcutState::new();
932        let chord = KeyChord::new(Key::Character('j'), Modifiers::default());
933        let mut map_a = ShortcutMap::new();
934        map_a.insert(
935            chord.key.clone(),
936            chord.modifiers,
937            Action::Custom("a".into()),
938        );
939        let mut map_b = ShortcutMap::new();
940        map_b.insert(
941            chord.key.clone(),
942            chord.modifiers,
943            Action::Custom("b".into()),
944        );
945        let dispose_a = with_runtime_state(&a, || InstallShortcutMapWithKey("same-install", map_a));
946        let dispose_b = with_runtime_state(&b, || InstallShortcutMapWithKey("same-install", map_b));
947        assert_eq!(a.resolve_action(&chord), Some(Action::Custom("a".into())));
948        assert_eq!(b.resolve_action(&chord), Some(Action::Custom("b".into())));
949        dispose_a.run();
950        assert!(a.resolve_action(&chord).is_none());
951        assert_eq!(b.resolve_action(&chord), Some(Action::Custom("b".into())));
952        dispose_b.run();
953    }
954
955    #[test]
956    fn scoped_shortcut_effects_are_isolated_per_runtime() {
957        use crate::{effects::Dispose, runtime::ComposeGuard, scope::Scope};
958        use std::cell::Cell;
959
960        fn compose(state: &ShortcutState, action: &'static str, fired: &Rc<Cell<usize>>) -> Scope {
961            let scope = Scope::new();
962            let state = state.clone();
963            let fired = fired.clone();
964            scope.run(|| {
965                let _guard = ComposeGuard::begin();
966                with_runtime_state(&state, || {
967                    crate::scope::scoped_effect_once(move || {
968                        let mut map = ShortcutMap::new();
969                        map.insert(
970                            Key::Character('j'),
971                            Modifiers::default(),
972                            Action::Custom(action.into()),
973                        );
974                        let map_disposer = InstallShortcutMapWithKey("shared-scoped", map);
975                        let handler_disposer = InstallShortcutHandlerWithKey(
976                            "shared-scoped",
977                            Rc::new(move |_action| {
978                                fired.set(fired.get() + 1);
979                                true
980                            }),
981                        );
982                        Dispose::new(move || {
983                            map_disposer.run();
984                            handler_disposer.run();
985                        })
986                    });
987                });
988            });
989            scope
990        }
991
992        let a = ShortcutState::new();
993        let b = ShortcutState::new();
994        let fired_a = Rc::new(Cell::new(0));
995        let fired_b = Rc::new(Cell::new(0));
996        let scope_a = compose(&a, "a", &fired_a);
997        let scope_b = compose(&b, "b", &fired_b);
998        let chord = KeyChord::new(Key::Character('j'), Modifiers::default());
999        let action_a = a.resolve_action(&chord).unwrap();
1000        let action_b = b.resolve_action(&chord).unwrap();
1001        assert!(a.handle(action_a));
1002        assert!(b.handle(action_b));
1003        assert_eq!(fired_a.get(), 1);
1004        assert_eq!(fired_b.get(), 1);
1005        scope_a.dispose();
1006        scope_b.dispose();
1007    }
1008
1009    #[test]
1010    fn keyed_map_updates_and_cleans_up() {
1011        set_default_map(ShortcutMap::new());
1012        let chord = KeyChord::new(Key::Character('m'), Modifiers::default());
1013        let mut first = ShortcutMap::new();
1014        first.insert(
1015            chord.key.clone(),
1016            chord.modifiers,
1017            Action::Custom("first".into()),
1018        );
1019        let mut second = ShortcutMap::new();
1020        second.insert(
1021            chord.key.clone(),
1022            chord.modifiers,
1023            Action::Custom("second".into()),
1024        );
1025        let key = "shortcut-test-map-update";
1026        let disposer = install_shortcut_map_with_key(key, first);
1027        assert_eq!(
1028            resolve_action(chord.clone()),
1029            Some(Action::Custom("first".into()))
1030        );
1031        let _ = install_shortcut_map_with_key(key, second);
1032        assert_eq!(
1033            resolve_action(chord.clone()),
1034            Some(Action::Custom("second".into()))
1035        );
1036        disposer.run();
1037        assert_eq!(resolve_action(chord), None);
1038    }
1039
1040    #[test]
1041    fn handler_restores_base_after_keyed_cleanup() {
1042        let base = Rc::new(|_action: Action| false);
1043        let installed = Rc::new(|_action: Action| true);
1044        set(Some(base));
1045        let key = "shortcut-test-handler-restore";
1046        let disposer = install_shortcut_handler_with_key(key, installed);
1047        assert!(handle(Action::Copy));
1048        disposer.run();
1049        assert!(!handle(Action::Copy));
1050        set(None);
1051    }
1052}