winapi_easy/input/
hotkey.rs

1//! Global hotkeys.
2
3use 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
30/// Registers global hotkeys.
31///
32/// # Multithreading
33///
34/// This type is not [`Send`] and [`Sync`] because the hotkeys are registered only to the current thread.
35pub 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    /// Registers a new hotkey set with the system.
49    ///
50    /// # Panics
51    ///
52    /// Will panic if more than 1 instance is created per thread.
53    #[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    /// Adds a hotkey.
68    ///
69    /// Not all key combinations may work as hotkeys.
70    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/// Modifier key than cannot be used by itself for hotkeys.
141#[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/// A combination of modifier keys.
151#[derive(Copy, Clone, Eq, PartialEq, Debug)]
152pub struct ModifierCombination(u32);
153
154/// A combination of zero or more modifiers and exactly one normal key.
155#[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            // Changes the hotkey behavior so that the keyboard auto-repeat does not yield multiple hotkey notifications.
165            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}