Skip to main content

gasm_host/
keymap.rs

1//! Keyboard layouts: the same text format as the web player (`gasm-host.js`
2//! `parseKeymap`). A line per binding: `<pad 1-4> <button> <key code>...`, where key
3//! codes are W3C `KeyboardEvent.code` names (winit's `KeyCode` uses the same names).
4//! Keyboard bindings for pad N >= 2 apply while fewer than N gamepads are connected.
5
6use winit::keyboard::KeyCode;
7
8pub const DEFAULT_KEYMAP: &str = include_str!("default-keymap.txt");
9pub const BUTTONS: [&str; 12] = ["a", "b", "x", "y", "l", "r", "select", "start", "up", "down", "left", "right"];
10
11/// (key, zero-based pad, button bit)
12pub type Keymap = Vec<(KeyCode, usize, u32)>;
13
14pub fn parse(text: &str) -> Result<Keymap, String> {
15    let mut map = Vec::new();
16    let mut errors = Vec::new();
17    for (i, raw) in text.lines().enumerate() {
18        let line = raw.split('#').next().unwrap_or("").trim();
19        if line.is_empty() {
20            continue;
21        }
22        let mut parts = line.split_whitespace();
23        let (pad, button) = (parts.next(), parts.next());
24        let keys: Vec<&str> = parts.collect();
25        let pad = pad.and_then(|p| p.parse::<usize>().ok()).filter(|p| (1..=4).contains(p));
26        let bit = button.and_then(|b| BUTTONS.iter().position(|n| n.eq_ignore_ascii_case(b)));
27        let (Some(pad), Some(bit), false) = (pad, bit, keys.is_empty()) else {
28            errors.push(format!("line {}: expected \"<pad 1-4> <button> <key>...\", got {line:?}", i + 1));
29            continue;
30        };
31        for k in keys {
32            match key_from_code(normalize(k)) {
33                Some(KeyCode::Escape) => errors.push(format!("line {}: Escape is reserved (quit)", i + 1)),
34                Some(code) => map.push((code, pad - 1, 1u32 << bit)),
35                None => errors.push(format!("line {}: unknown key code {k:?}", i + 1)),
36            }
37        }
38    }
39    if errors.is_empty() { Ok(map) } else { Err(errors.join("\n")) }
40}
41
42/// Pads from held keys; keyboard pad N (zero-based n >= 1) only while `gamepads <= n`.
43pub fn pads(map: &Keymap, held: &std::collections::HashSet<KeyCode>, gamepads: usize) -> [u32; 4] {
44    let mut pads = [0u32; 4];
45    for (key, pad, bit) in map {
46        if held.contains(key) && (*pad == 0 || gamepads <= *pad) {
47            pads[*pad] |= bit;
48        }
49    }
50    pads
51}
52
53macro_rules! codes {
54    ($($name:ident),* $(,)?) => {
55        /// W3C KeyboardEvent.code name -> winit KeyCode.
56        pub fn key_from_code(s: &str) -> Option<KeyCode> {
57            match s { $(stringify!($name) => Some(KeyCode::$name),)* _ => None }
58        }
59    };
60}
61
62codes!(
63    KeyA, KeyB, KeyC, KeyD, KeyE, KeyF, KeyG, KeyH, KeyI, KeyJ, KeyK, KeyL, KeyM,
64    KeyN, KeyO, KeyP, KeyQ, KeyR, KeyS, KeyT, KeyU, KeyV, KeyW, KeyX, KeyY, KeyZ,
65    Digit0, Digit1, Digit2, Digit3, Digit4, Digit5, Digit6, Digit7, Digit8, Digit9,
66    ArrowUp, ArrowDown, ArrowLeft, ArrowRight,
67    Numpad0, Numpad1, Numpad2, Numpad3, Numpad4, Numpad5, Numpad6, Numpad7, Numpad8, Numpad9,
68    NumpadAdd, NumpadSubtract, NumpadMultiply, NumpadDivide, NumpadDecimal, NumpadEnter, NumpadEqual, NumpadComma,
69    F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
70    Space, Enter, Tab, Backspace, Escape, CapsLock,
71    ShiftLeft, ShiftRight, ControlLeft, ControlRight, AltLeft, AltRight, SuperLeft, SuperRight,
72    Comma, Period, Slash, Semicolon, Quote, BracketLeft, BracketRight, Backslash, Minus, Equal, Backquote, IntlBackslash,
73    Insert, Delete, Home, End, PageUp, PageDown,
74    F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24,
75    PrintScreen, ScrollLock, Pause, NumLock, ContextMenu, IntlRo, IntlYen,
76);
77
78/// gasm raw key codes (GASM_KEY_*): index = code, W3C KeyboardEvent.code names.
79/// Must equal spec/abi.json (checked by scripts/gen-abi.mjs --check).
80pub const KEY_CODES: [&str; 122] = [
81    "", "Escape", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "Backquote", "Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "Minus", "Equal", "Backspace", "Tab", "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", "BracketLeft", "BracketRight", "Backslash", "CapsLock", "Semicolon", "Quote", "Enter", "ShiftLeft", "IntlBackslash", "Comma", "Period", "Slash", "ShiftRight", "ControlLeft", "MetaLeft", "AltLeft", "Space", "AltRight", "MetaRight", "ContextMenu", "ControlRight", "PrintScreen", "ScrollLock", "Pause", "Insert", "Home", "PageUp", "Delete", "End", "PageDown", "ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight", "NumLock", "NumpadDivide", "NumpadMultiply", "NumpadSubtract", "NumpadAdd", "NumpadEnter", "NumpadDecimal", "Numpad0", "Numpad1", "Numpad2", "Numpad3", "Numpad4", "Numpad5", "Numpad6", "Numpad7", "Numpad8", "Numpad9", "NumpadEqual", "NumpadComma", "IntlRo", "IntlYen", "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24",
82];
83
84/// GASM_KEY_* code of a winit key (0 if it has none).
85pub fn gasm_key(code: KeyCode) -> u16 {
86    use std::sync::OnceLock;
87    static MAP: OnceLock<Vec<(KeyCode, u16)>> = OnceLock::new();
88    MAP.get_or_init(|| {
89        KEY_CODES.iter().enumerate().skip(1).filter_map(|(i, n)| key_from_code(normalize(n)).map(|k| (k, i as u16))).collect()
90    })
91    .iter()
92    .find(|(k, _)| *k == code)
93    .map_or(0, |(_, i)| *i)
94}
95
96/// Browser names for keys whose winit name differs.
97pub fn normalize(s: &str) -> &str {
98    match s { "MetaLeft" | "OSLeft" => "SuperLeft", "MetaRight" | "OSRight" => "SuperRight", other => other }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::collections::HashSet;
105
106    #[test]
107    fn default_keymap_parses_and_two_players_share_a_keyboard() {
108        let map = parse(DEFAULT_KEYMAP).unwrap();
109        let held: HashSet<_> = [KeyCode::KeyX, KeyCode::KeyL, KeyCode::ControlRight].into();
110        let p = pads(&map, &held, 0);
111        assert_eq!(p[0], 1 << 0); // X = A on pad 1
112        assert_eq!(p[1], (1 << 11) | (1 << 7)); // L = right, RCtrl = start on pad 2
113        assert_eq!(pads(&map, &held, 2)[1], 0); // two gamepads: keyboard player 2 off
114        assert!(parse("1 jump KeyX").is_err());
115        assert!(parse("5 a KeyX").is_err());
116        assert!(parse("1 a Escape").is_err());
117    }
118}