Skip to main content

concinnity_core/components/
input_key.rs

1// src/components/input_key.rs
2
3// Declare a key enum from one table, so the variant list, the serde spelling,
4// the short label, and the exhaustive `ALL` array cannot drift apart. Each
5// entry is `Variant` (its label is the variant name) or `Variant => "label"`
6// when the settings menu shows something shorter.
7// The settings-menu label for one table entry: the override when given, the
8// variant name otherwise.
9
10macro_rules! key_label {
11    ($variant:ident) => {
12        stringify!($variant)
13    };
14    ($variant:ident => $label:literal) => {
15        $label
16    };
17}
18
19macro_rules! define_keys {
20    ($($variant:ident $(=> $label:literal)?),* $(,)?) => {
21        /// A canonical, backend-agnostic keyboard key.
22        ///
23        /// Each rendering backend maps its native key codes (macOS NSEvent key
24        /// codes, Windows virtual keys, GLFW keys) to and from this enum, so a
25        /// key binding can be stored and shown the same way everywhere. Unit
26        /// variants serialize to their name, so a persisted binding survives a
27        /// build.
28        #[derive(
29            Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
30        )]
31        // Each variant is one key name; the vocabulary is described above
32        // rather than restated per variant.
33        #[expect(missing_docs, reason = "each variant is one key name; the vocabulary is documented on the enum")]
34        pub enum InputKey {
35            $($variant),*
36        }
37
38        impl InputKey {
39            /// Every declared key, in declaration order.
40            pub const ALL: &'static [InputKey] = &[$(InputKey::$variant),*];
41
42            /// The canonical variant name, matching the serialized form and how
43            /// a [KeyBinding](#keybinding) stores its `key` (e.g. `"W"`,
44            /// `"Space"`, `"Enter"`, `"Control"`). Unlike
45            /// [display_name](#method.display_name) this is the exact
46            /// enum-variant spelling, so it round-trips with serde.
47            pub fn name(self) -> &'static str {
48                match self {
49                    $(InputKey::$variant => stringify!($variant)),*
50                }
51            }
52
53            /// A short label for the settings menu (e.g. `"W"`, `"Space"`,
54            /// `"Ctrl"`). Defaults to [name](#method.name) unless the key
55            /// declared a shorter one.
56            pub fn display_name(self) -> &'static str {
57                match self {
58                    $(InputKey::$variant => key_label!($variant $(=> $label)?)),*
59                }
60            }
61        }
62    };
63}
64
65define_keys! {
66    A, B, C, D, E, F, G, H, I, J, K, L, M,
67    N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
68    Num0 => "0",
69    Num1 => "1",
70    Num2 => "2",
71    Num3 => "3",
72    Num4 => "4",
73    Num5 => "5",
74    Num6 => "6",
75    Num7 => "7",
76    Num8 => "8",
77    Num9 => "9",
78    Space,
79    Tab,
80    Enter,
81    Backspace => "Bksp",
82    Delete => "Del",
83    Shift,
84    Control => "Ctrl",
85    Alt,
86    Up,
87    Down,
88    Left,
89    Right,
90    Minus => "-",
91    Equals => "=",
92    LeftBracket => "[",
93    RightBracket => "]",
94    Backslash => "\\",
95    Semicolon => ";",
96    Quote => "'",
97    Comma => ",",
98    Period => ".",
99    Slash => "/",
100    Backtick => "`",
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use alloc::format;
107
108    #[test]
109    fn serializes_to_variant_name() {
110        // A unit variant serializes to its name, so a persisted binding is
111        // readable and stable across builds.
112        let json = serde_json::to_string(&InputKey::W).unwrap();
113        assert_eq!(json, "\"W\"");
114        let back: InputKey = serde_json::from_str(&json).unwrap();
115        assert_eq!(back, InputKey::W);
116    }
117
118    #[test]
119    fn display_names_are_short() {
120        assert_eq!(InputKey::W.display_name(), "W");
121        assert_eq!(InputKey::Space.display_name(), "Space");
122        assert_eq!(InputKey::Shift.display_name(), "Shift");
123        assert_eq!(InputKey::Num1.display_name(), "1");
124        assert_eq!(InputKey::Backspace.display_name(), "Bksp");
125        assert_eq!(InputKey::Control.display_name(), "Ctrl");
126        assert_eq!(InputKey::Minus.display_name(), "-");
127    }
128
129    #[test]
130    fn all_variants_cover_name_and_display() {
131        // For every variant: name() and display_name() are non-empty, name()
132        // equals the serde spelling, and the binding round-trips. This walks
133        // both full match statements, not just a hand-picked sample.
134        for &key in InputKey::ALL {
135            assert!(!key.name().is_empty(), "name empty for {key:?}");
136            assert!(!key.display_name().is_empty(), "display empty for {key:?}");
137            let json = serde_json::to_string(&key).unwrap();
138            assert_eq!(
139                json,
140                format!("\"{}\"", key.name()),
141                "serde vs name for {key:?}"
142            );
143            let back: InputKey = serde_json::from_str(&json).unwrap();
144            assert_eq!(back, key, "round trip for {key:?}");
145        }
146    }
147
148    #[test]
149    fn variant_names_are_unique() {
150        // Names double as persisted identifiers, so no two variants may share
151        // one.
152        let mut seen = alloc::collections::BTreeSet::new();
153        for &key in InputKey::ALL {
154            assert!(seen.insert(key.name()), "duplicate name {}", key.name());
155        }
156    }
157}