muda_win/
accelerator.rs

1//! Accelerators describe keyboard shortcuts for menu items.
2//!
3//! [`Accelerator`s](crate::accelerator::Accelerator) are used to define a keyboard shortcut consisting
4//! of an optional combination of modifier keys (provided by [`Modifiers`]) and
5//! one key ([`Code`]).
6//!
7//! # Examples
8//! They can be created directly
9//! ```no_run
10//! # use muda_win::accelerator::{Accelerator, Modifiers, Code};
11//! let accelerator = Accelerator::new(Some(Modifiers::SHIFT), Code::KeyQ);
12//! let accelerator_without_mods = Accelerator::new(None, Code::KeyQ);
13//! ```
14//! or from `&str`, note that all modifiers
15//! have to be listed before the non-modifier key, `shift+alt+KeyQ` is legal,
16//! whereas `shift+q+alt` is not.
17//! ```no_run
18//! # use muda_win::accelerator::{Accelerator};
19//! let accelerator: Accelerator = "shift+alt+KeyQ".parse().unwrap();
20//! # // This assert exists to ensure a test breaks once the
21//! # // statement above about ordering is no longer valid.
22//! # assert!("shift+KeyQ+alt".parse::<Accelerator>().is_err());
23//! ```
24//!
25
26pub use keyboard_types::{Code, Modifiers};
27use std::error::Error as StdError;
28use std::{borrow::Borrow, hash::Hash, str::FromStr};
29
30pub const CMD_OR_CTRL: Modifiers = Modifiers::CONTROL;
31
32/// Errors that can occur while parsing an accelerator.
33#[derive(Debug)]
34pub enum AcceleratorParseError {
35    /// Couldn't recognize a key as valid for an accelerator.
36    /// If you feel like it should be, please report this to <https://github.com/win-rs/muda-win>.
37    UnsupportedKey(String),
38    /// Found an empty token while parsing an accelerator.
39    EmptyToken(String),
40    /// Invalid accelerator format.
41    /// An accelerator should have the modifiers first and only one main key, e.g., "Shift + Alt + K".
42    InvalidFormat(String),
43}
44
45impl std::fmt::Display for AcceleratorParseError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            AcceleratorParseError::UnsupportedKey(key) => write!(
49                f,
50                "Couldn't recognize \"{}\" as a valid key for accelerator, if you feel like it should be, please report this to https://github.com/win-rs/muda-win",
51                key
52            ),
53            AcceleratorParseError::EmptyToken(token) => {
54                write!(f, "Found empty token while parsing accelerator: {}", token)
55            }
56            AcceleratorParseError::InvalidFormat(format) => write!(
57                f,
58                "Invalid accelerator format: \"{}\", an accelerator should have the modifiers first and only one main key, for example: \"Shift + Alt + K\"",
59                format
60            ),
61        }
62    }
63}
64
65impl StdError for AcceleratorParseError {}
66
67/// A keyboard shortcut that consists of an optional combination
68/// of modifier keys (provided by [`Modifiers`] and
69/// one key ([`Code`]).
70#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct Accelerator {
73    pub(crate) mods: Modifiers,
74    pub(crate) key: Code,
75    id: u32,
76}
77
78impl Accelerator {
79    /// Creates a new accelerator to define keyboard shortcuts throughout your application.
80    /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`]
81    pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
82        let mut mods = mods.unwrap_or_else(Modifiers::empty);
83        if mods.contains(Modifiers::META) {
84            mods.remove(Modifiers::META);
85            mods.insert(Modifiers::SUPER);
86        }
87
88        let id = Self::generate_hash(mods, key);
89
90        Self { mods, key, id }
91    }
92
93    fn generate_hash(mods: Modifiers, key: Code) -> u32 {
94        let mut accelerator_str = String::new();
95        if mods.contains(Modifiers::SHIFT) {
96            accelerator_str.push_str("shift+")
97        }
98        if mods.contains(Modifiers::CONTROL) {
99            accelerator_str.push_str("control+")
100        }
101        if mods.contains(Modifiers::ALT) {
102            accelerator_str.push_str("alt+")
103        }
104        if mods.contains(Modifiers::SUPER) {
105            accelerator_str.push_str("super+")
106        }
107        accelerator_str.push_str(&key.to_string());
108
109        let mut hasher = std::collections::hash_map::DefaultHasher::new();
110        accelerator_str.hash(&mut hasher);
111        std::hash::Hasher::finish(&hasher) as u32
112    }
113
114    /// Returns the id associated with this accelerator
115    /// which is a hash of the string representation of modifiers and key within this accelerator.
116    pub fn id(&self) -> u32 {
117        self.id
118    }
119
120    /// Returns the modifier for this accelerator
121    pub fn modifiers(&self) -> Modifiers {
122        self.mods
123    }
124
125    /// Returns the code for this accelerator
126    pub fn key(&self) -> Code {
127        self.key
128    }
129
130    /// Returns `true` if this [`Code`] and [`Modifiers`] matches this `Accelerator`.
131    pub fn matches(&self, modifiers: impl Borrow<Modifiers>, key: impl Borrow<Code>) -> bool {
132        // Should be a const but const bit_or doesn't work here.
133        let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER;
134        let modifiers = modifiers.borrow();
135        let key = key.borrow();
136        self.mods == *modifiers & base_mods && self.key == *key
137    }
138}
139
140impl FromStr for Accelerator {
141    type Err = AcceleratorParseError;
142    fn from_str(accelerator_string: &str) -> Result<Self, Self::Err> {
143        parse_accelerator(accelerator_string)
144    }
145}
146
147impl TryFrom<&str> for Accelerator {
148    type Error = AcceleratorParseError;
149
150    fn try_from(value: &str) -> Result<Self, Self::Error> {
151        parse_accelerator(value)
152    }
153}
154
155impl TryFrom<String> for Accelerator {
156    type Error = AcceleratorParseError;
157
158    fn try_from(value: String) -> Result<Self, Self::Error> {
159        parse_accelerator(&value)
160    }
161}
162
163fn parse_accelerator(accelerator: &str) -> Result<Accelerator, AcceleratorParseError> {
164    let tokens = accelerator.split('+').collect::<Vec<&str>>();
165
166    let mut mods = Modifiers::empty();
167    let mut key = None;
168
169    match tokens.len() {
170        // single key accelerator
171        1 => {
172            key = Some(parse_key(tokens[0])?);
173        }
174
175        // modifiers and key comobo accelerator
176        _ => {
177            for raw in tokens {
178                let token = raw.trim();
179
180                if token.is_empty() {
181                    return Err(AcceleratorParseError::EmptyToken(accelerator.to_string()));
182                }
183
184                if key.is_some() {
185                    // At this point we have parsed the modifiers and a main key, so by reaching
186                    // this code, the function either received more than one main key or
187                    //  the accelerator is not in the right order
188                    // examples:
189                    // 1. "Ctrl+Shift+C+A" => only one main key should be allowd.
190                    // 2. "Ctrl+C+Shift" => wrong order
191                    return Err(AcceleratorParseError::InvalidFormat(
192                        accelerator.to_string(),
193                    ));
194                }
195
196                match token.to_uppercase().as_str() {
197                    "OPTION" | "ALT" => {
198                        mods |= Modifiers::ALT;
199                    }
200                    "CONTROL" | "CTRL" => {
201                        mods |= Modifiers::CONTROL;
202                    }
203                    "COMMAND" | "CMD" | "SUPER" => {
204                        mods |= Modifiers::META;
205                    }
206                    "SHIFT" => {
207                        mods |= Modifiers::SHIFT;
208                    }
209                    "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
210                        mods |= Modifiers::CONTROL;
211                    }
212                    _ => {
213                        key = Some(parse_key(token)?);
214                    }
215                }
216            }
217        }
218    }
219
220    let key = key.ok_or_else(|| AcceleratorParseError::InvalidFormat(accelerator.to_string()))?;
221    Ok(Accelerator::new(Some(mods), key))
222}
223
224fn parse_key(key: &str) -> Result<Code, AcceleratorParseError> {
225    use Code::*;
226    match key.to_uppercase().as_str() {
227        "BACKQUOTE" | "`" => Ok(Backquote),
228        "BACKSLASH" | "\\" => Ok(Backslash),
229        "BRACKETLEFT" | "[" => Ok(BracketLeft),
230        "BRACKETRIGHT" | "]" => Ok(BracketRight),
231        "COMMA" | "," => Ok(Comma),
232        "DIGIT0" | "0" => Ok(Digit0),
233        "DIGIT1" | "1" => Ok(Digit1),
234        "DIGIT2" | "2" => Ok(Digit2),
235        "DIGIT3" | "3" => Ok(Digit3),
236        "DIGIT4" | "4" => Ok(Digit4),
237        "DIGIT5" | "5" => Ok(Digit5),
238        "DIGIT6" | "6" => Ok(Digit6),
239        "DIGIT7" | "7" => Ok(Digit7),
240        "DIGIT8" | "8" => Ok(Digit8),
241        "DIGIT9" | "9" => Ok(Digit9),
242        "EQUAL" | "=" => Ok(Equal),
243        "KEYA" | "A" => Ok(KeyA),
244        "KEYB" | "B" => Ok(KeyB),
245        "KEYC" | "C" => Ok(KeyC),
246        "KEYD" | "D" => Ok(KeyD),
247        "KEYE" | "E" => Ok(KeyE),
248        "KEYF" | "F" => Ok(KeyF),
249        "KEYG" | "G" => Ok(KeyG),
250        "KEYH" | "H" => Ok(KeyH),
251        "KEYI" | "I" => Ok(KeyI),
252        "KEYJ" | "J" => Ok(KeyJ),
253        "KEYK" | "K" => Ok(KeyK),
254        "KEYL" | "L" => Ok(KeyL),
255        "KEYM" | "M" => Ok(KeyM),
256        "KEYN" | "N" => Ok(KeyN),
257        "KEYO" | "O" => Ok(KeyO),
258        "KEYP" | "P" => Ok(KeyP),
259        "KEYQ" | "Q" => Ok(KeyQ),
260        "KEYR" | "R" => Ok(KeyR),
261        "KEYS" | "S" => Ok(KeyS),
262        "KEYT" | "T" => Ok(KeyT),
263        "KEYU" | "U" => Ok(KeyU),
264        "KEYV" | "V" => Ok(KeyV),
265        "KEYW" | "W" => Ok(KeyW),
266        "KEYX" | "X" => Ok(KeyX),
267        "KEYY" | "Y" => Ok(KeyY),
268        "KEYZ" | "Z" => Ok(KeyZ),
269        "MINUS" | "-" => Ok(Minus),
270        "PERIOD" | "." => Ok(Period),
271        "QUOTE" | "'" => Ok(Quote),
272        "SEMICOLON" | ";" => Ok(Semicolon),
273        "SLASH" | "/" => Ok(Slash),
274        "BACKSPACE" => Ok(Backspace),
275        "CAPSLOCK" => Ok(CapsLock),
276        "ENTER" => Ok(Enter),
277        "SPACE" => Ok(Space),
278        "TAB" => Ok(Tab),
279        "DELETE" => Ok(Delete),
280        "END" => Ok(End),
281        "HOME" => Ok(Home),
282        "INSERT" => Ok(Insert),
283        "PAGEDOWN" => Ok(PageDown),
284        "PAGEUP" => Ok(PageUp),
285        "PRINTSCREEN" => Ok(PrintScreen),
286        "SCROLLLOCK" => Ok(ScrollLock),
287        "ARROWDOWN" | "DOWN" => Ok(ArrowDown),
288        "ARROWLEFT" | "LEFT" => Ok(ArrowLeft),
289        "ARROWRIGHT" | "RIGHT" => Ok(ArrowRight),
290        "ARROWUP" | "UP" => Ok(ArrowUp),
291        "NUMLOCK" => Ok(NumLock),
292        "NUMPAD0" | "NUM0" => Ok(Numpad0),
293        "NUMPAD1" | "NUM1" => Ok(Numpad1),
294        "NUMPAD2" | "NUM2" => Ok(Numpad2),
295        "NUMPAD3" | "NUM3" => Ok(Numpad3),
296        "NUMPAD4" | "NUM4" => Ok(Numpad4),
297        "NUMPAD5" | "NUM5" => Ok(Numpad5),
298        "NUMPAD6" | "NUM6" => Ok(Numpad6),
299        "NUMPAD7" | "NUM7" => Ok(Numpad7),
300        "NUMPAD8" | "NUM8" => Ok(Numpad8),
301        "NUMPAD9" | "NUM9" => Ok(Numpad9),
302        "NUMPADADD" | "NUMADD" | "NUMPADPLUS" | "NUMPLUS" => Ok(NumpadAdd),
303        "NUMPADDECIMAL" | "NUMDECIMAL" => Ok(NumpadDecimal),
304        "NUMPADDIVIDE" | "NUMDIVIDE" => Ok(NumpadDivide),
305        "NUMPADENTER" | "NUMENTER" => Ok(NumpadEnter),
306        "NUMPADEQUAL" | "NUMEQUAL" => Ok(NumpadEqual),
307        "NUMPADMULTIPLY" | "NUMMULTIPLY" => Ok(NumpadMultiply),
308        "NUMPADSUBTRACT" | "NUMSUBTRACT" => Ok(NumpadSubtract),
309        "ESCAPE" | "ESC" => Ok(Escape),
310        "F1" => Ok(F1),
311        "F2" => Ok(F2),
312        "F3" => Ok(F3),
313        "F4" => Ok(F4),
314        "F5" => Ok(F5),
315        "F6" => Ok(F6),
316        "F7" => Ok(F7),
317        "F8" => Ok(F8),
318        "F9" => Ok(F9),
319        "F10" => Ok(F10),
320        "F11" => Ok(F11),
321        "F12" => Ok(F12),
322        "AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(AudioVolumeDown),
323        "AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp),
324        "AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute),
325        "F13" => Ok(F13),
326        "F14" => Ok(F14),
327        "F15" => Ok(F15),
328        "F16" => Ok(F16),
329        "F17" => Ok(F17),
330        "F18" => Ok(F18),
331        "F19" => Ok(F19),
332        "F20" => Ok(F20),
333        "F21" => Ok(F21),
334        "F22" => Ok(F22),
335        "F23" => Ok(F23),
336        "F24" => Ok(F24),
337
338        _ => Err(AcceleratorParseError::UnsupportedKey(key.to_string())),
339    }
340}
341
342#[test]
343fn test_parse_accelerator() {
344    macro_rules! assert_parse_accelerator {
345        ($key:literal, $lrh:expr) => {
346            let r = parse_accelerator($key).unwrap();
347            let l = $lrh;
348            assert_eq!(r.mods, l.mods);
349            assert_eq!(r.key, l.key);
350        };
351    }
352
353    assert_parse_accelerator!(
354        "KeyX",
355        Accelerator {
356            mods: Modifiers::empty(),
357            key: Code::KeyX,
358            id: 0,
359        }
360    );
361
362    assert_parse_accelerator!(
363        "CTRL+KeyX",
364        Accelerator {
365            mods: Modifiers::CONTROL,
366            key: Code::KeyX,
367            id: 0,
368        }
369    );
370
371    assert_parse_accelerator!(
372        "SHIFT+KeyC",
373        Accelerator {
374            mods: Modifiers::SHIFT,
375            key: Code::KeyC,
376            id: 0,
377        }
378    );
379
380    assert_parse_accelerator!(
381        "SHIFT+KeyC",
382        Accelerator {
383            mods: Modifiers::SHIFT,
384            key: Code::KeyC,
385            id: 0,
386        }
387    );
388
389    assert_parse_accelerator!(
390        "super+ctrl+SHIFT+alt+ArrowUp",
391        Accelerator {
392            mods: Modifiers::SUPER | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT,
393            key: Code::ArrowUp,
394            id: 0,
395        }
396    );
397    assert_parse_accelerator!(
398        "Digit5",
399        Accelerator {
400            mods: Modifiers::empty(),
401            key: Code::Digit5,
402            id: 0,
403        }
404    );
405    assert_parse_accelerator!(
406        "KeyG",
407        Accelerator {
408            mods: Modifiers::empty(),
409            key: Code::KeyG,
410            id: 0,
411        }
412    );
413
414    assert_parse_accelerator!(
415        "SHiFT+F12",
416        Accelerator {
417            mods: Modifiers::SHIFT,
418            key: Code::F12,
419            id: 0,
420        }
421    );
422
423    assert_parse_accelerator!(
424        "CmdOrCtrl+Space",
425        Accelerator {
426            #[cfg(target_os = "macos")]
427            mods: Modifiers::SUPER,
428            #[cfg(not(target_os = "macos"))]
429            mods: Modifiers::CONTROL,
430            key: Code::Space,
431            id: 0,
432        }
433    );
434}
435
436#[test]
437fn test_equality() {
438    let h1 = parse_accelerator("Shift+KeyR").unwrap();
439    let h2 = parse_accelerator("Shift+KeyR").unwrap();
440    let h3 = Accelerator::new(Some(Modifiers::SHIFT), Code::KeyR);
441    let h4 = parse_accelerator("Alt+KeyR").unwrap();
442    let h5 = parse_accelerator("Alt+KeyR").unwrap();
443    let h6 = parse_accelerator("KeyR").unwrap();
444
445    assert!(h1 == h2 && h2 == h3 && h3 != h4 && h4 == h5 && h5 != h6);
446    assert!(
447        h1.id() == h2.id()
448            && h2.id() == h3.id()
449            && h3.id() != h4.id()
450            && h4.id() == h5.id()
451            && h5.id() != h6.id()
452    );
453}