1mod standard;
33
34mod focus_region;
35
36#[doc(hidden)]
37pub use focus_region::{NEXT_REGION, PREVIOUS_REGION, focus_region_shortcuts};
38pub(crate) use focus_region::{region_direction, register_focus_region_shortcuts};
39pub(crate) use standard::register_standard_shortcuts;
40pub use standard::{STANDARD_EDIT_CATEGORY, StandardEdit};
41
42pub(crate) fn is_framework_shortcut_id(id: &str) -> bool {
46 StandardEdit::from_id(id).is_some() || region_direction(id).is_some()
47}
48
49fn framework_default(id: &str) -> Option<Shortcut> {
52 StandardEdit::from_id(id)
53 .map(StandardEdit::shortcut)
54 .or_else(|| focus_region::region_default(id))
55}
56
57use crate::event::{Key, Modifiers};
58use crate::intent::Intent;
59use crate::signal::{Prop, Signal};
60use crate::widget::EventContext;
61use crate::widget_id::WidgetId;
62use std::collections::HashMap;
63use std::fmt;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
71pub struct KeyStroke {
72 pub key: Key,
73 pub modifiers: Modifiers,
74}
75
76impl KeyStroke {
77 pub fn new(key: Key, modifiers: Modifiers) -> Self {
78 Self { key, modifiers }
79 }
80
81 pub fn ctrl(key: Key) -> Self {
92 Self::new(key, Modifiers::CTRL)
93 }
94
95 pub fn ctrl_shift(key: Key) -> Self {
96 Self::new(key, Modifiers::CTRL | Modifiers::SHIFT)
97 }
98
99 pub fn command(key: Key) -> Self {
107 Self::new(key, Modifiers::COMMAND)
108 }
109
110 pub fn command_shift(key: Key) -> Self {
113 Self::new(key, Modifiers::COMMAND | Modifiers::SHIFT)
114 }
115
116 pub fn alt(key: Key) -> Self {
117 Self::new(key, Modifiers::ALT)
118 }
119
120 pub(crate) fn without_shift_for_punctuation(self) -> Option<Self> {
126 let Key::Character(c) = self.key else {
127 return None;
128 };
129 let others = self.modifiers.without(Modifiers::SHIFT);
130 if !self.modifiers.shift() || others == Modifiers::NONE || c.is_alphanumeric() {
131 return None;
132 }
133 Some(Self::new(self.key, others))
134 }
135
136 pub fn with_command_convention(self) -> Self {
154 self.with_command_convention_using(Modifiers::COMMAND)
155 }
156
157 pub(crate) fn with_command_convention_using(self, command: Modifiers) -> Self {
167 Self::new(
168 self.key,
169 self.modifiers.with_command_convention_using(command),
170 )
171 }
172}
173
174impl fmt::Display for KeyStroke {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(f, "{}{}", self.modifiers, self.key)
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum ShortcutScope {
194 Global,
199 Scoped(WidgetId),
207}
208
209fn scopes_can_collide(a: ShortcutScope, b: ShortcutScope) -> bool {
216 match (a, b) {
217 (ShortcutScope::Scoped(x), ShortcutScope::Scoped(y)) => x == y,
218 _ => true,
219 }
220}
221
222pub type ShortcutOnActivate = Box<dyn FnMut(KeyStroke, &mut EventContext) -> Intent>;
232
233pub type KeyCaptureCallback = Box<dyn FnOnce(KeyStroke, &mut ShortcutRegistry, &mut EventContext)>;
240
241pub(crate) type KeyCaptureSlot = std::rc::Rc<std::cell::RefCell<Option<KeyCaptureCallback>>>;
244
245#[must_use = "key capture is cancelled when the CaptureHandle is dropped"]
260pub struct CaptureHandle {
261 slot: KeyCaptureSlot,
262}
263
264impl std::fmt::Debug for CaptureHandle {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 f.debug_struct("CaptureHandle")
267 .field("armed", &self.slot.borrow().is_some())
268 .finish()
269 }
270}
271
272impl CaptureHandle {
273 pub(crate) fn new(slot: KeyCaptureSlot) -> Self {
274 Self { slot }
275 }
276
277 pub fn is_armed(&self) -> bool {
280 self.slot.borrow().is_some()
281 }
282
283 pub fn cancel(self) {
287 drop(self);
289 }
290}
291
292impl Drop for CaptureHandle {
293 fn drop(&mut self) {
294 self.slot.borrow_mut().take();
298 }
299}
300
301pub struct Shortcut {
313 pub id: &'static str,
317 pub name: Prop<String>,
319 pub category: Option<&'static str>,
326 pub category_label: Option<Prop<String>>,
332 pub description: Option<Prop<String>>,
334 pub primary: Option<KeyStroke>,
337 pub secondary: Option<KeyStroke>,
340 pub intent: Option<&'static str>,
343 pub on_activate: Option<ShortcutOnActivate>,
347 pub scope: ShortcutScope,
351 pub propagate_when_disabled: bool,
356 pub enabled_when: Option<Prop<bool>>,
362 pub literal_modifiers: bool,
366}
367
368impl fmt::Debug for Shortcut {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 f.debug_struct("Shortcut")
371 .field("id", &self.id)
372 .field("name", &self.name)
373 .field("category", &self.category)
374 .field("category_label", &self.category_label)
375 .field("description", &self.description)
376 .field("primary", &self.primary)
377 .field("secondary", &self.secondary)
378 .field("intent", &self.intent)
379 .field(
380 "on_activate",
381 &self.on_activate.as_ref().map(|_| "<closure>"),
382 )
383 .field("scope", &self.scope)
384 .field("propagate_when_disabled", &self.propagate_when_disabled)
385 .field("enabled_when", &self.enabled_when.is_some())
386 .field("literal_modifiers", &self.literal_modifiers)
387 .finish()
388 }
389}
390
391impl Shortcut {
392 pub fn category_text(&self) -> Option<String> {
395 match (&self.category_label, self.category) {
396 (Some(label), Some(_)) => Some(label.get()),
397 (_, Some(key)) => Some(key.to_string()),
398 (_, None) => None,
399 }
400 }
401
402 #[allow(clippy::new_ret_no_self)]
404 pub fn new(id: &'static str) -> ShortcutBuilder {
405 ShortcutBuilder {
406 inner: Shortcut {
407 id,
408 name: Prop::Static(String::new()),
409 category: None,
410 category_label: None,
411 description: None,
412 primary: None,
413 secondary: None,
414 intent: None,
415 on_activate: None,
416 scope: ShortcutScope::Global,
417 propagate_when_disabled: true,
418 enabled_when: None,
419 literal_modifiers: false,
420 },
421 }
422 }
423
424 pub fn is_enabled(&self) -> bool {
427 self.enabled_when.as_ref().map(|s| s.get()).unwrap_or(true)
428 }
429
430 pub fn intent_name(&self) -> &'static str {
433 self.intent.unwrap_or(self.id)
434 }
435
436 pub fn matches_default(&self, keystroke: KeyStroke) -> bool {
445 self.matches_default_using(keystroke, Modifiers::COMMAND)
446 }
447
448 pub(crate) fn matches_default_using(&self, keystroke: KeyStroke, command: Modifiers) -> bool {
451 let (primary, secondary) = self.declared_keystrokes_using(command);
452 primary == Some(keystroke) || secondary == Some(keystroke)
453 }
454
455 pub fn declared_keystrokes(&self) -> (Option<KeyStroke>, Option<KeyStroke>) {
464 self.declared_keystrokes_using(Modifiers::COMMAND)
465 }
466
467 pub(crate) fn declared_keystrokes_using(
476 &self,
477 command: Modifiers,
478 ) -> (Option<KeyStroke>, Option<KeyStroke>) {
479 if self.literal_modifiers {
480 (self.primary, self.secondary)
481 } else {
482 (
483 self.primary
484 .map(|k| k.with_command_convention_using(command)),
485 self.secondary
486 .map(|k| k.with_command_convention_using(command)),
487 )
488 }
489 }
490}
491
492pub struct ShortcutBuilder {
496 inner: Shortcut,
497}
498
499impl ShortcutBuilder {
500 pub fn name(mut self, name: impl Into<Prop<String>>) -> Self {
504 self.inner.name = name.into();
505 self
506 }
507
508 pub fn category(mut self, category: &'static str) -> Self {
512 self.inner.category = Some(category);
513 self
514 }
515
516 pub fn category_label(mut self, label: impl Into<Prop<String>>) -> Self {
521 self.inner.category_label = Some(label.into());
522 self
523 }
524
525 pub fn literal_modifiers(mut self) -> Self {
542 self.inner.literal_modifiers = true;
543 self
544 }
545
546 pub fn description(mut self, description: impl Into<Prop<String>>) -> Self {
549 self.inner.description = Some(description.into());
550 self
551 }
552
553 pub fn primary(mut self, keystroke: KeyStroke) -> Self {
554 self.inner.primary = Some(keystroke);
555 self
556 }
557
558 pub fn secondary(mut self, keystroke: KeyStroke) -> Self {
559 self.inner.secondary = Some(keystroke);
560 self
561 }
562
563 pub fn intent(mut self, intent: &'static str) -> Self {
565 self.inner.intent = Some(intent);
566 self
567 }
568
569 pub fn on_activate<R>(
577 mut self,
578 mut f: impl FnMut(KeyStroke, &mut EventContext) -> R + 'static,
579 ) -> Self
580 where
581 R: Into<Intent>,
582 {
583 self.inner.on_activate = Some(Box::new(move |ks, ctx| f(ks, ctx).into()));
584 self
585 }
586
587 pub fn scope_to(mut self, id: WidgetId) -> Self {
590 self.inner.scope = ShortcutScope::Scoped(id);
591 self
592 }
593
594 pub fn scope(mut self, scope: ShortcutScope) -> Self {
595 self.inner.scope = scope;
596 self
597 }
598
599 pub fn global(mut self) -> Self {
601 self.inner.scope = ShortcutScope::Global;
602 self
603 }
604
605 pub fn propagate_when_disabled(mut self, propagate: bool) -> Self {
609 self.inner.propagate_when_disabled = propagate;
610 self
611 }
612
613 pub fn enabled_when(mut self, signal: impl Into<Prop<bool>>) -> Self {
622 self.inner.enabled_when = Some(signal.into());
623 self
624 }
625
626 pub fn build(self) -> Shortcut {
627 self.inner
628 }
629}
630
631#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
644pub enum SlotOverride {
645 #[default]
648 Default,
649 Bound(KeyStroke),
651 Unbound,
653}
654
655impl SlotOverride {
656 pub fn resolve(self, fallback: Option<KeyStroke>) -> Option<KeyStroke> {
659 match self {
660 SlotOverride::Default => fallback,
661 SlotOverride::Bound(ks) => Some(ks),
662 SlotOverride::Unbound => None,
663 }
664 }
665
666 pub fn is_touched(self) -> bool {
668 !matches!(self, SlotOverride::Default)
669 }
670}
671
672#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
676pub struct KeyStrokeOverride {
677 pub primary: SlotOverride,
678 pub secondary: SlotOverride,
679}
680
681impl KeyStrokeOverride {
682 pub fn is_empty(self) -> bool {
686 !self.primary.is_touched() && !self.secondary.is_touched()
687 }
688}
689
690#[derive(Debug, Clone, Copy)]
702pub struct EffectiveShortcut<'a> {
703 pub shortcut: &'a Shortcut,
704 pub primary: Option<KeyStroke>,
705 pub secondary: Option<KeyStroke>,
706 pub enabled: bool,
707}
708
709impl EffectiveShortcut<'_> {
710 pub fn matches(&self, keystroke: KeyStroke) -> bool {
711 self.primary == Some(keystroke) || self.secondary == Some(keystroke)
712 }
713}
714
715pub struct ShortcutRegistry {
730 defaults: HashMap<&'static str, Shortcut>,
731 overrides: HashMap<String, KeyStrokeOverride>,
732 by_owner: HashMap<WidgetId, Vec<&'static str>>,
738 owner_by_id: HashMap<&'static str, WidgetId>,
742 shared_owners: HashMap<&'static str, Vec<WidgetId>>,
746 version: Signal<u64>,
747 resolved: HashMap<&'static str, Signal<Option<KeyStroke>>>,
757 framework_labels: HashMap<&'static str, FrameworkLabels>,
763}
764
765#[derive(Debug, Clone)]
770struct FrameworkLabels {
771 name: Prop<String>,
772 category_label: Option<Prop<String>>,
773 description: Option<Prop<String>>,
774}
775
776impl Default for ShortcutRegistry {
777 fn default() -> Self {
778 Self::new()
779 }
780}
781
782impl fmt::Debug for ShortcutRegistry {
783 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
784 f.debug_struct("ShortcutRegistry")
785 .field("defaults", &self.defaults)
786 .field("overrides", &self.overrides)
787 .field("by_owner", &self.by_owner)
788 .field("version", &self.version.get())
789 .finish()
790 }
791}
792
793impl ShortcutRegistry {
794 pub fn new() -> Self {
795 Self {
796 defaults: HashMap::new(),
797 overrides: HashMap::new(),
798 by_owner: HashMap::new(),
799 owner_by_id: HashMap::new(),
800 shared_owners: HashMap::new(),
801 version: Signal::new(0),
802 resolved: HashMap::new(),
803 framework_labels: HashMap::new(),
804 }
805 }
806
807 pub fn version(&self) -> &Signal<u64> {
811 &self.version
812 }
813
814 pub fn effective_primary_signal(&mut self, id: &'static str) -> Signal<Option<KeyStroke>> {
825 if let Some(sig) = self.resolved.get(id) {
826 return sig.clone();
827 }
828 let current = self
829 .defaults
830 .get(id)
831 .and_then(|s| self.resolved_keystrokes(id, s).0);
832 let sig = Signal::new(current);
833 self.resolved.insert(id, sig.clone());
834 sig
835 }
836
837 pub fn register(&mut self, shortcut: Shortcut) -> Option<Shortcut> {
845 let id = shortcut.id;
846 if is_framework_shortcut_id(id) {
847 self.framework_labels.insert(
848 id,
849 FrameworkLabels {
850 name: shortcut.name.clone(),
851 category_label: shortcut.category_label.clone(),
852 description: shortcut.description.clone(),
853 },
854 );
855 }
856 let previous = self.defaults.insert(id, shortcut);
857 self.detach_owner_index(id);
861 self.detach_shared(id);
862 self.bump_version();
863 self.refresh_resolved(id);
864 previous
865 }
866
867 pub fn register_owned(&mut self, shortcut: Shortcut, owner: WidgetId) -> Option<Shortcut> {
875 let id = shortcut.id;
876 let previous = self.defaults.insert(id, shortcut);
877 self.detach_owner_index(id);
878 self.detach_shared(id);
879 self.by_owner.entry(owner).or_default().push(id);
880 self.owner_by_id.insert(id, owner);
881 self.bump_version();
882 self.refresh_resolved(id);
883 previous
884 }
885
886 pub fn register_shared(&mut self, shortcut: Shortcut, owner: WidgetId) -> Option<Shortcut> {
906 let id = shortcut.id;
907 let previous = self.defaults.insert(id, shortcut);
908 self.detach_owner_index(id);
909 let owners = self.shared_owners.entry(id).or_default();
910 if !owners.contains(&owner) {
911 owners.push(owner);
912 }
913 let ids = self.by_owner.entry(owner).or_default();
914 if !ids.contains(&id) {
915 ids.push(id);
916 }
917 self.bump_version();
918 self.refresh_resolved(id);
919 previous
920 }
921
922 pub fn unregister(&mut self, id: &str) -> Option<Shortcut> {
930 self.framework_labels.remove(id);
931 let removed = self.defaults.remove(id);
932 if removed.is_some() {
933 self.detach_owner_index(id);
934 self.detach_shared(id);
935 self.bump_version();
936 self.refresh_resolved(id);
937 }
938 removed
939 }
940
941 pub fn unregister_all_for_owner(&mut self, owner: WidgetId) {
956 let Some(ids) = self.by_owner.remove(&owner) else {
957 return;
958 };
959 let mut any = false;
960 for id in ids {
961 if let Some(owners) = self.shared_owners.get_mut(id) {
963 owners.retain(|o| *o != owner);
964 if !owners.is_empty() {
965 continue;
966 }
967 self.shared_owners.remove(id);
968 }
969 self.owner_by_id.remove(id);
970 if let Some(mut restored) = self.framework_restore(id) {
973 any = true;
974 let labels = self.framework_labels[id].clone();
975 restored.name = labels.name;
976 restored.category_label = labels.category_label;
977 restored.description = labels.description;
978 self.defaults.insert(id, restored);
979 self.refresh_resolved(id);
980 continue;
981 }
982 if self.defaults.remove(id).is_some() {
983 any = true;
984 }
985 self.refresh_resolved(id);
987 }
988 if any {
989 self.bump_version();
990 }
991 }
992
993 fn framework_restore(&self, id: &'static str) -> Option<Shortcut> {
997 if !self.framework_labels.contains_key(id) {
998 return None;
999 }
1000 framework_default(id)
1001 }
1002
1003 pub fn owner_of(&self, id: &str) -> Option<WidgetId> {
1005 self.owner_by_id.get(id).copied()
1006 }
1007
1008 #[doc(hidden)]
1012 pub fn has_owner(&self, id: &str) -> bool {
1013 self.owner_by_id.contains_key(id) || self.shared_owners.contains_key(id)
1014 }
1015
1016 pub(crate) fn owners(&self, id: &str) -> Vec<WidgetId> {
1022 match self.owner_by_id.get(id) {
1023 Some(owner) => vec![*owner],
1024 None => self.shared_owners.get(id).cloned().unwrap_or_default(),
1025 }
1026 }
1027
1028 pub fn len(&self) -> usize {
1029 self.defaults.len()
1030 }
1031
1032 pub fn is_empty(&self) -> bool {
1033 self.defaults.is_empty()
1034 }
1035
1036 pub fn iter_defaults(&self) -> impl Iterator<Item = &Shortcut> {
1039 self.defaults.values()
1040 }
1041
1042 pub fn get_default(&self, id: &str) -> Option<&Shortcut> {
1044 self.defaults.get(id)
1045 }
1046
1047 pub(crate) fn invoke_on_activate(
1057 &mut self,
1058 id: &str,
1059 keystroke: KeyStroke,
1060 ctx: &mut EventContext,
1061 ) -> Option<Intent> {
1062 let shortcut = self.defaults.get_mut(id)?;
1063 let intent_name = shortcut.intent_name();
1064 let intent = match &mut shortcut.on_activate {
1065 Some(handler) => handler(keystroke, ctx),
1066 None => Intent::new(intent_name),
1067 };
1068 Some(intent)
1069 }
1070
1071 pub fn override_for(&self, id: &str) -> Option<KeyStrokeOverride> {
1073 self.overrides.get(id).copied()
1074 }
1075
1076 pub fn put_override(&mut self, id: impl Into<String>, override_: KeyStrokeOverride) {
1079 let id = id.into();
1080 self.overrides.insert(id.clone(), override_);
1081 self.bump_version();
1082 self.refresh_resolved(&id);
1083 }
1084
1085 pub fn rebind_primary(&mut self, id: impl Into<String>, keystroke: Option<KeyStroke>) {
1090 let id = id.into();
1091 let entry = self.overrides.entry(id.clone()).or_default();
1092 entry.primary = match keystroke {
1093 Some(ks) => SlotOverride::Bound(ks),
1094 None => SlotOverride::Unbound,
1095 };
1096 self.bump_version();
1097 self.refresh_resolved(&id);
1098 }
1099
1100 pub fn rebind_secondary(&mut self, id: impl Into<String>, keystroke: Option<KeyStroke>) {
1103 let id = id.into();
1104 let entry = self.overrides.entry(id.clone()).or_default();
1105 entry.secondary = match keystroke {
1106 Some(ks) => SlotOverride::Bound(ks),
1107 None => SlotOverride::Unbound,
1108 };
1109 self.bump_version();
1110 self.refresh_resolved(&id);
1113 }
1114
1115 pub fn clear_override(&mut self, id: &str) {
1117 if self.overrides.remove(id).is_some() {
1118 self.bump_version();
1119 self.refresh_resolved(id);
1120 }
1121 }
1122
1123 pub fn clear_all_overrides(&mut self) {
1126 if !self.overrides.is_empty() {
1127 self.overrides.clear();
1128 self.bump_version();
1129 self.refresh_all_resolved();
1130 }
1131 }
1132
1133 pub fn export_overrides(&self) -> HashMap<String, KeyStrokeOverride> {
1137 self.overrides.clone()
1138 }
1139
1140 pub fn import_overrides(&mut self, overrides: HashMap<String, KeyStrokeOverride>) {
1146 self.overrides = overrides;
1147 self.bump_version();
1148 self.refresh_all_resolved();
1149 }
1150
1151 pub fn effective(&self, id: &str) -> Option<EffectiveShortcut<'_>> {
1155 let shortcut = self.defaults.get(id)?;
1156 let (primary, secondary) = self.resolved_keystrokes(id, shortcut);
1157 Some(EffectiveShortcut {
1158 shortcut,
1159 primary,
1160 secondary,
1161 enabled: shortcut.is_enabled(),
1162 })
1163 }
1164
1165 pub fn iter_effective(&self) -> impl Iterator<Item = EffectiveShortcut<'_>> {
1173 let mut items: Vec<EffectiveShortcut<'_>> = self
1174 .defaults
1175 .iter()
1176 .map(|(id, shortcut)| {
1177 let (primary, secondary) = self.resolved_keystrokes(id, shortcut);
1178 EffectiveShortcut {
1179 shortcut,
1180 primary,
1181 secondary,
1182 enabled: shortcut.is_enabled(),
1183 }
1184 })
1185 .collect();
1186 items.sort_by(|a, b| {
1187 a.shortcut
1188 .category
1189 .cmp(&b.shortcut.category)
1190 .then(a.shortcut.id.cmp(b.shortcut.id))
1191 });
1192 items.into_iter()
1193 }
1194
1195 pub fn find_conflict(
1215 &self,
1216 keystroke: KeyStroke,
1217 excluding_id: Option<&str>,
1218 ) -> Option<&'static str> {
1219 let self_scope = excluding_id
1220 .and_then(|eid| self.defaults.get(eid))
1221 .map(|s| s.scope);
1222 self.defaults.iter().find_map(|(&id, shortcut)| {
1223 if excluding_id == Some(id) {
1224 return None;
1225 }
1226 let (primary, secondary) = self.resolved_keystrokes(id, shortcut);
1227 if primary != Some(keystroke) && secondary != Some(keystroke) {
1228 return None;
1229 }
1230 match self_scope {
1233 Some(self_scope) if !scopes_can_collide(self_scope, shortcut.scope) => None,
1234 _ => Some(id),
1235 }
1236 })
1237 }
1238
1239 pub fn matches_by_keystroke(
1261 &self,
1262 keystroke: KeyStroke,
1263 ) -> impl Iterator<Item = EffectiveShortcut<'_>> {
1264 let exact: Vec<_> = self
1265 .iter_effective()
1266 .filter(|s| s.enabled && s.matches(keystroke))
1267 .collect();
1268 if exact.is_empty()
1269 && let Some(relaxed) = keystroke.without_shift_for_punctuation()
1270 {
1271 return self
1272 .iter_effective()
1273 .filter(|s| s.enabled && s.matches(relaxed))
1274 .collect::<Vec<_>>()
1275 .into_iter();
1276 }
1277 exact.into_iter()
1278 }
1279
1280 pub fn find_by_keystroke(&self, keystroke: KeyStroke) -> Option<EffectiveShortcut<'_>> {
1291 self.matches_by_keystroke(keystroke).next()
1292 }
1293
1294 fn resolved_keystrokes(
1295 &self,
1296 id: &str,
1297 shortcut: &Shortcut,
1298 ) -> (Option<KeyStroke>, Option<KeyStroke>) {
1299 let ov = self.overrides.get(id).copied().unwrap_or_default();
1300 let (primary, secondary) = shortcut.declared_keystrokes();
1306 (ov.primary.resolve(primary), ov.secondary.resolve(secondary))
1307 }
1308
1309 fn bump_version(&self) {
1310 self.version.set(self.version.get().wrapping_add(1));
1311 }
1312
1313 fn refresh_resolved(&self, id: &str) {
1319 if let Some(sig) = self.resolved.get(id) {
1320 let current = self
1321 .defaults
1322 .get(id)
1323 .and_then(|s| self.resolved_keystrokes(id, s).0);
1324 if sig.get() != current {
1325 sig.set(current);
1326 }
1327 }
1328 }
1329
1330 fn refresh_all_resolved(&self) {
1335 let ids: Vec<&'static str> = self.resolved.keys().copied().collect();
1336 for id in ids {
1337 self.refresh_resolved(id);
1338 }
1339 }
1340
1341 fn detach_shared(&mut self, id: &str) {
1344 let Some(owners) = self.shared_owners.remove(id) else {
1345 return;
1346 };
1347 for owner in owners {
1348 if let Some(ids) = self.by_owner.get_mut(&owner) {
1349 ids.retain(|entry| *entry != id);
1350 if ids.is_empty() {
1351 self.by_owner.remove(&owner);
1352 }
1353 }
1354 }
1355 }
1356
1357 fn detach_owner_index(&mut self, id: &str) {
1360 let Some(owner) = self.owner_by_id.remove(id) else {
1361 return;
1362 };
1363 if let Some(vec) = self.by_owner.get_mut(&owner) {
1364 vec.retain(|entry| *entry != id);
1365 if vec.is_empty() {
1366 self.by_owner.remove(&owner);
1367 }
1368 }
1369 }
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374 use super::*;
1375
1376 #[test]
1379 fn builder_defaults_are_sane() {
1380 let s = Shortcut::new("editor.format.bold").name("Bold").build();
1381 assert_eq!(s.id, "editor.format.bold");
1382 assert_eq!(s.intent_name(), "editor.format.bold");
1383 assert_eq!(s.name.get(), "Bold");
1384 assert_eq!(s.scope, ShortcutScope::Global);
1385 assert!(s.propagate_when_disabled);
1386 assert!(s.primary.is_none());
1387 assert!(s.secondary.is_none());
1388 }
1389
1390 #[test]
1391 fn builder_intent_overrides_id() {
1392 let s = Shortcut::new("app.save_as").intent("app.save").build();
1393 assert_eq!(s.intent_name(), "app.save");
1394 }
1395
1396 #[test]
1397 fn builder_scope_variants() {
1398 use slotmap::KeyData;
1399 let id: WidgetId = KeyData::from_ffi(7).into();
1400
1401 let g = Shortcut::new("foo").build();
1402 assert_eq!(g.scope, ShortcutScope::Global);
1403
1404 let s = Shortcut::new("bar").scope_to(id).build();
1405 assert_eq!(s.scope, ShortcutScope::Scoped(id));
1406
1407 let e = Shortcut::new("baz")
1408 .scope(ShortcutScope::Scoped(id))
1409 .build();
1410 assert_eq!(e.scope, ShortcutScope::Scoped(id));
1411
1412 let back = Shortcut::new("qux").scope_to(id).global().build();
1413 assert_eq!(back.scope, ShortcutScope::Global);
1414 }
1415
1416 #[test]
1417 fn shortcut_matches_default_primary_and_secondary() {
1418 let s = Shortcut::new("edit.undo")
1419 .primary(KeyStroke::command(Key::Z))
1420 .secondary(KeyStroke::alt(Key::Backspace))
1421 .build();
1422 assert!(s.matches_default(KeyStroke::command(Key::Z)));
1423 assert!(s.matches_default(KeyStroke::alt(Key::Backspace)));
1424 assert!(!s.matches_default(KeyStroke::command(Key::Y)));
1425 }
1426
1427 #[test]
1430 fn register_upserts_and_preserves_override() {
1431 let mut reg = ShortcutRegistry::new();
1432 reg.register(
1433 Shortcut::new("app.save")
1434 .name("Save")
1435 .primary(KeyStroke::command(Key::S))
1436 .build(),
1437 );
1438
1439 reg.rebind_primary("app.save", Some(KeyStroke::command_shift(Key::S)));
1441 assert_eq!(
1442 reg.effective("app.save").unwrap().primary,
1443 Some(KeyStroke::command_shift(Key::S))
1444 );
1445
1446 reg.register(
1449 Shortcut::new("app.save")
1450 .name("Save")
1451 .primary(KeyStroke::command(Key::S))
1452 .build(),
1453 );
1454 assert_eq!(
1455 reg.effective("app.save").unwrap().primary,
1456 Some(KeyStroke::command_shift(Key::S))
1457 );
1458
1459 reg.register(
1462 Shortcut::new("app.save")
1463 .name("Save (renamed)")
1464 .primary(KeyStroke::alt(Key::S))
1465 .build(),
1466 );
1467 let eff = reg.effective("app.save").unwrap();
1468 assert_eq!(eff.primary, Some(KeyStroke::command_shift(Key::S)));
1469 assert_eq!(eff.shortcut.name.get(), "Save (renamed)");
1470 }
1471
1472 #[test]
1473 fn clear_override_restores_default() {
1474 let mut reg = ShortcutRegistry::new();
1475 reg.register(
1476 Shortcut::new("app.save")
1477 .primary(KeyStroke::command(Key::S))
1478 .build(),
1479 );
1480 reg.rebind_primary("app.save", Some(KeyStroke::command_shift(Key::S)));
1481 reg.clear_override("app.save");
1482 assert_eq!(
1483 reg.effective("app.save").unwrap().primary,
1484 Some(KeyStroke::command(Key::S))
1485 );
1486 }
1487
1488 #[test]
1491 fn override_survives_unregister_and_reregister() {
1492 let mut reg = ShortcutRegistry::new();
1493 reg.register(
1494 Shortcut::new("editor.format.bold")
1495 .primary(KeyStroke::command(Key::B))
1496 .build(),
1497 );
1498 reg.rebind_primary("editor.format.bold", Some(KeyStroke::command_shift(Key::B)));
1499
1500 reg.unregister("editor.format.bold");
1501 assert!(reg.effective("editor.format.bold").is_none());
1502 assert_eq!(
1503 reg.override_for("editor.format.bold").unwrap().primary,
1504 SlotOverride::Bound(KeyStroke::command_shift(Key::B))
1505 );
1506
1507 reg.register(
1508 Shortcut::new("editor.format.bold")
1509 .primary(KeyStroke::command(Key::B))
1510 .build(),
1511 );
1512 assert_eq!(
1513 reg.effective("editor.format.bold").unwrap().primary,
1514 Some(KeyStroke::command_shift(Key::B))
1515 );
1516 }
1517
1518 #[test]
1521 fn version_bumps_on_every_mutation() {
1522 let mut reg = ShortcutRegistry::new();
1523 let v0 = reg.version().get();
1524
1525 reg.register(Shortcut::new("a").build());
1526 let v1 = reg.version().get();
1527 assert!(v1 > v0);
1528
1529 reg.rebind_primary("a", Some(KeyStroke::command(Key::A)));
1530 let v2 = reg.version().get();
1531 assert!(v2 > v1);
1532
1533 reg.rebind_secondary("a", Some(KeyStroke::alt(Key::A)));
1534 let v3 = reg.version().get();
1535 assert!(v3 > v2);
1536
1537 reg.clear_override("a");
1538 let v4 = reg.version().get();
1539 assert!(v4 > v3);
1540
1541 reg.unregister("a");
1542 let v5 = reg.version().get();
1543 assert!(v5 > v4);
1544 }
1545
1546 #[test]
1549 fn per_id_signal_seeds_isolates_and_tracks() {
1550 use std::cell::Cell;
1551 use std::rc::Rc;
1552
1553 let mut reg = ShortcutRegistry::new();
1554 reg.register(
1555 Shortcut::new("work.new")
1556 .primary(KeyStroke::command(Key::N))
1557 .build(),
1558 );
1559
1560 let sig = reg.effective_primary_signal("work.new");
1562 assert_eq!(sig.get(), Some(KeyStroke::command(Key::N)));
1563
1564 let hits = Rc::new(Cell::new(0usize));
1566 let _h = {
1567 let hits = hits.clone();
1568 sig.observe(move |_| hits.set(hits.get() + 1))
1569 };
1570
1571 reg.register(
1575 Shortcut::new("outline.open_to_side")
1576 .primary(KeyStroke::command(Key::Enter))
1577 .build(),
1578 );
1579 reg.unregister("outline.open_to_side");
1580 assert_eq!(
1581 hits.get(),
1582 0,
1583 "unrelated shortcut churn must not notify a per-id observer"
1584 );
1585 assert_eq!(sig.get(), Some(KeyStroke::command(Key::N)));
1586
1587 reg.rebind_primary("work.new", Some(KeyStroke::command_shift(Key::N)));
1589 assert_eq!(sig.get(), Some(KeyStroke::command_shift(Key::N)));
1590 assert_eq!(hits.get(), 1);
1591
1592 reg.unregister("work.new");
1594 assert_eq!(sig.get(), None);
1595 assert_eq!(hits.get(), 2);
1596 }
1597
1598 #[test]
1599 fn per_id_signal_observed_before_registration_goes_live_on_register() {
1600 let mut reg = ShortcutRegistry::new();
1605 let sig = reg.effective_primary_signal("late.cmd");
1606 assert_eq!(sig.get(), None);
1607
1608 reg.register(
1609 Shortcut::new("late.cmd")
1610 .primary(KeyStroke::command(Key::S))
1611 .build(),
1612 );
1613 assert_eq!(sig.get(), Some(KeyStroke::command(Key::S)));
1614
1615 reg.rebind_primary("late.cmd", Some(KeyStroke::command_shift(Key::S)));
1617 assert_eq!(sig.get(), Some(KeyStroke::command_shift(Key::S)));
1618 }
1619
1620 #[test]
1623 fn find_by_keystroke_uses_effective() {
1624 let mut reg = ShortcutRegistry::new();
1625 reg.register(
1626 Shortcut::new("app.save")
1627 .primary(KeyStroke::command(Key::S))
1628 .build(),
1629 );
1630 assert_eq!(
1631 reg.find_by_keystroke(KeyStroke::command(Key::S))
1632 .map(|s| s.shortcut.id),
1633 Some("app.save")
1634 );
1635
1636 reg.rebind_primary("app.save", Some(KeyStroke::command_shift(Key::S)));
1637
1638 assert!(reg.find_by_keystroke(KeyStroke::command(Key::S)).is_none());
1639 assert_eq!(
1640 reg.find_by_keystroke(KeyStroke::command_shift(Key::S))
1641 .map(|s| s.shortcut.id),
1642 Some("app.save")
1643 );
1644 }
1645
1646 #[test]
1649 fn a_punctuation_chord_typed_with_shift_matches_the_unshifted_declaration() {
1650 let mut reg = ShortcutRegistry::new();
1651 reg.register(
1652 Shortcut::new("code.toggle_comment")
1653 .primary(KeyStroke::command(Key::Character('/')))
1654 .build(),
1655 );
1656 let french = KeyStroke::command_shift(Key::Character('/'));
1659 assert_eq!(
1660 reg.find_by_keystroke(french).map(|s| s.shortcut.id),
1661 Some("code.toggle_comment")
1662 );
1663 assert!(
1665 reg.find_by_keystroke(KeyStroke::command(Key::Character('/')))
1666 .is_some()
1667 );
1668 assert!(
1670 reg.find_by_keystroke(KeyStroke::new(Key::Character('/'), Modifiers::SHIFT))
1671 .is_none()
1672 );
1673 }
1674
1675 #[test]
1676 fn shift_is_never_ignored_for_a_letter_or_digit() {
1677 let mut reg = ShortcutRegistry::new();
1678 reg.register(
1679 Shortcut::new("a.plain")
1680 .primary(KeyStroke::command(Key::D))
1681 .build(),
1682 );
1683 reg.register(
1684 Shortcut::new("a.digit")
1685 .primary(KeyStroke::command(Key::Character('1')))
1686 .build(),
1687 );
1688 assert!(
1689 reg.find_by_keystroke(KeyStroke::command_shift(Key::D))
1690 .is_none()
1691 );
1692 assert!(
1693 reg.find_by_keystroke(KeyStroke::new(
1694 Key::Character('1'),
1695 Modifiers::COMMAND | Modifiers::SHIFT
1696 ))
1697 .is_none()
1698 );
1699 }
1700
1701 #[test]
1702 fn an_exact_shifted_declaration_wins_over_the_relaxed_one() {
1703 let mut reg = ShortcutRegistry::new();
1704 reg.register(
1705 Shortcut::new("a.plain")
1706 .primary(KeyStroke::command(Key::Character('/')))
1707 .build(),
1708 );
1709 reg.register(
1710 Shortcut::new("a.shifted")
1711 .primary(KeyStroke::command_shift(Key::Character('/')))
1712 .build(),
1713 );
1714 let ids: Vec<_> = reg
1715 .matches_by_keystroke(KeyStroke::command_shift(Key::Character('/')))
1716 .map(|s| s.shortcut.id)
1717 .collect();
1718 assert_eq!(ids, ["a.shifted"]);
1719 }
1720
1721 fn registry_with_standard_edits() -> ShortcutRegistry {
1726 let mut reg = ShortcutRegistry::new();
1727 super::register_standard_shortcuts(&mut reg);
1728 let mut paste = StandardEdit::Paste.shortcut();
1729 paste.name = "Coller".into();
1730 reg.register(paste);
1731 reg
1732 }
1733
1734 #[test]
1735 fn a_standard_edit_comes_back_when_its_last_owner_goes() {
1736 use slotmap::KeyData;
1737 let page: WidgetId = KeyData::from_ffi(1).into();
1738 let other: WidgetId = KeyData::from_ffi(2).into();
1739 use std::cell::Cell;
1740 use std::rc::Rc;
1741 let mut reg = registry_with_standard_edits();
1742 let chord = reg.effective("edit.paste").unwrap().primary;
1743 let observed = reg.effective_primary_signal("edit.paste");
1744
1745 let mut renamed = StandardEdit::Paste.shortcut();
1746 renamed.name = "Paste here".into();
1747 reg.register_owned(renamed, page);
1748 let ticks = Rc::new(Cell::new(0));
1749 let counter = ticks.clone();
1750 let _watch = observed.observe(move |_| counter.set(counter.get() + 1));
1751 let version = reg.version().get();
1752
1753 reg.unregister_all_for_owner(page);
1754 let paste = reg.get_default("edit.paste").expect("still registered");
1755 assert_eq!(paste.name.get(), "Coller", "the last anonymous name");
1756 assert!(!reg.has_owner("edit.paste"));
1757 assert_eq!(reg.effective("edit.paste").unwrap().primary, chord);
1758 assert_eq!(reg.version().get(), version + 1, "one tick per teardown");
1759 assert_eq!(ticks.get(), 0, "the chord did not change, so no tick");
1760
1761 let mut shared = StandardEdit::Copy.shortcut();
1763 shared.name = "Copy here".into();
1764 reg.register_shared(StandardEdit::Copy.shortcut(), page);
1765 reg.register_shared(shared, other);
1766 reg.unregister_all_for_owner(page);
1767 assert_eq!(
1768 reg.get_default("edit.copy").unwrap().name.get(),
1769 "Copy here"
1770 );
1771 reg.unregister_all_for_owner(other);
1772 assert_eq!(reg.get_default("edit.copy").unwrap().name.get(), "Copy");
1773 assert!(!reg.has_owner("edit.copy"));
1774 }
1775
1776 #[test]
1777 fn a_users_rebind_survives_the_restore_of_a_standard_edit() {
1778 use slotmap::KeyData;
1779 use std::cell::Cell;
1780 use std::rc::Rc;
1781 let page: WidgetId = KeyData::from_ffi(1).into();
1782 let mut reg = registry_with_standard_edits();
1783 let rebound = KeyStroke::ctrl(Key::B);
1784 reg.rebind_primary("edit.paste", Some(rebound));
1785 let resolved = reg.effective("edit.paste").unwrap().primary;
1786 let observed = reg.effective_primary_signal("edit.paste");
1787 reg.register_owned(StandardEdit::Paste.shortcut(), page);
1788 let ticks = Rc::new(Cell::new(0));
1789 let counter = ticks.clone();
1790 let _watch = observed.observe(move |_| counter.set(counter.get() + 1));
1791
1792 reg.unregister_all_for_owner(page);
1793 assert_eq!(resolved, Some(rebound), "a user's chord is taken literally");
1794 assert_eq!(reg.effective("edit.paste").unwrap().primary, resolved);
1795 assert_eq!(ticks.get(), 0, "the user's chord did not change");
1796 }
1797
1798 #[test]
1799 fn an_explicit_unregister_keeps_a_standard_edit_gone() {
1800 use slotmap::KeyData;
1801 let page: WidgetId = KeyData::from_ffi(1).into();
1802 let mut reg = registry_with_standard_edits();
1803 reg.unregister("edit.paste");
1804 reg.register_owned(StandardEdit::Paste.shortcut(), page);
1805 reg.unregister_all_for_owner(page);
1806 assert!(reg.get_default("edit.paste").is_none());
1807 }
1808
1809 #[test]
1810 fn a_standard_edit_never_registered_anonymously_goes_with_its_owner() {
1811 use slotmap::KeyData;
1812 let page: WidgetId = KeyData::from_ffi(1).into();
1813 let mut reg = ShortcutRegistry::new();
1814 reg.register_owned(StandardEdit::Paste.shortcut(), page);
1815 reg.unregister_all_for_owner(page);
1816 assert!(reg.get_default("edit.paste").is_none());
1817 }
1818
1819 #[test]
1820 fn unregister_all_for_owner_removes_only_owner_entries() {
1821 use slotmap::KeyData;
1822 let editor: WidgetId = KeyData::from_ffi(1).into();
1823 let other: WidgetId = KeyData::from_ffi(2).into();
1824
1825 let mut reg = ShortcutRegistry::new();
1826 reg.register_owned(
1827 Shortcut::new("editor.format.bold")
1828 .primary(KeyStroke::command(Key::B))
1829 .build(),
1830 editor,
1831 );
1832 reg.register_owned(
1833 Shortcut::new("editor.format.italic")
1834 .primary(KeyStroke::command(Key::I))
1835 .build(),
1836 editor,
1837 );
1838 reg.register_owned(
1839 Shortcut::new("app.save")
1840 .primary(KeyStroke::command(Key::S))
1841 .build(),
1842 other,
1843 );
1844 assert_eq!(reg.len(), 3);
1845
1846 reg.unregister_all_for_owner(editor);
1847 assert_eq!(reg.len(), 1);
1848 assert!(reg.get_default("editor.format.bold").is_none());
1849 assert!(reg.get_default("editor.format.italic").is_none());
1850 assert!(reg.get_default("app.save").is_some());
1851 assert_eq!(reg.owner_of("app.save"), Some(other));
1852 }
1853
1854 #[test]
1855 fn anonymous_register_drops_prior_owner_index() {
1856 use slotmap::KeyData;
1857 let editor: WidgetId = KeyData::from_ffi(42).into();
1858
1859 let mut reg = ShortcutRegistry::new();
1860 reg.register_owned(Shortcut::new("foo").build(), editor);
1861 assert_eq!(reg.owner_of("foo"), Some(editor));
1862
1863 reg.register(Shortcut::new("foo").build());
1866 assert_eq!(reg.owner_of("foo"), None);
1867
1868 reg.unregister_all_for_owner(editor);
1869 assert!(reg.get_default("foo").is_some());
1870 }
1871
1872 #[test]
1873 fn a_shared_default_lives_until_its_last_owner_goes() {
1874 use slotmap::KeyData;
1875 let first: WidgetId = KeyData::from_ffi(21).into();
1876 let second: WidgetId = KeyData::from_ffi(22).into();
1877 let bold = || {
1878 Shortcut::new("format.bold")
1879 .primary(KeyStroke::ctrl(Key::B))
1880 .build()
1881 };
1882
1883 let mut reg = ShortcutRegistry::new();
1884 reg.register_shared(bold(), first);
1885 reg.register_shared(bold(), second);
1886 reg.register_shared(bold(), first);
1888 assert_eq!(reg.len(), 1, "one id, however many editors declare it");
1889 assert_eq!(reg.owner_of("format.bold"), None);
1890 reg.rebind_secondary("format.bold", Some(KeyStroke::ctrl(Key::G)));
1891
1892 reg.unregister_all_for_owner(first);
1893 assert!(
1894 reg.get_default("format.bold").is_some(),
1895 "the second editor still holds it"
1896 );
1897 reg.unregister_all_for_owner(first);
1898 assert!(reg.get_default("format.bold").is_some(), "idempotent");
1899
1900 reg.unregister_all_for_owner(second);
1901 assert!(
1902 reg.get_default("format.bold").is_none(),
1903 "gone with the last"
1904 );
1905 assert!(
1906 reg.override_for("format.bold").is_some(),
1907 "the user's binding waits for the next editor"
1908 );
1909 reg.register_shared(bold(), first);
1910 assert_eq!(
1911 reg.effective("format.bold").unwrap().secondary,
1912 Some(KeyStroke::ctrl(Key::G))
1913 );
1914 }
1915
1916 #[test]
1917 fn has_owner_answers_for_owned_and_shared_registrations() {
1918 use slotmap::KeyData;
1919 let editor: WidgetId = KeyData::from_ffi(41).into();
1920 let mut reg = ShortcutRegistry::new();
1921 reg.register(Shortcut::new("a").build());
1922 reg.register_owned(Shortcut::new("b").build(), editor);
1923 reg.register_shared(Shortcut::new("c").build(), editor);
1924 assert!(!reg.has_owner("a"));
1925 assert!(reg.has_owner("b"));
1926 assert!(reg.has_owner("c"));
1927 assert!(!reg.has_owner("missing"));
1928 reg.register(Shortcut::new("c").build());
1929 assert!(!reg.has_owner("c"), "an anonymous upsert takes it over");
1930 }
1931
1932 #[test]
1933 fn registering_a_shared_id_owned_hands_it_to_its_one_owner() {
1934 use slotmap::KeyData;
1935 let editor: WidgetId = KeyData::from_ffi(31).into();
1936 let app: WidgetId = KeyData::from_ffi(32).into();
1937
1938 let mut reg = ShortcutRegistry::new();
1939 reg.register_shared(Shortcut::new("format.bold").build(), editor);
1940 reg.register_owned(Shortcut::new("format.bold").build(), app);
1941 assert_eq!(reg.owner_of("format.bold"), Some(app));
1942
1943 reg.unregister_all_for_owner(editor);
1944 assert!(
1945 reg.get_default("format.bold").is_some(),
1946 "the editor no longer holds it"
1947 );
1948 reg.unregister_all_for_owner(app);
1949 assert!(reg.get_default("format.bold").is_none());
1950 }
1951
1952 #[test]
1953 fn reregister_with_new_owner_reassigns() {
1954 use slotmap::KeyData;
1955 let a: WidgetId = KeyData::from_ffi(10).into();
1956 let b: WidgetId = KeyData::from_ffi(11).into();
1957
1958 let mut reg = ShortcutRegistry::new();
1959 reg.register_owned(Shortcut::new("bar").build(), a);
1960 reg.register_owned(Shortcut::new("bar").build(), b);
1961 assert_eq!(reg.owner_of("bar"), Some(b));
1962
1963 reg.unregister_all_for_owner(a);
1965 assert!(reg.get_default("bar").is_some());
1966 reg.unregister_all_for_owner(b);
1967 assert!(reg.get_default("bar").is_none());
1968 }
1969
1970 #[test]
1973 fn shortcut_is_enabled_defaults_true() {
1974 let s = Shortcut::new("foo").build();
1975 assert!(s.is_enabled());
1976 }
1977
1978 #[test]
1979 fn shortcut_is_enabled_follows_signal() {
1980 let enabled = Signal::new(false);
1981 let s = Shortcut::new("foo").enabled_when(enabled.clone()).build();
1982 assert!(!s.is_enabled());
1983 enabled.set(true);
1984 assert!(s.is_enabled());
1985 }
1986
1987 #[test]
1988 fn find_by_keystroke_skips_disabled() {
1989 let enabled = Signal::new(false);
1990 let mut reg = ShortcutRegistry::new();
1991 reg.register(
1992 Shortcut::new("app.save")
1993 .primary(KeyStroke::command(Key::S))
1994 .enabled_when(enabled.clone())
1995 .build(),
1996 );
1997 assert!(reg.find_by_keystroke(KeyStroke::command(Key::S)).is_none());
1999
2000 enabled.set(true);
2002 assert_eq!(
2003 reg.find_by_keystroke(KeyStroke::command(Key::S))
2004 .map(|s| s.shortcut.id),
2005 Some("app.save")
2006 );
2007 }
2008
2009 #[test]
2010 fn overrides_round_trip_through_export_import() {
2011 let mut reg = ShortcutRegistry::new();
2012 reg.register(
2013 Shortcut::new("app.save")
2014 .primary(KeyStroke::command(Key::S))
2015 .build(),
2016 );
2017 reg.rebind_primary("app.save", Some(KeyStroke::command_shift(Key::S)));
2018
2019 let snapshot = reg.export_overrides();
2020 assert_eq!(snapshot.len(), 1);
2021
2022 let mut reg2 = ShortcutRegistry::new();
2024 reg2.register(
2025 Shortcut::new("app.save")
2026 .primary(KeyStroke::command(Key::S))
2027 .build(),
2028 );
2029 assert_eq!(
2030 reg2.effective("app.save").unwrap().primary,
2031 Some(KeyStroke::command(Key::S))
2032 );
2033
2034 reg2.import_overrides(snapshot);
2035 assert_eq!(
2036 reg2.effective("app.save").unwrap().primary,
2037 Some(KeyStroke::command_shift(Key::S))
2038 );
2039 }
2040
2041 #[test]
2042 fn clear_all_overrides_restores_every_default() {
2043 let mut reg = ShortcutRegistry::new();
2044 reg.register(
2045 Shortcut::new("a")
2046 .primary(KeyStroke::command(Key::A))
2047 .build(),
2048 );
2049 reg.register(
2050 Shortcut::new("b")
2051 .primary(KeyStroke::command(Key::B))
2052 .build(),
2053 );
2054 reg.rebind_primary("a", Some(KeyStroke::alt(Key::A)));
2055 reg.rebind_primary("b", Some(KeyStroke::alt(Key::B)));
2056
2057 reg.clear_all_overrides();
2058 assert_eq!(
2059 reg.effective("a").unwrap().primary,
2060 Some(KeyStroke::command(Key::A))
2061 );
2062 assert_eq!(
2063 reg.effective("b").unwrap().primary,
2064 Some(KeyStroke::command(Key::B))
2065 );
2066 }
2067
2068 #[test]
2069 fn untouched_slot_tracks_live_default_after_reregistration() {
2070 let mut reg = ShortcutRegistry::new();
2076 reg.register(
2077 Shortcut::new("foo")
2078 .primary(KeyStroke::command(Key::S))
2079 .build(),
2080 );
2081
2082 reg.rebind_primary("foo", Some(KeyStroke::command_shift(Key::S)));
2083
2084 reg.register(
2088 Shortcut::new("foo")
2089 .primary(KeyStroke::command(Key::S))
2090 .secondary(KeyStroke::alt(Key::S))
2091 .build(),
2092 );
2093
2094 let eff = reg.effective("foo").unwrap();
2095 assert_eq!(eff.primary, Some(KeyStroke::command_shift(Key::S)));
2096 assert_eq!(
2097 eff.secondary,
2098 Some(KeyStroke::alt(Key::S)),
2099 "untouched secondary slot must reflect the new default"
2100 );
2101 }
2102
2103 #[test]
2104 fn rebind_primary_none_is_explicit_unbind_not_delegate() {
2105 let mut reg = ShortcutRegistry::new();
2110 reg.register(
2111 Shortcut::new("foo")
2112 .primary(KeyStroke::command(Key::S))
2113 .build(),
2114 );
2115 reg.rebind_primary("foo", None);
2116 assert_eq!(reg.effective("foo").unwrap().primary, None);
2117
2118 reg.clear_override("foo");
2120 assert_eq!(
2121 reg.effective("foo").unwrap().primary,
2122 Some(KeyStroke::command(Key::S))
2123 );
2124 }
2125
2126 #[test]
2127 fn find_conflict_skips_excluded_id_and_respects_overrides() {
2128 let mut reg = ShortcutRegistry::new();
2129 reg.register(
2130 Shortcut::new("a")
2131 .primary(KeyStroke::command(Key::X))
2132 .build(),
2133 );
2134 reg.register(
2135 Shortcut::new("b")
2136 .primary(KeyStroke::command(Key::Y))
2137 .build(),
2138 );
2139
2140 assert_eq!(
2143 reg.find_conflict(KeyStroke::command(Key::X), Some("a")),
2144 None
2145 );
2146 assert_eq!(
2147 reg.find_conflict(KeyStroke::command(Key::X), None),
2148 Some("a")
2149 );
2150 assert_eq!(reg.find_conflict(KeyStroke::command(Key::Z), None), None);
2152
2153 reg.rebind_primary("b", Some(KeyStroke::command(Key::X)));
2156 assert_eq!(
2157 reg.find_conflict(KeyStroke::command(Key::X), Some("b")),
2158 Some("a")
2159 );
2160 assert_eq!(
2161 reg.find_conflict(KeyStroke::command(Key::X), Some("a")),
2162 Some("b")
2163 );
2164 }
2165
2166 #[test]
2167 fn find_conflict_is_scope_aware() {
2168 use slotmap::KeyData;
2169 let panel_a: WidgetId = KeyData::from_ffi(11).into();
2170 let panel_b: WidgetId = KeyData::from_ffi(22).into();
2171
2172 let mut reg = ShortcutRegistry::new();
2173 reg.register(
2176 Shortcut::new("a.delete")
2177 .scope_to(panel_a)
2178 .primary(KeyStroke::new(Key::Delete, Modifiers::NONE))
2179 .build(),
2180 );
2181 reg.register(
2182 Shortcut::new("b.delete")
2183 .scope_to(panel_b)
2184 .primary(KeyStroke::new(Key::Delete, Modifiers::NONE))
2185 .build(),
2186 );
2187 assert_eq!(
2188 reg.find_conflict(
2189 KeyStroke::new(Key::Delete, Modifiers::NONE),
2190 Some("a.delete")
2191 ),
2192 None,
2193 "Delete in a different panel scope is not a conflict"
2194 );
2195
2196 reg.register(
2198 Shortcut::new("a.delete2")
2199 .scope_to(panel_a)
2200 .primary(KeyStroke::new(Key::Delete, Modifiers::NONE))
2201 .build(),
2202 );
2203 assert_eq!(
2204 reg.find_conflict(
2205 KeyStroke::new(Key::Delete, Modifiers::NONE),
2206 Some("a.delete")
2207 ),
2208 Some("a.delete2"),
2209 "same-scope same-chord is a real conflict"
2210 );
2211
2212 reg.register(
2214 Shortcut::new("g.delete")
2215 .global()
2216 .primary(KeyStroke::new(Key::Delete, Modifiers::NONE))
2217 .build(),
2218 );
2219 let hit = reg.find_conflict(
2223 KeyStroke::new(Key::Delete, Modifiers::NONE),
2224 Some("g.delete"),
2225 );
2226 assert!(
2227 matches!(hit, Some("a.delete" | "a.delete2" | "b.delete")),
2228 "a global chord conflicts with any scoped binding, got {hit:?}"
2229 );
2230 assert_eq!(
2231 reg.find_conflict(
2232 KeyStroke::new(Key::Delete, Modifiers::NONE),
2233 Some("b.delete")
2234 ),
2235 Some("g.delete"),
2236 "a scoped binding conflicts with a global on the same chord"
2237 );
2238 }
2239
2240 #[test]
2241 fn the_category_shows_its_label_but_orders_by_its_key() {
2242 let mut reg = ShortcutRegistry::default();
2243 reg.register(
2245 Shortcut::new("help.topics")
2246 .category("z-help")
2247 .category_label("Aide")
2248 .build(),
2249 );
2250 reg.register(
2251 Shortcut::new("work.new")
2252 .category("a-file")
2253 .category_label("Fichier")
2254 .build(),
2255 );
2256 reg.register(Shortcut::new("bare").category("raw").build());
2257 reg.register(Shortcut::new("loose").build());
2258 let shown: Vec<(&str, Option<String>)> = reg
2259 .iter_effective()
2260 .map(|e| (e.shortcut.id, e.shortcut.category_text()))
2261 .collect();
2262 assert_eq!(
2263 shown,
2264 vec![
2265 ("loose", None),
2266 ("work.new", Some("Fichier".to_string())),
2267 ("bare", Some("raw".to_string())),
2268 ("help.topics", Some("Aide".to_string())),
2269 ]
2270 );
2271 }
2272
2273 #[test]
2274 fn iter_effective_order_is_deterministic_by_category_then_id() {
2275 let mut reg = ShortcutRegistry::new();
2276 reg.register(Shortcut::new("z.last").category("edit").build());
2277 reg.register(Shortcut::new("a.first").category("edit").build());
2278 reg.register(Shortcut::new("m.file").category("app").build());
2279
2280 let ids: Vec<&str> = reg.iter_effective().map(|e| e.shortcut.id).collect();
2281 assert_eq!(ids, vec!["m.file", "a.first", "z.last"]);
2282 }
2283
2284 #[test]
2285 fn iter_effective_still_includes_disabled_with_flag() {
2286 let enabled = Signal::new(false);
2287 let mut reg = ShortcutRegistry::new();
2288 reg.register(
2289 Shortcut::new("app.save")
2290 .primary(KeyStroke::command(Key::S))
2291 .enabled_when(enabled.clone())
2292 .build(),
2293 );
2294 let all: Vec<_> = reg.iter_effective().collect();
2295 assert_eq!(all.len(), 1);
2296 assert!(!all[0].enabled, "settings UI must see disabled state");
2297
2298 enabled.set(true);
2299 let all: Vec<_> = reg.iter_effective().collect();
2300 assert!(all[0].enabled);
2301 }
2302
2303 #[test]
2304 fn secondary_keystroke_matches_via_effective() {
2305 let mut reg = ShortcutRegistry::new();
2306 reg.register(
2307 Shortcut::new("edit.undo")
2308 .primary(KeyStroke::command(Key::Z))
2309 .secondary(KeyStroke::alt(Key::Backspace))
2310 .build(),
2311 );
2312 assert_eq!(
2313 reg.find_by_keystroke(KeyStroke::alt(Key::Backspace))
2314 .map(|s| s.shortcut.id),
2315 Some("edit.undo")
2316 );
2317 }
2318
2319 #[test]
2328 fn a_declared_ctrl_chord_resolves_to_the_platform_accelerator() {
2329 let mut reg = ShortcutRegistry::new();
2330 reg.register(
2331 Shortcut::new("editor.find")
2332 .primary(KeyStroke::ctrl(Key::F))
2333 .secondary(KeyStroke::ctrl_shift(Key::F))
2334 .build(),
2335 );
2336 let eff = reg.effective("editor.find").unwrap();
2337 assert_eq!(eff.primary, Some(KeyStroke::command(Key::F)));
2338 assert_eq!(eff.secondary, Some(KeyStroke::command_shift(Key::F)));
2339
2340 assert!(
2342 reg.find_by_keystroke(KeyStroke::command(Key::F)).is_some(),
2343 "the platform's accelerator must fire a Ctrl-declared shortcut"
2344 );
2345 }
2346
2347 #[test]
2348 fn literal_modifiers_pins_a_declaration_to_physical_control() {
2349 let mut reg = ShortcutRegistry::new();
2350 reg.register(
2351 Shortcut::new("view.next_tab")
2352 .literal_modifiers()
2353 .primary(KeyStroke::ctrl(Key::Tab))
2354 .build(),
2355 );
2356 assert_eq!(
2357 reg.effective("view.next_tab").unwrap().primary,
2358 Some(KeyStroke::new(Key::Tab, Modifiers::CTRL)),
2359 "Ctrl+Tab must stay Ctrl+Tab — ⌘⇥ is the macOS application switcher"
2360 );
2361 }
2362
2363 #[test]
2364 fn a_declared_super_chord_is_left_alone() {
2365 let mut reg = ShortcutRegistry::new();
2366 reg.register(
2367 Shortcut::new("a")
2368 .primary(KeyStroke::new(Key::S, Modifiers::SUPER))
2369 .build(),
2370 );
2371 reg.register(
2372 Shortcut::new("b")
2373 .primary(KeyStroke::new(Key::B, Modifiers::CTRL | Modifiers::SUPER))
2374 .build(),
2375 );
2376 assert_eq!(
2377 reg.effective("a").unwrap().primary,
2378 Some(KeyStroke::new(Key::S, Modifiers::SUPER))
2379 );
2380 assert_eq!(
2381 reg.effective("b").unwrap().primary,
2382 Some(KeyStroke::new(Key::B, Modifiers::CTRL | Modifiers::SUPER)),
2383 "Ctrl+Super is a genuine two-modifier chord, not a Ctrl to rewrite"
2384 );
2385 }
2386
2387 #[test]
2388 fn a_user_override_is_taken_literally() {
2389 let mut reg = ShortcutRegistry::new();
2393 reg.register(
2394 Shortcut::new("editor.find")
2395 .primary(KeyStroke::ctrl(Key::F))
2396 .build(),
2397 );
2398 let literal_control = KeyStroke::new(Key::G, Modifiers::CTRL);
2399 reg.rebind_primary("editor.find", Some(literal_control));
2400 assert_eq!(
2401 reg.effective("editor.find").unwrap().primary,
2402 Some(literal_control)
2403 );
2404
2405 reg.clear_override("editor.find");
2408 assert_eq!(
2409 reg.effective("editor.find").unwrap().primary,
2410 Some(KeyStroke::command(Key::F))
2411 );
2412 }
2413
2414 #[test]
2415 fn find_conflict_sees_the_resolved_chord() {
2416 let mut reg = ShortcutRegistry::new();
2421 reg.register(
2422 Shortcut::new("editor.find")
2423 .primary(KeyStroke::ctrl(Key::F))
2424 .build(),
2425 );
2426 assert_eq!(
2427 reg.find_conflict(KeyStroke::command(Key::F), None),
2428 Some("editor.find")
2429 );
2430 }
2431
2432 #[test]
2433 fn matches_default_follows_the_convention_and_its_opt_out() {
2434 let converted = Shortcut::new("a").primary(KeyStroke::ctrl(Key::F)).build();
2435 assert!(converted.matches_default(KeyStroke::command(Key::F)));
2436
2437 let literal = Shortcut::new("b")
2438 .literal_modifiers()
2439 .primary(KeyStroke::ctrl(Key::Tab))
2440 .build();
2441 assert!(literal.matches_default(KeyStroke::new(Key::Tab, Modifiers::CTRL)));
2442 }
2443
2444 const MAC: Modifiers = Modifiers::SUPER;
2458 const PC: Modifiers = Modifiers::CTRL;
2460
2461 #[test]
2462 fn the_mac_branch_moves_a_declared_ctrl_chord_off_physical_control() {
2463 let save = Shortcut::new("app.save")
2464 .primary(KeyStroke::ctrl(Key::S))
2465 .build();
2466 let physical_control = KeyStroke::new(Key::S, Modifiers::CTRL);
2467
2468 assert_eq!(
2469 save.declared_keystrokes_using(MAC).0,
2470 Some(KeyStroke::new(Key::S, Modifiers::SUPER)),
2471 "a declared Ctrl chord is the platform accelerator: ⌘S on macOS"
2472 );
2473 assert!(
2474 save.matches_default_using(KeyStroke::new(Key::S, Modifiers::SUPER), MAC),
2475 "⌘S must fire the shortcut the app declared as Ctrl+S"
2476 );
2477 assert!(
2478 !save.matches_default_using(physical_control, MAC),
2479 "⌃S must NOT fire it — Control is the macOS text system's, and \
2480 dispatch matches the resolved chord by equality"
2481 );
2482 }
2483
2484 #[test]
2485 fn the_pc_branch_leaves_a_declared_ctrl_chord_on_physical_control() {
2486 let save = Shortcut::new("app.save")
2489 .primary(KeyStroke::ctrl(Key::S))
2490 .build();
2491 let physical_control = KeyStroke::new(Key::S, Modifiers::CTRL);
2492
2493 assert_eq!(
2494 save.declared_keystrokes_using(PC).0,
2495 Some(physical_control),
2496 "nothing is rewritten where Ctrl already is the accelerator"
2497 );
2498 assert!(save.matches_default_using(physical_control, PC));
2499 assert!(
2500 !save.matches_default_using(KeyStroke::new(Key::S, Modifiers::SUPER), PC),
2501 "Super is a distinct modifier on Windows and Linux, not the accelerator"
2502 );
2503 }
2504
2505 #[test]
2506 fn literal_modifiers_keeps_physical_control_reachable_on_the_mac_branch() {
2507 let next_tab = Shortcut::new("view.next_tab")
2511 .literal_modifiers()
2512 .primary(KeyStroke::ctrl(Key::Tab))
2513 .build();
2514
2515 assert!(
2516 next_tab.matches_default_using(KeyStroke::new(Key::Tab, Modifiers::CTRL), MAC),
2517 "literal_modifiers must keep ⌃⇥ firing on macOS"
2518 );
2519 assert!(
2520 !next_tab.matches_default_using(KeyStroke::new(Key::Tab, Modifiers::SUPER), MAC),
2521 "and must not silently answer to ⌘⇥, which the OS takes first"
2522 );
2523 }
2524
2525 #[test]
2526 fn an_explicit_super_or_ctrl_super_declaration_survives_the_mac_branch() {
2527 let super_only = Shortcut::new("a")
2528 .primary(KeyStroke::new(Key::S, Modifiers::SUPER))
2529 .build();
2530 assert_eq!(
2531 super_only.declared_keystrokes_using(MAC).0,
2532 Some(KeyStroke::new(Key::S, Modifiers::SUPER)),
2533 "already the accelerator — the rewrite is idempotent, not additive"
2534 );
2535
2536 let both = Shortcut::new("b")
2537 .primary(KeyStroke::new(Key::B, Modifiers::CTRL | Modifiers::SUPER))
2538 .build();
2539 assert_eq!(
2540 both.declared_keystrokes_using(MAC).0,
2541 Some(KeyStroke::new(Key::B, Modifiers::CTRL | Modifiers::SUPER)),
2542 "⌃⌘B is a genuine two-modifier chord, not a Ctrl awaiting rewrite"
2543 );
2544 }
2545
2546 #[test]
2547 fn the_secondary_chord_follows_the_same_branch() {
2548 let find = Shortcut::new("editor.find")
2549 .primary(KeyStroke::ctrl(Key::F))
2550 .secondary(KeyStroke::ctrl_shift(Key::F))
2551 .build();
2552 assert_eq!(
2553 find.declared_keystrokes_using(MAC).1,
2554 Some(KeyStroke::new(Key::F, Modifiers::SUPER | Modifiers::SHIFT)),
2555 "the secondary slot is resolved too, not just the primary"
2556 );
2557 assert!(
2558 !find.matches_default_using(
2559 KeyStroke::new(Key::F, Modifiers::CTRL | Modifiers::SHIFT),
2560 MAC
2561 ),
2562 "⌃⇧F must miss on macOS for the same reason ⌃F does"
2563 );
2564 }
2565
2566 #[test]
2567 fn the_registry_dispatches_the_current_platform_accelerator_and_only_that() {
2568 let mut reg = ShortcutRegistry::new();
2574 reg.register(
2575 Shortcut::new("app.save")
2576 .primary(KeyStroke::ctrl(Key::S))
2577 .build(),
2578 );
2579
2580 assert!(
2581 reg.find_by_keystroke(KeyStroke::command(Key::S)).is_some(),
2582 "the platform accelerator must fire a Ctrl-declared shortcut"
2583 );
2584 assert_eq!(
2585 reg.find_by_keystroke(KeyStroke::new(Key::S, Modifiers::CTRL))
2586 .is_some(),
2587 !cfg!(target_os = "macos"),
2588 "physical Control fires it only where Control is the accelerator; \
2589 on macOS the chord is ⌘S and ⌃S belongs to the text system"
2590 );
2591 }
2592}