Skip to main content

iced_cube/
keys.rs

1//! Keyboard shortcuts that apps can rebind.
2//!
3//! iced has no focus model for most widgets, so shortcuts are app-wide: the
4//! app subscribes with [`subscription`], stores each [`Event`] it receives,
5//! and resolves it through a component's [`Keymap`] into that component's
6//! own action. Every interactive component has an `Action` enum and a
7//! `default_keymap()`, and an app can [`bind`](Keymap::bind),
8//! [`unbind`](Keymap::unbind) or [`clear`](Keymap::clear) the defaults.
9//!
10//! ```no_run
11//! use iced::Subscription;
12//! use iced::keyboard::key::Named;
13//! use iced_cube::keys::{self, Chord, Keymap};
14//! use iced_cube::navigation::tabs::{self, Action};
15//!
16//! #[derive(Debug, Clone)]
17//! enum Message {
18//!     Key(keys::Event),
19//!     Tabs(tabs::Event<u8>),
20//! }
21//!
22//! struct App {
23//!     tabs: tabs::State<u8>,
24//!     keymap: Keymap<Action>,
25//! }
26//!
27//! impl App {
28//!     fn new() -> Self {
29//!         let keymap = tabs::default_keymap()
30//!             .unbind(&Chord::named(Named::ArrowLeft))
31//!             .unbind(&Chord::named(Named::ArrowRight));
32//!         Self { tabs: tabs::State::new([tabs::tab(1, "One"), tabs::tab(2, "Two")]), keymap }
33//!     }
34//!
35//!     fn update(&mut self, message: Message) {
36//!         let event = match message {
37//!             Message::Key(key) => self
38//!                 .keymap
39//!                 .resolve_event(&key)
40//!                 .and_then(|action| action.event(&self.tabs)),
41//!             Message::Tabs(event) => Some(event),
42//!         };
43//!         if let Some(event) = event {
44//!             let _ = self.tabs.update(event);
45//!         }
46//!     }
47//!
48//!     fn subscription(&self) -> Subscription<Message> {
49//!         keys::subscription().map(Message::Key)
50//!     }
51//! }
52//! ```
53
54use std::fmt;
55use std::str::FromStr;
56
57use iced::Subscription;
58use iced::keyboard::{self, Key, Modifiers, key::Named};
59
60/// A key press delivered by [`subscription`].
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Event {
63    pub key: Key,
64    pub modifiers: Modifiers,
65}
66
67/// Key presses that no widget captured, such as a focused text input.
68pub fn subscription() -> Subscription<Event> {
69    keyboard::listen().filter_map(|event| match event {
70        keyboard::Event::KeyPressed { key, modifiers, .. } => Some(Event { key, modifiers }),
71        _ => None,
72    })
73}
74
75/// Named keys a [`Chord`] understands: the canonical name first, then aliases.
76const NAMED: &[(Named, &[&str])] = &[
77    (Named::Enter, &["Enter", "Return"]),
78    (Named::Tab, &["Tab"]),
79    (Named::Space, &["Space", "Spacebar"]),
80    (Named::Escape, &["Escape", "Esc"]),
81    (Named::Backspace, &["Backspace"]),
82    (Named::Delete, &["Delete", "Del"]),
83    (Named::Insert, &["Insert", "Ins"]),
84    (Named::ArrowUp, &["ArrowUp", "Up"]),
85    (Named::ArrowDown, &["ArrowDown", "Down"]),
86    (Named::ArrowLeft, &["ArrowLeft", "Left"]),
87    (Named::ArrowRight, &["ArrowRight", "Right"]),
88    (Named::Home, &["Home"]),
89    (Named::End, &["End"]),
90    (Named::PageUp, &["PageUp", "PgUp"]),
91    (Named::PageDown, &["PageDown", "PgDn"]),
92    (Named::F1, &["F1"]),
93    (Named::F2, &["F2"]),
94    (Named::F3, &["F3"]),
95    (Named::F4, &["F4"]),
96    (Named::F5, &["F5"]),
97    (Named::F6, &["F6"]),
98    (Named::F7, &["F7"]),
99    (Named::F8, &["F8"]),
100    (Named::F9, &["F9"]),
101    (Named::F10, &["F10"]),
102    (Named::F11, &["F11"]),
103    (Named::F12, &["F12"]),
104];
105
106/// Display name of the logo key on this platform.
107const LOGO: &str = if cfg!(target_os = "macos") {
108    "Cmd"
109} else {
110    "Super"
111};
112
113/// A key plus the modifiers held with it, such as `Ctrl+Shift+Tab`.
114///
115/// Parse one from a string: modifiers and the key are joined by `+`, and
116/// case does not matter. Modifiers are `Ctrl`, `Shift`, `Alt` (or
117/// `Option`), `Cmd` (or `Super`, `Meta`) and `Mod`, which is Cmd on macOS
118/// and Ctrl elsewhere. Keys are a single character (`K`, `/`, `+`) or a
119/// name: `Enter`, `Tab`, `Space`, `Escape`, `Backspace`, `Delete`,
120/// `Insert`, `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight`, `Home`,
121/// `End`, `PageUp`, `PageDown` and `F1` to `F12`.
122///
123/// ```
124/// use iced_cube::keys::Chord;
125///
126/// let chord: Chord = "ctrl+shift+tab".parse().unwrap();
127/// assert_eq!(chord.to_string(), "Ctrl+Shift+Tab");
128/// ```
129#[derive(Debug, Clone, PartialEq, Eq, Hash)]
130pub struct Chord {
131    key: Key,
132    modifiers: Modifiers,
133}
134
135impl Chord {
136    /// A chord for `key`. Characters are stored in lower case.
137    pub fn new(key: Key, modifiers: Modifiers) -> Self {
138        Self {
139            key: normalise(key),
140            modifiers,
141        }
142    }
143
144    /// A named key with no modifiers.
145    pub fn named(key: Named) -> Self {
146        Self::new(Key::Named(key), Modifiers::empty())
147    }
148
149    /// A character key with no modifiers.
150    pub fn character(key: char) -> Self {
151        Self::new(Key::Character(key.to_string().into()), Modifiers::empty())
152    }
153
154    pub fn ctrl(self) -> Self {
155        self.with(Modifiers::CTRL)
156    }
157
158    pub fn shift(self) -> Self {
159        self.with(Modifiers::SHIFT)
160    }
161
162    pub fn alt(self) -> Self {
163        self.with(Modifiers::ALT)
164    }
165
166    /// Adds the logo key: Cmd on macOS, Super or the Windows key elsewhere.
167    pub fn logo(self) -> Self {
168        self.with(Modifiers::LOGO)
169    }
170
171    /// Adds Cmd on macOS and Ctrl elsewhere.
172    pub fn command(self) -> Self {
173        self.with(Modifiers::COMMAND)
174    }
175
176    fn with(mut self, modifiers: Modifiers) -> Self {
177        self.modifiers |= modifiers;
178        self
179    }
180
181    pub fn key(&self) -> &Key {
182        &self.key
183    }
184
185    pub fn modifiers(&self) -> Modifiers {
186        self.modifiers
187    }
188
189    /// Whether a key press with exactly these modifiers triggers the chord.
190    pub fn matches(&self, key: &Key, modifiers: Modifiers) -> bool {
191        self.modifiers == modifiers && same_key(&self.key, key)
192    }
193}
194
195fn normalise(key: Key) -> Key {
196    match key {
197        Key::Character(text) => Key::Character(text.to_lowercase().into()),
198        key => key,
199    }
200}
201
202fn same_key(chord: &Key, pressed: &Key) -> bool {
203    match (chord, pressed) {
204        (Key::Character(a), Key::Character(b)) => a.as_str() == b.to_lowercase(),
205        (a, b) => a == b,
206    }
207}
208
209/// Why a string is not a valid [`Chord`].
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum ParseChordError {
212    Empty,
213    /// A `+` with nothing on one side, such as `Ctrl+`.
214    MissingKey,
215    UnknownModifier(String),
216    UnknownKey(String),
217}
218
219impl fmt::Display for ParseChordError {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        match self {
222            Self::Empty => write!(f, "empty key chord"),
223            Self::MissingKey => write!(f, "key chord has an empty part"),
224            Self::UnknownModifier(name) => write!(f, "unknown modifier \"{name}\""),
225            Self::UnknownKey(name) => write!(f, "unknown key \"{name}\""),
226        }
227    }
228}
229
230impl std::error::Error for ParseChordError {}
231
232impl FromStr for Chord {
233    type Err = ParseChordError;
234
235    fn from_str(value: &str) -> Result<Self, Self::Err> {
236        let value = value.trim();
237        if value.is_empty() {
238            return Err(ParseChordError::Empty);
239        }
240
241        // A trailing "++" means the key itself is "+".
242        let (modifiers, key) = match value.strip_suffix("++") {
243            Some(rest) => (Some(rest), "+"),
244            None if value == "+" => (None, "+"),
245            None => match value.rsplit_once('+') {
246                Some((rest, key)) => (Some(rest), key),
247                None => (None, value),
248            },
249        };
250
251        let mut chord = Chord::new(parse_key(key.trim())?, Modifiers::empty());
252        let Some(modifiers) = modifiers else {
253            return Ok(chord);
254        };
255        for name in modifiers.split('+') {
256            chord = chord.with(parse_modifier(name.trim())?);
257        }
258        Ok(chord)
259    }
260}
261
262impl TryFrom<&str> for Chord {
263    type Error = ParseChordError;
264
265    fn try_from(value: &str) -> Result<Self, Self::Error> {
266        value.parse()
267    }
268}
269
270fn parse_key(name: &str) -> Result<Key, ParseChordError> {
271    if name.is_empty() {
272        return Err(ParseChordError::MissingKey);
273    }
274    if let Some((named, _)) = NAMED
275        .iter()
276        .find(|(_, names)| names.iter().any(|alias| alias.eq_ignore_ascii_case(name)))
277    {
278        return Ok(Key::Named(*named));
279    }
280    let mut chars = name.chars();
281    match (chars.next(), chars.next()) {
282        (Some(_), None) => Ok(Key::Character(name.into())),
283        _ => Err(ParseChordError::UnknownKey(name.to_owned())),
284    }
285}
286
287fn parse_modifier(name: &str) -> Result<Modifiers, ParseChordError> {
288    if name.is_empty() {
289        return Err(ParseChordError::MissingKey);
290    }
291    let modifiers = match name.to_ascii_lowercase().as_str() {
292        "ctrl" | "control" => Modifiers::CTRL,
293        "shift" => Modifiers::SHIFT,
294        "alt" | "option" | "opt" => Modifiers::ALT,
295        "cmd" | "command" | "super" | "meta" | "logo" | "win" => Modifiers::LOGO,
296        "mod" => Modifiers::COMMAND,
297        _ => return Err(ParseChordError::UnknownModifier(name.to_owned())),
298    };
299    Ok(modifiers)
300}
301
302impl fmt::Display for Chord {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        for (modifier, name) in [
305            (Modifiers::CTRL, "Ctrl"),
306            (Modifiers::ALT, "Alt"),
307            (Modifiers::SHIFT, "Shift"),
308            (Modifiers::LOGO, LOGO),
309        ] {
310            if self.modifiers.contains(modifier) {
311                write!(f, "{name}+")?;
312            }
313        }
314        match &self.key {
315            Key::Named(named) => match NAMED.iter().find(|(key, _)| key == named) {
316                Some((_, names)) => write!(f, "{}", names.first().copied().unwrap_or_default()),
317                None => write!(f, "{named:?}"),
318            },
319            Key::Character(text) => write!(f, "{}", text.to_uppercase()),
320            Key::Unidentified => write!(f, "Unidentified"),
321        }
322    }
323}
324
325/// A component action that can be bound to keys.
326pub trait Action: Copy + PartialEq + fmt::Debug + 'static {
327    /// Every action, in the order they are documented.
328    const ALL: &'static [Self];
329
330    /// The component's default shortcuts.
331    fn defaults() -> Keymap<Self>;
332
333    /// A short name, such as `"Next"`.
334    fn name(self) -> &'static str;
335
336    /// One sentence describing what the action does.
337    fn description(self) -> &'static str;
338}
339
340/// Chords bound to actions. Each chord triggers at most one action; an
341/// action may have several chords.
342#[derive(Debug, Clone, PartialEq)]
343pub struct Keymap<A> {
344    bindings: Vec<(Chord, A)>,
345}
346
347impl<A> Default for Keymap<A> {
348    fn default() -> Self {
349        Self {
350            bindings: Vec::new(),
351        }
352    }
353}
354
355impl<A> Keymap<A> {
356    /// An empty keymap.
357    pub fn new() -> Self {
358        Self::default()
359    }
360
361    /// Binds `chord` to `action`, replacing whatever the chord did before.
362    pub fn bind(mut self, chord: Chord, action: A) -> Self {
363        match self.bindings.iter_mut().find(|(bound, _)| *bound == chord) {
364            Some(binding) => binding.1 = action,
365            None => self.bindings.push((chord, action)),
366        }
367        self
368    }
369
370    /// Removes the binding for `chord`, if there is one.
371    pub fn unbind(mut self, chord: &Chord) -> Self {
372        self.bindings.retain(|(bound, _)| bound != chord);
373        self
374    }
375
376    /// Removes every binding.
377    pub fn clear(mut self) -> Self {
378        self.bindings.clear();
379        self
380    }
381
382    /// Every binding, in the order it was added.
383    pub fn bindings(&self) -> impl Iterator<Item = (&Chord, &A)> {
384        self.bindings.iter().map(|(chord, action)| (chord, action))
385    }
386
387    pub fn len(&self) -> usize {
388        self.bindings.len()
389    }
390
391    pub fn is_empty(&self) -> bool {
392        self.bindings.is_empty()
393    }
394}
395
396impl<A: Clone> Keymap<A> {
397    /// The action bound to a key press, if any.
398    pub fn resolve(&self, key: &Key, modifiers: Modifiers) -> Option<A> {
399        self.bindings
400            .iter()
401            .find(|(chord, _)| chord.matches(key, modifiers))
402            .map(|(_, action)| action.clone())
403    }
404
405    /// The action bound to a key press from [`subscription`], if any.
406    pub fn resolve_event(&self, event: &Event) -> Option<A> {
407        self.resolve(&event.key, event.modifiers)
408    }
409}
410
411impl<A: Clone + PartialEq> Keymap<A> {
412    /// The chords bound to `action`.
413    pub fn chords(&self, action: &A) -> Vec<&Chord> {
414        self.bindings
415            .iter()
416            .filter(|(_, bound)| bound == action)
417            .map(|(chord, _)| chord)
418            .collect()
419    }
420
421    /// Removes every chord bound to `action`.
422    pub fn unbind_action(mut self, action: &A) -> Self {
423        self.bindings.retain(|(_, bound)| bound != action);
424        self
425    }
426
427    /// Chords grouped by action, in the order each action first appears.
428    pub fn grouped(&self) -> Vec<(A, Vec<Chord>)> {
429        let mut groups: Vec<(A, Vec<Chord>)> = Vec::new();
430        for (chord, action) in &self.bindings {
431            match groups.iter_mut().find(|(bound, _)| bound == action) {
432                Some((_, chords)) => chords.push(chord.clone()),
433                None => groups.push((action.clone(), vec![chord.clone()])),
434            }
435        }
436        groups
437    }
438}
439
440impl<A: Action> Keymap<A> {
441    /// The default shortcuts of the component that owns `A`.
442    pub fn defaults() -> Self {
443        A::defaults()
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    fn chord(value: &str) -> Chord {
452        value.parse().unwrap()
453    }
454
455    fn character(value: &str) -> Key {
456        Key::Character(value.into())
457    }
458
459    #[test]
460    fn parses_named_keys_with_modifiers() {
461        let parsed = chord("Ctrl+Shift+Tab");
462        assert_eq!(parsed.key(), &Key::Named(Named::Tab));
463        assert_eq!(parsed.modifiers(), Modifiers::CTRL | Modifiers::SHIFT);
464        assert_eq!(chord("Escape"), Chord::named(Named::Escape));
465        assert_eq!(chord("ArrowRight"), Chord::named(Named::ArrowRight));
466    }
467
468    #[test]
469    fn parsing_ignores_case_and_accepts_aliases() {
470        assert_eq!(chord("ctrl+k"), chord("Ctrl+K"));
471        assert_eq!(chord("CONTROL + K"), chord("Ctrl+K"));
472        assert_eq!(chord("esc"), Chord::named(Named::Escape));
473        assert_eq!(chord("right"), Chord::named(Named::ArrowRight));
474        assert_eq!(chord("Option+PgDn"), Chord::named(Named::PageDown).alt());
475        assert_eq!(chord("Meta+S"), Chord::character('s').logo());
476    }
477
478    #[test]
479    fn mod_is_the_platform_command_key() {
480        let parsed = chord("Mod+K");
481        assert_eq!(parsed.modifiers(), Modifiers::COMMAND);
482        if cfg!(target_os = "macos") {
483            assert_eq!(parsed, chord("Cmd+K"));
484        } else {
485            assert_eq!(parsed, chord("Ctrl+K"));
486        }
487    }
488
489    #[test]
490    fn plus_can_be_the_key() {
491        assert_eq!(chord("+").key(), &character("+"));
492        let zoom = chord("Ctrl++");
493        assert_eq!(zoom.key(), &character("+"));
494        assert_eq!(zoom.modifiers(), Modifiers::CTRL);
495    }
496
497    #[test]
498    fn rejects_invalid_chords() {
499        assert_eq!("".parse::<Chord>(), Err(ParseChordError::Empty));
500        assert_eq!("  ".parse::<Chord>(), Err(ParseChordError::Empty));
501        assert_eq!("Ctrl+".parse::<Chord>(), Err(ParseChordError::MissingKey));
502        assert_eq!("Ctrl++K".parse::<Chord>(), Err(ParseChordError::MissingKey));
503        assert_eq!(
504            "Hyper+K".parse::<Chord>(),
505            Err(ParseChordError::UnknownModifier("Hyper".into()))
506        );
507        assert_eq!(
508            "Ctrl+Banana".parse::<Chord>(),
509            Err(ParseChordError::UnknownKey("Banana".into()))
510        );
511    }
512
513    #[test]
514    fn displays_in_canonical_form() {
515        assert_eq!(chord("shift+ctrl+tab").to_string(), "Ctrl+Shift+Tab");
516        assert_eq!(chord("ctrl+k").to_string(), "Ctrl+K");
517        assert_eq!(chord("esc").to_string(), "Escape");
518        assert_eq!(chord("alt+up").to_string(), "Alt+ArrowUp");
519        assert_eq!(chord("Ctrl++").to_string(), "Ctrl++");
520    }
521
522    #[test]
523    fn display_round_trips() {
524        for value in ["Ctrl+Alt+Shift+Cmd+F5", "Space", "Mod+/", "Shift+End", "+"] {
525            let parsed = chord(value);
526            assert_eq!(chord(&parsed.to_string()), parsed, "{value}");
527        }
528    }
529
530    #[test]
531    fn matches_exact_modifiers_and_any_case() {
532        let save = chord("Ctrl+S");
533        assert!(save.matches(&character("s"), Modifiers::CTRL));
534        assert!(save.matches(&character("S"), Modifiers::CTRL));
535        assert!(!save.matches(&character("s"), Modifiers::empty()));
536        assert!(!save.matches(&character("s"), Modifiers::CTRL | Modifiers::SHIFT));
537        assert!(!save.matches(&Key::Named(Named::Tab), Modifiers::CTRL));
538        assert!(chord("Tab").matches(&Key::Named(Named::Tab), Modifiers::empty()));
539    }
540
541    #[derive(Debug, Clone, Copy, PartialEq)]
542    enum Test {
543        Open,
544        Close,
545    }
546
547    fn keymap() -> Keymap<Test> {
548        Keymap::new()
549            .bind(chord("Ctrl+O"), Test::Open)
550            .bind(chord("Enter"), Test::Open)
551            .bind(chord("Escape"), Test::Close)
552    }
553
554    #[test]
555    fn resolves_bound_chords_only() {
556        let keymap = keymap();
557        assert_eq!(
558            keymap.resolve(&character("o"), Modifiers::CTRL),
559            Some(Test::Open)
560        );
561        assert_eq!(
562            keymap.resolve(&Key::Named(Named::Escape), Modifiers::empty()),
563            Some(Test::Close)
564        );
565        assert_eq!(keymap.resolve(&character("o"), Modifiers::empty()), None);
566        let event = Event {
567            key: Key::Named(Named::Enter),
568            modifiers: Modifiers::empty(),
569        };
570        assert_eq!(keymap.resolve_event(&event), Some(Test::Open));
571    }
572
573    #[test]
574    fn bind_replaces_an_existing_chord() {
575        let keymap = keymap().bind(chord("Enter"), Test::Close);
576        assert_eq!(keymap.len(), 3);
577        assert_eq!(
578            keymap.resolve(&Key::Named(Named::Enter), Modifiers::empty()),
579            Some(Test::Close)
580        );
581    }
582
583    #[test]
584    fn unbind_and_clear() {
585        let keymap = keymap().unbind(&chord("Escape"));
586        assert_eq!(
587            keymap.resolve(&Key::Named(Named::Escape), Modifiers::empty()),
588            None
589        );
590        assert_eq!(keymap.len(), 2);
591        let unchanged = keymap.clone().unbind(&chord("F1"));
592        assert_eq!(unchanged, keymap);
593        assert!(keymap.clear().is_empty());
594    }
595
596    #[test]
597    fn chords_grouped_and_unbound_by_action() {
598        let keymap = keymap();
599        assert_eq!(keymap.chords(&Test::Open).len(), 2);
600        let grouped = keymap.grouped();
601        assert_eq!(grouped.len(), 2);
602        assert_eq!(grouped[0].0, Test::Open);
603        assert_eq!(grouped[0].1, vec![chord("Ctrl+O"), chord("Enter")]);
604        assert_eq!(grouped[1], (Test::Close, vec![chord("Escape")]));
605        let closed = keymap.unbind_action(&Test::Open);
606        assert_eq!(closed.len(), 1);
607    }
608}