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(
493 state: &ShortcutState,
494 slot: &str,
495 key: &str,
496 cleanup: Dispose,
497) -> Dispose {
498 let cleanup_key = format!(
499 "runtime-shortcut-{slot}-cleanup:{}:{key}",
500 Rc::as_ptr(&state.runtime_installs) as usize
501 );
502 let owner =
503 crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
504 if !crate::runtime::keyed_disposer_has_owner(&cleanup_key, &cleanup, &owner) {
505 crate::runtime::register_keyed_disposer_for_owner(
506 cleanup_key.clone(),
507 cleanup.clone(),
508 owner.clone(),
509 );
510 if let Some(scope) = crate::scope::current_scope() {
511 let registered = cleanup.clone();
512 let scope_key = cleanup_key.clone();
513 let scope_owner = owner.clone();
514 scope.add_disposer(move || {
515 crate::runtime::remove_keyed_disposer_for_owner(
516 &scope_key,
517 ®istered,
518 &scope_owner,
519 );
520 });
521 }
522 }
523 runtime_owner_disposer(cleanup_key, cleanup, owner)
524}
525
526fn install_runtime_map(state: &ShortcutState, key: String, map: ShortcutMap) -> Dispose {
527 let token = {
528 let mut installs = state.runtime_installs.borrow_mut();
529 if let Some(entry) = installs.maps.iter_mut().find(|entry| entry.key == key) {
530 entry.map = map;
531 return entry.cleanup.clone();
532 }
533 let token = installs.next_token;
534 installs.next_token = installs.next_token.wrapping_add(1);
535 token
536 };
537 let weak: Weak<RefCell<RuntimeShortcutInstalls>> = Rc::downgrade(&state.runtime_installs);
540 let cleanup_key = key.clone();
541 let cleanup = Dispose::new(move || {
542 let Some(installs) = weak.upgrade() else {
543 return;
544 };
545 let mut installs = installs.borrow_mut();
546 if let Some(index) = installs
547 .maps
548 .iter()
549 .position(|entry| entry.key == cleanup_key && entry.token == token)
550 {
551 installs.maps.remove(index);
552 }
553 });
554 let disposer = register_runtime_cleanup(state, "map", &key, cleanup);
555 state
556 .runtime_installs
557 .borrow_mut()
558 .maps
559 .push(RuntimeShortcutMapEntry {
560 key,
561 token,
562 map,
563 cleanup: disposer.clone(),
564 });
565 disposer
566}
567
568fn install_runtime_handler(state: &ShortcutState, key: String, handler: Handler) -> Dispose {
569 let token = {
570 let mut installs = state.runtime_installs.borrow_mut();
571 if let Some(entry) = installs.handlers.iter_mut().find(|entry| entry.key == key) {
572 entry.handler = handler;
573 return entry.cleanup.clone();
574 }
575 let token = installs.next_token;
576 installs.next_token = installs.next_token.wrapping_add(1);
577 token
578 };
579 let weak: Weak<RefCell<RuntimeShortcutInstalls>> = Rc::downgrade(&state.runtime_installs);
580 let cleanup_key = key.clone();
581 let cleanup = Dispose::new(move || {
582 let Some(installs) = weak.upgrade() else {
583 return;
584 };
585 let mut installs = installs.borrow_mut();
586 if let Some(index) = installs
587 .handlers
588 .iter()
589 .position(|entry| entry.key == cleanup_key && entry.token == token)
590 {
591 installs.handlers.remove(index);
592 }
593 });
594 let disposer = register_runtime_cleanup(state, "handler", &key, cleanup);
595 state
596 .runtime_installs
597 .borrow_mut()
598 .handlers
599 .push(RuntimeShortcutHandlerEntry {
600 key,
601 token,
602 handler,
603 cleanup: disposer.clone(),
604 });
605 disposer
606}
607
608fn install_shortcut_map_state(key: impl Into<String>, map: ShortcutMap) -> Dispose {
609 let key = key.into();
610 if let Some(state) = active_runtime_state() {
611 return install_runtime_map(&state, key, map);
612 }
613 let state_key = format!("shortcut-map-state:{key}");
614 let state: Rc<RefCell<MapInstallState>> = remember_with_key(state_key, || {
615 RefCell::new(MapInstallState {
616 token: 0,
617 active: false,
618 cleanup: None,
619 })
620 });
621 let (token, cleanup, installed) = {
622 let weak: Weak<RefCell<MapInstallState>> = Rc::downgrade(&state);
623 let mut state = state.borrow_mut();
624 if state.active {
625 (
626 state.token,
627 state.cleanup.clone().expect("active map state"),
628 false,
629 )
630 } else {
631 let token = next_installer_id();
632 let cleanup = Dispose::new(move || {
633 let removed = SCOPES
634 .try_with(|scopes| {
635 scopes
636 .try_borrow_mut()
637 .ok()
638 .and_then(|mut scopes| scopes.remove(token))
639 })
640 .ok()
641 .flatten();
642 drop(removed);
643 if let Some(state) = weak.upgrade() {
644 let old = {
645 let mut state = state.borrow_mut();
646 if state.token == token {
647 state.active = false;
648 state.cleanup.take()
649 } else {
650 None
651 }
652 };
653 drop(old);
654 }
655 });
656 state.token = token;
657 state.active = true;
658 state.cleanup = Some(cleanup.clone());
659 (token, cleanup, true)
660 }
661 };
662 if installed {
663 SCOPES.with(|scopes| scopes.borrow_mut().push_installed(token, map));
664 } else {
665 let old = SCOPES
666 .try_with(|scopes| {
667 scopes
668 .try_borrow_mut()
669 .ok()
670 .and_then(|mut scopes| scopes.update(token, map))
671 })
672 .ok()
673 .flatten();
674 drop(old);
675 }
676 let cleanup_key = map_cleanup_key(&key);
677 let owner =
678 crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
679 if !crate::runtime::keyed_disposer_has_owner(&cleanup_key, &cleanup, &owner) {
680 crate::runtime::register_keyed_disposer_for_owner(
681 cleanup_key.clone(),
682 cleanup.clone(),
683 owner.clone(),
684 );
685 if let Some(scope) = crate::scope::current_scope() {
686 let registered = cleanup.clone();
687 let scope_key = cleanup_key.clone();
688 let scope_owner = owner.clone();
689 scope.add_disposer(move || {
690 crate::runtime::remove_keyed_disposer_for_owner(
691 &scope_key,
692 ®istered,
693 &scope_owner,
694 );
695 });
696 }
697 }
698 owner_disposer(cleanup_key, cleanup, owner)
699}
700
701pub fn install_shortcut_map_with_key(key: impl Into<String>, map: ShortcutMap) -> Dispose {
702 install_shortcut_map_state(key, map)
703}
704
705#[allow(non_snake_case)]
706pub fn InstallShortcutMapWithKey(key: impl Into<String>, map: ShortcutMap) -> Dispose {
707 install_shortcut_map_state(key, map)
708}
709
710#[track_caller]
711#[allow(non_snake_case)]
712pub fn InstallShortcutMap(map: ShortcutMap) -> Dispose {
713 let location = std::panic::Location::caller();
714 install_shortcut_map_state(
715 format!(
716 "{}:{}:{}",
717 location.file(),
718 location.line(),
719 location.column()
720 ),
721 map,
722 )
723}
724
725pub fn install_shortcut_handler_state(key: impl Into<String>, handler: Handler) -> Dispose {
726 let key = key.into();
727 if let Some(state) = active_runtime_state() {
728 return install_runtime_handler(&state, key, handler);
729 }
730 let state_key = format!("shortcut-handler-state:{key}");
731 let state: Rc<RefCell<HandlerInstallState>> = remember_with_key(state_key, || {
732 RefCell::new(HandlerInstallState {
733 token: 0,
734 active: false,
735 cleanup: None,
736 })
737 });
738 let (token, cleanup, installed) = {
739 let weak: Weak<RefCell<HandlerInstallState>> = Rc::downgrade(&state);
740 let mut state = state.borrow_mut();
741 if state.active {
742 (
743 state.token,
744 state.cleanup.clone().expect("active handler state"),
745 false,
746 )
747 } else {
748 let token = next_installer_id();
749 let cleanup = Dispose::new(move || {
750 let removed = HANDLER_ENTRIES
751 .try_with(|entries| {
752 entries.try_borrow_mut().ok().and_then(|mut entries| {
753 let index = entries.iter().position(|(id, _)| *id == token)?;
754 Some(entries.remove(index).1)
755 })
756 })
757 .ok()
758 .flatten();
759 drop(removed);
760 if let Some(state) = weak.upgrade() {
761 let old = {
762 let mut state = state.borrow_mut();
763 if state.token == token {
764 state.active = false;
765 state.cleanup.take()
766 } else {
767 None
768 }
769 };
770 drop(old);
771 }
772 sync_handler();
773 });
774 state.token = token;
775 state.active = true;
776 state.cleanup = Some(cleanup.clone());
777 (token, cleanup, true)
778 }
779 };
780 if installed {
781 let old = HANDLER_ENTRIES
782 .try_with(|entries| {
783 entries.try_borrow_mut().ok().map(|mut entries| {
784 entries.push((token, handler));
785 None::<Handler>
786 })
787 })
788 .ok()
789 .flatten();
790 drop(old);
791 sync_handler();
792 } else {
793 let old = HANDLER_ENTRIES
794 .try_with(|entries| {
795 entries.try_borrow_mut().ok().and_then(|mut entries| {
796 let entry = entries.iter_mut().find(|(id, _)| *id == token)?;
797 let old = entry.1.clone();
798 entry.1 = handler;
799 Some(old)
800 })
801 })
802 .ok()
803 .flatten();
804 drop(old);
805 sync_handler();
806 }
807 let cleanup_key = handler_cleanup_key(&key);
808 let owner =
809 crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
810 if !crate::runtime::keyed_disposer_has_owner(&cleanup_key, &cleanup, &owner) {
811 crate::runtime::register_keyed_disposer_for_owner(
812 cleanup_key.clone(),
813 cleanup.clone(),
814 owner.clone(),
815 );
816 if let Some(scope) = crate::scope::current_scope() {
817 let registered = cleanup.clone();
818 let scope_key = cleanup_key.clone();
819 let scope_owner = owner.clone();
820 scope.add_disposer(move || {
821 crate::runtime::remove_keyed_disposer_for_owner(
822 &scope_key,
823 ®istered,
824 &scope_owner,
825 );
826 });
827 }
828 }
829 owner_disposer(cleanup_key, cleanup, owner)
830}
831
832pub fn install_shortcut_handler_with_key(key: impl Into<String>, handler: Handler) -> Dispose {
833 install_shortcut_handler_state(key, handler)
834}
835
836#[allow(non_snake_case)]
837pub fn InstallShortcutHandlerWithKey(key: impl Into<String>, handler: Handler) -> Dispose {
838 install_shortcut_handler_state(key, handler)
839}
840
841#[track_caller]
842#[allow(non_snake_case)]
843pub fn InstallShortcutHandler(handler: Handler) -> Dispose {
844 let location = std::panic::Location::caller();
845 install_shortcut_handler_state(
846 format!(
847 "{}:{}:{}",
848 location.file(),
849 location.line(),
850 location.column()
851 ),
852 handler,
853 )
854}
855
856pub fn default_chord_for(action: &Action) -> Option<KeyChord> {
857 let cmd = Modifiers {
859 command: true,
860 ctrl: !cfg!(target_os = "macos"),
861 ..Modifiers::default()
862 };
863 match action {
864 Action::Copy => Some(KeyChord::new(Key::Character('c'), cmd)),
865 Action::Cut => Some(KeyChord::new(Key::Character('x'), cmd)),
866 Action::Paste => Some(KeyChord::new(Key::Character('v'), cmd)),
867 Action::SelectAll => Some(KeyChord::new(Key::Character('a'), cmd)),
868 Action::Undo => Some(KeyChord::new(Key::Character('z'), cmd)),
869 Action::Redo => Some(KeyChord::new(
870 Key::Character('z'),
871 Modifiers {
872 command: true,
873 shift: true,
874 ctrl: !cfg!(target_os = "macos"),
875 ..Modifiers::default()
876 },
877 )),
878 Action::Find => Some(KeyChord::new(Key::Character('f'), cmd)),
879 Action::Save => Some(KeyChord::new(Key::Character('s'), cmd)),
880 Action::FocusNext => Some(KeyChord::new(Key::Tab, Modifiers::default())),
881 Action::FocusPrevious => Some(KeyChord::new(
882 Key::Tab,
883 Modifiers {
884 shift: true,
885 ..Modifiers::default()
886 },
887 )),
888 Action::FocusLeft => Some(KeyChord::new(Key::ArrowLeft, Modifiers::default())),
889 Action::FocusRight => Some(KeyChord::new(Key::ArrowRight, Modifiers::default())),
890 Action::FocusUp => Some(KeyChord::new(Key::ArrowUp, Modifiers::default())),
891 Action::FocusDown => Some(KeyChord::new(Key::ArrowDown, Modifiers::default())),
892 _ => None,
893 }
894}
895
896pub fn default_map() -> ShortcutMap {
897 let mut map = ShortcutMap::new();
898 let actions = vec![
899 Action::Copy,
900 Action::Cut,
901 Action::Paste,
902 Action::SelectAll,
903 Action::Undo,
904 Action::Redo,
905 Action::Find,
906 Action::Save,
907 Action::FocusNext,
908 Action::FocusPrevious,
909 Action::FocusLeft,
910 Action::FocusRight,
911 Action::FocusUp,
912 Action::FocusDown,
913 ];
914 for action in actions {
915 if let Some(chord) = default_chord_for(&action) {
916 map.insert(chord.key, chord.modifiers, action);
917 }
918 }
919 map
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925
926 #[test]
927 fn resolve_action_prefers_scopes() {
928 let mut map = ShortcutMap::new();
929 map.insert(
930 Key::Character('k'),
931 Modifiers::default(),
932 Action::Custom("one".into()),
933 );
934 set_default_map(map);
935
936 let mut scope = ShortcutMap::new();
937 scope.insert(
938 Key::Character('k'),
939 Modifiers::default(),
940 Action::Custom("two".into()),
941 );
942
943 SCOPES.with(|scopes| scopes.borrow_mut().push(scope));
944
945 let chord = KeyChord::new(Key::Character('k'), Modifiers::default());
946 assert_eq!(
947 resolve_action(chord.clone()),
948 Some(Action::Custom("two".into()))
949 );
950
951 SCOPES.with(|scopes| scopes.borrow_mut().pop());
952 assert_eq!(resolve_action(chord), Some(Action::Custom("one".into())));
953 }
954
955 #[test]
956 fn runtime_installers_are_isolated() {
957 let a = ShortcutState::new();
958 let b = ShortcutState::new();
959 let chord = KeyChord::new(Key::Character('j'), Modifiers::default());
960 let mut map_a = ShortcutMap::new();
961 map_a.insert(
962 chord.key.clone(),
963 chord.modifiers,
964 Action::Custom("a".into()),
965 );
966 let mut map_b = ShortcutMap::new();
967 map_b.insert(
968 chord.key.clone(),
969 chord.modifiers,
970 Action::Custom("b".into()),
971 );
972 let dispose_a = with_runtime_state(&a, || InstallShortcutMapWithKey("same-install", map_a));
973 let dispose_b = with_runtime_state(&b, || InstallShortcutMapWithKey("same-install", map_b));
974 assert_eq!(a.resolve_action(&chord), Some(Action::Custom("a".into())));
975 assert_eq!(b.resolve_action(&chord), Some(Action::Custom("b".into())));
976 dispose_a.run();
977 assert!(a.resolve_action(&chord).is_none());
978 assert_eq!(b.resolve_action(&chord), Some(Action::Custom("b".into())));
979 dispose_b.run();
980 }
981
982 #[test]
983 fn scoped_shortcut_effects_are_isolated_per_runtime() {
984 use crate::{effects::Dispose, runtime::ComposeGuard, scope::Scope};
985 use std::cell::Cell;
986
987 fn compose(state: &ShortcutState, action: &'static str, fired: &Rc<Cell<usize>>) -> Scope {
988 let scope = Scope::new();
989 let state = state.clone();
990 let fired = fired.clone();
991 scope.run(|| {
992 with_runtime_state(&state, || {
993 crate::scope::scoped_effect_once(move || {
994 let mut map = ShortcutMap::new();
995 map.insert(
996 Key::Character('j'),
997 Modifiers::default(),
998 Action::Custom(action.into()),
999 );
1000 let map_disposer = InstallShortcutMapWithKey("shared-scoped", map);
1001 let handler_disposer = InstallShortcutHandlerWithKey(
1002 "shared-scoped",
1003 Rc::new(move |_action| {
1004 fired.set(fired.get() + 1);
1005 true
1006 }),
1007 );
1008 Dispose::new(move || {
1009 map_disposer.run();
1010 handler_disposer.run();
1011 })
1012 });
1013 });
1014 });
1015 scope
1016 }
1017
1018 let a = ShortcutState::new();
1019 let b = ShortcutState::new();
1020 let fired_a = Rc::new(Cell::new(0));
1021 let fired_b = Rc::new(Cell::new(0));
1022 let (scope_a, scope_b) = {
1026 let _guard = ComposeGuard::begin();
1027 (compose(&a, "a", &fired_a), compose(&b, "b", &fired_b))
1028 };
1029 let chord = KeyChord::new(Key::Character('j'), Modifiers::default());
1030 let action_a = a.resolve_action(&chord).unwrap();
1031 let action_b = b.resolve_action(&chord).unwrap();
1032 assert!(a.handle(action_a));
1033 assert!(b.handle(action_b));
1034 assert_eq!(fired_a.get(), 1);
1035 assert_eq!(fired_b.get(), 1);
1036 scope_a.dispose();
1037 scope_b.dispose();
1038 }
1039
1040 #[test]
1041 fn keyed_map_updates_and_cleans_up() {
1042 set_default_map(ShortcutMap::new());
1043 let chord = KeyChord::new(Key::Character('m'), Modifiers::default());
1044 let mut first = ShortcutMap::new();
1045 first.insert(
1046 chord.key.clone(),
1047 chord.modifiers,
1048 Action::Custom("first".into()),
1049 );
1050 let mut second = ShortcutMap::new();
1051 second.insert(
1052 chord.key.clone(),
1053 chord.modifiers,
1054 Action::Custom("second".into()),
1055 );
1056 let key = "shortcut-test-map-update";
1057 let disposer = install_shortcut_map_with_key(key, first);
1058 assert_eq!(
1059 resolve_action(chord.clone()),
1060 Some(Action::Custom("first".into()))
1061 );
1062 let _ = install_shortcut_map_with_key(key, second);
1063 assert_eq!(
1064 resolve_action(chord.clone()),
1065 Some(Action::Custom("second".into()))
1066 );
1067 disposer.run();
1068 assert_eq!(resolve_action(chord), None);
1069 }
1070
1071 #[test]
1072 fn handler_restores_base_after_keyed_cleanup() {
1073 let base = Rc::new(|_action: Action| false);
1074 let installed = Rc::new(|_action: Action| true);
1075 set(Some(base));
1076 let key = "shortcut-test-handler-restore";
1077 let disposer = install_shortcut_handler_with_key(key, installed);
1078 assert!(handle(Action::Copy));
1079 disposer.run();
1080 assert!(!handle(Action::Copy));
1081 set(None);
1082 }
1083}