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