Skip to main content

ferroui_base/input/
key_gesture.rs

1use super::platform::KeyGestureFormatInfo;
2use super::{Key, KeyEventArgs, KeyModifiers, ParseKeyModifiersError};
3use crate::utilities::FormatError;
4use std::fmt;
5use std::rc::Rc;
6use std::str::FromStr;
7
8/// Defines a keyboard input combination.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub struct KeyGesture {
11    key: Key,
12    key_modifiers: KeyModifiers,
13}
14
15impl KeyGesture {
16    /// Creates a gesture for a key with modifiers.
17    pub const fn new(key: Key, modifiers: KeyModifiers) -> Self {
18        Self { key, key_modifiers: modifiers }
19    }
20
21    /// Creates a gesture for a key without modifiers.
22    pub const fn from_key(key: Key) -> Self {
23        Self { key, key_modifiers: KeyModifiers::NONE }
24    }
25
26    /// The key of the gesture.
27    #[inline]
28    pub const fn key(&self) -> Key {
29        self.key
30    }
31
32    /// The modifier keys of the gesture.
33    #[inline]
34    pub const fn key_modifiers(&self) -> KeyModifiers {
35        self.key_modifiers
36    }
37
38    /// Parses a gesture such as `Ctrl+Shift+A`.
39    ///
40    /// Parts are separated by `+`. The last part that names a key decides
41    /// the key (`+`, `-`, `.` and `,` name the corresponding OEM keys);
42    /// every other part must be a modifier: `Ctrl`/`Control`, `Shift`,
43    /// `Alt`, `Meta`/`Cmd`/`Win`/`⌘`.
44    pub fn parse(gesture: &str) -> Result<KeyGesture, ParseKeyModifiersError> {
45        // A plain split can't be used here because "Ctrl++" is a perfectly
46        // valid key gesture.
47        let mut key = Key::None;
48        let mut key_modifiers = KeyModifiers::NONE;
49        let mut cstart = 0;
50
51        for (c, ch) in gesture.char_indices().chain(std::iter::once((gesture.len(), '\0'))) {
52            let is_last = c == gesture.len();
53
54            if is_last || (ch == '+' && cstart != c) {
55                let part = gesture[cstart..c].trim();
56
57                match Self::try_parse_key(part) {
58                    Some(parsed) => key = parsed,
59                    None => {
60                        // A part that is not a key resets the key, as the
61                        // failed key parse does upstream.
62                        key = Key::None;
63                        key_modifiers |= Self::parse_modifier(part)?;
64                    }
65                }
66
67                cstart = c + 1;
68            }
69        }
70
71        Ok(KeyGesture::new(key, key_modifiers))
72    }
73
74    /// Whether a key event matches the gesture: same modifiers and the
75    /// same key, where the numpad operation keys match their regular
76    /// counterparts.
77    pub fn matches(&self, key_event: Option<&KeyEventArgs>) -> bool {
78        key_event.is_some_and(|key_event| {
79            key_event.key_modifiers == self.key_modifiers
80                && Self::resolve_num_pad_operation_key(key_event.key) == Self::resolve_num_pad_operation_key(self.key)
81        })
82    }
83
84    fn try_parse_key(key_str: &str) -> Option<Key> {
85        match key_str {
86            "+" => return Some(Key::OemPlus),
87            "-" => return Some(Key::OemMinus),
88            "." => return Some(Key::OemPeriod),
89            "," => return Some(Key::OemComma),
90            _ => {}
91        }
92
93        Key::parse(key_str).ok()
94    }
95
96    fn parse_modifier(modifier: &str) -> Result<KeyModifiers, ParseKeyModifiersError> {
97        if modifier.eq_ignore_ascii_case("ctrl") {
98            return Ok(KeyModifiers::CONTROL);
99        }
100
101        if modifier.eq_ignore_ascii_case("cmd") || modifier.eq_ignore_ascii_case("win") || modifier == "⌘" {
102            return Ok(KeyModifiers::META);
103        }
104
105        KeyModifiers::parse(modifier)
106    }
107
108    fn resolve_num_pad_operation_key(key: Key) -> Key {
109        match key {
110            Key::Add => Key::OemPlus,
111            Key::Subtract => Key::OemMinus,
112            Key::Decimal => Key::OemPeriod,
113            _ => key,
114        }
115    }
116}
117
118impl FromStr for KeyGesture {
119    type Err = ParseKeyModifiersError;
120
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        Self::parse(s)
123    }
124}
125
126impl KeyGesture {
127    /// Formats the gesture with the platform's names for keys and
128    /// modifiers (the `p` format): the given format info, or the one
129    /// registered with the locator, or the invariant one.
130    pub fn to_platform_string(&self, format_provider: Option<Rc<KeyGestureFormatInfo>>) -> String {
131        self.format_with(&KeyGestureFormatInfo::get_instance(format_provider))
132    }
133
134    /// Returns the gesture as a string formatted according to the format string and the
135    /// format provider.
136    ///
137    /// The format is `None`, an empty string or `"g"` for the invariant format, or `"p"`
138    /// for the platform specific format: the given format info, or the one registered with
139    /// the locator, or the invariant one.
140    ///
141    /// Fails with a [`FormatError`] if the format string is anything else.
142    pub fn to_string_format(
143        &self,
144        format: Option<&str>,
145        format_provider: Option<Rc<KeyGestureFormatInfo>>,
146    ) -> Result<String, FormatError> {
147        let format_info = match format {
148            None | Some("") | Some("g") => KeyGestureFormatInfo::invariant(),
149            Some("p") => KeyGestureFormatInfo::get_instance(format_provider),
150            _ => return Err(FormatError::new("Unknown format specifier")),
151        };
152
153        Ok(self.format_with(&format_info))
154    }
155
156    /// Formats the gesture with the given format info.
157    pub fn format_with(&self, format_info: &KeyGestureFormatInfo) -> String {
158        let mut s = String::new();
159        let mut part = |text: &str| {
160            if !s.is_empty() {
161                s.push('+');
162            }
163            s.push_str(text);
164        };
165
166        if self.key_modifiers.contains(KeyModifiers::CONTROL) {
167            part(format_info.ctrl());
168        }
169
170        if self.key_modifiers.contains(KeyModifiers::SHIFT) {
171            part(format_info.shift());
172        }
173
174        if self.key_modifiers.contains(KeyModifiers::ALT) {
175            part(format_info.alt());
176        }
177
178        if self.key_modifiers.contains(KeyModifiers::META) {
179            part(format_info.meta());
180        }
181
182        if self.key != Key::None || self.key_modifiers == KeyModifiers::NONE {
183            part(&format_info.format_key(self.key));
184        }
185
186        s
187    }
188}
189
190/// Formats the gesture in the invariant format, e.g. `Ctrl+Shift+A`.
191impl fmt::Display for KeyGesture {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        let mut any = false;
194        let mut part = |f: &mut fmt::Formatter<'_>, s: &str| -> fmt::Result {
195            if any {
196                f.write_str("+")?;
197            }
198            any = true;
199            f.write_str(s)
200        };
201
202        if self.key_modifiers.contains(KeyModifiers::CONTROL) {
203            part(f, "Ctrl")?;
204        }
205
206        if self.key_modifiers.contains(KeyModifiers::SHIFT) {
207            part(f, "Shift")?;
208        }
209
210        if self.key_modifiers.contains(KeyModifiers::ALT) {
211            part(f, "Alt")?;
212        }
213
214        if self.key_modifiers.contains(KeyModifiers::META) {
215            part(f, "Cmd")?;
216        }
217
218        if self.key != Key::None || self.key_modifiers == KeyModifiers::NONE {
219            part(f, self.key.name())?;
220        }
221
222        Ok(())
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn key_gesture_is_able_to_parse_sample_data() {
232        let data = [
233            ("Ctrl+A", KeyGesture::new(Key::A, KeyModifiers::CONTROL)),
234            ("  \tShift\t+Alt +B", KeyGesture::new(Key::B, KeyModifiers::SHIFT | KeyModifiers::ALT)),
235            ("Control++", KeyGesture::new(Key::OemPlus, KeyModifiers::CONTROL)),
236            ("Shift+⌘+A", KeyGesture::new(Key::A, KeyModifiers::META | KeyModifiers::SHIFT)),
237            ("Shift+Cmd+A", KeyGesture::new(Key::A, KeyModifiers::META | KeyModifiers::SHIFT)),
238            ("None", KeyGesture::from_key(Key::None)),
239            ("Alt+Shift", KeyGesture::new(Key::None, KeyModifiers::ALT | KeyModifiers::SHIFT)),
240        ];
241
242        for (text, gesture) in data {
243            assert_eq!(KeyGesture::parse(text), Ok(gesture), "{text}");
244        }
245    }
246
247    #[test]
248    fn key_gesture_parse_rejects_unknown_parts() {
249        assert!(KeyGesture::parse("Ctrl+NotAKey").is_err());
250        assert!("Bogus+A".parse::<KeyGesture>().is_err());
251    }
252
253    #[test]
254    fn key_gesture_matches_num_pad_to_regular_digit() {
255        let data = [(Key::OemMinus, Key::Subtract), (Key::OemPlus, Key::Add), (Key::OemPeriod, Key::Decimal)];
256
257        for (gesture_key, pressed_key) in data {
258            let key_gesture = KeyGesture::from_key(gesture_key);
259            let mut args = KeyEventArgs::new();
260            args.key = pressed_key;
261
262            assert!(key_gesture.matches(Some(&args)));
263        }
264
265        assert!(!KeyGesture::from_key(Key::A).matches(None));
266    }
267
268    #[test]
269    fn platform_format_uses_overrides() {
270        let mut overrides = std::collections::HashMap::new();
271        overrides.insert(Key::Return, "\u{21A9}".to_string());
272        let info = KeyGestureFormatInfo::new(Some(overrides), "\u{2318}", "\u{2303}", "\u{2325}", "\u{21E7}");
273
274        assert_eq!(KeyGesture::new(Key::Return, KeyModifiers::META).format_with(&info), "\u{2318}+\u{21A9}");
275        assert_eq!(KeyGesture::new(Key::OemPlus, KeyModifiers::CONTROL).format_with(&info), "\u{2303}++");
276        assert_eq!(KeyGesture::new(Key::Left, KeyModifiers::SHIFT).format_with(&info), "\u{21E7}+Left Arrow");
277        assert_eq!(KeyGesture::from_key(Key::A).format_with(&info), "A");
278
279        // Without a registered format info the platform format is the
280        // invariant one.
281        let gesture = KeyGesture::new(Key::OemPlus, KeyModifiers::CONTROL | KeyModifiers::META);
282        assert_eq!(gesture.to_platform_string(None), gesture.to_string());
283        assert_eq!(gesture.to_string(), "Ctrl+Cmd+OemPlus");
284    }
285
286    // Not an upstream test: the format strings of `ToString(string, IFormatProvider)`.
287    #[test]
288    fn to_string_format_selects_the_format_info() {
289        let gesture = KeyGesture::new(Key::A, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
290
291        assert_eq!(gesture.to_string_format(None, None).unwrap(), "Ctrl+Shift+A");
292        assert_eq!(gesture.to_string_format(Some(""), None).unwrap(), "Ctrl+Shift+A");
293        assert_eq!(gesture.to_string_format(Some("g"), None).unwrap(), "Ctrl+Shift+A");
294
295        let info = Rc::new(KeyGestureFormatInfo::new(None, "\u{2318}", "\u{2303}", "\u{2325}", "\u{21E7}"));
296
297        // The invariant format ignores the provider; the platform format uses it.
298        assert_eq!(gesture.to_string_format(Some("g"), Some(info.clone())).unwrap(), "Ctrl+Shift+A");
299        assert_eq!(gesture.to_string_format(Some("p"), Some(info)).unwrap(), "\u{2303}+\u{21E7}+A");
300
301        assert_eq!(gesture.to_string_format(Some("x"), None), Err(FormatError::new("Unknown format specifier")));
302    }
303
304    #[test]
305    fn to_string_produces_correct_results() {
306        let data = [
307            (KeyGesture::from_key(Key::A), "A"),
308            (KeyGesture::new(Key::A, KeyModifiers::CONTROL), "Ctrl+A"),
309            (KeyGesture::new(Key::A, KeyModifiers::CONTROL | KeyModifiers::SHIFT), "Ctrl+Shift+A"),
310            (KeyGesture::new(Key::A, KeyModifiers::ALT | KeyModifiers::SHIFT), "Shift+Alt+A"),
311            (
312                KeyGesture::new(Key::A, KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT),
313                "Ctrl+Shift+Alt+A",
314            ),
315            (KeyGesture::new(Key::A, KeyModifiers::META | KeyModifiers::SHIFT), "Shift+Cmd+A"),
316            (KeyGesture::from_key(Key::None), "None"),
317            (KeyGesture::new(Key::None, KeyModifiers::ALT | KeyModifiers::SHIFT), "Shift+Alt"),
318        ];
319
320        for (gesture, expected) in data {
321            assert_eq!(gesture.to_string(), expected);
322        }
323    }
324}