keyboard_types/shortcuts.rs
1use crate::{Key, KeyState, KeyboardEvent, Modifiers};
2
3/// Match keyboard shortcuts and execute actions.
4///
5/// Every shortcut consists of a list of modifier keys pressed and a
6/// single non-modifier key pressed.
7///
8/// The Control + C shortcut requires the user to hold down the Control
9/// modifier key. When the C key is pressed the action (usually copy)
10/// is triggered. The event is consumed so other matchers don't also
11/// act on the shortcut. This is also true for the release of the
12/// C key as else only key release events would be forwarded.
13///
14/// ASCII letters are compared ignoring case. Only takes
15/// the shift, control, alt and meta modifiers into account.
16/// If other modifiers beside those expected are found
17/// the shortcut is not matched.
18pub struct ShortcutMatcher<T> {
19 state: KeyState,
20 key: Key,
21 modifiers: Modifiers,
22 matched: bool,
23 value: Option<T>,
24}
25
26impl<T> ShortcutMatcher<T> {
27 /// Create a new shortcut matcher.
28 pub fn new(state: KeyState, key: Key, mut modifiers: Modifiers) -> ShortcutMatcher<T> {
29 modifiers &= Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::META;
30 ShortcutMatcher {
31 state,
32 key,
33 modifiers,
34 matched: false,
35 value: None,
36 }
37 }
38
39 /// Create a new matcher from an event.
40 ///
41 /// Only state, key and modifiers are stored. The other attributes are discarded.
42 pub fn from_event(key_event: KeyboardEvent) -> ShortcutMatcher<T> {
43 ShortcutMatcher::new(key_event.state, key_event.key, key_event.modifiers)
44 }
45
46 /// Test a keyboard shortcut.
47 ///
48 /// If the modifiers are active and the key is pressed,
49 /// execute the provided function.
50 ///
51 /// ```rust
52 /// # use keyboard_types::{Key, KeyboardEvent, Modifiers, NamedKey, ShortcutMatcher};
53 /// # fn do_something() {}
54 /// # fn forward_event() {}
55 /// # let event = KeyboardEvent {
56 /// # state: keyboard_types::KeyState::Down,
57 /// # key: Key::Named(NamedKey::Enter),
58 /// # code: keyboard_types::Code::Enter,
59 /// # location: keyboard_types::Location::Standard,
60 /// # modifiers: Modifiers::empty(),
61 /// # repeat: false,
62 /// # is_composing: false,
63 /// # };
64 /// // Create a matcher from a keyboard event.
65 /// // Shortcuts are tested in-order.
66 /// ShortcutMatcher::from_event(event)
67 /// // Do something if the Tab key is pressed.
68 /// .shortcut(Modifiers::empty(), Key::Named(NamedKey::Tab), do_something)
69 /// // If Shift + Tab are pressed do something.
70 /// // This is executed because the previous shortcut requires modifiers to be empty.
71 /// .shortcut(Modifiers::SHIFT, Key::Named(NamedKey::Tab), do_something)
72 /// // Instead of named keys letters and other characters can be used.
73 /// .shortcut(Modifiers::CONTROL, 'L', do_something)
74 /// // Multiple modifiers are combined with bitwise OR (`|`) to form a new mask.
75 /// .shortcut(Modifiers::CONTROL | Modifiers::SHIFT, 'X', do_something)
76 /// // If none of the previous shortcuts matched forward the event.
77 /// .otherwise(forward_event);
78 /// ```
79 pub fn shortcut<K, F>(mut self, modifiers: Modifiers, key: K, f: F) -> ShortcutMatcher<T>
80 where
81 K: MatchKey,
82 F: (FnOnce() -> T),
83 {
84 if self.matched {
85 return self;
86 }
87 if modifiers == self.modifiers && key.match_key(&self.key) {
88 if self.state.is_down() {
89 self.value = Some(f());
90 }
91 self.matched = true;
92 }
93 self
94 }
95
96 /// Only test a shortcut if the enabled flag is set.
97 ///
98 /// If the `enabled` flag is true behaves the same as
99 /// `shortcut` otherwise does nothing.
100 ///
101 /// This is especially useful for platform specific shortcuts.
102 ///
103 /// ```rust
104 /// # use keyboard_types::{Key, KeyboardEvent, Modifiers, NamedKey, ShortcutMatcher};
105 /// # fn copy() {}
106 /// # fn quit() {}
107 /// # let event = KeyboardEvent {
108 /// # state: keyboard_types::KeyState::Down,
109 /// # key: Key::Named(NamedKey::Enter),
110 /// # code: keyboard_types::Code::Enter,
111 /// # location: keyboard_types::Location::Standard,
112 /// # modifiers: Modifiers::empty(),
113 /// # repeat: false,
114 /// # is_composing: false,
115 /// # };
116 /// ShortcutMatcher::from_event(event)
117 /// .shortcut(Modifiers::CONTROL, 'c', copy)
118 /// .optional_shortcut(cfg!(target_os="macos"), Modifiers::META, 'q', quit)
119 /// .shortcut(Modifiers::CONTROL, 'w', quit);
120 /// ```
121 ///
122 /// In the example the app supports the copy action on all platforms
123 /// and can be closed with Control + W everywhere but additionally
124 /// with Command + Q on Mac OS.
125 pub fn optional_shortcut<K, F>(
126 self,
127 enabled: bool,
128 modifiers: Modifiers,
129 key: K,
130 f: F,
131 ) -> ShortcutMatcher<T>
132 where
133 K: MatchKey,
134 F: (FnOnce() -> T),
135 {
136 if !enabled {
137 return self;
138 }
139 self.shortcut(modifiers, key, f)
140 }
141
142 /// Execute the function is no keyboard shortcut matched.
143 ///
144 /// Note that the passed function is executed on both
145 /// keydown and keyup unlike the shortcuts which only
146 /// run on keydown.
147 pub fn otherwise<F>(self, f: F) -> Option<T>
148 where
149 F: (FnOnce() -> T),
150 {
151 if !self.matched {
152 Some(f())
153 } else {
154 self.value
155 }
156 }
157}
158
159pub trait MatchKey {
160 fn match_key(&self, key: &Key) -> bool;
161}
162
163impl MatchKey for Key {
164 fn match_key(&self, key: &Key) -> bool {
165 self == key
166 }
167}
168
169impl MatchKey for char {
170 fn match_key(&self, key: &Key) -> bool {
171 match key {
172 Key::Character(text) => {
173 let mut buf = [0; 4];
174 text.eq_ignore_ascii_case(self.encode_utf8(&mut buf))
175 }
176 _ => false,
177 }
178 }
179}