Skip to main content

concinnity_core/components/
gamepad_button.rs

1// src/components/gamepad_button.rs
2
3// Declare the button enum from one table, so the variant list, the serde
4// spelling, the short label, and the exhaustive `ALL` slice cannot drift
5// apart. Each entry is `Variant`, or `Variant => "label"` when the settings
6// menu shows something shorter.
7
8macro_rules! button_label {
9    ($variant:ident) => {
10        stringify!($variant)
11    };
12    ($variant:ident => $label:literal) => {
13        $label
14    };
15}
16
17macro_rules! define_buttons {
18    ($($variant:ident $(=> $label:literal)?),* $(,)?) => {
19        /// A canonical, vendor-neutral gamepad button.
20        ///
21        /// Face buttons are named by position (`South` is the bottom face
22        /// button: Xbox A, PlayStation Cross) so a persisted binding reads the
23        /// same for every controller brand. Unit variants serialize to their
24        /// name, so a persisted binding survives a build, like [InputKey](#inputkey).
25        #[derive(
26            Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
27        )]
28        // Each variant is one button name; the vocabulary is described above
29        // rather than restated per variant.
30        #[expect(missing_docs, reason = "each variant is one button name; the vocabulary is documented on the enum")]
31        pub enum GamepadButton {
32            $($variant),*
33        }
34
35        impl GamepadButton {
36            /// Every declared button, in a stable display order.
37            pub const ALL: &'static [GamepadButton] = &[$(GamepadButton::$variant),*];
38
39            /// The canonical variant name, matching the serialized form (e.g.
40            /// `"South"`, `"LeftShoulder"`). Like [InputKey::name](#method.name)
41            /// this is the exact enum-variant spelling, so it round-trips with
42            /// serde.
43            pub fn name(self) -> &'static str {
44                match self {
45                    $(GamepadButton::$variant => stringify!($variant)),*
46                }
47            }
48
49            /// A short label for the settings menu (e.g. `"South"`, `"LB"`,
50            /// `"L3"`). Defaults to [name](#method.name) unless the button
51            /// declared a shorter one.
52            pub fn display_name(self) -> &'static str {
53                match self {
54                    $(GamepadButton::$variant => button_label!($variant $(=> $label)?)),*
55                }
56            }
57        }
58    };
59}
60
61define_buttons! {
62    South,
63    East,
64    West,
65    North,
66    LeftShoulder => "LB",
67    RightShoulder => "RB",
68    LeftTrigger => "LT",
69    RightTrigger => "RT",
70    LeftStick => "L3",
71    RightStick => "R3",
72    DpadUp => "D-Up",
73    DpadDown => "D-Down",
74    DpadLeft => "D-Left",
75    DpadRight => "D-Right",
76    Start,
77    Select,
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use alloc::format;
84
85    #[test]
86    fn serializes_to_variant_name() {
87        let json = serde_json::to_string(&GamepadButton::South).unwrap();
88        assert_eq!(json, "\"South\"");
89        let back: GamepadButton = serde_json::from_str(&json).unwrap();
90        assert_eq!(back, GamepadButton::South);
91    }
92
93    #[test]
94    fn all_variants_cover_name_and_display() {
95        // For every variant: name() and display_name() are non-empty, name()
96        // equals the serde spelling, and the binding round-trips.
97        for &button in GamepadButton::ALL {
98            assert!(!button.name().is_empty(), "name empty for {button:?}");
99            assert!(
100                !button.display_name().is_empty(),
101                "display empty for {button:?}"
102            );
103            let json = serde_json::to_string(&button).unwrap();
104            assert_eq!(
105                json,
106                format!("\"{}\"", button.name()),
107                "serde vs name for {button:?}"
108            );
109            let back: GamepadButton = serde_json::from_str(&json).unwrap();
110            assert_eq!(back, button, "round trip for {button:?}");
111        }
112    }
113
114    #[test]
115    fn variant_names_are_unique() {
116        // Names double as persisted identifiers, so no two variants may share
117        // one (this also guards ALL against an accidental duplicate).
118        let mut seen = alloc::collections::BTreeSet::new();
119        for &button in GamepadButton::ALL {
120            assert!(
121                seen.insert(button.name()),
122                "duplicate name {}",
123                button.name()
124            );
125        }
126        assert_eq!(seen.len(), GamepadButton::ALL.len());
127    }
128}