Skip to main content

arcane_engine/platform/
gamepad.rs

1use std::collections::HashSet;
2
3/// Standard gamepad button names (Xbox layout as canonical).
4/// These match the TypeScript GamepadButton type.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum GamepadButton {
7    A,
8    B,
9    X,
10    Y,
11    LeftBumper,
12    RightBumper,
13    LeftTrigger,
14    RightTrigger,
15    Select,
16    Start,
17    LeftStick,
18    RightStick,
19    DPadUp,
20    DPadDown,
21    DPadLeft,
22    DPadRight,
23    Guide,
24}
25
26impl GamepadButton {
27    /// Parse from the TS string name.
28    pub fn from_str(s: &str) -> Option<Self> {
29        match s {
30            "A" => Some(Self::A),
31            "B" => Some(Self::B),
32            "X" => Some(Self::X),
33            "Y" => Some(Self::Y),
34            "LeftBumper" => Some(Self::LeftBumper),
35            "RightBumper" => Some(Self::RightBumper),
36            "LeftTrigger" => Some(Self::LeftTrigger),
37            "RightTrigger" => Some(Self::RightTrigger),
38            "Select" => Some(Self::Select),
39            "Start" => Some(Self::Start),
40            "LeftStick" => Some(Self::LeftStick),
41            "RightStick" => Some(Self::RightStick),
42            "DPadUp" => Some(Self::DPadUp),
43            "DPadDown" => Some(Self::DPadDown),
44            "DPadLeft" => Some(Self::DPadLeft),
45            "DPadRight" => Some(Self::DPadRight),
46            "Guide" => Some(Self::Guide),
47            _ => None,
48        }
49    }
50
51    /// Convert to the TS string name.
52    pub fn as_str(&self) -> &'static str {
53        match self {
54            Self::A => "A",
55            Self::B => "B",
56            Self::X => "X",
57            Self::Y => "Y",
58            Self::LeftBumper => "LeftBumper",
59            Self::RightBumper => "RightBumper",
60            Self::LeftTrigger => "LeftTrigger",
61            Self::RightTrigger => "RightTrigger",
62            Self::Select => "Select",
63            Self::Start => "Start",
64            Self::LeftStick => "LeftStick",
65            Self::RightStick => "RightStick",
66            Self::DPadUp => "DPadUp",
67            Self::DPadDown => "DPadDown",
68            Self::DPadLeft => "DPadLeft",
69            Self::DPadRight => "DPadRight",
70            Self::Guide => "Guide",
71        }
72    }
73}
74
75/// Standard gamepad axis names.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum GamepadAxis {
78    LeftStickX,
79    LeftStickY,
80    RightStickX,
81    RightStickY,
82    LeftTrigger,
83    RightTrigger,
84}
85
86impl GamepadAxis {
87    pub fn from_str(s: &str) -> Option<Self> {
88        match s {
89            "LeftStickX" => Some(Self::LeftStickX),
90            "LeftStickY" => Some(Self::LeftStickY),
91            "RightStickX" => Some(Self::RightStickX),
92            "RightStickY" => Some(Self::RightStickY),
93            "LeftTrigger" => Some(Self::LeftTrigger),
94            "RightTrigger" => Some(Self::RightTrigger),
95            _ => None,
96        }
97    }
98}
99
100/// Gamepad state snapshot for a single gamepad.
101#[derive(Debug, Clone)]
102pub struct GamepadState {
103    pub name: String,
104    pub connected: bool,
105    /// Buttons currently held.
106    pub buttons_down: HashSet<GamepadButton>,
107    /// Buttons pressed this frame.
108    pub buttons_pressed: HashSet<GamepadButton>,
109    /// Buttons released this frame.
110    pub buttons_released: HashSet<GamepadButton>,
111    /// Axis values (-1.0 to 1.0 for sticks, 0.0 to 1.0 for triggers).
112    pub axes: [f32; 6], // Indexed by GamepadAxis discriminant order
113}
114
115impl Default for GamepadState {
116    fn default() -> Self {
117        Self {
118            name: String::new(),
119            connected: false,
120            buttons_down: HashSet::new(),
121            buttons_pressed: HashSet::new(),
122            buttons_released: HashSet::new(),
123            axes: [0.0; 6],
124        }
125    }
126}
127
128impl GamepadState {
129    pub fn begin_frame(&mut self) {
130        self.buttons_pressed.clear();
131        self.buttons_released.clear();
132    }
133
134    pub fn button_down(&mut self, button: GamepadButton) {
135        if self.buttons_down.insert(button) {
136            self.buttons_pressed.insert(button);
137        }
138    }
139
140    pub fn button_up(&mut self, button: GamepadButton) {
141        if self.buttons_down.remove(&button) {
142            self.buttons_released.insert(button);
143        }
144    }
145
146    pub fn set_axis(&mut self, axis: GamepadAxis, value: f32) {
147        let idx = axis as usize;
148        if idx < self.axes.len() {
149            self.axes[idx] = value;
150        }
151    }
152
153    pub fn get_axis(&self, axis: GamepadAxis) -> f32 {
154        let idx = axis as usize;
155        if idx < self.axes.len() {
156            self.axes[idx]
157        } else {
158            0.0
159        }
160    }
161
162    pub fn is_button_down(&self, button: GamepadButton) -> bool {
163        self.buttons_down.contains(&button)
164    }
165
166    pub fn is_button_pressed(&self, button: GamepadButton) -> bool {
167        self.buttons_pressed.contains(&button)
168    }
169}
170
171/// Manages all connected gamepads. Wraps gilrs.
172pub struct GamepadManager {
173    gilrs: gilrs::Gilrs,
174    /// State for each gamepad slot (up to 4).
175    pub gamepads: [GamepadState; 4],
176    /// gilrs ID -> slot index mapping.
177    id_to_slot: std::collections::HashMap<gilrs::GamepadId, usize>,
178    /// Number of connected gamepads.
179    pub connected_count: u32,
180}
181
182impl GamepadManager {
183    pub fn new() -> Option<Self> {
184        let gilrs = match gilrs::Gilrs::new() {
185            Ok(g) => g,
186            Err(e) => {
187                eprintln!("[gamepad] Failed to initialize gilrs: {e}");
188                return None;
189            }
190        };
191
192        let mut mgr = Self {
193            gilrs,
194            gamepads: Default::default(),
195            id_to_slot: std::collections::HashMap::new(),
196            connected_count: 0,
197        };
198
199        // Register initially connected gamepads
200        let initial: Vec<(gilrs::GamepadId, String)> = mgr
201            .gilrs
202            .gamepads()
203            .map(|(id, gp)| (id, gp.name().to_string()))
204            .collect();
205
206        for (id, name) in initial {
207            mgr.connect_gamepad(id, &name);
208        }
209
210        Some(mgr)
211    }
212
213    /// Call at start of frame to clear per-frame state.
214    pub fn begin_frame(&mut self) {
215        for gp in &mut self.gamepads {
216            if gp.connected {
217                gp.begin_frame();
218            }
219        }
220    }
221
222    /// Poll gilrs events and update state. Call once per frame.
223    pub fn update(&mut self) {
224        while let Some(event) = self.gilrs.next_event() {
225            use gilrs::EventType;
226            match event.event {
227                EventType::Connected => {
228                    let gp = self.gilrs.gamepad(event.id);
229                    let name = gp.name().to_string();
230                    self.connect_gamepad(event.id, &name);
231                }
232                EventType::Disconnected => {
233                    self.disconnect_gamepad(event.id);
234                }
235                EventType::ButtonPressed(btn, _) => {
236                    if let (Some(slot), Some(button)) =
237                        (self.id_to_slot.get(&event.id), map_gilrs_button(btn))
238                    {
239                        self.gamepads[*slot].button_down(button);
240                    }
241                }
242                EventType::ButtonReleased(btn, _) => {
243                    if let (Some(slot), Some(button)) =
244                        (self.id_to_slot.get(&event.id), map_gilrs_button(btn))
245                    {
246                        self.gamepads[*slot].button_up(button);
247                    }
248                }
249                EventType::AxisChanged(axis, value, _) => {
250                    if let (Some(slot), Some(ga)) =
251                        (self.id_to_slot.get(&event.id), map_gilrs_axis(axis))
252                    {
253                        self.gamepads[*slot].set_axis(ga, value);
254                    }
255                }
256                _ => {}
257            }
258        }
259    }
260
261    fn connect_gamepad(&mut self, id: gilrs::GamepadId, name: &str) {
262        // Find first empty slot
263        for (i, gp) in self.gamepads.iter_mut().enumerate() {
264            if !gp.connected {
265                gp.connected = true;
266                gp.name = name.to_string();
267                gp.buttons_down.clear();
268                gp.axes = [0.0; 6];
269                self.id_to_slot.insert(id, i);
270                self.connected_count += 1;
271                eprintln!("[gamepad] Connected: {} (slot {})", name, i);
272                return;
273            }
274        }
275        eprintln!("[gamepad] No free slot for: {name}");
276    }
277
278    fn disconnect_gamepad(&mut self, id: gilrs::GamepadId) {
279        if let Some(slot) = self.id_to_slot.remove(&id) {
280            let gp = &mut self.gamepads[slot];
281            eprintln!("[gamepad] Disconnected: {} (slot {})", gp.name, slot);
282            *gp = GamepadState::default();
283            self.connected_count -= 1;
284        }
285    }
286
287    /// Get state of the first connected gamepad (convenience for single-player).
288    pub fn primary(&self) -> &GamepadState {
289        for gp in &self.gamepads {
290            if gp.connected {
291                return gp;
292            }
293        }
294        // Return a default disconnected state
295        &self.gamepads[0]
296    }
297}
298
299/// Map gilrs button to our canonical GamepadButton.
300fn map_gilrs_button(btn: gilrs::Button) -> Option<GamepadButton> {
301    use gilrs::Button;
302    match btn {
303        Button::South => Some(GamepadButton::A),
304        Button::East => Some(GamepadButton::B),
305        Button::West => Some(GamepadButton::X),
306        Button::North => Some(GamepadButton::Y),
307        Button::LeftTrigger => Some(GamepadButton::LeftBumper),
308        Button::RightTrigger => Some(GamepadButton::RightBumper),
309        Button::LeftTrigger2 => Some(GamepadButton::LeftTrigger),
310        Button::RightTrigger2 => Some(GamepadButton::RightTrigger),
311        Button::Select => Some(GamepadButton::Select),
312        Button::Start => Some(GamepadButton::Start),
313        Button::LeftThumb => Some(GamepadButton::LeftStick),
314        Button::RightThumb => Some(GamepadButton::RightStick),
315        Button::DPadUp => Some(GamepadButton::DPadUp),
316        Button::DPadDown => Some(GamepadButton::DPadDown),
317        Button::DPadLeft => Some(GamepadButton::DPadLeft),
318        Button::DPadRight => Some(GamepadButton::DPadRight),
319        Button::Mode => Some(GamepadButton::Guide),
320        _ => None,
321    }
322}
323
324/// Map gilrs axis to our canonical GamepadAxis.
325fn map_gilrs_axis(axis: gilrs::Axis) -> Option<GamepadAxis> {
326    use gilrs::Axis;
327    match axis {
328        Axis::LeftStickX => Some(GamepadAxis::LeftStickX),
329        Axis::LeftStickY => Some(GamepadAxis::LeftStickY),
330        Axis::RightStickX => Some(GamepadAxis::RightStickX),
331        Axis::RightStickY => Some(GamepadAxis::RightStickY),
332        Axis::LeftZ => Some(GamepadAxis::LeftTrigger),
333        Axis::RightZ => Some(GamepadAxis::RightTrigger),
334        _ => None,
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn gamepad_button_from_str_roundtrips() {
344        let buttons = [
345            "A", "B", "X", "Y", "LeftBumper", "RightBumper",
346            "LeftTrigger", "RightTrigger", "Select", "Start",
347            "LeftStick", "RightStick", "DPadUp", "DPadDown",
348            "DPadLeft", "DPadRight", "Guide",
349        ];
350        for name in buttons {
351            let btn = GamepadButton::from_str(name).unwrap();
352            assert_eq!(btn.as_str(), name);
353        }
354    }
355
356    #[test]
357    fn gamepad_button_from_str_unknown_returns_none() {
358        assert!(GamepadButton::from_str("Unknown").is_none());
359    }
360
361    #[test]
362    fn gamepad_axis_from_str_valid() {
363        assert!(GamepadAxis::from_str("LeftStickX").is_some());
364        assert!(GamepadAxis::from_str("LeftStickY").is_some());
365        assert!(GamepadAxis::from_str("RightStickX").is_some());
366        assert!(GamepadAxis::from_str("RightStickY").is_some());
367        assert!(GamepadAxis::from_str("LeftTrigger").is_some());
368        assert!(GamepadAxis::from_str("RightTrigger").is_some());
369    }
370
371    #[test]
372    fn gamepad_axis_from_str_invalid() {
373        assert!(GamepadAxis::from_str("Invalid").is_none());
374    }
375
376    #[test]
377    fn gamepad_state_button_down_pressed() {
378        let mut state = GamepadState::default();
379        state.button_down(GamepadButton::A);
380        assert!(state.is_button_down(GamepadButton::A));
381        assert!(state.is_button_pressed(GamepadButton::A));
382    }
383
384    #[test]
385    fn gamepad_state_begin_frame_clears_pressed() {
386        let mut state = GamepadState::default();
387        state.button_down(GamepadButton::A);
388        state.begin_frame();
389        assert!(state.is_button_down(GamepadButton::A));
390        assert!(!state.is_button_pressed(GamepadButton::A));
391    }
392
393    #[test]
394    fn gamepad_state_button_up_releases() {
395        let mut state = GamepadState::default();
396        state.button_down(GamepadButton::A);
397        state.begin_frame();
398        state.button_up(GamepadButton::A);
399        assert!(!state.is_button_down(GamepadButton::A));
400        assert!(state.buttons_released.contains(&GamepadButton::A));
401    }
402
403    #[test]
404    fn gamepad_state_held_button_does_not_re_press() {
405        let mut state = GamepadState::default();
406        state.button_down(GamepadButton::A);
407        state.begin_frame();
408        state.button_down(GamepadButton::A);
409        assert!(state.is_button_down(GamepadButton::A));
410        assert!(!state.is_button_pressed(GamepadButton::A));
411    }
412
413    #[test]
414    fn gamepad_state_axis_set_and_get() {
415        let mut state = GamepadState::default();
416        state.set_axis(GamepadAxis::LeftStickX, 0.75);
417        assert!((state.get_axis(GamepadAxis::LeftStickX) - 0.75).abs() < f32::EPSILON);
418    }
419
420    #[test]
421    fn gamepad_state_default_axes_are_zero() {
422        let state = GamepadState::default();
423        for i in 0..6 {
424            assert_eq!(state.axes[i], 0.0);
425        }
426    }
427
428    #[test]
429    fn gamepad_state_multiple_buttons() {
430        let mut state = GamepadState::default();
431        state.button_down(GamepadButton::A);
432        state.button_down(GamepadButton::B);
433        state.button_down(GamepadButton::X);
434        assert!(state.is_button_down(GamepadButton::A));
435        assert!(state.is_button_down(GamepadButton::B));
436        assert!(state.is_button_down(GamepadButton::X));
437        assert!(!state.is_button_down(GamepadButton::Y));
438    }
439}