winapi_easy/input/
hotkey.rs1use std::cell::Cell;
4use std::collections::HashMap;
5use std::io;
6use std::marker::PhantomData;
7use std::ops::Add;
8
9use num_enum::IntoPrimitive;
10use windows::Win32::UI::Input::KeyboardAndMouse::{
11 HOT_KEY_MODIFIERS,
12 MOD_ALT,
13 MOD_CONTROL,
14 MOD_NOREPEAT,
15 MOD_SHIFT,
16 MOD_WIN,
17 RegisterHotKey,
18 UnregisterHotKey,
19};
20
21use crate::input::VirtualKey;
22use crate::internal::ResultExt;
23use crate::messaging::{
24 ThreadMessage,
25 ThreadMessageLoop,
26};
27
28pub type HotkeyId = u8;
29
30pub struct GlobalHotkeySet {
36 hotkey_defs: HashMap<HotkeyId, HotkeyDef>,
37 _marker: PhantomData<*mut ()>,
38}
39
40#[cfg(test)]
41static_assertions::assert_not_impl_any!(GlobalHotkeySet: Send, Sync);
42
43impl GlobalHotkeySet {
44 thread_local! {
45 static RUNNING: Cell<bool> = const { Cell::new(false) };
46 }
47
48 #[expect(clippy::new_without_default)]
54 pub fn new() -> Self {
55 assert!(
56 !Self::RUNNING.get(),
57 "Only one hotkey set may be active per thread"
58 );
59 Self::RUNNING.set(true);
60 let hotkey_defs = Default::default();
61 Self {
62 hotkey_defs,
63 _marker: PhantomData,
64 }
65 }
66
67 pub fn add_hotkey<KC>(&mut self, user_id: HotkeyId, key_combination: KC) -> io::Result<()>
71 where
72 KC: Into<KeyCombination>,
73 {
74 let new_def = HotkeyDef::new(user_id, key_combination.into())?;
75 self.hotkey_defs.insert(user_id, new_def);
76 Ok(())
77 }
78
79 pub fn listen_for_hotkeys<E, F>(&mut self, mut listener: F) -> Result<(), E>
80 where
81 E: From<io::Error>,
82 F: FnMut(HotkeyId) -> Result<(), E>,
83 {
84 let message_listener = |message| {
85 if let ThreadMessage::Hotkey(hotkey_id) = message {
86 #[expect(clippy::missing_panics_doc)]
87 {
88 assert!(self.hotkey_defs.contains_key(&hotkey_id));
89 }
90 listener(hotkey_id)
91 } else {
92 Ok(())
93 }
94 };
95 ThreadMessageLoop::new().run_thread_message_loop_internal(message_listener, false, None)
96 }
97}
98
99impl Drop for GlobalHotkeySet {
100 fn drop(&mut self) {
101 Self::RUNNING.set(false);
102 }
103}
104
105#[derive(Debug)]
106struct HotkeyDef {
107 user_id: HotkeyId,
108 #[expect(dead_code)]
109 key_combination: KeyCombination,
110}
111
112impl HotkeyDef {
113 fn new(user_id: HotkeyId, key_combination: KeyCombination) -> io::Result<Self> {
114 unsafe {
115 RegisterHotKey(
116 None,
117 user_id.into(),
118 HOT_KEY_MODIFIERS(key_combination.modifiers.0),
119 key_combination.key.into(),
120 )
121 }?;
122 Ok(Self {
123 user_id,
124 key_combination,
125 })
126 }
127
128 fn unregister(&self) -> io::Result<()> {
129 unsafe { UnregisterHotKey(None, self.user_id.into()) }?;
130 Ok(())
131 }
132}
133
134impl Drop for HotkeyDef {
135 fn drop(&mut self) {
136 self.unregister().unwrap_or_default_and_print_error();
137 }
138}
139
140#[derive(IntoPrimitive, Copy, Clone, Eq, PartialEq, Debug)]
142#[repr(u32)]
143pub enum Modifier {
144 Alt = MOD_ALT.0,
145 Ctrl = MOD_CONTROL.0,
146 Shift = MOD_SHIFT.0,
147 Win = MOD_WIN.0,
148}
149
150#[derive(Copy, Clone, Eq, PartialEq, Debug)]
152pub struct ModifierCombination(u32);
153
154#[derive(Copy, Clone, Eq, PartialEq, Debug)]
156pub struct KeyCombination {
157 modifiers: ModifierCombination,
158 key: VirtualKey,
159}
160
161impl KeyCombination {
162 fn new_from(modifiers: ModifierCombination, key: VirtualKey) -> Self {
163 KeyCombination {
164 modifiers: ModifierCombination(modifiers.0 | MOD_NOREPEAT.0),
166 key,
167 }
168 }
169}
170
171impl From<Modifier> for ModifierCombination {
172 fn from(modifier: Modifier) -> Self {
173 ModifierCombination(modifier.into())
174 }
175}
176
177impl From<VirtualKey> for KeyCombination {
178 fn from(key: VirtualKey) -> Self {
179 KeyCombination::new_from(ModifierCombination(0), key)
180 }
181}
182
183impl<T2> Add<T2> for Modifier
184where
185 T2: Into<ModifierCombination>,
186{
187 type Output = ModifierCombination;
188
189 fn add(self, rhs: T2) -> Self::Output {
190 rhs.into() + self
191 }
192}
193
194impl<T2> Add<T2> for ModifierCombination
195where
196 T2: Into<ModifierCombination>,
197{
198 type Output = ModifierCombination;
199
200 fn add(self, rhs: T2) -> Self::Output {
201 #[expect(clippy::suspicious_arithmetic_impl)]
202 ModifierCombination(self.0 | rhs.into().0)
203 }
204}
205
206impl Add<VirtualKey> for ModifierCombination {
207 type Output = KeyCombination;
208
209 fn add(self, rhs: VirtualKey) -> Self::Output {
210 KeyCombination::new_from(self, rhs)
211 }
212}
213
214impl Add<VirtualKey> for Modifier {
215 type Output = KeyCombination;
216
217 fn add(self, rhs: VirtualKey) -> Self::Output {
218 KeyCombination::new_from(self.into(), rhs)
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn create_hotkey_listener() -> io::Result<()> {
228 let mut message_loop = ThreadMessageLoop::new();
229 let mut hotkeys = GlobalHotkeySet::new();
230 hotkeys.add_hotkey(
231 0,
232 Modifier::Ctrl + Modifier::Alt + Modifier::Shift + VirtualKey::Oem1,
233 )?;
234 ThreadMessageLoop::post_quit_message();
235 message_loop.run()?;
236 Ok(())
237 }
238}