1use std::collections::HashSet;
2
3#[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 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 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#[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#[derive(Debug, Clone)]
102pub struct GamepadState {
103 pub name: String,
104 pub connected: bool,
105 pub buttons_down: HashSet<GamepadButton>,
107 pub buttons_pressed: HashSet<GamepadButton>,
109 pub buttons_released: HashSet<GamepadButton>,
111 pub axes: [f32; 6], }
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
171pub struct GamepadManager {
173 gilrs: gilrs::Gilrs,
174 pub gamepads: [GamepadState; 4],
176 id_to_slot: std::collections::HashMap<gilrs::GamepadId, usize>,
178 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 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 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 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 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 pub fn primary(&self) -> &GamepadState {
289 for gp in &self.gamepads {
290 if gp.connected {
291 return gp;
292 }
293 }
294 &self.gamepads[0]
296 }
297}
298
299fn 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
324fn 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}