StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use std::borrow::Cow; |
| 2 | use std::collections::HashMap; |
| 3 | |
| 4 | use lazy_static::lazy_static; |
| 5 | |
| 6 | use winit::event::ElementState; |
| 7 | #[cfg(windows)] |
| 8 | use winit::keyboard::NativeKey; |
| 9 | use winit::keyboard::{Key, ModifiersState, NamedKey}; |
| 10 | #[cfg(not(target_family = "wasm"))] |
| 11 | use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; |
| 12 | |
| 13 | use crate::platform::KEYS_TO_IGNORE; |
| 14 | use crate::{event::KeyEventDetails, keymap::Keystroke}; |
| 15 | |
| 16 | use super::WindowState; |
| 17 | |
| 18 | lazy_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. |
| 60 | pub 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. |
| 139 | fn 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")] |
| 146 | fn 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")`. |
| 153 | fn 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")] |
| 161 | fn 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 | |
| 179 | fn 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. |
| 195 | fn 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"] |
| 258 | mod tests; |
| 259 |