1use egui::{Context, Key, KeyboardShortcut, Modifiers};
56use std::collections::HashMap;
57use std::hash::Hash;
58
59pub trait InputBinding {
64 fn matches(&self, ctx: &Context) -> bool;
66
67 fn consume(&self, ctx: &Context) -> bool;
71
72 fn display(&self) -> String;
77
78 fn as_keyboard_shortcut(&self) -> Option<KeyboardShortcut>;
80}
81
82impl InputBinding for KeyboardShortcut {
83 fn matches(&self, ctx: &Context) -> bool {
84 ctx.input(|i| i.modifiers == self.modifiers && i.key_pressed(self.logical_key))
85 }
86
87 fn consume(&self, ctx: &Context) -> bool {
88 ctx.input_mut(|i| i.consume_shortcut(self))
89 }
90
91 fn display(&self) -> String {
92 self.format(&modifier_names(), self.logical_key == Key::Plus)
93 }
94
95 fn as_keyboard_shortcut(&self) -> Option<KeyboardShortcut> {
96 Some(*self)
97 }
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Hash)]
110pub struct DynamicShortcut {
111 pub modifiers: Modifiers,
113 pub key: Key,
115}
116
117impl DynamicShortcut {
118 pub const fn new(modifiers: Modifiers, key: Key) -> Self {
120 Self { modifiers, key }
121 }
122
123 pub const fn key_only(key: Key) -> Self {
125 Self::new(Modifiers::NONE, key)
126 }
127
128 pub const fn to_keyboard_shortcut(&self) -> KeyboardShortcut {
130 KeyboardShortcut::new(self.modifiers, self.key)
131 }
132}
133
134impl From<KeyboardShortcut> for DynamicShortcut {
135 fn from(shortcut: KeyboardShortcut) -> Self {
136 Self {
137 modifiers: shortcut.modifiers,
138 key: shortcut.logical_key,
139 }
140 }
141}
142
143impl From<DynamicShortcut> for KeyboardShortcut {
144 fn from(shortcut: DynamicShortcut) -> Self {
145 KeyboardShortcut::new(shortcut.modifiers, shortcut.key)
146 }
147}
148
149impl InputBinding for DynamicShortcut {
150 fn matches(&self, ctx: &Context) -> bool {
151 self.to_keyboard_shortcut().matches(ctx)
152 }
153
154 fn consume(&self, ctx: &Context) -> bool {
155 self.to_keyboard_shortcut().consume(ctx)
156 }
157
158 fn display(&self) -> String {
159 self.to_keyboard_shortcut().display()
160 }
161
162 fn as_keyboard_shortcut(&self) -> Option<KeyboardShortcut> {
163 Some(self.to_keyboard_shortcut())
164 }
165}
166
167#[derive(Clone, Debug)]
197pub struct ActionBindings<A> {
198 bindings: HashMap<A, DynamicShortcut>,
200 defaults: HashMap<A, DynamicShortcut>,
202}
203
204impl<A> Default for ActionBindings<A> {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl<A> ActionBindings<A> {
211 pub fn new() -> Self {
213 Self {
214 bindings: HashMap::new(),
215 defaults: HashMap::new(),
216 }
217 }
218}
219
220impl<A: Eq + Hash + Clone> ActionBindings<A> {
221 pub fn with_default(mut self, action: A, shortcut: impl Into<DynamicShortcut>) -> Self {
225 self.register_default(action, shortcut);
226 self
227 }
228
229 pub fn register_default(&mut self, action: A, shortcut: impl Into<DynamicShortcut>) {
233 let shortcut = shortcut.into();
234 self.defaults.insert(action.clone(), shortcut.clone());
235 self.bindings.insert(action, shortcut);
236 }
237
238 pub fn register_defaults<I, S>(&mut self, iter: I)
249 where
250 I: IntoIterator<Item = (A, S)>,
251 S: Into<DynamicShortcut>,
252 {
253 for (action, shortcut) in iter {
254 self.register_default(action, shortcut);
255 }
256 }
257
258 pub fn rebind(&mut self, action: &A, shortcut: DynamicShortcut) -> Option<DynamicShortcut> {
262 self.bindings.insert(action.clone(), shortcut)
263 }
264
265 pub fn reset(&mut self, action: &A) -> bool {
269 if let Some(default) = self.defaults.get(action) {
270 self.bindings.insert(action.clone(), default.clone());
271 true
272 } else {
273 false
274 }
275 }
276
277 pub fn reset_all(&mut self) {
279 self.bindings = self.defaults.clone();
280 }
281
282 pub fn get(&self, action: &A) -> Option<&DynamicShortcut> {
284 self.bindings.get(action)
285 }
286
287 pub fn get_default(&self, action: &A) -> Option<&DynamicShortcut> {
289 self.defaults.get(action)
290 }
291
292 pub fn is_modified(&self, action: &A) -> bool {
294 match (self.bindings.get(action), self.defaults.get(action)) {
295 (Some(current), Some(default)) => current != default,
296 _ => false,
297 }
298 }
299
300 pub fn find_action(&self, shortcut: &DynamicShortcut) -> Option<&A> {
304 self.bindings
305 .iter()
306 .find(|(_, s)| *s == shortcut)
307 .map(|(a, _)| a)
308 }
309
310 pub fn find_conflicts(&self) -> Vec<(&A, &A)> {
314 let mut conflicts = Vec::new();
315 let actions: Vec<_> = self.bindings.keys().collect();
316
317 for i in 0..actions.len() {
318 for j in (i + 1)..actions.len() {
319 if self.bindings.get(actions[i]) == self.bindings.get(actions[j]) {
320 conflicts.push((actions[i], actions[j]));
321 }
322 }
323 }
324
325 conflicts
326 }
327
328 pub fn iter(&self) -> impl Iterator<Item = (&A, &DynamicShortcut)> {
330 self.bindings.iter()
331 }
332
333 pub fn len(&self) -> usize {
335 self.bindings.len()
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.bindings.is_empty()
341 }
342
343 pub fn remove(&mut self, action: &A) -> Option<DynamicShortcut> {
347 self.defaults.remove(action);
348 self.bindings.remove(action)
349 }
350
351 pub fn check_triggered(&self, ctx: &Context) -> Option<&A> {
355 for (action, shortcut) in &self.bindings {
356 if shortcut.consume(ctx) {
357 return Some(action);
358 }
359 }
360 None
361 }
362}
363
364fn modifier_names() -> egui::ModifierNames<'static> {
368 egui::ModifierNames::NAMES
370}
371
372#[derive(Clone, Debug, Default)]
377pub struct ShortcutGroup {
378 shortcuts: Vec<DynamicShortcut>,
379}
380
381impl ShortcutGroup {
382 pub fn new() -> Self {
384 Self::default()
385 }
386
387 pub fn with(mut self, shortcut: impl Into<DynamicShortcut>) -> Self {
389 self.shortcuts.push(shortcut.into());
390 self
391 }
392
393 pub fn add(&mut self, shortcut: impl Into<DynamicShortcut>) {
395 self.shortcuts.push(shortcut.into());
396 }
397
398 pub fn matches(&self, ctx: &Context) -> bool {
400 self.shortcuts.iter().any(|s| s.matches(ctx))
401 }
402
403 pub fn consume(&self, ctx: &Context) -> bool {
405 for shortcut in &self.shortcuts {
406 if shortcut.consume(ctx) {
407 return true;
408 }
409 }
410 false
411 }
412}
413
414impl InputBinding for ShortcutGroup {
415 fn matches(&self, ctx: &Context) -> bool {
416 ShortcutGroup::matches(self, ctx)
417 }
418
419 fn consume(&self, ctx: &Context) -> bool {
420 ShortcutGroup::consume(self, ctx)
421 }
422
423 fn display(&self) -> String {
424 self.shortcuts
425 .iter()
426 .map(|s| s.display())
427 .collect::<Vec<_>>()
428 .join(" / ")
429 }
430
431 fn as_keyboard_shortcut(&self) -> Option<KeyboardShortcut> {
432 self.shortcuts
433 .first()
434 .and_then(|s| s.as_keyboard_shortcut())
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
443 #[allow(dead_code)]
444 enum TestAction {
445 Save,
446 Undo,
447 Redo,
448 Copy,
449 }
450
451 #[test]
452 fn test_dynamic_shortcut_creation() {
453 let shortcut = DynamicShortcut::new(Modifiers::COMMAND, Key::S);
454 assert_eq!(shortcut.modifiers, Modifiers::COMMAND);
455 assert_eq!(shortcut.key, Key::S);
456 }
457
458 #[test]
459 fn test_dynamic_shortcut_from_keyboard_shortcut() {
460 let ks = KeyboardShortcut::new(Modifiers::CTRL, Key::Z);
461 let ds = DynamicShortcut::from(ks);
462 assert_eq!(ds.modifiers, Modifiers::CTRL);
463 assert_eq!(ds.key, Key::Z);
464 }
465
466 #[test]
467 fn test_action_bindings_defaults() {
468 let bindings = ActionBindings::new()
469 .with_default(
470 TestAction::Save,
471 DynamicShortcut::new(Modifiers::COMMAND, Key::S),
472 )
473 .with_default(
474 TestAction::Undo,
475 DynamicShortcut::new(Modifiers::COMMAND, Key::Z),
476 );
477
478 assert_eq!(bindings.len(), 2);
479 assert_eq!(
480 bindings.get(&TestAction::Save),
481 Some(&DynamicShortcut::new(Modifiers::COMMAND, Key::S))
482 );
483 }
484
485 #[test]
486 fn test_action_bindings_rebind() {
487 let mut bindings = ActionBindings::new().with_default(
488 TestAction::Save,
489 DynamicShortcut::new(Modifiers::COMMAND, Key::S),
490 );
491
492 let old = bindings.rebind(
494 &TestAction::Save,
495 DynamicShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::S),
496 );
497
498 assert_eq!(old, Some(DynamicShortcut::new(Modifiers::COMMAND, Key::S)));
499 assert_eq!(
500 bindings.get(&TestAction::Save),
501 Some(&DynamicShortcut::new(
502 Modifiers::CTRL.plus(Modifiers::SHIFT),
503 Key::S
504 ))
505 );
506 assert!(bindings.is_modified(&TestAction::Save));
507 }
508
509 #[test]
510 fn test_action_bindings_reset() {
511 let mut bindings = ActionBindings::new().with_default(
512 TestAction::Save,
513 DynamicShortcut::new(Modifiers::COMMAND, Key::S),
514 );
515
516 bindings.rebind(
518 &TestAction::Save,
519 DynamicShortcut::new(Modifiers::CTRL, Key::S),
520 );
521 assert!(bindings.is_modified(&TestAction::Save));
522
523 bindings.reset(&TestAction::Save);
524 assert!(!bindings.is_modified(&TestAction::Save));
525 assert_eq!(
526 bindings.get(&TestAction::Save),
527 Some(&DynamicShortcut::new(Modifiers::COMMAND, Key::S))
528 );
529 }
530
531 #[test]
532 fn test_find_action() {
533 let bindings = ActionBindings::new()
534 .with_default(
535 TestAction::Save,
536 DynamicShortcut::new(Modifiers::COMMAND, Key::S),
537 )
538 .with_default(
539 TestAction::Undo,
540 DynamicShortcut::new(Modifiers::COMMAND, Key::Z),
541 );
542
543 let found = bindings.find_action(&DynamicShortcut::new(Modifiers::COMMAND, Key::S));
544 assert_eq!(found, Some(&TestAction::Save));
545
546 let not_found = bindings.find_action(&DynamicShortcut::new(Modifiers::COMMAND, Key::X));
547 assert_eq!(not_found, None);
548 }
549
550 #[test]
551 fn test_find_conflicts() {
552 let mut bindings = ActionBindings::new()
553 .with_default(
554 TestAction::Save,
555 DynamicShortcut::new(Modifiers::COMMAND, Key::S),
556 )
557 .with_default(
558 TestAction::Undo,
559 DynamicShortcut::new(Modifiers::COMMAND, Key::Z),
560 );
561
562 assert!(bindings.find_conflicts().is_empty());
564
565 bindings.rebind(
567 &TestAction::Undo,
568 DynamicShortcut::new(Modifiers::COMMAND, Key::S),
569 );
570
571 let conflicts = bindings.find_conflicts();
572 assert_eq!(conflicts.len(), 1);
573 }
574
575 #[test]
576 fn test_shortcut_group() {
577 let group = ShortcutGroup::new()
578 .with(DynamicShortcut::new(Modifiers::COMMAND, Key::Z))
579 .with(DynamicShortcut::new(Modifiers::CTRL, Key::Z));
580
581 let display = group.display();
583 assert!(
584 display.contains(" / "),
585 "Expected separator in: {}",
586 display
587 );
588 assert!(display.contains("Z"), "Expected key Z in: {}", display);
589 }
590}