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//! # #[cfg(feature = "tabs")]
12//! # mod example {
13//! use iced::Subscription;
14//! use iced::keyboard::key::Named;
15//! use iced_cube::keys::{self, Chord, Keymap};
16//! use iced_cube::navigation::tabs::{self, Action};
17//!
18//! #[derive(Debug, Clone)]
19//! enum Message {
20//!     Key(keys::Event),
21//!     Tabs(tabs::Event<u8>),
22//! }
23//!
24//! struct App {
25//!     tabs: tabs::State<u8>,
26//!     keymap: Keymap<Action>,
27//! }
28//!
29//! impl App {
30//!     fn new() -> Self {
31//!         let keymap = tabs::default_keymap()
32//!             .unbind(&Chord::named(Named::ArrowLeft))
33//!             .unbind(&Chord::named(Named::ArrowRight));
34//!         Self { tabs: tabs::State::new([tabs::tab(1, "One"), tabs::tab(2, "Two")]), keymap }
35//!     }
36//!
37//!     fn update(&mut self, message: Message) {
38//!         let event = match message {
39//!             Message::Key(key) => self
40//!                 .keymap
41//!                 .resolve_event(&key)
42//!                 .and_then(|action| action.event(&self.tabs)),
43//!             Message::Tabs(event) => Some(event),
44//!         };
45//!         if let Some(event) = event {
46//!             let _ = self.tabs.update(event);
47//!         }
48//!     }
49//!
50//!     fn subscription(&self) -> Subscription<Message> {
51//!         keys::subscription().map(Message::Key)
52//!     }
53//! }
54//! # }
55//! ```
56
57use std::fmt;
58use std::hash::Hash;
59use std::str::FromStr;
60
61use iced::Subscription;
62use iced::advanced::subscription::{self as recipe, EventStream, Hasher, Recipe};
63use iced::event::Status;
64use iced::futures::{StreamExt, future};
65use iced::keyboard::{self, Key, Modifiers, key::Named};
66use iced::{mouse, window};
67
68/// A key press delivered by [`subscription`].
69///
70/// A modifier key on its own, such as Alt, arrives as an event with no
71/// modifiers when it is released, and only if nothing else was pressed
72/// while it was held. So Alt followed by Up is one `Alt+ArrowUp` press,
73/// while Alt pressed and released alone is a lone `Alt`.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Event {
76    pub key: Key,
77    pub modifiers: Modifiers,
78}
79
80/// Key presses that no widget captured, such as a focused text input,
81/// plus lone modifier keys as described on [`Event`].
82pub fn subscription() -> Subscription<Event> {
83    recipe::from_recipe(Listen)
84}
85
86struct Listen;
87
88#[cfg(not(target_arch = "wasm32"))]
89type Stream = iced::futures::stream::BoxStream<'static, Event>;
90#[cfg(target_arch = "wasm32")]
91type Stream = iced::futures::stream::LocalBoxStream<'static, Event>;
92
93impl Recipe for Listen {
94    type Output = Event;
95
96    fn hash(&self, state: &mut Hasher) {
97        std::any::TypeId::of::<Self>().hash(state);
98    }
99
100    fn stream(self: Box<Self>, input: EventStream) -> Stream {
101        let mut tracker = Tracker::default();
102        Box::pin(input.filter_map(move |event| {
103            let recipe::Event::Interaction { event, status, .. } = event else {
104                return future::ready(None);
105            };
106            future::ready(tracker.update(&event, status == Status::Captured))
107        }))
108    }
109}
110
111/// Turns raw iced events into the [`Event`]s that [`subscription`]
112/// delivers. Apps that listen to window events themselves can feed it
113/// every event, captured or not, to get the same behaviour.
114#[derive(Debug, Clone, Default, PartialEq, Eq)]
115pub struct Tracker {
116    held: Option<Named>,
117}
118
119impl Tracker {
120    pub fn new() -> Self {
121        Self::default()
122    }
123
124    /// The key event `event` delivers, if any. `captured` is whether a
125    /// widget already used it; captured events are not delivered, but a
126    /// captured key press still stops a held modifier counting as lone.
127    pub fn update(&mut self, event: &iced::Event, captured: bool) -> Option<Event> {
128        match event {
129            iced::Event::Keyboard(keyboard::Event::KeyPressed {
130                key,
131                modifiers,
132                repeat,
133                ..
134            }) => self.pressed(key, *modifiers, *repeat, captured),
135            iced::Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
136                self.released(key, captured)
137            }
138            iced::Event::Mouse(
139                mouse::Event::ButtonPressed(_) | mouse::Event::WheelScrolled { .. },
140            )
141            | iced::Event::Window(window::Event::Unfocused) => {
142                self.held = None;
143                None
144            }
145            _ => None,
146        }
147    }
148
149    fn pressed(
150        &mut self,
151        key: &Key,
152        modifiers: Modifiers,
153        repeat: bool,
154        captured: bool,
155    ) -> Option<Event> {
156        let Some(modifier) = modifier_key(key) else {
157            self.held = None;
158            return (!captured).then(|| Event {
159                key: key.clone(),
160                modifiers,
161            });
162        };
163        if repeat && self.held == Some(modifier) {
164            return None;
165        }
166        let others = modifiers.difference(modifier_flag(modifier));
167        self.held = (self.held.is_none() && others.is_empty()).then_some(modifier);
168        None
169    }
170
171    fn released(&mut self, key: &Key, captured: bool) -> Option<Event> {
172        let modifier = modifier_key(key)?;
173        let lone = self.held.take() == Some(modifier);
174        (lone && !captured).then(|| Event {
175            key: Key::Named(modifier),
176            modifiers: Modifiers::empty(),
177        })
178    }
179}
180
181fn modifier_key(key: &Key) -> Option<Named> {
182    match key {
183        Key::Named(
184            named @ (Named::Alt
185            | Named::AltGraph
186            | Named::Control
187            | Named::Shift
188            | Named::Super
189            | Named::Meta),
190        ) => Some(*named),
191        _ => None,
192    }
193}
194
195fn modifier_flag(key: Named) -> Modifiers {
196    match key {
197        Named::Alt | Named::AltGraph => Modifiers::ALT,
198        Named::Control => Modifiers::CTRL,
199        Named::Shift => Modifiers::SHIFT,
200        Named::Super | Named::Meta => Modifiers::LOGO,
201        _ => Modifiers::empty(),
202    }
203}
204
205/// Named keys a [`Chord`] understands: the canonical name first, then aliases.
206const NAMED: &[(Named, &[&str])] = &[
207    (Named::Enter, &["Enter", "Return"]),
208    (Named::Tab, &["Tab"]),
209    (Named::Space, &["Space", "Spacebar"]),
210    (Named::Escape, &["Escape", "Esc"]),
211    (Named::Backspace, &["Backspace"]),
212    (Named::Delete, &["Delete", "Del"]),
213    (Named::Insert, &["Insert", "Ins"]),
214    (Named::ArrowUp, &["ArrowUp", "Up"]),
215    (Named::ArrowDown, &["ArrowDown", "Down"]),
216    (Named::ArrowLeft, &["ArrowLeft", "Left"]),
217    (Named::ArrowRight, &["ArrowRight", "Right"]),
218    (Named::Home, &["Home"]),
219    (Named::End, &["End"]),
220    (Named::PageUp, &["PageUp", "PgUp"]),
221    (Named::PageDown, &["PageDown", "PgDn"]),
222    (Named::ContextMenu, &["Menu", "ContextMenu", "Apps"]),
223    (Named::F1, &["F1"]),
224    (Named::F2, &["F2"]),
225    (Named::F3, &["F3"]),
226    (Named::F4, &["F4"]),
227    (Named::F5, &["F5"]),
228    (Named::F6, &["F6"]),
229    (Named::F7, &["F7"]),
230    (Named::F8, &["F8"]),
231    (Named::F9, &["F9"]),
232    (Named::F10, &["F10"]),
233    (Named::F11, &["F11"]),
234    (Named::F12, &["F12"]),
235];
236
237/// Display name of the logo key on this platform.
238const LOGO: &str = if cfg!(target_os = "macos") {
239    "Cmd"
240} else {
241    "Super"
242};
243
244/// A key plus the modifiers held with it, such as `Ctrl+Shift+Tab`.
245///
246/// Parse one from a string: modifiers and the key are joined by `+`, and
247/// case does not matter. Modifiers are `Ctrl`, `Shift`, `Alt` (or
248/// `Option`), `Cmd` (or `Super`, `Meta`) and `Mod`, which is Cmd on macOS
249/// and Ctrl elsewhere. Keys are a single character (`K`, `/`, `+`) or a
250/// name: `Enter`, `Tab`, `Space`, `Escape`, `Backspace`, `Delete`,
251/// `Insert`, `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight`, `Home`,
252/// `End`, `PageUp`, `PageDown`, `Menu` (the context menu key) and `F1` to
253/// `F12`.
254///
255/// ```
256/// use iced_cube::keys::Chord;
257///
258/// let chord: Chord = "ctrl+shift+tab".parse().unwrap();
259/// assert_eq!(chord.to_string(), "Ctrl+Shift+Tab");
260/// ```
261#[derive(Debug, Clone, PartialEq, Eq, Hash)]
262pub struct Chord {
263    key: Key,
264    modifiers: Modifiers,
265}
266
267impl Chord {
268    /// A chord for `key`. Characters are stored in lower case.
269    pub fn new(key: Key, modifiers: Modifiers) -> Self {
270        Self {
271            key: normalise(key),
272            modifiers,
273        }
274    }
275
276    /// A named key with no modifiers.
277    pub fn named(key: Named) -> Self {
278        Self::new(Key::Named(key), Modifiers::empty())
279    }
280
281    /// A character key with no modifiers.
282    pub fn character(key: char) -> Self {
283        Self::new(Key::Character(key.to_string().into()), Modifiers::empty())
284    }
285
286    pub fn ctrl(self) -> Self {
287        self.with(Modifiers::CTRL)
288    }
289
290    pub fn shift(self) -> Self {
291        self.with(Modifiers::SHIFT)
292    }
293
294    pub fn alt(self) -> Self {
295        self.with(Modifiers::ALT)
296    }
297
298    /// Adds the logo key: Cmd on macOS, Super or the Windows key elsewhere.
299    pub fn logo(self) -> Self {
300        self.with(Modifiers::LOGO)
301    }
302
303    /// Adds Cmd on macOS and Ctrl elsewhere.
304    pub fn command(self) -> Self {
305        self.with(Modifiers::COMMAND)
306    }
307
308    fn with(mut self, modifiers: Modifiers) -> Self {
309        self.modifiers |= modifiers;
310        self
311    }
312
313    pub fn key(&self) -> &Key {
314        &self.key
315    }
316
317    pub fn modifiers(&self) -> Modifiers {
318        self.modifiers
319    }
320
321    /// Whether a key press with exactly these modifiers triggers the chord.
322    pub fn matches(&self, key: &Key, modifiers: Modifiers) -> bool {
323        self.modifiers == modifiers && same_key(&self.key, key)
324    }
325}
326
327fn normalise(key: Key) -> Key {
328    match key {
329        Key::Character(text) => Key::Character(text.to_lowercase().into()),
330        key => key,
331    }
332}
333
334fn same_key(chord: &Key, pressed: &Key) -> bool {
335    match (chord, pressed) {
336        (Key::Character(a), Key::Character(b)) => a.as_str() == b.to_lowercase(),
337        (a, b) => a == b,
338    }
339}
340
341/// Why a string is not a valid [`Chord`].
342#[derive(Debug, Clone, PartialEq, Eq)]
343pub enum ParseChordError {
344    Empty,
345    /// A `+` with nothing on one side, such as `Ctrl+`.
346    MissingKey,
347    UnknownModifier(String),
348    UnknownKey(String),
349}
350
351impl fmt::Display for ParseChordError {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        match self {
354            Self::Empty => write!(f, "empty key chord"),
355            Self::MissingKey => write!(f, "key chord has an empty part"),
356            Self::UnknownModifier(name) => write!(f, "unknown modifier \"{name}\""),
357            Self::UnknownKey(name) => write!(f, "unknown key \"{name}\""),
358        }
359    }
360}
361
362impl std::error::Error for ParseChordError {}
363
364impl FromStr for Chord {
365    type Err = ParseChordError;
366
367    fn from_str(value: &str) -> Result<Self, Self::Err> {
368        let value = value.trim();
369        if value.is_empty() {
370            return Err(ParseChordError::Empty);
371        }
372
373        // A trailing "++" means the key itself is "+".
374        let (modifiers, key) = match value.strip_suffix("++") {
375            Some(rest) => (Some(rest), "+"),
376            None if value == "+" => (None, "+"),
377            None => match value.rsplit_once('+') {
378                Some((rest, key)) => (Some(rest), key),
379                None => (None, value),
380            },
381        };
382
383        let mut chord = Chord::new(parse_key(key.trim())?, Modifiers::empty());
384        let Some(modifiers) = modifiers else {
385            return Ok(chord);
386        };
387        for name in modifiers.split('+') {
388            chord = chord.with(parse_modifier(name.trim())?);
389        }
390        Ok(chord)
391    }
392}
393
394impl TryFrom<&str> for Chord {
395    type Error = ParseChordError;
396
397    fn try_from(value: &str) -> Result<Self, Self::Error> {
398        value.parse()
399    }
400}
401
402fn parse_key(name: &str) -> Result<Key, ParseChordError> {
403    if name.is_empty() {
404        return Err(ParseChordError::MissingKey);
405    }
406    if let Some((named, _)) = NAMED
407        .iter()
408        .find(|(_, names)| names.iter().any(|alias| alias.eq_ignore_ascii_case(name)))
409    {
410        return Ok(Key::Named(*named));
411    }
412    let mut chars = name.chars();
413    match (chars.next(), chars.next()) {
414        (Some(_), None) => Ok(Key::Character(name.into())),
415        _ => Err(ParseChordError::UnknownKey(name.to_owned())),
416    }
417}
418
419fn parse_modifier(name: &str) -> Result<Modifiers, ParseChordError> {
420    if name.is_empty() {
421        return Err(ParseChordError::MissingKey);
422    }
423    let modifiers = match name.to_ascii_lowercase().as_str() {
424        "ctrl" | "control" => Modifiers::CTRL,
425        "shift" => Modifiers::SHIFT,
426        "alt" | "option" | "opt" => Modifiers::ALT,
427        "cmd" | "command" | "super" | "meta" | "logo" | "win" => Modifiers::LOGO,
428        "mod" => Modifiers::COMMAND,
429        _ => return Err(ParseChordError::UnknownModifier(name.to_owned())),
430    };
431    Ok(modifiers)
432}
433
434impl fmt::Display for Chord {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        for (modifier, name) in [
437            (Modifiers::CTRL, "Ctrl"),
438            (Modifiers::ALT, "Alt"),
439            (Modifiers::SHIFT, "Shift"),
440            (Modifiers::LOGO, LOGO),
441        ] {
442            if self.modifiers.contains(modifier) {
443                write!(f, "{name}+")?;
444            }
445        }
446        match &self.key {
447            Key::Named(named) => match NAMED.iter().find(|(key, _)| key == named) {
448                Some((_, names)) => write!(f, "{}", names.first().copied().unwrap_or_default()),
449                None => write!(f, "{named:?}"),
450            },
451            Key::Character(text) => write!(f, "{}", text.to_uppercase()),
452            Key::Unidentified => write!(f, "Unidentified"),
453        }
454    }
455}
456
457/// A component action that can be bound to keys.
458pub trait Action: Copy + PartialEq + fmt::Debug + 'static {
459    /// Every action, in the order they are documented.
460    const ALL: &'static [Self];
461
462    /// The component's default shortcuts.
463    fn defaults() -> Keymap<Self>;
464
465    /// A short name, such as `"Next"`.
466    fn name(self) -> &'static str;
467
468    /// One sentence describing what the action does.
469    fn description(self) -> &'static str;
470}
471
472/// Chords bound to actions. Each chord triggers at most one action; an
473/// action may have several chords.
474#[derive(Debug, Clone, PartialEq)]
475pub struct Keymap<A> {
476    bindings: Vec<(Chord, A)>,
477}
478
479impl<A> Default for Keymap<A> {
480    fn default() -> Self {
481        Self {
482            bindings: Vec::new(),
483        }
484    }
485}
486
487impl<A> Keymap<A> {
488    /// An empty keymap.
489    pub fn new() -> Self {
490        Self::default()
491    }
492
493    /// Binds `chord` to `action`, replacing whatever the chord did before.
494    pub fn bind(mut self, chord: Chord, action: A) -> Self {
495        match self.bindings.iter_mut().find(|(bound, _)| *bound == chord) {
496            Some(binding) => binding.1 = action,
497            None => self.bindings.push((chord, action)),
498        }
499        self
500    }
501
502    /// Removes the binding for `chord`, if there is one.
503    pub fn unbind(mut self, chord: &Chord) -> Self {
504        self.bindings.retain(|(bound, _)| bound != chord);
505        self
506    }
507
508    /// Removes every binding.
509    pub fn clear(mut self) -> Self {
510        self.bindings.clear();
511        self
512    }
513
514    /// Every binding, in the order it was added.
515    pub fn bindings(&self) -> impl Iterator<Item = (&Chord, &A)> {
516        self.bindings.iter().map(|(chord, action)| (chord, action))
517    }
518
519    pub fn len(&self) -> usize {
520        self.bindings.len()
521    }
522
523    pub fn is_empty(&self) -> bool {
524        self.bindings.is_empty()
525    }
526}
527
528impl<A: Clone> Keymap<A> {
529    /// The action bound to a key press, if any.
530    pub fn resolve(&self, key: &Key, modifiers: Modifiers) -> Option<A> {
531        self.bindings
532            .iter()
533            .find(|(chord, _)| chord.matches(key, modifiers))
534            .map(|(_, action)| action.clone())
535    }
536
537    /// The action bound to a key press from [`subscription`], if any.
538    pub fn resolve_event(&self, event: &Event) -> Option<A> {
539        self.resolve(&event.key, event.modifiers)
540    }
541}
542
543impl<A: Clone + PartialEq> Keymap<A> {
544    /// The chords bound to `action`.
545    pub fn chords(&self, action: &A) -> Vec<&Chord> {
546        self.bindings
547            .iter()
548            .filter(|(_, bound)| bound == action)
549            .map(|(chord, _)| chord)
550            .collect()
551    }
552
553    /// Removes every chord bound to `action`.
554    pub fn unbind_action(mut self, action: &A) -> Self {
555        self.bindings.retain(|(_, bound)| bound != action);
556        self
557    }
558
559    /// Chords grouped by action, in the order each action first appears.
560    pub fn grouped(&self) -> Vec<(A, Vec<Chord>)> {
561        let mut groups: Vec<(A, Vec<Chord>)> = Vec::new();
562        for (chord, action) in &self.bindings {
563            match groups.iter_mut().find(|(bound, _)| bound == action) {
564                Some((_, chords)) => chords.push(chord.clone()),
565                None => groups.push((action.clone(), vec![chord.clone()])),
566            }
567        }
568        groups
569    }
570}
571
572impl<A: Action> Keymap<A> {
573    /// The default shortcuts of the component that owns `A`.
574    pub fn defaults() -> Self {
575        A::defaults()
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    fn chord(value: &str) -> Chord {
584        value.parse().unwrap()
585    }
586
587    fn character(value: &str) -> Key {
588        Key::Character(value.into())
589    }
590
591    #[test]
592    fn parses_named_keys_with_modifiers() {
593        let parsed = chord("Ctrl+Shift+Tab");
594        assert_eq!(parsed.key(), &Key::Named(Named::Tab));
595        assert_eq!(parsed.modifiers(), Modifiers::CTRL | Modifiers::SHIFT);
596        assert_eq!(chord("Escape"), Chord::named(Named::Escape));
597        assert_eq!(chord("ArrowRight"), Chord::named(Named::ArrowRight));
598    }
599
600    #[test]
601    fn parsing_ignores_case_and_accepts_aliases() {
602        assert_eq!(chord("ctrl+k"), chord("Ctrl+K"));
603        assert_eq!(chord("CONTROL + K"), chord("Ctrl+K"));
604        assert_eq!(chord("esc"), Chord::named(Named::Escape));
605        assert_eq!(chord("right"), Chord::named(Named::ArrowRight));
606        assert_eq!(chord("Option+PgDn"), Chord::named(Named::PageDown).alt());
607        assert_eq!(chord("Meta+S"), Chord::character('s').logo());
608        assert_eq!(chord("ContextMenu"), Chord::named(Named::ContextMenu));
609        assert_eq!(chord("Menu").to_string(), "Menu");
610    }
611
612    #[test]
613    fn mod_is_the_platform_command_key() {
614        let parsed = chord("Mod+K");
615        assert_eq!(parsed.modifiers(), Modifiers::COMMAND);
616        if cfg!(target_os = "macos") {
617            assert_eq!(parsed, chord("Cmd+K"));
618        } else {
619            assert_eq!(parsed, chord("Ctrl+K"));
620        }
621    }
622
623    #[test]
624    fn plus_can_be_the_key() {
625        assert_eq!(chord("+").key(), &character("+"));
626        let zoom = chord("Ctrl++");
627        assert_eq!(zoom.key(), &character("+"));
628        assert_eq!(zoom.modifiers(), Modifiers::CTRL);
629    }
630
631    #[test]
632    fn rejects_invalid_chords() {
633        assert_eq!("".parse::<Chord>(), Err(ParseChordError::Empty));
634        assert_eq!("  ".parse::<Chord>(), Err(ParseChordError::Empty));
635        assert_eq!("Ctrl+".parse::<Chord>(), Err(ParseChordError::MissingKey));
636        assert_eq!("Ctrl++K".parse::<Chord>(), Err(ParseChordError::MissingKey));
637        assert_eq!(
638            "Hyper+K".parse::<Chord>(),
639            Err(ParseChordError::UnknownModifier("Hyper".into()))
640        );
641        assert_eq!(
642            "Ctrl+Banana".parse::<Chord>(),
643            Err(ParseChordError::UnknownKey("Banana".into()))
644        );
645    }
646
647    #[test]
648    fn displays_in_canonical_form() {
649        assert_eq!(chord("shift+ctrl+tab").to_string(), "Ctrl+Shift+Tab");
650        assert_eq!(chord("ctrl+k").to_string(), "Ctrl+K");
651        assert_eq!(chord("esc").to_string(), "Escape");
652        assert_eq!(chord("alt+up").to_string(), "Alt+ArrowUp");
653        assert_eq!(chord("Ctrl++").to_string(), "Ctrl++");
654    }
655
656    #[test]
657    fn display_round_trips() {
658        for value in ["Ctrl+Alt+Shift+Cmd+F5", "Space", "Mod+/", "Shift+End", "+"] {
659            let parsed = chord(value);
660            assert_eq!(chord(&parsed.to_string()), parsed, "{value}");
661        }
662    }
663
664    #[test]
665    fn matches_exact_modifiers_and_any_case() {
666        let save = chord("Ctrl+S");
667        assert!(save.matches(&character("s"), Modifiers::CTRL));
668        assert!(save.matches(&character("S"), Modifiers::CTRL));
669        assert!(!save.matches(&character("s"), Modifiers::empty()));
670        assert!(!save.matches(&character("s"), Modifiers::CTRL | Modifiers::SHIFT));
671        assert!(!save.matches(&Key::Named(Named::Tab), Modifiers::CTRL));
672        assert!(chord("Tab").matches(&Key::Named(Named::Tab), Modifiers::empty()));
673    }
674
675    #[derive(Debug, Clone, Copy, PartialEq)]
676    enum Test {
677        Open,
678        Close,
679    }
680
681    fn keymap() -> Keymap<Test> {
682        Keymap::new()
683            .bind(chord("Ctrl+O"), Test::Open)
684            .bind(chord("Enter"), Test::Open)
685            .bind(chord("Escape"), Test::Close)
686    }
687
688    #[test]
689    fn resolves_bound_chords_only() {
690        let keymap = keymap();
691        assert_eq!(
692            keymap.resolve(&character("o"), Modifiers::CTRL),
693            Some(Test::Open)
694        );
695        assert_eq!(
696            keymap.resolve(&Key::Named(Named::Escape), Modifiers::empty()),
697            Some(Test::Close)
698        );
699        assert_eq!(keymap.resolve(&character("o"), Modifiers::empty()), None);
700        let event = Event {
701            key: Key::Named(Named::Enter),
702            modifiers: Modifiers::empty(),
703        };
704        assert_eq!(keymap.resolve_event(&event), Some(Test::Open));
705    }
706
707    #[test]
708    fn bind_replaces_an_existing_chord() {
709        let keymap = keymap().bind(chord("Enter"), Test::Close);
710        assert_eq!(keymap.len(), 3);
711        assert_eq!(
712            keymap.resolve(&Key::Named(Named::Enter), Modifiers::empty()),
713            Some(Test::Close)
714        );
715    }
716
717    #[test]
718    fn unbind_and_clear() {
719        let keymap = keymap().unbind(&chord("Escape"));
720        assert_eq!(
721            keymap.resolve(&Key::Named(Named::Escape), Modifiers::empty()),
722            None
723        );
724        assert_eq!(keymap.len(), 2);
725        let unchanged = keymap.clone().unbind(&chord("F1"));
726        assert_eq!(unchanged, keymap);
727        assert!(keymap.clear().is_empty());
728    }
729
730    #[test]
731    fn chords_grouped_and_unbound_by_action() {
732        let keymap = keymap();
733        assert_eq!(keymap.chords(&Test::Open).len(), 2);
734        let grouped = keymap.grouped();
735        assert_eq!(grouped.len(), 2);
736        assert_eq!(grouped[0].0, Test::Open);
737        assert_eq!(grouped[0].1, vec![chord("Ctrl+O"), chord("Enter")]);
738        assert_eq!(grouped[1], (Test::Close, vec![chord("Escape")]));
739        let closed = keymap.unbind_action(&Test::Open);
740        assert_eq!(closed.len(), 1);
741    }
742
743    fn press(key: Key, modifiers: Modifiers) -> iced::Event {
744        press_repeat(key, modifiers, false)
745    }
746
747    fn press_repeat(key: Key, modifiers: Modifiers, repeat: bool) -> iced::Event {
748        iced::Event::Keyboard(keyboard::Event::KeyPressed {
749            modified_key: key.clone(),
750            key,
751            physical_key: keyboard::key::Physical::Unidentified(
752                keyboard::key::NativeCode::Unidentified,
753            ),
754            location: keyboard::Location::Standard,
755            modifiers,
756            text: None,
757            repeat,
758        })
759    }
760
761    fn release(key: Key, modifiers: Modifiers) -> iced::Event {
762        iced::Event::Keyboard(keyboard::Event::KeyReleased {
763            modified_key: key.clone(),
764            key,
765            physical_key: keyboard::key::Physical::Unidentified(
766                keyboard::key::NativeCode::Unidentified,
767            ),
768            location: keyboard::Location::Standard,
769            modifiers,
770        })
771    }
772
773    fn feed(events: &[iced::Event]) -> Vec<Event> {
774        let mut tracker = Tracker::new();
775        events
776            .iter()
777            .filter_map(|event| tracker.update(event, false))
778            .collect()
779    }
780
781    fn alt() -> Key {
782        Key::Named(Named::Alt)
783    }
784
785    fn lone(named: Named) -> Event {
786        Event {
787            key: Key::Named(named),
788            modifiers: Modifiers::empty(),
789        }
790    }
791
792    #[test]
793    fn a_lone_modifier_arrives_on_release() {
794        let events = feed(&[
795            press(alt(), Modifiers::ALT),
796            release(alt(), Modifiers::empty()),
797        ]);
798        assert_eq!(events, vec![lone(Named::Alt)]);
799    }
800
801    #[test]
802    fn a_modifier_used_in_a_chord_is_not_lone() {
803        let up = Key::Named(Named::ArrowUp);
804        let events = feed(&[
805            press(alt(), Modifiers::ALT),
806            press(up.clone(), Modifiers::ALT),
807            release(up.clone(), Modifiers::ALT),
808            release(alt(), Modifiers::empty()),
809        ]);
810        assert_eq!(
811            events,
812            vec![Event {
813                key: up,
814                modifiers: Modifiers::ALT
815            }]
816        );
817    }
818
819    #[test]
820    fn key_repeat_keeps_a_modifier_lone() {
821        let events = feed(&[
822            press(alt(), Modifiers::ALT),
823            press_repeat(alt(), Modifiers::ALT, true),
824            release(alt(), Modifiers::empty()),
825        ]);
826        assert_eq!(events, vec![lone(Named::Alt)]);
827    }
828
829    #[test]
830    fn two_modifiers_together_are_not_lone() {
831        let ctrl = Key::Named(Named::Control);
832        let events = feed(&[
833            press(ctrl.clone(), Modifiers::CTRL),
834            press(alt(), Modifiers::CTRL | Modifiers::ALT),
835            release(alt(), Modifiers::CTRL),
836            release(ctrl, Modifiers::empty()),
837        ]);
838        assert!(events.is_empty());
839    }
840
841    #[test]
842    fn a_click_while_held_stops_a_modifier_being_lone() {
843        let events = feed(&[
844            press(alt(), Modifiers::ALT),
845            iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
846            release(alt(), Modifiers::empty()),
847        ]);
848        assert!(events.is_empty());
849    }
850
851    #[test]
852    fn captured_keys_are_not_delivered_but_still_break_a_lone_modifier() {
853        let mut tracker = Tracker::new();
854        let up = Key::Named(Named::ArrowUp);
855        assert_eq!(tracker.update(&press(alt(), Modifiers::ALT), false), None);
856        assert_eq!(tracker.update(&press(up, Modifiers::ALT), true), None);
857        assert_eq!(
858            tracker.update(&release(alt(), Modifiers::empty()), false),
859            None
860        );
861    }
862
863    #[test]
864    fn ordinary_presses_pass_through_unchanged() {
865        let events = feed(&[press(character("k"), Modifiers::CTRL)]);
866        assert_eq!(
867            events,
868            vec![Event {
869                key: character("k"),
870                modifiers: Modifiers::CTRL
871            }]
872        );
873    }
874}