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 Pinch {
16 delta_scale: f32,
17 },
18 PinchWithCenter {
19 delta_scale: f32,
20 center: Vec2,
21 },
22 Rotate {
26 delta_rotation: f32,
27 center: Vec2,
28 },
29 Pan {
33 delta: Vec2,
34 center: Vec2,
35 },
36}
37
38#[derive(Clone, Debug, PartialEq)]
42pub enum DragAction {
43 Press {
45 position: Vec2,
46 capture_id: u64,
47 kind: PointerKind,
48 modifiers: Modifiers,
49 },
50 Move {
52 position: Vec2,
53 modifiers: Modifiers,
54 },
55 Release {
57 position: Vec2,
58 modifiers: Modifiers,
59 },
60 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 ®istered,
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 ®istered,
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 ®istered,
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 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}