Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/delegate/global_hotkey.rs
1use std::{collections::HashMap, rc::Rc, str::FromStr, sync::Arc, thread};
2 
3use crate::keymap;
4use crate::windowing::winit::app::CustomEvent;
5use parking_lot::Mutex;
6use winit::event_loop::EventLoopProxy;
7 
8use global_hotkey::{
9 hotkey::{Code, HotKey, Modifiers},
10 GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState,
11};
12 
13/// Responsible for registering system-wide (global) hotkeys with the platform.
14pub struct GlobalHotKeyHandler {
15 platform_manager: std::cell::OnceCell<GlobalHotKeyManager>,
16 /// Maps the [`global_hotkey::hotkey::HotKey::id`], an opaque, hash-based integer, to our
17 /// [`keymap::Keystroke`].
18 hotkey_map: Arc<Mutex<HashMap<u32, keymap::Keystroke>>>,
19 event_loop_proxy: EventLoopProxy<CustomEvent>,
20}
21 
22impl GlobalHotKeyHandler {
23 pub fn new(
24 event_loop_proxy: EventLoopProxy<CustomEvent>,
25 ) -> Result<Self, global_hotkey::Error> {
26 Ok(Self {
27 platform_manager: Default::default(),
28 hotkey_map: Default::default(),
29 event_loop_proxy,
30 })
31 }
32 
33 pub fn register(&self, shortcut: keymap::Keystroke) {
34 let hotkey = match hotkey_for_keystroke(&shortcut) {
35 Ok(hotkey) => hotkey,
36 Err(e) => {
37 log::error!("invalid global hotkey: {e:?}");
38 return;
39 }
40 };
41 self.platform_manager().register(hotkey);
42 self.hotkey_map.lock().insert(hotkey.id(), shortcut);
43 }
44 
45 pub fn unregister(&self, shortcut: &keymap::Keystroke) {
46 let hotkey = match hotkey_for_keystroke(shortcut) {
47 Ok(hotkey) => hotkey,
48 Err(e) => {
49 log::error!("invalid global hotkey: {e:?}");
50 return;
51 }
52 };
53 self.platform_manager().unregister(hotkey);
54 self.hotkey_map.lock().remove(&hotkey.id());
55 }
56 
57 /// Returns a reference to a lazily-instantiated [`GlobalHotKeyManager`].
58 ///
59 /// We do this lazily because the [`GlobalHotKeyManager`] can interfere
60 /// with other libraries that use Xlib, leading to crashes. We don't want
61 /// to run the risk of this happening for users who haven't set any global
62 /// hotkeys.
63 fn platform_manager(&self) -> &GlobalHotKeyManager {
64 self.platform_manager.get_or_init(|| {
65 let platform_manager =
66 GlobalHotKeyManager::new().expect("x11 implementation never actually fails");
67 let thread_hotkey_map = self.hotkey_map.clone();
68 // When global hotkeys are triggered, events get published to a crossbeam channel.
69 // Since crossbeam channels are not async, we don't want to receive this on our
70 // background executor's thread pool, as that would block a thread. Therefore, we spawn
71 // a dedicated thread for receiving these events.
72 let event_loop_proxy = self.event_loop_proxy.clone();
73 thread::spawn(move || {
74 while let Ok(event) = GlobalHotKeyEvent::receiver().recv() {
75 // Trigger when the hotkey is released, _not_ pressed. This is due to an X11
76 // quirk where focus is transferred out of Warp windows after a global hotkey
77 // is pressed. This breaks our quake mode logic. However, focus is restored
78 // when the hotkey is released.
79 if event.state == HotKeyState::Released {
80 // Lookup the hash-based hotkey ID to the actual keystroke from our
81 // map.
82 if let Some(keystroke) = thread_hotkey_map.lock().get(&event.id) {
83 event_loop_proxy.send_event(CustomEvent::GlobalShortcutTriggered(
84 keystroke.clone(),
85 ));
86 }
87 }
88 }
89 });
90 platform_manager
91 })
92 }
93}
94 
95fn hotkey_for_keystroke(
96 keystroke: &keymap::Keystroke,
97) -> std::result::Result<HotKey, anyhow::Error> {
98 let mut mods = Modifiers::empty();
99 if keystroke.alt {
100 mods |= Modifiers::ALT;
101 }
102 if keystroke.cmd {
103 mods |= Modifiers::SUPER;
104 }
105 if keystroke.shift {
106 mods |= Modifiers::SHIFT;
107 }
108 if keystroke.ctrl {
109 mods |= Modifiers::CONTROL;
110 }
111 if keystroke.meta {
112 mods |= Modifiers::META;
113 }
114 let key = if keystroke.key.len() == 1 {
115 let c = keystroke
116 .key
117 .chars()
118 .next()
119 .expect("validated length already");
120 match c {
121 '`' | '~' => Code::Backquote,
122 '-' | '_' => Code::Minus,
123 '=' | '+' => Code::Equal,
124 '0'..='9' => Code::from_str(&format!("Digit{c}"))?,
125 '\t' => Code::Tab,
126 '!' => Code::Digit1,
127 '@' => Code::Digit2,
128 '#' => Code::Digit3,
129 '$' => Code::Digit4,
130 '%' => Code::Digit5,
131 '^' => Code::Digit6,
132 '&' => Code::Digit7,
133 '*' => Code::Digit8,
134 '(' => Code::Digit9,
135 ')' => Code::Digit0,
136 'a'..='z' | 'A'..='Z' => Code::from_str(&format!("Key{}", c.to_ascii_uppercase()))?,
137 '[' | '{' => Code::BracketLeft,
138 ']' | '}' => Code::BracketRight,
139 '\\' | '|' => Code::Backslash,
140 ';' => Code::Semicolon,
141 '\'' | '"' => Code::Quote,
142 ',' | '<' => Code::Comma,
143 '.' | '>' => Code::Period,
144 '/' | '?' => Code::Slash,
145 'ろ' => Code::IntlRo,
146 '¥' => Code::IntlYen,
147 ' ' => Code::Space,
148 _ => anyhow::bail!("Invalid global hotkey: {c}"),
149 }
150 } else {
151 // Must map each of [`keymap::VALID_SPECIAL_KEYS`] to [`global_hotkey::hotkey::Code`].
152 match keystroke.key.as_str() {
153 "backspace" => Code::Backspace,
154 "tab" => Code::Tab,
155 "enter" => Code::Enter,
156 "up" => Code::ArrowUp,
157 "down" => Code::ArrowDown,
158 "left" => Code::ArrowLeft,
159 "right" => Code::ArrowRight,
160 "home" => Code::Home,
161 "end" => Code::End,
162 "pageup" => Code::PageUp,
163 "pagedown" => Code::PageDown,
164 "insert" => Code::Insert,
165 "delete" => Code::Delete,
166 "escape" => Code::Escape,
167 "numpadenter" => Code::NumpadEnter,
168 "f1" => Code::F1,
169 "f2" => Code::F2,
170 "f3" => Code::F3,
171 "f4" => Code::F4,
172 "f5" => Code::F5,
173 "f6" => Code::F6,
174 "f7" => Code::F7,
175 "f8" => Code::F8,
176 "f9" => Code::F9,
177 "f10" => Code::F10,
178 "f11" => Code::F11,
179 "f12" => Code::F12,
180 "f13" => Code::F13,
181 "f14" => Code::F14,
182 "f15" => Code::F15,
183 "f16" => Code::F16,
184 "f17" => Code::F17,
185 "f18" => Code::F18,
186 "f19" => Code::F19,
187 "f20" => Code::F20,
188 s => anyhow::bail!("Invalid global hotkey: {s}"),
189 }
190 };
191 
192 Ok(HotKey::new(Some(mods), key))
193}
194