druid_shell/
hotkey.rs

1// Copyright 2019 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Hotkeys and helpers for parsing keyboard shortcuts.
16
17use std::borrow::Borrow;
18
19use tracing::warn;
20
21use crate::{IntoKey, KbKey, KeyEvent, Modifiers};
22
23// TODO: fix docstring
24
25/// A description of a keyboard shortcut.
26///
27/// This type is only intended to be used to describe shortcuts,
28/// and recognize them when they arrive.
29///
30/// # Examples
31///
32/// [`SysMods`] matches the Command key on macOS and Ctrl elsewhere:
33///
34/// ```
35/// use druid_shell::{HotKey, KbKey, KeyEvent, RawMods, SysMods};
36///
37/// let hotkey = HotKey::new(SysMods::Cmd, "a");
38///
39/// #[cfg(target_os = "macos")]
40/// assert!(hotkey.matches(KeyEvent::for_test(RawMods::Meta, "a")));
41///
42/// #[cfg(target_os = "windows")]
43/// assert!(hotkey.matches(KeyEvent::for_test(RawMods::Ctrl, "a")));
44/// ```
45///
46/// `None` matches only the key without modifiers:
47///
48/// ```
49/// use druid_shell::{HotKey, KbKey, KeyEvent, RawMods, SysMods};
50///
51/// let hotkey = HotKey::new(None, KbKey::ArrowLeft);
52///
53/// assert!(hotkey.matches(KeyEvent::for_test(RawMods::None, KbKey::ArrowLeft)));
54/// assert!(!hotkey.matches(KeyEvent::for_test(RawMods::Ctrl, KbKey::ArrowLeft)));
55/// ```
56///
57/// [`SysMods`]: enum.SysMods.html
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct HotKey {
60    pub(crate) mods: RawMods,
61    pub(crate) key: KbKey,
62}
63
64impl HotKey {
65    /// Create a new hotkey.
66    ///
67    /// The first argument describes the keyboard modifiers. This can be `None`,
68    /// or an instance of either [`SysMods`], or [`RawMods`]. [`SysMods`] unify the
69    /// 'Command' key on macOS with the 'Ctrl' key on other platforms.
70    ///
71    /// The second argument describes the non-modifier key. This can be either
72    /// a `&str` or a [`KbKey`]; the former is merely a convenient
73    /// shorthand for `KbKey::Character()`.
74    ///
75    /// # Examples
76    /// ```
77    /// use druid_shell::{HotKey, KbKey, RawMods, SysMods};
78    ///
79    /// let select_all = HotKey::new(SysMods::Cmd, "a");
80    /// let esc = HotKey::new(None, KbKey::Escape);
81    /// let macos_fullscreen = HotKey::new(RawMods::CtrlMeta, "f");
82    /// ```
83    ///
84    /// [`Key`]: keyboard_types::Key
85    /// [`SysMods`]: enum.SysMods.html
86    /// [`RawMods`]: enum.RawMods.html
87    pub fn new(mods: impl Into<Option<RawMods>>, key: impl IntoKey) -> Self {
88        HotKey {
89            mods: mods.into().unwrap_or(RawMods::None),
90            key: key.into_key(),
91        }
92        .warn_if_needed()
93    }
94
95    //TODO: figure out if we need to be normalizing case or something?
96    fn warn_if_needed(self) -> Self {
97        if let KbKey::Character(s) = &self.key {
98            let km: Modifiers = self.mods.into();
99            if km.shift() && s.chars().any(|c| c.is_lowercase()) {
100                warn!(
101                    "warning: HotKey {:?} includes shift, but text is lowercase. \
102                     Text is matched literally; this may cause problems.",
103                    &self
104                );
105            }
106        }
107        self
108    }
109
110    /// Returns `true` if this [`KeyEvent`] matches this `HotKey`.
111    ///
112    /// [`KeyEvent`]: KeyEvent
113    pub fn matches(&self, event: impl Borrow<KeyEvent>) -> bool {
114        // Should be a const but const bit_or doesn't work here.
115        let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::META;
116        let event = event.borrow();
117        self.mods == event.mods & base_mods && self.key == event.key
118    }
119}
120
121/// A platform-agnostic representation of keyboard modifiers, for command handling.
122///
123/// This does one thing: it allows specifying hotkeys that use the Command key
124/// on macOS, but use the Ctrl key on other platforms.
125#[derive(Debug, Clone, Copy)]
126pub enum SysMods {
127    None,
128    Shift,
129    /// Command on macOS, and Ctrl on Windows/Linux/OpenBSD/FreeBSD
130    Cmd,
131    /// Command + Alt on macOS, Ctrl + Alt on Windows/Linux/OpenBSD/FreeBSD
132    AltCmd,
133    /// Command + Shift on macOS, Ctrl + Shift on Windows/Linux/OpenBSD/FreeBSD
134    CmdShift,
135    /// Command + Alt + Shift on macOS, Ctrl + Alt + Shift on Windows/Linux/OpenBSD/FreeBSD
136    AltCmdShift,
137}
138
139//TODO: should something like this just _replace_ keymodifiers?
140/// A representation of the active modifier keys.
141///
142/// This is intended to be clearer than `Modifiers`, when describing hotkeys.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum RawMods {
145    None,
146    Alt,
147    Ctrl,
148    Meta,
149    Shift,
150    AltCtrl,
151    AltMeta,
152    AltShift,
153    CtrlShift,
154    CtrlMeta,
155    MetaShift,
156    AltCtrlMeta,
157    AltCtrlShift,
158    AltMetaShift,
159    CtrlMetaShift,
160    AltCtrlMetaShift,
161}
162
163impl std::cmp::PartialEq<Modifiers> for RawMods {
164    fn eq(&self, other: &Modifiers) -> bool {
165        let mods: Modifiers = (*self).into();
166        mods == *other
167    }
168}
169
170impl std::cmp::PartialEq<RawMods> for Modifiers {
171    fn eq(&self, other: &RawMods) -> bool {
172        other == self
173    }
174}
175
176impl std::cmp::PartialEq<Modifiers> for SysMods {
177    fn eq(&self, other: &Modifiers) -> bool {
178        let mods: RawMods = (*self).into();
179        mods == *other
180    }
181}
182
183impl std::cmp::PartialEq<SysMods> for Modifiers {
184    fn eq(&self, other: &SysMods) -> bool {
185        let other: RawMods = (*other).into();
186        &other == self
187    }
188}
189
190impl From<RawMods> for Modifiers {
191    fn from(src: RawMods) -> Modifiers {
192        let (alt, ctrl, meta, shift) = match src {
193            RawMods::None => (false, false, false, false),
194            RawMods::Alt => (true, false, false, false),
195            RawMods::Ctrl => (false, true, false, false),
196            RawMods::Meta => (false, false, true, false),
197            RawMods::Shift => (false, false, false, true),
198            RawMods::AltCtrl => (true, true, false, false),
199            RawMods::AltMeta => (true, false, true, false),
200            RawMods::AltShift => (true, false, false, true),
201            RawMods::CtrlMeta => (false, true, true, false),
202            RawMods::CtrlShift => (false, true, false, true),
203            RawMods::MetaShift => (false, false, true, true),
204            RawMods::AltCtrlMeta => (true, true, true, false),
205            RawMods::AltMetaShift => (true, false, true, true),
206            RawMods::AltCtrlShift => (true, true, false, true),
207            RawMods::CtrlMetaShift => (false, true, true, true),
208            RawMods::AltCtrlMetaShift => (true, true, true, true),
209        };
210        let mut mods = Modifiers::empty();
211        mods.set(Modifiers::ALT, alt);
212        mods.set(Modifiers::CONTROL, ctrl);
213        mods.set(Modifiers::META, meta);
214        mods.set(Modifiers::SHIFT, shift);
215        mods
216    }
217}
218
219// we do this so that HotKey::new can accept `None` as an initial argument.
220impl From<SysMods> for Option<RawMods> {
221    fn from(src: SysMods) -> Option<RawMods> {
222        Some(src.into())
223    }
224}
225
226impl From<SysMods> for RawMods {
227    fn from(src: SysMods) -> RawMods {
228        #[cfg(target_os = "macos")]
229        match src {
230            SysMods::None => RawMods::None,
231            SysMods::Shift => RawMods::Shift,
232            SysMods::Cmd => RawMods::Meta,
233            SysMods::AltCmd => RawMods::AltMeta,
234            SysMods::CmdShift => RawMods::MetaShift,
235            SysMods::AltCmdShift => RawMods::AltMetaShift,
236        }
237        #[cfg(not(target_os = "macos"))]
238        match src {
239            SysMods::None => RawMods::None,
240            SysMods::Shift => RawMods::Shift,
241            SysMods::Cmd => RawMods::Ctrl,
242            SysMods::AltCmd => RawMods::AltCtrl,
243            SysMods::CmdShift => RawMods::CtrlShift,
244            SysMods::AltCmdShift => RawMods::AltCtrlShift,
245        }
246    }
247}