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}