Skip to main content

kbd_iced/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! Iced key event conversions for `kbd`.
4//!
5//! This crate converts iced's keyboard events into `kbd`'s unified types
6//! so that GUI key events (from iced) and global hotkey events (from
7//! [`kbd-global`](https://docs.rs/kbd-global)) can feed into the same
8//! [`Dispatcher`](kbd::dispatcher::Dispatcher). This is useful in iced
9//! apps that want both in-window shortcuts and system-wide hotkeys
10//! handled through a single hotkey registry.
11//!
12//! iced defines its own W3C-derived key types: [`key::Code`] for physical
13//! key positions and [`key::Physical`] wrapping `Code` with an unidentified
14//! fallback. iced also has a logical key type for character/named key
15//! identity, but this crate only converts physical keys — they are
16//! layout-independent and match `kbd`'s model.
17//!
18//! # Extension traits
19//!
20//! - [`IcedKeyExt`] — converts an iced [`key::Code`] or [`key::Physical`]
21//!   to a [`kbd::key::Key`].
22//! - [`IcedModifiersExt`] — converts iced [`Modifiers`] to a
23//!   `Vec<Modifier>`.
24//! - [`IcedEventExt`] — converts an iced keyboard [`Event`] to a
25//!   [`kbd::hotkey::Hotkey`].
26//!
27//! # Key mapping
28//!
29//! | iced | kbd | Notes |
30//! |---|---|---|
31//! | `Code::KeyA` – `Code::KeyZ` | [`Key::A`] – [`Key::Z`] | Letters |
32//! | `Code::Digit0` – `Code::Digit9` | [`Key::DIGIT0`] – [`Key::DIGIT9`] | Digits |
33//! | `Code::F1` – `Code::F35` | [`Key::F1`] – [`Key::F35`] | Function keys |
34//! | `Code::Numpad0` – `Code::Numpad9` | [`Key::NUMPAD0`] – [`Key::NUMPAD9`] | Numpad |
35//! | `Code::Enter`, `Code::Escape`, … | [`Key::ENTER`], [`Key::ESCAPE`], … | Navigation / editing |
36//! | `Code::ControlLeft`, … | [`Key::CONTROL_LEFT`], … | Modifier keys as triggers |
37//! | `Code::SuperLeft` / `Code::Meta` | [`Key::META_LEFT`] | iced's Super = kbd's Meta |
38//! | `Code::MediaPlayPause`, … | [`Key::MEDIA_PLAY_PAUSE`], … | Media keys |
39//! | `Code::BrowserBack`, … | [`Key::BROWSER_BACK`], … | Browser keys |
40//! | `Code::Convert`, `Code::Lang1`, … | [`Key::CONVERT`], [`Key::LANG1`], … | CJK / international |
41//! | `Physical::Unidentified(_)` | `None` | No mapping possible |
42//!
43//! # Modifier mapping
44//!
45//! | iced | kbd |
46//! |---|---|
47//! | `CTRL` | [`Modifier::Ctrl`] |
48//! | `SHIFT` | [`Modifier::Shift`] |
49//! | `ALT` | [`Modifier::Alt`] |
50//! | `LOGO` | [`Modifier::Super`] |
51//!
52//! # Usage
53//!
54//! ```
55//! use iced_core::keyboard::{key::Code, Modifiers};
56//! use kbd::prelude::*;
57//! use kbd_iced::{IcedKeyExt, IcedModifiersExt};
58//!
59//! // Code conversion
60//! let key = Code::KeyA.to_key();
61//! assert_eq!(key, Some(Key::A));
62//!
63//! // Modifier conversion
64//! let mods = Modifiers::CTRL.to_modifiers();
65//! assert_eq!(mods, vec![Modifier::Ctrl]);
66//! ```
67
68use iced_core::keyboard::Event;
69use iced_core::keyboard::Modifiers;
70use iced_core::keyboard::key;
71use kbd::hotkey::Hotkey;
72use kbd::hotkey::Modifier;
73use kbd::key::Key;
74
75mod private {
76    pub trait Sealed {}
77    impl Sealed for iced_core::keyboard::key::Code {}
78    impl Sealed for iced_core::keyboard::key::Physical {}
79    impl Sealed for iced_core::keyboard::Modifiers {}
80    impl Sealed for iced_core::keyboard::Event {}
81}
82
83/// Convert an iced physical key type to a `kbd` [`Key`].
84///
85/// Returns `None` for keys that have no `kbd` equivalent (e.g.,
86/// `Unidentified`, keys beyond F24, international input keys).
87///
88/// This trait is sealed and cannot be implemented outside this crate.
89pub trait IcedKeyExt: private::Sealed {
90    /// Convert this iced key to a `kbd` [`Key`], or `None` if unmappable.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use iced_core::keyboard::key;
96    /// use kbd::prelude::*;
97    /// use kbd_iced::IcedKeyExt;
98    ///
99    /// assert_eq!(key::Code::KeyA.to_key(), Some(Key::A));
100    /// assert_eq!(key::Code::F5.to_key(), Some(Key::F5));
101    ///
102    /// let physical = key::Physical::Code(key::Code::Enter);
103    /// assert_eq!(physical.to_key(), Some(Key::ENTER));
104    /// ```
105    #[must_use]
106    fn to_key(&self) -> Option<Key>;
107}
108
109impl IcedKeyExt for key::Code {
110    #[allow(clippy::too_many_lines)]
111    fn to_key(&self) -> Option<Key> {
112        match self {
113            // Letters
114            key::Code::KeyA => Some(Key::A),
115            key::Code::KeyB => Some(Key::B),
116            key::Code::KeyC => Some(Key::C),
117            key::Code::KeyD => Some(Key::D),
118            key::Code::KeyE => Some(Key::E),
119            key::Code::KeyF => Some(Key::F),
120            key::Code::KeyG => Some(Key::G),
121            key::Code::KeyH => Some(Key::H),
122            key::Code::KeyI => Some(Key::I),
123            key::Code::KeyJ => Some(Key::J),
124            key::Code::KeyK => Some(Key::K),
125            key::Code::KeyL => Some(Key::L),
126            key::Code::KeyM => Some(Key::M),
127            key::Code::KeyN => Some(Key::N),
128            key::Code::KeyO => Some(Key::O),
129            key::Code::KeyP => Some(Key::P),
130            key::Code::KeyQ => Some(Key::Q),
131            key::Code::KeyR => Some(Key::R),
132            key::Code::KeyS => Some(Key::S),
133            key::Code::KeyT => Some(Key::T),
134            key::Code::KeyU => Some(Key::U),
135            key::Code::KeyV => Some(Key::V),
136            key::Code::KeyW => Some(Key::W),
137            key::Code::KeyX => Some(Key::X),
138            key::Code::KeyY => Some(Key::Y),
139            key::Code::KeyZ => Some(Key::Z),
140
141            // Digits
142            key::Code::Digit0 => Some(Key::DIGIT0),
143            key::Code::Digit1 => Some(Key::DIGIT1),
144            key::Code::Digit2 => Some(Key::DIGIT2),
145            key::Code::Digit3 => Some(Key::DIGIT3),
146            key::Code::Digit4 => Some(Key::DIGIT4),
147            key::Code::Digit5 => Some(Key::DIGIT5),
148            key::Code::Digit6 => Some(Key::DIGIT6),
149            key::Code::Digit7 => Some(Key::DIGIT7),
150            key::Code::Digit8 => Some(Key::DIGIT8),
151            key::Code::Digit9 => Some(Key::DIGIT9),
152
153            // Function keys
154            key::Code::F1 => Some(Key::F1),
155            key::Code::F2 => Some(Key::F2),
156            key::Code::F3 => Some(Key::F3),
157            key::Code::F4 => Some(Key::F4),
158            key::Code::F5 => Some(Key::F5),
159            key::Code::F6 => Some(Key::F6),
160            key::Code::F7 => Some(Key::F7),
161            key::Code::F8 => Some(Key::F8),
162            key::Code::F9 => Some(Key::F9),
163            key::Code::F10 => Some(Key::F10),
164            key::Code::F11 => Some(Key::F11),
165            key::Code::F12 => Some(Key::F12),
166            key::Code::F13 => Some(Key::F13),
167            key::Code::F14 => Some(Key::F14),
168            key::Code::F15 => Some(Key::F15),
169            key::Code::F16 => Some(Key::F16),
170            key::Code::F17 => Some(Key::F17),
171            key::Code::F18 => Some(Key::F18),
172            key::Code::F19 => Some(Key::F19),
173            key::Code::F20 => Some(Key::F20),
174            key::Code::F21 => Some(Key::F21),
175            key::Code::F22 => Some(Key::F22),
176            key::Code::F23 => Some(Key::F23),
177            key::Code::F24 => Some(Key::F24),
178            key::Code::F25 => Some(Key::F25),
179            key::Code::F26 => Some(Key::F26),
180            key::Code::F27 => Some(Key::F27),
181            key::Code::F28 => Some(Key::F28),
182            key::Code::F29 => Some(Key::F29),
183            key::Code::F30 => Some(Key::F30),
184            key::Code::F31 => Some(Key::F31),
185            key::Code::F32 => Some(Key::F32),
186            key::Code::F33 => Some(Key::F33),
187            key::Code::F34 => Some(Key::F34),
188            key::Code::F35 => Some(Key::F35),
189
190            // Navigation and editing
191            key::Code::Enter => Some(Key::ENTER),
192            key::Code::Escape => Some(Key::ESCAPE),
193            key::Code::Space => Some(Key::SPACE),
194            key::Code::Tab => Some(Key::TAB),
195            key::Code::Delete => Some(Key::DELETE),
196            key::Code::Backspace => Some(Key::BACKSPACE),
197            key::Code::Insert => Some(Key::INSERT),
198            key::Code::CapsLock => Some(Key::CAPS_LOCK),
199            key::Code::Home => Some(Key::HOME),
200            key::Code::End => Some(Key::END),
201            key::Code::PageUp => Some(Key::PAGE_UP),
202            key::Code::PageDown => Some(Key::PAGE_DOWN),
203            key::Code::ArrowUp => Some(Key::ARROW_UP),
204            key::Code::ArrowDown => Some(Key::ARROW_DOWN),
205            key::Code::ArrowLeft => Some(Key::ARROW_LEFT),
206            key::Code::ArrowRight => Some(Key::ARROW_RIGHT),
207
208            // Punctuation
209            key::Code::Minus => Some(Key::MINUS),
210            key::Code::Equal => Some(Key::EQUAL),
211            key::Code::BracketLeft => Some(Key::BRACKET_LEFT),
212            key::Code::BracketRight => Some(Key::BRACKET_RIGHT),
213            key::Code::Backslash => Some(Key::BACKSLASH),
214            key::Code::Semicolon => Some(Key::SEMICOLON),
215            key::Code::Quote => Some(Key::QUOTE),
216            key::Code::Backquote => Some(Key::BACKQUOTE),
217            key::Code::Comma => Some(Key::COMMA),
218            key::Code::Period => Some(Key::PERIOD),
219            key::Code::Slash => Some(Key::SLASH),
220
221            // Numpad
222            key::Code::Numpad0 => Some(Key::NUMPAD0),
223            key::Code::Numpad1 => Some(Key::NUMPAD1),
224            key::Code::Numpad2 => Some(Key::NUMPAD2),
225            key::Code::Numpad3 => Some(Key::NUMPAD3),
226            key::Code::Numpad4 => Some(Key::NUMPAD4),
227            key::Code::Numpad5 => Some(Key::NUMPAD5),
228            key::Code::Numpad6 => Some(Key::NUMPAD6),
229            key::Code::Numpad7 => Some(Key::NUMPAD7),
230            key::Code::Numpad8 => Some(Key::NUMPAD8),
231            key::Code::Numpad9 => Some(Key::NUMPAD9),
232            key::Code::NumpadDecimal => Some(Key::NUMPAD_DECIMAL),
233            key::Code::NumpadAdd => Some(Key::NUMPAD_ADD),
234            key::Code::NumpadSubtract => Some(Key::NUMPAD_SUBTRACT),
235            key::Code::NumpadMultiply => Some(Key::NUMPAD_MULTIPLY),
236            key::Code::NumpadDivide => Some(Key::NUMPAD_DIVIDE),
237            key::Code::NumpadEnter => Some(Key::NUMPAD_ENTER),
238            key::Code::NumpadEqual => Some(Key::NUMPAD_EQUAL),
239            key::Code::NumpadComma => Some(Key::NUMPAD_COMMA),
240            key::Code::NumpadBackspace => Some(Key::NUMPAD_BACKSPACE),
241            key::Code::NumpadClear => Some(Key::NUMPAD_CLEAR),
242            key::Code::NumpadClearEntry => Some(Key::NUMPAD_CLEAR_ENTRY),
243            key::Code::NumpadHash => Some(Key::NUMPAD_HASH),
244            key::Code::NumpadMemoryAdd => Some(Key::NUMPAD_MEMORY_ADD),
245            key::Code::NumpadMemoryClear => Some(Key::NUMPAD_MEMORY_CLEAR),
246            key::Code::NumpadMemoryRecall => Some(Key::NUMPAD_MEMORY_RECALL),
247            key::Code::NumpadMemoryStore => Some(Key::NUMPAD_MEMORY_STORE),
248            key::Code::NumpadMemorySubtract => Some(Key::NUMPAD_MEMORY_SUBTRACT),
249            key::Code::NumpadParenLeft => Some(Key::NUMPAD_PAREN_LEFT),
250            key::Code::NumpadParenRight => Some(Key::NUMPAD_PAREN_RIGHT),
251            key::Code::NumpadStar => Some(Key::NUMPAD_STAR),
252
253            // Modifiers — iced uses SuperLeft/SuperRight where W3C uses MetaLeft/MetaRight.
254            // Meta is iced's legacy alias for the Super key (no left/right distinction).
255            key::Code::ControlLeft => Some(Key::CONTROL_LEFT),
256            key::Code::ControlRight => Some(Key::CONTROL_RIGHT),
257            key::Code::ShiftLeft => Some(Key::SHIFT_LEFT),
258            key::Code::ShiftRight => Some(Key::SHIFT_RIGHT),
259            key::Code::AltLeft => Some(Key::ALT_LEFT),
260            key::Code::AltRight => Some(Key::ALT_RIGHT),
261            key::Code::SuperLeft | key::Code::Meta => Some(Key::META_LEFT),
262            key::Code::SuperRight => Some(Key::META_RIGHT),
263
264            // Media keys
265            key::Code::AudioVolumeUp => Some(Key::AUDIO_VOLUME_UP),
266            key::Code::AudioVolumeDown => Some(Key::AUDIO_VOLUME_DOWN),
267            key::Code::AudioVolumeMute => Some(Key::AUDIO_VOLUME_MUTE),
268            key::Code::MediaPlayPause => Some(Key::MEDIA_PLAY_PAUSE),
269            key::Code::MediaStop => Some(Key::MEDIA_STOP),
270            key::Code::MediaTrackNext => Some(Key::MEDIA_TRACK_NEXT),
271            key::Code::MediaTrackPrevious => Some(Key::MEDIA_TRACK_PREVIOUS),
272            key::Code::MediaSelect => Some(Key::MEDIA_SELECT),
273
274            // Browser keys
275            key::Code::BrowserBack => Some(Key::BROWSER_BACK),
276            key::Code::BrowserFavorites => Some(Key::BROWSER_FAVORITES),
277            key::Code::BrowserForward => Some(Key::BROWSER_FORWARD),
278            key::Code::BrowserHome => Some(Key::BROWSER_HOME),
279            key::Code::BrowserRefresh => Some(Key::BROWSER_REFRESH),
280            key::Code::BrowserSearch => Some(Key::BROWSER_SEARCH),
281            key::Code::BrowserStop => Some(Key::BROWSER_STOP),
282
283            // System keys
284            key::Code::PrintScreen => Some(Key::PRINT_SCREEN),
285            key::Code::ScrollLock => Some(Key::SCROLL_LOCK),
286            key::Code::Pause => Some(Key::PAUSE),
287            key::Code::NumLock => Some(Key::NUM_LOCK),
288            key::Code::ContextMenu => Some(Key::CONTEXT_MENU),
289            key::Code::Power => Some(Key::POWER),
290            key::Code::Sleep => Some(Key::SLEEP),
291            key::Code::WakeUp => Some(Key::WAKE_UP),
292            key::Code::Eject => Some(Key::EJECT),
293
294            // Clipboard / editing keys
295            key::Code::Copy => Some(Key::COPY),
296            key::Code::Cut => Some(Key::CUT),
297            key::Code::Paste => Some(Key::PASTE),
298            key::Code::Undo => Some(Key::UNDO),
299            key::Code::Find => Some(Key::FIND),
300            key::Code::Help => Some(Key::HELP),
301            key::Code::Open => Some(Key::OPEN),
302            key::Code::Select => Some(Key::SELECT),
303            key::Code::Again => Some(Key::AGAIN),
304            key::Code::Props => Some(Key::PROPS),
305            key::Code::Abort => Some(Key::ABORT),
306            key::Code::Resume => Some(Key::RESUME),
307            key::Code::Suspend => Some(Key::SUSPEND),
308
309            // Fn and legacy
310            key::Code::Fn => Some(Key::FN),
311            key::Code::FnLock => Some(Key::FN_LOCK),
312            key::Code::Hyper => Some(Key::HYPER),
313            key::Code::Turbo => Some(Key::TURBO),
314
315            // CJK / international
316            key::Code::Convert => Some(Key::CONVERT),
317            key::Code::NonConvert => Some(Key::NON_CONVERT),
318            key::Code::KanaMode => Some(Key::KANA_MODE),
319            key::Code::Hiragana => Some(Key::HIRAGANA),
320            key::Code::Katakana => Some(Key::KATAKANA),
321            key::Code::Lang1 => Some(Key::LANG1),
322            key::Code::Lang2 => Some(Key::LANG2),
323            key::Code::Lang3 => Some(Key::LANG3),
324            key::Code::Lang4 => Some(Key::LANG4),
325            key::Code::Lang5 => Some(Key::LANG5),
326            key::Code::IntlBackslash => Some(Key::INTL_BACKSLASH),
327            key::Code::IntlRo => Some(Key::INTL_RO),
328            key::Code::IntlYen => Some(Key::INTL_YEN),
329
330            // App launch keys
331            key::Code::LaunchApp1 => Some(Key::LAUNCH_APP1),
332            key::Code::LaunchApp2 => Some(Key::LAUNCH_APP2),
333            key::Code::LaunchMail => Some(Key::LAUNCH_MAIL),
334
335            _ => None,
336        }
337    }
338}
339
340impl IcedKeyExt for key::Physical {
341    fn to_key(&self) -> Option<Key> {
342        match self {
343            key::Physical::Code(code) => code.to_key(),
344            key::Physical::Unidentified(_) => None,
345        }
346    }
347}
348
349/// Convert iced [`Modifiers`] bitflags to a sorted `Vec<Modifier>`.
350///
351/// Iced uses `LOGO` for the Super/Meta/Windows key. This maps to
352/// `Modifier::Super` in `kbd`.
353///
354/// This trait is sealed and cannot be implemented outside this crate.
355pub trait IcedModifiersExt: private::Sealed {
356    /// Convert these iced modifier flags to a `Vec<Modifier>`.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use iced_core::keyboard::Modifiers;
362    /// use kbd::prelude::*;
363    /// use kbd_iced::IcedModifiersExt;
364    ///
365    /// let mods = (Modifiers::CTRL | Modifiers::SHIFT).to_modifiers();
366    /// assert_eq!(mods, vec![Modifier::Ctrl, Modifier::Shift]);
367    /// ```
368    #[must_use]
369    fn to_modifiers(&self) -> Vec<Modifier>;
370}
371
372impl IcedModifiersExt for Modifiers {
373    fn to_modifiers(&self) -> Vec<Modifier> {
374        Modifier::collect_active([
375            (self.control(), Modifier::Ctrl),
376            (self.shift(), Modifier::Shift),
377            (self.alt(), Modifier::Alt),
378            (self.logo(), Modifier::Super),
379        ])
380    }
381}
382
383/// Convert an iced keyboard [`Event`] to a `kbd` [`Hotkey`].
384///
385/// Uses the physical key from the event for layout-independent matching.
386/// Returns `None` for `ModifiersChanged` events (no key trigger) and
387/// for events with unidentified physical keys.
388///
389/// When the key is itself a modifier (e.g., `ControlLeft`), the
390/// corresponding modifier flag is stripped from the modifiers — iced
391/// includes the pressed modifier key in its own modifier state, but
392/// `kbd` treats the key as the trigger, not as a modifier of itself.
393/// This trait is sealed and cannot be implemented outside this crate.
394pub trait IcedEventExt: private::Sealed {
395    /// Convert this keyboard event to a [`Hotkey`], or `None` if unmappable.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use iced_core::keyboard::{Event, Location, Modifiers, key};
401    /// use kbd::prelude::*;
402    /// use kbd_iced::IcedEventExt;
403    ///
404    /// let event = Event::KeyPressed {
405    ///     key: iced_core::keyboard::Key::Unidentified,
406    ///     modified_key: iced_core::keyboard::Key::Unidentified,
407    ///     physical_key: key::Physical::Code(key::Code::KeyS),
408    ///     location: Location::Standard,
409    ///     modifiers: Modifiers::CTRL,
410    ///     text: None,
411    ///     repeat: false,
412    /// };
413    /// assert_eq!(
414    ///     event.to_hotkey(),
415    ///     Some(Hotkey::new(Key::S).modifier(Modifier::Ctrl)),
416    /// );
417    /// ```
418    #[must_use]
419    fn to_hotkey(&self) -> Option<Hotkey>;
420}
421
422impl IcedEventExt for Event {
423    fn to_hotkey(&self) -> Option<Hotkey> {
424        let (physical_key, modifiers) = match self {
425            Event::KeyPressed {
426                physical_key,
427                modifiers,
428                ..
429            }
430            | Event::KeyReleased {
431                physical_key,
432                modifiers,
433                ..
434            } => (physical_key, modifiers),
435            Event::ModifiersChanged(_) => return None,
436        };
437
438        let key = physical_key.to_key()?;
439        let mut mods = modifiers.to_modifiers();
440
441        // Strip the modifier that corresponds to the key itself.
442        if let Some(self_modifier) = Modifier::from_key(key) {
443            mods.retain(|m| *m != self_modifier);
444        }
445
446        Some(Hotkey::with_modifiers(key, mods))
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use iced_core::keyboard::Event;
453    use iced_core::keyboard::Location;
454    use iced_core::keyboard::Modifiers;
455    use iced_core::keyboard::key;
456    use kbd::hotkey::Hotkey;
457    use kbd::hotkey::Modifier;
458    use kbd::key::Key;
459
460    use super::*;
461
462    // IcedKeyExt — Code
463
464    #[test]
465    fn code_letters() {
466        assert_eq!(key::Code::KeyA.to_key(), Some(Key::A));
467        assert_eq!(key::Code::KeyZ.to_key(), Some(Key::Z));
468    }
469
470    #[test]
471    fn code_digits() {
472        assert_eq!(key::Code::Digit0.to_key(), Some(Key::DIGIT0));
473        assert_eq!(key::Code::Digit9.to_key(), Some(Key::DIGIT9));
474    }
475
476    #[test]
477    fn code_function_keys() {
478        assert_eq!(key::Code::F1.to_key(), Some(Key::F1));
479        assert_eq!(key::Code::F12.to_key(), Some(Key::F12));
480        assert_eq!(key::Code::F24.to_key(), Some(Key::F24));
481        assert_eq!(key::Code::F25.to_key(), Some(Key::F25));
482    }
483
484    #[test]
485    fn code_navigation() {
486        assert_eq!(key::Code::Enter.to_key(), Some(Key::ENTER));
487        assert_eq!(key::Code::Escape.to_key(), Some(Key::ESCAPE));
488        assert_eq!(key::Code::Backspace.to_key(), Some(Key::BACKSPACE));
489        assert_eq!(key::Code::Tab.to_key(), Some(Key::TAB));
490        assert_eq!(key::Code::Space.to_key(), Some(Key::SPACE));
491        assert_eq!(key::Code::Delete.to_key(), Some(Key::DELETE));
492        assert_eq!(key::Code::Insert.to_key(), Some(Key::INSERT));
493        assert_eq!(key::Code::Home.to_key(), Some(Key::HOME));
494        assert_eq!(key::Code::End.to_key(), Some(Key::END));
495        assert_eq!(key::Code::PageUp.to_key(), Some(Key::PAGE_UP));
496        assert_eq!(key::Code::PageDown.to_key(), Some(Key::PAGE_DOWN));
497        assert_eq!(key::Code::ArrowUp.to_key(), Some(Key::ARROW_UP));
498        assert_eq!(key::Code::ArrowDown.to_key(), Some(Key::ARROW_DOWN));
499        assert_eq!(key::Code::ArrowLeft.to_key(), Some(Key::ARROW_LEFT));
500        assert_eq!(key::Code::ArrowRight.to_key(), Some(Key::ARROW_RIGHT));
501    }
502
503    #[test]
504    fn code_modifiers() {
505        assert_eq!(key::Code::ControlLeft.to_key(), Some(Key::CONTROL_LEFT));
506        assert_eq!(key::Code::ControlRight.to_key(), Some(Key::CONTROL_RIGHT));
507        assert_eq!(key::Code::ShiftLeft.to_key(), Some(Key::SHIFT_LEFT));
508        assert_eq!(key::Code::ShiftRight.to_key(), Some(Key::SHIFT_RIGHT));
509        assert_eq!(key::Code::AltLeft.to_key(), Some(Key::ALT_LEFT));
510        assert_eq!(key::Code::AltRight.to_key(), Some(Key::ALT_RIGHT));
511        // iced's SuperLeft/Right → kbd's MetaLeft/MetaRight
512        assert_eq!(key::Code::SuperLeft.to_key(), Some(Key::META_LEFT));
513        assert_eq!(key::Code::SuperRight.to_key(), Some(Key::META_RIGHT));
514        // iced's legacy Meta (no left/right) → defaults to MetaLeft
515        assert_eq!(key::Code::Meta.to_key(), Some(Key::META_LEFT));
516    }
517
518    #[test]
519    fn code_punctuation() {
520        assert_eq!(key::Code::Minus.to_key(), Some(Key::MINUS));
521        assert_eq!(key::Code::Equal.to_key(), Some(Key::EQUAL));
522        assert_eq!(key::Code::BracketLeft.to_key(), Some(Key::BRACKET_LEFT));
523        assert_eq!(key::Code::BracketRight.to_key(), Some(Key::BRACKET_RIGHT));
524        assert_eq!(key::Code::Backslash.to_key(), Some(Key::BACKSLASH));
525        assert_eq!(key::Code::Semicolon.to_key(), Some(Key::SEMICOLON));
526        assert_eq!(key::Code::Quote.to_key(), Some(Key::QUOTE));
527        assert_eq!(key::Code::Backquote.to_key(), Some(Key::BACKQUOTE));
528        assert_eq!(key::Code::Comma.to_key(), Some(Key::COMMA));
529        assert_eq!(key::Code::Period.to_key(), Some(Key::PERIOD));
530        assert_eq!(key::Code::Slash.to_key(), Some(Key::SLASH));
531    }
532
533    #[test]
534    fn code_numpad() {
535        assert_eq!(key::Code::Numpad0.to_key(), Some(Key::NUMPAD0));
536        assert_eq!(key::Code::Numpad9.to_key(), Some(Key::NUMPAD9));
537        assert_eq!(key::Code::NumpadDecimal.to_key(), Some(Key::NUMPAD_DECIMAL));
538        assert_eq!(key::Code::NumpadAdd.to_key(), Some(Key::NUMPAD_ADD));
539        assert_eq!(
540            key::Code::NumpadSubtract.to_key(),
541            Some(Key::NUMPAD_SUBTRACT)
542        );
543        assert_eq!(
544            key::Code::NumpadMultiply.to_key(),
545            Some(Key::NUMPAD_MULTIPLY)
546        );
547        assert_eq!(key::Code::NumpadDivide.to_key(), Some(Key::NUMPAD_DIVIDE));
548        assert_eq!(key::Code::NumpadEnter.to_key(), Some(Key::NUMPAD_ENTER));
549    }
550
551    #[test]
552    fn code_media() {
553        assert_eq!(
554            key::Code::MediaPlayPause.to_key(),
555            Some(Key::MEDIA_PLAY_PAUSE)
556        );
557        assert_eq!(key::Code::MediaStop.to_key(), Some(Key::MEDIA_STOP));
558        assert_eq!(
559            key::Code::MediaTrackNext.to_key(),
560            Some(Key::MEDIA_TRACK_NEXT)
561        );
562        assert_eq!(
563            key::Code::MediaTrackPrevious.to_key(),
564            Some(Key::MEDIA_TRACK_PREVIOUS)
565        );
566        assert_eq!(
567            key::Code::AudioVolumeUp.to_key(),
568            Some(Key::AUDIO_VOLUME_UP)
569        );
570        assert_eq!(
571            key::Code::AudioVolumeDown.to_key(),
572            Some(Key::AUDIO_VOLUME_DOWN)
573        );
574        assert_eq!(
575            key::Code::AudioVolumeMute.to_key(),
576            Some(Key::AUDIO_VOLUME_MUTE)
577        );
578    }
579
580    #[test]
581    fn code_system() {
582        assert_eq!(key::Code::PrintScreen.to_key(), Some(Key::PRINT_SCREEN));
583        assert_eq!(key::Code::ScrollLock.to_key(), Some(Key::SCROLL_LOCK));
584        assert_eq!(key::Code::Pause.to_key(), Some(Key::PAUSE));
585        assert_eq!(key::Code::NumLock.to_key(), Some(Key::NUM_LOCK));
586        assert_eq!(key::Code::ContextMenu.to_key(), Some(Key::CONTEXT_MENU));
587        assert_eq!(key::Code::Power.to_key(), Some(Key::POWER));
588    }
589
590    #[test]
591    fn code_extended_keys() {
592        assert_eq!(key::Code::F25.to_key(), Some(Key::F25));
593        assert_eq!(key::Code::F35.to_key(), Some(Key::F35));
594        assert_eq!(key::Code::BrowserBack.to_key(), Some(Key::BROWSER_BACK));
595        assert_eq!(key::Code::Copy.to_key(), Some(Key::COPY));
596        assert_eq!(key::Code::Sleep.to_key(), Some(Key::SLEEP));
597        assert_eq!(key::Code::IntlBackslash.to_key(), Some(Key::INTL_BACKSLASH));
598        assert_eq!(key::Code::NumpadEqual.to_key(), Some(Key::NUMPAD_EQUAL));
599        assert_eq!(key::Code::Fn.to_key(), Some(Key::FN));
600        assert_eq!(key::Code::LaunchMail.to_key(), Some(Key::LAUNCH_MAIL));
601        assert_eq!(key::Code::Convert.to_key(), Some(Key::CONVERT));
602        assert_eq!(key::Code::Lang1.to_key(), Some(Key::LANG1));
603    }
604
605    // IcedKeyExt — Physical
606
607    #[test]
608    fn physical_code_to_key() {
609        let physical = key::Physical::Code(key::Code::KeyA);
610        assert_eq!(physical.to_key(), Some(Key::A));
611    }
612
613    #[test]
614    fn physical_unidentified_returns_none() {
615        let physical = key::Physical::Unidentified(key::NativeCode::Unidentified);
616        assert_eq!(physical.to_key(), None);
617    }
618
619    // IcedModifiersExt
620
621    #[test]
622    fn empty_modifiers() {
623        assert_eq!(Modifiers::empty().to_modifiers(), Vec::<Modifier>::new());
624    }
625
626    #[test]
627    fn single_modifiers() {
628        assert_eq!(Modifiers::CTRL.to_modifiers(), vec![Modifier::Ctrl]);
629        assert_eq!(Modifiers::SHIFT.to_modifiers(), vec![Modifier::Shift]);
630        assert_eq!(Modifiers::ALT.to_modifiers(), vec![Modifier::Alt]);
631        // iced's LOGO = kbd's Super
632        assert_eq!(Modifiers::LOGO.to_modifiers(), vec![Modifier::Super]);
633    }
634
635    #[test]
636    fn combined_modifiers() {
637        let mods = Modifiers::CTRL | Modifiers::SHIFT;
638        assert_eq!(mods.to_modifiers(), vec![Modifier::Ctrl, Modifier::Shift]);
639    }
640
641    #[test]
642    fn all_modifiers() {
643        let mods = Modifiers::CTRL | Modifiers::SHIFT | Modifiers::ALT | Modifiers::LOGO;
644        assert_eq!(
645            mods.to_modifiers(),
646            vec![
647                Modifier::Ctrl,
648                Modifier::Shift,
649                Modifier::Alt,
650                Modifier::Super,
651            ]
652        );
653    }
654
655    // IcedEventExt
656
657    fn make_key_pressed(physical_key: key::Physical, modifiers: Modifiers) -> Event {
658        Event::KeyPressed {
659            key: iced_core::keyboard::Key::Unidentified,
660            modified_key: iced_core::keyboard::Key::Unidentified,
661            physical_key,
662            location: Location::Standard,
663            modifiers,
664            text: None,
665            repeat: false,
666        }
667    }
668
669    fn make_key_released(physical_key: key::Physical, modifiers: Modifiers) -> Event {
670        Event::KeyReleased {
671            key: iced_core::keyboard::Key::Unidentified,
672            modified_key: iced_core::keyboard::Key::Unidentified,
673            physical_key,
674            location: Location::Standard,
675            modifiers,
676        }
677    }
678
679    #[test]
680    fn simple_key_press_to_hotkey() {
681        let event = make_key_pressed(key::Physical::Code(key::Code::KeyC), Modifiers::empty());
682        assert_eq!(event.to_hotkey(), Some(Hotkey::new(Key::C)));
683    }
684
685    #[test]
686    fn key_press_with_ctrl_to_hotkey() {
687        let event = make_key_pressed(key::Physical::Code(key::Code::KeyC), Modifiers::CTRL);
688        assert_eq!(
689            event.to_hotkey(),
690            Some(Hotkey::new(Key::C).modifier(Modifier::Ctrl))
691        );
692    }
693
694    #[test]
695    fn key_press_with_multiple_modifiers() {
696        let event = make_key_pressed(
697            key::Physical::Code(key::Code::KeyA),
698            Modifiers::CTRL | Modifiers::SHIFT,
699        );
700        assert_eq!(
701            event.to_hotkey(),
702            Some(
703                Hotkey::new(Key::A)
704                    .modifier(Modifier::Ctrl)
705                    .modifier(Modifier::Shift)
706            )
707        );
708    }
709
710    #[test]
711    fn key_release_to_hotkey() {
712        let event = make_key_released(key::Physical::Code(key::Code::KeyC), Modifiers::CTRL);
713        assert_eq!(
714            event.to_hotkey(),
715            Some(Hotkey::new(Key::C).modifier(Modifier::Ctrl))
716        );
717    }
718
719    #[test]
720    fn modifiers_changed_returns_none() {
721        let event = Event::ModifiersChanged(Modifiers::CTRL);
722        assert_eq!(event.to_hotkey(), None);
723    }
724
725    #[test]
726    fn unidentified_key_event_returns_none() {
727        let event = make_key_pressed(
728            key::Physical::Unidentified(key::NativeCode::Unidentified),
729            Modifiers::empty(),
730        );
731        assert_eq!(event.to_hotkey(), None);
732    }
733
734    #[test]
735    fn modifier_key_strips_self() {
736        // Pressing ShiftLeft — iced includes SHIFT in modifiers.
737        // Hotkey should be just "ShiftLeft", not "Shift+ShiftLeft".
738        let event = make_key_pressed(key::Physical::Code(key::Code::ShiftLeft), Modifiers::SHIFT);
739        assert_eq!(event.to_hotkey(), Some(Hotkey::new(Key::SHIFT_LEFT)));
740    }
741
742    #[test]
743    fn modifier_key_keeps_other_modifiers() {
744        // Pressing ControlLeft while Shift is already held
745        let event = make_key_pressed(
746            key::Physical::Code(key::Code::ControlLeft),
747            Modifiers::SHIFT | Modifiers::CTRL,
748        );
749        assert_eq!(
750            event.to_hotkey(),
751            Some(Hotkey::new(Key::CONTROL_LEFT).modifier(Modifier::Shift))
752        );
753    }
754
755    #[test]
756    fn ctrl_shift_f5_to_hotkey() {
757        let event = make_key_pressed(
758            key::Physical::Code(key::Code::F5),
759            Modifiers::CTRL | Modifiers::SHIFT,
760        );
761        assert_eq!(
762            event.to_hotkey(),
763            Some(
764                Hotkey::new(Key::F5)
765                    .modifier(Modifier::Ctrl)
766                    .modifier(Modifier::Shift)
767            )
768        );
769    }
770
771    #[test]
772    fn space_to_hotkey() {
773        let event = make_key_pressed(key::Physical::Code(key::Code::Space), Modifiers::empty());
774        assert_eq!(event.to_hotkey(), Some(Hotkey::new(Key::SPACE)));
775    }
776
777    #[test]
778    fn super_key_strips_self() {
779        // Pressing SuperLeft — iced includes LOGO in modifiers.
780        let event = make_key_pressed(key::Physical::Code(key::Code::SuperLeft), Modifiers::LOGO);
781        assert_eq!(event.to_hotkey(), Some(Hotkey::new(Key::META_LEFT)));
782    }
783}