Skip to main content

wingy_core/
keyboard.rs

1//! Types related to the keyboard.
2
3use bitflags::bitflags;
4pub use keyboard_types::{Code as KeyCode, Location as KeyLocation, NamedKey};
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7pub use smol_str::SmolStr;
8
9/// Contains the platform-native physical key identifier
10///
11/// The exact values vary from platform to platform (which is part of why this is a per-platform
12/// enum), but the values are primarily tied to the key's physical location on the keyboard.
13///
14/// This enum is primarily used to store raw keycodes when Wingy doesn't map a given native
15/// physical key identifier to a meaningful [`KeyCode`] variant. In the presence of identifiers we
16/// haven't mapped for you yet, this lets you use use [`KeyCode`] to:
17///
18/// - Correctly match key press and release events.
19/// - On non-Web platforms, support assigning keybinds to virtually any key through a UI.
20#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
22#[non_exhaustive]
23pub enum NativeKeyCode {
24    Unidentified,
25    /// An Android "scancode".
26    Android(u32),
27    /// A macOS "scancode".
28    MacOS(u16),
29    /// A Windows "scancode".
30    Windows(u16),
31    /// An XKB "keycode".
32    Xkb(u32),
33    /// An OpenHarmony "scancode".
34    Ohos(u32),
35}
36
37impl std::fmt::Debug for NativeKeyCode {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        use NativeKeyCode::{Android, MacOS, Ohos, Unidentified, Windows, Xkb};
40        let mut debug_tuple;
41        match self {
42            Unidentified => {
43                debug_tuple = f.debug_tuple("Unidentified");
44            },
45            Android(code) => {
46                debug_tuple = f.debug_tuple("Android");
47                debug_tuple.field(&format_args!("0x{code:04X}"));
48            },
49            MacOS(code) => {
50                debug_tuple = f.debug_tuple("MacOS");
51                debug_tuple.field(&format_args!("0x{code:04X}"));
52            },
53            Windows(code) => {
54                debug_tuple = f.debug_tuple("Windows");
55                debug_tuple.field(&format_args!("0x{code:04X}"));
56            },
57            Xkb(code) => {
58                debug_tuple = f.debug_tuple("Xkb");
59                debug_tuple.field(&format_args!("0x{code:04X}"));
60            },
61            Ohos(code) => {
62                debug_tuple = f.debug_tuple("OpenHarmony");
63                debug_tuple.field(&format_args!("0x{code:04X}"));
64            },
65        }
66        debug_tuple.finish()
67    }
68}
69
70/// Contains the platform-native logical key identifier
71///
72/// Exactly what that means differs from platform to platform, but the values are to some degree
73/// tied to the currently active keyboard layout. The same key on the same keyboard may also report
74/// different values on different platforms, which is one of the reasons this is a per-platform
75/// enum.
76///
77/// This enum is primarily used to store raw keysym when Wingy doesn't map a given native logical
78/// key identifier to a meaningful [`Key`] variant. This lets you use [`Key`], and let the user
79/// define keybinds which work in the presence of identifiers we haven't mapped for you yet.
80#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
81#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
82#[non_exhaustive]
83pub enum NativeKey {
84    Unidentified,
85    /// An Android "keycode", which is similar to a "virtual-key code" on Windows.
86    Android(u32),
87    /// A macOS "scancode". There does not appear to be any direct analogue to either keysyms or
88    /// "virtual-key" codes in macOS, so we report the scancode instead.
89    MacOS(u16),
90    /// A Windows "virtual-key code".
91    Windows(u16),
92    /// An XKB "keysym".
93    Xkb(u32),
94    /// A "key value string".
95    Web(SmolStr),
96    /// An OpenHarmony "keycode", which is similar to a "virtual-key code" on Windows.
97    Ohos(u32),
98}
99
100impl std::fmt::Debug for NativeKey {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        use NativeKey::{Android, MacOS, Ohos, Unidentified, Web, Windows, Xkb};
103        let mut debug_tuple;
104        match self {
105            Unidentified => {
106                debug_tuple = f.debug_tuple("Unidentified");
107            },
108            Android(code) => {
109                debug_tuple = f.debug_tuple("Android");
110                debug_tuple.field(&format_args!("0x{code:04X}"));
111            },
112            MacOS(code) => {
113                debug_tuple = f.debug_tuple("MacOS");
114                debug_tuple.field(&format_args!("0x{code:04X}"));
115            },
116            Windows(code) => {
117                debug_tuple = f.debug_tuple("Windows");
118                debug_tuple.field(&format_args!("0x{code:04X}"));
119            },
120            Xkb(code) => {
121                debug_tuple = f.debug_tuple("Xkb");
122                debug_tuple.field(&format_args!("0x{code:04X}"));
123            },
124            Web(code) => {
125                debug_tuple = f.debug_tuple("Web");
126                debug_tuple.field(code);
127            },
128            Ohos(code) => {
129                debug_tuple = f.debug_tuple("OpenHarmony");
130                debug_tuple.field(code);
131            },
132        }
133        debug_tuple.finish()
134    }
135}
136
137impl From<NativeKeyCode> for NativeKey {
138    #[inline]
139    fn from(code: NativeKeyCode) -> Self {
140        match code {
141            NativeKeyCode::Unidentified => NativeKey::Unidentified,
142            NativeKeyCode::Android(x) => NativeKey::Android(x),
143            NativeKeyCode::MacOS(x) => NativeKey::MacOS(x),
144            NativeKeyCode::Windows(x) => NativeKey::Windows(x),
145            NativeKeyCode::Xkb(x) => NativeKey::Xkb(x),
146            NativeKeyCode::Ohos(x) => NativeKey::Ohos(x),
147        }
148    }
149}
150
151impl PartialEq<NativeKey> for NativeKeyCode {
152    #[allow(clippy::cmp_owned)] // uses less code than direct match; target is stack allocated
153    #[inline]
154    fn eq(&self, rhs: &NativeKey) -> bool {
155        NativeKey::from(*self) == *rhs
156    }
157}
158
159impl PartialEq<NativeKeyCode> for NativeKey {
160    #[inline]
161    fn eq(&self, rhs: &NativeKeyCode) -> bool {
162        rhs == self
163    }
164}
165
166/// Represents the location of a physical key.
167///
168/// Wingy will not emit [`KeyCode::Unidentified`] when it cannot recognize the key, instead it will
169/// emit [`PhysicalKey::Unidentified`] with additional data about the key.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
171#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
172#[allow(clippy::exhaustive_enums)]
173pub enum PhysicalKey {
174    /// A known key code
175    Code(KeyCode),
176    /// This variant is used when the key cannot be translated to a [`KeyCode`]
177    ///
178    /// The native keycode is provided (if available) so you're able to more reliably match
179    /// key-press and key-release events by hashing the [`PhysicalKey`]. It is also possible to use
180    /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform.
181    Unidentified(NativeKeyCode),
182}
183
184impl From<KeyCode> for PhysicalKey {
185    #[inline]
186    fn from(code: KeyCode) -> Self {
187        PhysicalKey::Code(code)
188    }
189}
190
191impl From<PhysicalKey> for KeyCode {
192    #[inline]
193    fn from(key: PhysicalKey) -> Self {
194        match key {
195            PhysicalKey::Code(code) => code,
196            PhysicalKey::Unidentified(_) => KeyCode::Unidentified,
197        }
198    }
199}
200
201impl From<NativeKeyCode> for PhysicalKey {
202    #[inline]
203    fn from(code: NativeKeyCode) -> Self {
204        PhysicalKey::Unidentified(code)
205    }
206}
207
208impl PartialEq<KeyCode> for PhysicalKey {
209    #[inline]
210    fn eq(&self, rhs: &KeyCode) -> bool {
211        match self {
212            PhysicalKey::Code(code) => code == rhs,
213            _ => false,
214        }
215    }
216}
217
218impl PartialEq<PhysicalKey> for KeyCode {
219    #[inline]
220    fn eq(&self, rhs: &PhysicalKey) -> bool {
221        rhs == self
222    }
223}
224
225impl PartialEq<NativeKeyCode> for PhysicalKey {
226    #[inline]
227    fn eq(&self, rhs: &NativeKeyCode) -> bool {
228        match self {
229            PhysicalKey::Unidentified(code) => code == rhs,
230            _ => false,
231        }
232    }
233}
234
235impl PartialEq<PhysicalKey> for NativeKeyCode {
236    #[inline]
237    fn eq(&self, rhs: &PhysicalKey) -> bool {
238        rhs == self
239    }
240}
241
242/// Key represents the meaning of a keypress.
243///
244/// This is a superset of the UI Events Specification's [`KeyboardEvent.key`] with
245/// additions:
246/// - All simple variants are wrapped under the `Named` variant
247/// - The `Unidentified` variant here, can still identify a key through it's `NativeKeyCode`.
248/// - The `Dead` variant here, can specify the character which is inserted when pressing the
249///   dead-key twice.
250///
251/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/
252#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
253#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
254#[allow(clippy::exhaustive_enums)]
255pub enum Key<Str = SmolStr> {
256    /// A simple (unparameterised) action
257    Named(NamedKey),
258
259    /// A key string that corresponds to the character typed by the user, taking into account the
260    /// user’s current locale setting, and any system-level keyboard mapping overrides that are in
261    /// effect.
262    Character(Str),
263
264    /// This variant is used when the key cannot be translated to any other variant.
265    ///
266    /// The native key is provided (if available) in order to allow the user to specify keybindings
267    /// for keys which are not defined by this API, mainly through some sort of UI.
268    Unidentified(NativeKey),
269
270    /// Contains the text representation of the dead-key when available.
271    ///
272    /// ## Platform-specific
273    /// - **Web:** Always contains `None`
274    Dead(Option<char>),
275}
276
277impl From<NamedKey> for Key {
278    #[inline]
279    fn from(action: NamedKey) -> Self {
280        Key::Named(action)
281    }
282}
283
284impl From<NativeKey> for Key {
285    #[inline]
286    fn from(code: NativeKey) -> Self {
287        Key::Unidentified(code)
288    }
289}
290
291impl<Str> PartialEq<NamedKey> for Key<Str> {
292    #[inline]
293    fn eq(&self, rhs: &NamedKey) -> bool {
294        match self {
295            Key::Named(a) => a == rhs,
296            _ => false,
297        }
298    }
299}
300
301impl<Str: PartialEq<str>> PartialEq<str> for Key<Str> {
302    #[inline]
303    fn eq(&self, rhs: &str) -> bool {
304        match self {
305            Key::Character(s) => s == rhs,
306            _ => false,
307        }
308    }
309}
310
311impl<Str: PartialEq<str>> PartialEq<&str> for Key<Str> {
312    #[inline]
313    fn eq(&self, rhs: &&str) -> bool {
314        self == *rhs
315    }
316}
317
318impl<Str> PartialEq<NativeKey> for Key<Str> {
319    #[inline]
320    fn eq(&self, rhs: &NativeKey) -> bool {
321        match self {
322            Key::Unidentified(code) => code == rhs,
323            _ => false,
324        }
325    }
326}
327
328impl<Str> PartialEq<Key<Str>> for NativeKey {
329    #[inline]
330    fn eq(&self, rhs: &Key<Str>) -> bool {
331        rhs == self
332    }
333}
334
335impl Key<SmolStr> {
336    /// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on
337    /// `Key`. All other variants remain unchanged.
338    pub fn as_ref(&self) -> Key<&str> {
339        match self {
340            Key::Named(a) => Key::Named(*a),
341            Key::Character(ch) => Key::Character(ch.as_str()),
342            Key::Dead(d) => Key::Dead(*d),
343            Key::Unidentified(u) => Key::Unidentified(u.clone()),
344        }
345    }
346}
347
348impl Key {
349    /// Convert a key to its approximate textual equivalent.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// # #[cfg(target_family = "wasm")]
355    /// # wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
356    /// # #[cfg_attr(target_family = "wasm", wasm_bindgen_test::wasm_bindgen_test)]
357    /// # fn main() {
358    /// use wingy_core::keyboard::{Key, NamedKey};
359    ///
360    /// assert_eq!(Key::Character("a".into()).to_text(), Some("a"));
361    /// assert_eq!(Key::Named(NamedKey::Enter).to_text(), Some("\r"));
362    /// assert_eq!(Key::Named(NamedKey::F20).to_text(), None);
363    /// # }
364    /// ```
365    pub fn to_text(&self) -> Option<&str> {
366        match self {
367            Key::Named(action) => match action {
368                NamedKey::Enter => Some("\r"),
369                NamedKey::Backspace => Some("\x08"),
370                NamedKey::Tab => Some("\t"),
371                NamedKey::Escape => Some("\x1b"),
372                _ => None,
373            },
374            Key::Character(ch) => Some(ch.as_str()),
375            _ => None,
376        }
377    }
378}
379
380bitflags! {
381    /// Represents the current logical state of the keyboard modifiers
382    ///
383    /// Each flag represents a modifier and is set if this modifier is active.
384    ///
385    /// Note that the modifier key can be physically released with the modifier
386    /// still being marked as active, as in the case of sticky modifiers.
387    /// See [`ModifiersKeyState`] for more details on what "sticky" means.
388    #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
389    #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
390    pub struct ModifiersState: u32 {
391        /// The "shift" key.
392        const SHIFT = 0b100;
393        /// The "control" key.
394        const CONTROL = 0b100 << 3;
395        /// The "alt" key.
396        const ALT = 0b100 << 6;
397        /// This is the "windows" key on PC and "command" key on Mac.
398        const META = 0b100 << 9;
399        #[deprecated = "use META instead"]
400        const SUPER = Self::META.bits();
401    }
402}
403
404impl ModifiersState {
405    /// Returns whether the shift modifier is active.
406    pub fn shift_key(&self) -> bool {
407        self.intersects(Self::SHIFT)
408    }
409
410    /// Returns whether the control modifier is active.
411    pub fn control_key(&self) -> bool {
412        self.intersects(Self::CONTROL)
413    }
414
415    /// Returns whether the alt modifier is active.
416    pub fn alt_key(&self) -> bool {
417        self.intersects(Self::ALT)
418    }
419
420    /// Returns whether the meta modifier is active.
421    pub fn meta_key(&self) -> bool {
422        self.intersects(Self::META)
423    }
424}
425
426/// The logical state of the particular modifiers key.
427///
428/// NOTE: while the modifier can only be in a binary active/inactive state, it might be helpful to
429/// note the context re. how its state changes by physical key events.
430///
431/// `↓` / `↑` denote physical press/release[^1]:
432///
433/// | Type              | Activated           | Deactivated | Comment |
434/// | ----------------- | :-----------------: | :---------: | ------- |
435/// | __Regular__       | `↓`                 | `↑`         | Active while being held |
436/// | __Sticky__        | `↓`                 | `↓` unless lock is enabled<br>`↓`/`↑`[^2] __non__-sticky key | Temporarily "stuck"; other `Sticky` keys have no effect |
437/// | __Sticky Locked__ | `↓` <br>if `Sticky` | `↓`         | Similar to `Toggle`, but deactivating `↓` turns on `Regular` effect |
438/// | __Toggle__        | `↓`                 | `↓`         | `↑` from the activating `↓` has no effect |
439///
440/// `Sticky` effect avoids the need to press and hold multiple modifiers for a single shortcut and
441/// is usually a platform-wide option that affects modifiers _commonly_ used in shortcuts:
442/// <kbd>Shift</kbd>, <kbd>Control</kbd>, <kbd>Alt</kbd>, <kbd>Meta</kbd>.
443///
444/// `Toggle` type is typically a property of a modifier, for example, <kbd>Caps Lock</kbd>.
445///
446/// These active states are __not__ differentiated here.
447///
448/// [^1]: For virtual/on-screen keyboards physical press/release can be a mouse click or a finger tap or a voice command.
449/// [^2]: platform-dependent
450#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
451#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
452#[allow(clippy::exhaustive_enums)]
453pub enum ModifiersKeyState {
454    /// The particular modifier is active or logically, but not necessarily physically, pressed.
455    Pressed,
456    /// The state of the key is unknown.
457    ///
458    /// Can include cases when the key is active or logically pressed, for example, when a sticky
459    /// **Shift** is active, the OS might not preserve information that it was activated by
460    /// RightShift, so the state of [`ModifiersKeys::RSHIFT`] will be unknown while the state
461    /// of [`ModifiersState::SHIFT`] will be active.
462    #[default]
463    Unknown,
464}
465
466// NOTE: the exact modifier key is not used to represent modifiers state in the
467// first place due to a fact that modifiers state could be changed without any
468// key being pressed and on some platforms like Wayland/X11 which key resulted
469// in modifiers change is hidden, also, not that it really matters.
470//
471// The reason this API is even exposed is mostly to provide a way for users
472// to treat modifiers differently based on their position, which is required
473// on macOS due to their AltGr/Option situation.
474bitflags! {
475    #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
476    #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
477    pub struct ModifiersKeys: u8 {
478        const LSHIFT   = 0b0000_0001;
479        const RSHIFT   = 0b0000_0010;
480        const LCONTROL = 0b0000_0100;
481        const RCONTROL = 0b0000_1000;
482        const LALT     = 0b0001_0000;
483        const RALT     = 0b0010_0000;
484        const LMETA    = 0b0100_0000;
485        const RMETA    = 0b1000_0000;
486        #[deprecated = "use LMETA instead"]
487        const LSUPER   = Self::LMETA.bits();
488        #[deprecated = "use RMETA instead"]
489        const RSUPER   = Self::RMETA.bits();
490    }
491}