1pub use egui_command;
27use {
28 egui::{Context, Key, KeyboardShortcut, Modifiers},
29 egui_command::{CommandId, CommandSource, CommandTriggered},
30 parking_lot::RwLock,
31 std::{collections::HashMap, sync::Arc},
32};
33
34#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
36pub struct Shortcut {
37 pub key: Key,
38 pub mods: Modifiers,
39}
40
41pub type ShortcutMap<C> = HashMap<Shortcut, C>;
43
44pub struct ShortcutScope<C> {
50 pub name: &'static str,
51 pub shortcuts: ShortcutMap<C>,
52 pub consume: bool,
53}
54
55impl<C> ShortcutScope<C> {
56 pub fn new(name: &'static str, shortcuts: ShortcutMap<C>, consume: bool) -> Self {
58 Self {
59 name,
60 shortcuts,
61 consume,
62 }
63 }
64}
65
66pub struct ShortcutManager<C> {
73 global: Arc<RwLock<ShortcutMap<C>>>,
74 stack: Vec<ShortcutScope<C>>,
75}
76
77impl<C: Clone> ShortcutManager<C> {
78 pub fn new(global: Arc<RwLock<ShortcutMap<C>>>) -> Self {
79 Self {
80 global,
81 stack: Vec::new(),
82 }
83 }
84
85 pub fn push_scope(&mut self, scope: ShortcutScope<C>) { self.stack.push(scope); }
87
88 pub fn pop_scope(&mut self) { self.stack.pop(); }
90
91 pub fn register_global(&mut self, sc: Shortcut, cmd: C) { self.global.write().insert(sc, cmd); }
93
94 pub fn dispatch(&self, ctx: &Context) -> Vec<CommandTriggered>
102 where
103 C: Into<CommandId>,
104 {
105 if ctx.wants_keyboard_input() {
106 return Vec::new();
107 }
108
109 self.dispatch_raw_inner(ctx, None)
110 .into_iter()
111 .map(|cmd| CommandTriggered::new(cmd.into(), CommandSource::Keyboard))
112 .collect()
113 }
114
115 pub fn dispatch_raw_with_extra(&self, ctx: &Context, extra: Option<&ShortcutMap<C>>) -> Vec<C> {
123 if ctx.wants_keyboard_input() {
124 return Vec::new();
125 }
126
127 self.dispatch_raw_inner(ctx, extra)
128 }
129
130 pub fn dispatch_raw(&self, ctx: &Context) -> Vec<C> {
134 if ctx.wants_keyboard_input() {
135 return Vec::new();
136 }
137
138 self.dispatch_raw_inner(ctx, None)
139 }
140
141 fn dispatch_raw_inner(&self, ctx: &Context, extra: Option<&ShortcutMap<C>>) -> Vec<C> {
147 let mut triggered: Vec<C> = Vec::new();
148
149 ctx.input_mut(|input| {
150 let mut consumed: Vec<KeyboardShortcut> = Vec::new();
151
152 for event in &input.events {
153 let egui::Event::Key {
154 key,
155 pressed: true,
156 repeat: false,
157 modifiers,
158 ..
159 } = event
160 else {
161 continue;
162 };
163
164 if let Some(extra_map) = extra
166 && let Some((shortcut, cmd)) = best_shortcut_match(extra_map, *key, *modifiers)
167 {
168 triggered.push(cmd.clone());
169 consumed.push(shortcut.to_keyboard_shortcut());
170 continue;
171 }
172
173 let mut stop_propagation = false;
174 for scope in self.stack.iter().rev() {
175 if let Some((shortcut, cmd)) = best_shortcut_match(&scope.shortcuts, *key, *modifiers) {
176 triggered.push(cmd.clone());
177 consumed.push(shortcut.to_keyboard_shortcut());
178 if scope.consume {
179 stop_propagation = true;
180 break;
181 }
182 }
183 }
184 if stop_propagation {
185 continue;
186 }
187
188 let global = self.global.read();
190 if let Some((shortcut, cmd)) = best_shortcut_match(&global, *key, *modifiers) {
191 triggered.push(cmd.clone());
192 consumed.push(shortcut.to_keyboard_shortcut());
193 }
194 }
195
196 for shortcut in consumed {
197 input.consume_shortcut(&shortcut);
198 }
199 });
200
201 triggered
202 }
203}
204
205fn best_shortcut_match<C>(
206 map: &ShortcutMap<C>,
207 key: Key,
208 pressed_modifiers: Modifiers,
209) -> Option<(Shortcut, &C)> {
210 map.iter()
211 .filter(|(shortcut, _)| shortcut.key == key && pressed_modifiers.matches_logically(shortcut.mods))
212 .max_by_key(|(shortcut, _)| shortcut.specificity())
213 .map(|(shortcut, command)| (*shortcut, command))
214}
215
216impl Shortcut {
217 fn specificity(self) -> u8 {
218 self.mods.alt as u8
219 + self.mods.shift as u8
220 + self.mods.ctrl as u8
221 + self.mods.command as u8
222 + self.mods.mac_cmd as u8
223 }
224
225 fn to_keyboard_shortcut(self) -> KeyboardShortcut {
226 KeyboardShortcut::new(self.mods, self.key)
227 }
228}
229
230pub fn shortcut(sc: &str) -> Shortcut {
234 let mut mods = Modifiers::default();
235 let mut key = None;
236
237 for part in sc.split('+') {
238 let part = part.trim();
239 match part.to_uppercase().as_str() {
240 "CTRL" | "CONTROL" => mods.ctrl = true,
241 "ALT" => mods.alt = true,
242 "SHIFT" => mods.shift = true,
243 "META" => mods.mac_cmd = true,
244 "CMD" | "COMMAND" => mods.command = true,
245 _ => key = Key::from_name(part),
248 }
249 }
250
251 Shortcut {
252 key: key.expect("Invalid key in shortcut string"),
253 mods,
254 }
255}
256
257#[macro_export]
267macro_rules! shortcut_map {
268 ($($key:expr => $cmd:expr),* $(,)?) => {{
269 #[allow(unused_mut)]
270 let mut map = $crate::ShortcutMap::new();
271 $(map.insert($crate::shortcut($key), $cmd);)*
272 map
273 }};
274}
275
276#[cfg(test)]
277mod tests {
278 use {
279 super::*,
280 egui::{Event, Key, Modifiers, RawInput},
281 };
282
283 fn key_event(key: Key, modifiers: Modifiers) -> Event {
284 Event::Key {
285 key,
286 physical_key: None,
287 pressed: true,
288 repeat: false,
289 modifiers,
290 }
291 }
292
293 fn dispatch_raw_events(manager: &ShortcutManager<u32>, events: Vec<Event>) -> Vec<u32> {
294 let ctx = Context::default();
295 let mut triggered = None;
296
297 let _ = ctx.run(
298 RawInput {
299 events,
300 ..RawInput::default()
301 },
302 |ctx| {
303 triggered = Some(manager.dispatch_raw(ctx));
304 },
305 );
306
307 triggered.expect("dispatch should run exactly once")
308 }
309
310 #[test]
311 fn shortcut_single_key() {
312 let sc = shortcut("F1");
313 assert_eq!(sc.key, Key::F1);
314 assert_eq!(sc.mods, Modifiers::default());
315 }
316
317 #[test]
318 fn shortcut_ctrl_s() {
319 let sc = shortcut("Ctrl+S");
320 assert_eq!(sc.key, Key::S);
321 assert!(sc.mods.ctrl);
322 assert!(!sc.mods.alt);
323 assert!(!sc.mods.shift);
324 }
325
326 #[test]
327 fn shortcut_alt_shift_x() {
328 let sc = shortcut("Alt+Shift+X");
329 assert_eq!(sc.key, Key::X);
330 assert!(sc.mods.alt);
331 assert!(sc.mods.shift);
332 assert!(!sc.mods.ctrl);
333 }
334
335 #[test]
336 fn shortcut_control_alias() {
337 let sc = shortcut("Control+A");
338 assert!(sc.mods.ctrl);
339 assert_eq!(sc.key, Key::A);
340 }
341
342 #[test]
343 fn shortcut_command_sets_logical_command_modifier() {
344 let sc = shortcut("Cmd+S");
345 assert_eq!(sc.key, Key::S);
346 assert!(sc.mods.command);
347 assert!(!sc.mods.mac_cmd);
348 }
349
350 #[test]
351 #[should_panic]
352 fn shortcut_invalid_key_panics() { shortcut("Ctrl+NotAKey"); }
353
354 #[test]
355 fn shortcut_map_macro_builds_correctly() {
356 let map = shortcut_map![
357 "F1" => 1u32,
358 "F2" => 2u32,
359 ];
360 assert_eq!(map.get(&shortcut("F1")), Some(&1u32));
361 assert_eq!(map.get(&shortcut("F2")), Some(&2u32));
362 assert_eq!(map.get(&shortcut("F3")), None);
363 }
364
365 #[test]
366 fn shortcut_map_macro_empty() {
367 let map: ShortcutMap<u32> = shortcut_map![];
368 assert!(map.is_empty());
369 }
370
371 #[test]
372 fn shortcut_equality_and_hash() {
373 use std::collections::HashMap;
374 let mut m: HashMap<Shortcut, &str> = HashMap::new();
375 m.insert(shortcut("Ctrl+S"), "save");
376 assert_eq!(m[&shortcut("Ctrl+S")], "save");
377 assert!(!m.contains_key(&shortcut("Ctrl+Z")));
378 }
379
380 #[test]
381 fn non_consuming_scope_still_allows_global_fallback() {
382 let global = Arc::new(RwLock::new(shortcut_map!["Ctrl+S" => 1u32]));
383 let mut manager = ShortcutManager::new(global);
384 manager.push_scope(ShortcutScope::new(
385 "editor",
386 shortcut_map!["Ctrl+S" => 2u32],
387 false,
388 ));
389
390 let triggered = dispatch_raw_events(&manager, vec![key_event(Key::S, Modifiers::CTRL)]);
391 assert_eq!(triggered, vec![2, 1]);
392 }
393
394 #[test]
395 fn consuming_scope_blocks_global_fallback() {
396 let global = Arc::new(RwLock::new(shortcut_map!["Ctrl+S" => 1u32]));
397 let mut manager = ShortcutManager::new(global);
398 manager.push_scope(ShortcutScope::new(
399 "editor",
400 shortcut_map!["Ctrl+S" => 2u32],
401 true,
402 ));
403
404 let triggered = dispatch_raw_events(&manager, vec![key_event(Key::S, Modifiers::CTRL)]);
405 assert_eq!(triggered, vec![2]);
406 }
407
408 #[test]
409 fn logical_command_shortcut_matches_command_input() {
410 let global = Arc::new(RwLock::new(shortcut_map!["Cmd+S" => 7u32]));
411 let manager = ShortcutManager::new(global);
412
413 let triggered = dispatch_raw_events(&manager, vec![key_event(Key::S, Modifiers::COMMAND)]);
414 assert_eq!(triggered, vec![7]);
415 }
416
417 #[test]
418 fn more_specific_shortcut_wins_with_logical_matching() {
419 let global = Arc::new(RwLock::new(shortcut_map![
420 "Ctrl+S" => 1u32,
421 "Ctrl+Shift+S" => 2u32,
422 ]));
423 let manager = ShortcutManager::new(global);
424
425 let triggered = dispatch_raw_events(
426 &manager,
427 vec![key_event(Key::S, Modifiers::CTRL | Modifiers::SHIFT)],
428 );
429 assert_eq!(triggered, vec![2]);
430 }
431}