#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyDescriptor {
pub key: String,
pub code: Option<&'static str>,
pub virtual_key_code: Option<i64>,
pub text: Option<String>,
}
pub fn describe_key(key: &str, control_or_meta_held: bool) -> KeyDescriptor {
let (canonical, code, vk, text) = named_key(key)
.or_else(|| printable_key(key))
.unwrap_or((None, None, None, None));
KeyDescriptor {
key: canonical.unwrap_or_else(|| key.to_string()),
code,
virtual_key_code: vk,
text: if control_or_meta_held { None } else { text },
}
}
type KeyFacts = (
Option<String>,
Option<&'static str>,
Option<i64>,
Option<String>,
);
fn dom(name: &str) -> Option<String> {
Some(name.to_string())
}
fn named_key(key: &str) -> Option<KeyFacts> {
let facts: KeyFacts = match key {
"Backspace" => (dom("Backspace"), Some("Backspace"), Some(8), None),
"Tab" => (dom("Tab"), Some("Tab"), Some(9), None),
"Enter" | "Return" => (
dom("Enter"),
Some("Enter"),
Some(13),
Some("\r".to_string()),
),
"Escape" | "Esc" => (dom("Escape"), Some("Escape"), Some(27), None),
"Delete" => (dom("Delete"), Some("Delete"), Some(46), None),
" " | "Space" | "Spacebar" => (dom(" "), Some("Space"), Some(32), Some(" ".to_string())),
"ArrowLeft" | "Left" => (dom("ArrowLeft"), Some("ArrowLeft"), Some(37), None),
"ArrowUp" | "Up" => (dom("ArrowUp"), Some("ArrowUp"), Some(38), None),
"ArrowRight" | "Right" => (dom("ArrowRight"), Some("ArrowRight"), Some(39), None),
"ArrowDown" | "Down" => (dom("ArrowDown"), Some("ArrowDown"), Some(40), None),
"Home" => (dom("Home"), Some("Home"), Some(36), None),
"End" => (dom("End"), Some("End"), Some(35), None),
"PageUp" => (dom("PageUp"), Some("PageUp"), Some(33), None),
"PageDown" => (dom("PageDown"), Some("PageDown"), Some(34), None),
"Shift" => (dom("Shift"), Some("ShiftLeft"), Some(16), None),
"Control" => (dom("Control"), Some("ControlLeft"), Some(17), None),
"Alt" => (dom("Alt"), Some("AltLeft"), Some(18), None),
"Meta" => (dom("Meta"), Some("MetaLeft"), Some(91), None),
_ => return None,
};
Some(facts)
}
fn printable_key(key: &str) -> Option<KeyFacts> {
let mut chars = key.chars();
let ch = chars.next()?;
if chars.next().is_some() {
return None;
}
let text = Some(ch.to_string());
let upper = ch.to_ascii_uppercase();
let (code, vk) = match upper {
'A'..='Z' => (
Some(LETTER_CODES[(upper as u8 - b'A') as usize]),
Some(upper as i64),
),
'0'..='9' => (
Some(DIGIT_CODES[(upper as u8 - b'0') as usize]),
Some(upper as i64),
),
_ => (None, None),
};
Some((Some(key.to_string()), code, vk, text))
}
const LETTER_CODES: [&str; 26] = [
"KeyA", "KeyB", "KeyC", "KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL",
"KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT", "KeyU", "KeyV", "KeyW", "KeyX",
"KeyY", "KeyZ",
];
const DIGIT_CODES: [&str; 10] = [
"Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8",
"Digit9",
];
#[cfg(test)]
mod tests {
#[test]
fn every_alias_reaches_the_page_under_its_canonical_dom_name() {
for (alias, canonical) in [
("Return", "Enter"),
("Enter", "Enter"),
("Esc", "Escape"),
("Escape", "Escape"),
("Left", "ArrowLeft"),
("Up", "ArrowUp"),
("Right", "ArrowRight"),
("Down", "ArrowDown"),
("Space", " "),
("Spacebar", " "),
(" ", " "),
] {
assert_eq!(
super::describe_key(alias, false).key,
canonical,
"{alias} must be delivered as {canonical}"
);
}
assert_eq!(super::describe_key("a", false).key, "a");
assert_eq!(super::describe_key("F13", false).key, "F13");
}
use super::*;
#[test]
fn backspace_carries_the_virtual_key_code_that_makes_it_delete() {
let d = describe_key("Backspace", false);
assert_eq!(d.virtual_key_code, Some(8));
assert_eq!(d.code, Some("Backspace"));
assert_eq!(d.text, None, "Backspace inserts nothing");
}
#[test]
fn the_arrows_carry_their_virtual_key_codes() {
for (key, vk) in [
("ArrowLeft", 37),
("ArrowUp", 38),
("ArrowRight", 39),
("ArrowDown", 40),
] {
let d = describe_key(key, false);
assert_eq!(d.virtual_key_code, Some(vk), "{key}");
assert_eq!(d.text, None, "{key} moves the caret, it does not type");
}
}
#[test]
fn enter_carries_both_the_key_code_and_the_carriage_return() {
let d = describe_key("Enter", false);
assert_eq!(d.virtual_key_code, Some(13));
assert_eq!(d.text.as_deref(), Some("\r"));
}
#[test]
fn tab_moves_focus_without_typing_anything() {
let d = describe_key("Tab", false);
assert_eq!(d.code, Some("Tab"));
assert_eq!(d.virtual_key_code, Some(9));
assert_eq!(d.text, None);
}
#[test]
fn a_printable_letter_types_itself() {
let d = describe_key("a", false);
assert_eq!(d.text.as_deref(), Some("a"));
assert_eq!(d.code, Some("KeyA"));
assert_eq!(
d.virtual_key_code,
Some(65),
"virtual key codes are defined on the uppercase letter"
);
assert_eq!(describe_key("A", false).virtual_key_code, Some(65));
}
#[test]
fn a_digit_types_itself() {
let d = describe_key("7", false);
assert_eq!(d.text.as_deref(), Some("7"));
assert_eq!(d.code, Some("Digit7"));
assert_eq!(d.virtual_key_code, Some(b'7' as i64));
}
#[test]
fn a_shortcut_does_not_also_type_its_letter() {
let plain = describe_key("a", false);
let shortcut = describe_key("a", true);
assert_eq!(plain.text.as_deref(), Some("a"));
assert_eq!(shortcut.text, None);
assert_eq!(
shortcut.virtual_key_code, plain.virtual_key_code,
"the key itself is unchanged — only what it inserts"
);
}
#[test]
fn punctuation_still_types_even_without_a_physical_key_description() {
let d = describe_key("!", false);
assert_eq!(d.text.as_deref(), Some("!"));
assert_eq!(d.code, None, "guessing the physical key would be worse");
}
#[test]
fn an_unknown_key_name_still_dispatches_by_name() {
let d = describe_key("F13", false);
assert_eq!(d.key, "F13");
assert_eq!(d.code, None);
assert_eq!(d.virtual_key_code, None);
assert_eq!(d.text, None);
}
#[test]
fn space_is_recognised_by_every_spelling_a_client_might_send() {
for key in [" ", "Space", "Spacebar"] {
let d = describe_key(key, false);
assert_eq!(d.virtual_key_code, Some(32), "{key}");
assert_eq!(d.text.as_deref(), Some(" "), "{key}");
}
}
}