1#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct KeyDescriptor {
23 pub key: String,
25 pub code: Option<&'static str>,
27 pub virtual_key_code: Option<i64>,
30 pub text: Option<String>,
33}
34
35pub fn describe_key(key: &str, control_or_meta_held: bool) -> KeyDescriptor {
41 let (canonical, code, vk, text) = named_key(key)
42 .or_else(|| printable_key(key))
43 .unwrap_or((None, None, None, None));
44 KeyDescriptor {
45 key: canonical.unwrap_or_else(|| key.to_string()),
54 code,
55 virtual_key_code: vk,
56 text: if control_or_meta_held { None } else { text },
57 }
58}
59
60type KeyFacts = (
62 Option<String>,
63 Option<&'static str>,
64 Option<i64>,
65 Option<String>,
66);
67
68fn dom(name: &str) -> Option<String> {
72 Some(name.to_string())
73}
74
75fn named_key(key: &str) -> Option<KeyFacts> {
76 let facts: KeyFacts = match key {
77 "Backspace" => (dom("Backspace"), Some("Backspace"), Some(8), None),
80 "Tab" => (dom("Tab"), Some("Tab"), Some(9), None),
84 "Enter" | "Return" => (
85 dom("Enter"),
86 Some("Enter"),
87 Some(13),
88 Some("\r".to_string()),
89 ),
90 "Escape" | "Esc" => (dom("Escape"), Some("Escape"), Some(27), None),
91 "Delete" => (dom("Delete"), Some("Delete"), Some(46), None),
92 " " | "Space" | "Spacebar" => (dom(" "), Some("Space"), Some(32), Some(" ".to_string())),
93 "ArrowLeft" | "Left" => (dom("ArrowLeft"), Some("ArrowLeft"), Some(37), None),
95 "ArrowUp" | "Up" => (dom("ArrowUp"), Some("ArrowUp"), Some(38), None),
96 "ArrowRight" | "Right" => (dom("ArrowRight"), Some("ArrowRight"), Some(39), None),
97 "ArrowDown" | "Down" => (dom("ArrowDown"), Some("ArrowDown"), Some(40), None),
98 "Home" => (dom("Home"), Some("Home"), Some(36), None),
99 "End" => (dom("End"), Some("End"), Some(35), None),
100 "PageUp" => (dom("PageUp"), Some("PageUp"), Some(33), None),
101 "PageDown" => (dom("PageDown"), Some("PageDown"), Some(34), None),
102 "Shift" => (dom("Shift"), Some("ShiftLeft"), Some(16), None),
104 "Control" => (dom("Control"), Some("ControlLeft"), Some(17), None),
105 "Alt" => (dom("Alt"), Some("AltLeft"), Some(18), None),
106 "Meta" => (dom("Meta"), Some("MetaLeft"), Some(91), None),
107 _ => return None,
108 };
109 Some(facts)
110}
111
112fn printable_key(key: &str) -> Option<KeyFacts> {
116 let mut chars = key.chars();
117 let ch = chars.next()?;
118 if chars.next().is_some() {
119 return None;
122 }
123 let text = Some(ch.to_string());
124 let upper = ch.to_ascii_uppercase();
127 let (code, vk) = match upper {
128 'A'..='Z' => (
129 Some(LETTER_CODES[(upper as u8 - b'A') as usize]),
130 Some(upper as i64),
131 ),
132 '0'..='9' => (
133 Some(DIGIT_CODES[(upper as u8 - b'0') as usize]),
134 Some(upper as i64),
135 ),
136 _ => (None, None),
140 };
141 Some((Some(key.to_string()), code, vk, text))
143}
144
145const LETTER_CODES: [&str; 26] = [
146 "KeyA", "KeyB", "KeyC", "KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL",
147 "KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT", "KeyU", "KeyV", "KeyW", "KeyX",
148 "KeyY", "KeyZ",
149];
150
151const DIGIT_CODES: [&str; 10] = [
152 "Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8",
153 "Digit9",
154];
155
156#[cfg(test)]
157mod tests {
158 #[test]
164 fn every_alias_reaches_the_page_under_its_canonical_dom_name() {
165 for (alias, canonical) in [
166 ("Return", "Enter"),
167 ("Enter", "Enter"),
168 ("Esc", "Escape"),
169 ("Escape", "Escape"),
170 ("Left", "ArrowLeft"),
171 ("Up", "ArrowUp"),
172 ("Right", "ArrowRight"),
173 ("Down", "ArrowDown"),
174 ("Space", " "),
175 ("Spacebar", " "),
176 (" ", " "),
177 ] {
178 assert_eq!(
179 super::describe_key(alias, false).key,
180 canonical,
181 "{alias} must be delivered as {canonical}"
182 );
183 }
184 assert_eq!(super::describe_key("a", false).key, "a");
187 assert_eq!(super::describe_key("F13", false).key, "F13");
188 }
189
190 use super::*;
191
192 #[test]
196 fn backspace_carries_the_virtual_key_code_that_makes_it_delete() {
197 let d = describe_key("Backspace", false);
198 assert_eq!(d.virtual_key_code, Some(8));
199 assert_eq!(d.code, Some("Backspace"));
200 assert_eq!(d.text, None, "Backspace inserts nothing");
201 }
202
203 #[test]
204 fn the_arrows_carry_their_virtual_key_codes() {
205 for (key, vk) in [
206 ("ArrowLeft", 37),
207 ("ArrowUp", 38),
208 ("ArrowRight", 39),
209 ("ArrowDown", 40),
210 ] {
211 let d = describe_key(key, false);
212 assert_eq!(d.virtual_key_code, Some(vk), "{key}");
213 assert_eq!(d.text, None, "{key} moves the caret, it does not type");
214 }
215 }
216
217 #[test]
219 fn enter_carries_both_the_key_code_and_the_carriage_return() {
220 let d = describe_key("Enter", false);
221 assert_eq!(d.virtual_key_code, Some(13));
222 assert_eq!(d.text.as_deref(), Some("\r"));
223 }
224
225 #[test]
228 fn tab_moves_focus_without_typing_anything() {
229 let d = describe_key("Tab", false);
230 assert_eq!(d.code, Some("Tab"));
231 assert_eq!(d.virtual_key_code, Some(9));
232 assert_eq!(d.text, None);
233 }
234
235 #[test]
236 fn a_printable_letter_types_itself() {
237 let d = describe_key("a", false);
238 assert_eq!(d.text.as_deref(), Some("a"));
239 assert_eq!(d.code, Some("KeyA"));
240 assert_eq!(
241 d.virtual_key_code,
242 Some(65),
243 "virtual key codes are defined on the uppercase letter"
244 );
245 assert_eq!(describe_key("A", false).virtual_key_code, Some(65));
246 }
247
248 #[test]
249 fn a_digit_types_itself() {
250 let d = describe_key("7", false);
251 assert_eq!(d.text.as_deref(), Some("7"));
252 assert_eq!(d.code, Some("Digit7"));
253 assert_eq!(d.virtual_key_code, Some(b'7' as i64));
254 }
255
256 #[test]
259 fn a_shortcut_does_not_also_type_its_letter() {
260 let plain = describe_key("a", false);
261 let shortcut = describe_key("a", true);
262 assert_eq!(plain.text.as_deref(), Some("a"));
263 assert_eq!(shortcut.text, None);
264 assert_eq!(
265 shortcut.virtual_key_code, plain.virtual_key_code,
266 "the key itself is unchanged — only what it inserts"
267 );
268 }
269
270 #[test]
271 fn punctuation_still_types_even_without_a_physical_key_description() {
272 let d = describe_key("!", false);
273 assert_eq!(d.text.as_deref(), Some("!"));
274 assert_eq!(d.code, None, "guessing the physical key would be worse");
275 }
276
277 #[test]
280 fn an_unknown_key_name_still_dispatches_by_name() {
281 let d = describe_key("F13", false);
282 assert_eq!(d.key, "F13");
283 assert_eq!(d.code, None);
284 assert_eq!(d.virtual_key_code, None);
285 assert_eq!(d.text, None);
286 }
287
288 #[test]
289 fn space_is_recognised_by_every_spelling_a_client_might_send() {
290 for key in [" ", "Space", "Spacebar"] {
291 let d = describe_key(key, false);
292 assert_eq!(d.virtual_key_code, Some(32), "{key}");
293 assert_eq!(d.text.as_deref(), Some(" "), "{key}");
294 }
295 }
296}