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/event_loop/key_events.rs
1use std::borrow::Cow;
2use std::collections::HashMap;
3 
4use lazy_static::lazy_static;
5 
6use winit::event::ElementState;
7#[cfg(windows)]
8use winit::keyboard::NativeKey;
9use winit::keyboard::{Key, ModifiersState, NamedKey};
10#[cfg(not(target_family = "wasm"))]
11use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
12 
13use crate::platform::KEYS_TO_IGNORE;
14use crate::{event::KeyEventDetails, keymap::Keystroke};
15 
16use super::WindowState;
17 
18lazy_static! {
19 /// Mapping between a printable ASCII character and its corresponding control code had `ctrl`
20 /// been pressed. For example: `ctrl-c` corresponds to the `^C` control code, which has an ASCII
21 /// value of 03. See <https://www.geeksforgeeks.org/control-characters/> for more details.
22 static ref CONTROL_CHARACTER_MAP: HashMap<&'static str, &'static str> = HashMap::from_iter([
23 ("@", "\x00"),
24 ("a", "\x01"),
25 ("b", "\x02"),
26 ("c", "\x03"),
27 ("d", "\x04"),
28 ("e", "\x05"),
29 ("f", "\x06"),
30 ("g", "\x07"),
31 ("h", "\x08"),
32 ("i", "\x09"),
33 ("j", "\x0A"),
34 ("k", "\x0B"),
35 ("l", "\x0C"),
36 ("m", "\x0D"),
37 ("n", "\x0E"),
38 ("o", "\x0F"),
39 ("p", "\x10"),
40 ("q", "\x11"),
41 ("r", "\x12"),
42 ("s", "\x13"),
43 ("t", "\x14"),
44 ("u", "\x15"),
45 ("v", "\x16"),
46 ("w", "\x17"),
47 ("x", "\x18"),
48 ("y", "\x19"),
49 ("z", "\x1A"),
50 ("[", "\x1B"),
51 ("\\", "\x1C"),
52 ("]", "\x1D"),
53 ("^", "\x1E"),
54 ("_", "\x1F"),
55 ]);
56}
57 
58/// Converts a KeyboardInput event to a UI framework event, returning None
59/// if no UI framework event should be emitted.
60pub fn convert_keyboard_input_event(
61 input: winit::event::KeyEvent,
62 window_state: &WindowState,
63 is_synthetic: bool,
64) -> Option<crate::Event> {
65 if input.state != ElementState::Pressed {
66 return None;
67 }
68 
69 // Ignore any synthetic keypresses that winit generated for keys that were
70 // already pressed when a window gained focus. Three examples of how these
71 // cause problems:
72 // 1. An alt-tab to a window can end up inserting a tab into the input if
73 // alt is released before tab.
74 // 2. Using a keyboard shortcut to open a new window can open many new
75 // windows, as the new window will receive a synthetic event for the
76 // shortcut that opened it, opening _another_ new window, and so on.
77 // 3. The ctrl-d shortcut for sending an EOF to the shell can end up
78 // being sent to additional sessions if there was ony one session in
79 // the window, as it will close the window and then be synthetically
80 // generated for the next window in the stack.
81 if is_synthetic {
82 return None;
83 }
84 
85 let chars = text_with_modifiers(&input, window_state.modifiers)
86 .unwrap_or_default()
87 .to_owned();
88 
89 let key_without_modifiers = get_key_without_modifiers(&input);
90 
91 let shift = window_state.modifiers.shift_key();
92 
93 let logical_key = match &input.logical_key {
94 // When keystrokes with ctrl-alt are pressed on Windows, `input.logical_key` is
95 // Unidentified.
96 #[cfg(windows)]
97 Key::Unidentified(NativeKey::Windows(_))
98 if window_state
99 .modifiers
100 .contains(ModifiersState::CONTROL | ModifiersState::ALT) =>
101 {
102 input.key_without_modifiers()
103 }
104 _ => input.logical_key,
105 };
106 let input_key = get_input_key(&logical_key, shift);
107 
108 let key = convert_key(input_key)?.to_string();
109 
110 let keystroke = Keystroke {
111 ctrl: window_state.modifiers.control_key(),
112 alt: window_state.modifiers.alt_key(),
113 shift,
114 cmd: window_state.modifiers.super_key(),
115 meta: false,
116 key,
117 };
118 
119 // Ignore any keystrokes that we're purposefully not handling. (I.e. cmdorctrl-v needs to fall back
120 // to the browser implementation on the web.)
121 if KEYS_TO_IGNORE.contains(&keystroke) {
122 return None;
123 }
124 
125 Some(crate::event::Event::KeyDown {
126 keystroke,
127 chars,
128 details: KeyEventDetails {
129 left_alt: window_state.left_alt_pressed,
130 right_alt: window_state.right_alt_pressed,
131 key_without_modifiers,
132 },
133 is_composing: false,
134 })
135}
136 
137#[cfg(not(target_family = "wasm"))]
138/// Returns the base key without any modifiers applied, or `None` if it cannot be determined.
139fn get_key_without_modifiers(input: &winit::event::KeyEvent) -> Option<String> {
140 let unmodified = input.key_without_modifiers();
141 let unmodified_input = get_input_key(&unmodified, false);
142 convert_key(unmodified_input).map(|k| k.to_string())
143}
144 
145#[cfg(target_family = "wasm")]
146fn get_key_without_modifiers(_input: &winit::event::KeyEvent) -> Option<String> {
147 None
148}
149 
150#[cfg(not(target_family = "wasm"))]
151/// Returns the text of the [`winit::event::KeyEvent`] with the characters modified by `ctrl`.
152/// For example, `Ctrl+a` produces `Some("\x01")`.
153fn text_with_modifiers(
154 key_event: &winit::event::KeyEvent,
155 _modifier_state: ModifiersState,
156) -> Option<&str> {
157 key_event.text_with_all_modifiers()
158}
159 
160#[cfg(target_family = "wasm")]
161fn text_with_modifiers(
162 key_event: &winit::event::KeyEvent,
163 modifier_state: ModifiersState,
164) -> Option<&str> {
165 // Provide the bare-minimum amount of support for mapping modifiers to their corresponding
166 // ASCII character. This is not actually fully functional because keys like `@` require the
167 // addition of the `SHIFT` key, which doesn't yet work here.
168 // TODO(wasm): Extend this to support all of the function/shift/arrow keys.
169 match (modifier_state, &key_event.logical_key) {
170 (ModifiersState::CONTROL, Key::Character(character))
171 if CONTROL_CHARACTER_MAP.contains_key(character.as_str()) =>
172 {
173 CONTROL_CHARACTER_MAP.get(character.as_str()).copied()
174 }
175 (_, key) => key.to_text(),
176 }
177}
178 
179fn get_input_key(logical_key: &Key, is_shift: bool) -> Key {
180 use winit::keyboard::Key::Character;
181 match (logical_key, is_shift) {
182 // If the key is a character AND shift is pressed, we force the key to uppercase.
183 // If the key is a character AND shift is NOT pressed, we force the key to lowercase.
184 // This is to align with existing behavior where we expect bindings with shift
185 // to have uppercase characters, and bindings without shift to have lowercase characters.
186 // See strato_ui::keymap::Keystroke::parse and warp::util::bindings::cmd_or_ctrl_shift.
187 (Character(character), true) => Character(character.to_uppercase().into()),
188 (Character(character), false) => Character(character.to_lowercase().into()),
189 (non_char_key, _) => non_char_key.clone(),
190 }
191}
192 
193/// Converts a winit [`winit::keyboard::Key`] to the corresponding string version
194/// expected by the UI framework.
195fn convert_key(key: Key) -> Option<Cow<'static, str>> {
196 use winit::keyboard::Key::*;
197 
198 let value = match key {
199 Character(char) => return Some(char.to_string().into()),
200 Named(NamedKey::Enter) => "enter",
201 Named(NamedKey::Tab) => "tab",
202 Named(NamedKey::Space) => " ",
203 Named(NamedKey::ArrowDown) => "down",
204 Named(NamedKey::ArrowLeft) => "left",
205 Named(NamedKey::ArrowRight) => "right",
206 Named(NamedKey::ArrowUp) => "up",
207 Named(NamedKey::End) => "end",
208 Named(NamedKey::Home) => "home",
209 Named(NamedKey::PageDown) => "pagedown",
210 Named(NamedKey::PageUp) => "pageup",
211 Named(NamedKey::Backspace) => "backspace",
212 Named(NamedKey::Delete) => "delete",
213 Named(NamedKey::Insert) => "insert",
214 Named(NamedKey::Escape) => "escape",
215 Named(NamedKey::F1) => "f1",
216 Named(NamedKey::F2) => "f2",
217 Named(NamedKey::F3) => "f3",
218 Named(NamedKey::F4) => "f4",
219 Named(NamedKey::F5) => "f5",
220 Named(NamedKey::F6) => "f6",
221 Named(NamedKey::F7) => "f7",
222 Named(NamedKey::F8) => "f8",
223 Named(NamedKey::F9) => "f9",
224 Named(NamedKey::F10) => "f10",
225 Named(NamedKey::F11) => "f11",
226 Named(NamedKey::F12) => "f12",
227 Named(NamedKey::F13) => "f13",
228 Named(NamedKey::F14) => "f14",
229 Named(NamedKey::F15) => "f15",
230 Named(NamedKey::F16) => "f16",
231 Named(NamedKey::F17) => "f17",
232 Named(NamedKey::F18) => "f18",
233 Named(NamedKey::F19) => "f19",
234 Named(NamedKey::F20) => "f20",
235 Named(NamedKey::F21) => "f21",
236 Named(NamedKey::F22) => "f22",
237 Named(NamedKey::F23) => "f23",
238 Named(NamedKey::F24) => "f24",
239 Named(NamedKey::F25) => "f25",
240 Named(NamedKey::F26) => "f26",
241 Named(NamedKey::F27) => "f27",
242 Named(NamedKey::F28) => "f28",
243 Named(NamedKey::F29) => "f29",
244 Named(NamedKey::F30) => "f30",
245 Named(NamedKey::F31) => "f31",
246 Named(NamedKey::F32) => "f32",
247 Named(NamedKey::F33) => "f33",
248 Named(NamedKey::F34) => "f34",
249 Named(NamedKey::F35) => "f35",
250 _ => return None,
251 };
252 
253 Some(Cow::Borrowed(value))
254}
255 
256#[cfg(test)]
257#[path = "key_events_tests.rs"]
258mod tests;
259