1use std::collections::{BTreeMap, HashMap, HashSet};
7
8#[cfg(target_os = "macos")]
9#[allow(unsafe_code)]
10mod macos;
11#[cfg(target_os = "macos")]
12use macos::Backend;
13#[cfg(not(target_os = "macos"))]
14mod portable;
15#[cfg(not(target_os = "macos"))]
16use portable::Backend;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct GamepadId(pub(crate) usize);
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub enum Button {
24 South,
25 East,
26 West,
27 North,
28 DPadUp,
29 DPadDown,
30 DPadLeft,
31 DPadRight,
32 LeftTrigger,
33 RightTrigger,
34 LeftTrigger2,
35 RightTrigger2,
36 LeftThumb,
37 RightThumb,
38 Select,
39 Start,
40 Mode,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub enum Axis {
46 LeftStickX,
47 LeftStickY,
48 RightStickX,
49 RightStickY,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq)]
53pub enum EventType {
54 Connected,
55 Disconnected,
56 ButtonPressed(Button),
57 ButtonReleased(Button),
58 AxisChanged(Axis, f32),
59}
60#[derive(Clone, Copy, Debug, PartialEq)]
61pub struct Event {
62 pub id: GamepadId,
63 pub event: EventType,
64}
65
66#[derive(Debug)]
67pub struct Gamepad {
68 name: String,
69 pressed: HashSet<Button>,
70 axes: HashMap<Axis, f32>,
71}
72impl Gamepad {
73 pub fn name(&self) -> &str {
74 &self.name
75 }
76 pub fn is_pressed(&self, button: Button) -> bool {
77 self.pressed.contains(&button)
78 }
79 pub fn value(&self, axis: Axis) -> f32 {
80 self.axes.get(&axis).copied().unwrap_or(0.0)
81 }
82}
83
84pub(crate) enum Update {
85 Connected(GamepadId, String),
86 Input(Event),
87}
88
89#[derive(Default)]
90struct State {
91 pads: BTreeMap<GamepadId, Gamepad>,
92}
93impl State {
94 fn apply(&mut self, update: Update) -> Option<Event> {
95 let event = match update {
96 Update::Connected(id, name) => {
97 self.pads.insert(
98 id,
99 Gamepad {
100 name,
101 pressed: HashSet::new(),
102 axes: HashMap::new(),
103 },
104 );
105 return Some(Event {
106 id,
107 event: EventType::Connected,
108 });
109 }
110 Update::Input(event) => event,
111 };
112 if event.event == EventType::Disconnected {
113 self.pads.remove(&event.id)?;
114 } else {
115 let pad = self.pads.get_mut(&event.id)?;
117 match event.event {
118 EventType::ButtonPressed(button) => {
119 pad.pressed.insert(button);
120 }
121 EventType::ButtonReleased(button) => {
122 pad.pressed.remove(&button);
123 }
124 EventType::AxisChanged(axis, value) => {
125 pad.axes.insert(axis, value);
126 }
127 _ => {}
128 }
129 }
130 Some(event)
131 }
132}
133
134pub struct Gamepads {
135 backend: Backend,
136 state: State,
137}
138impl Gamepads {
139 pub fn new() -> Result<Self, String> {
140 Ok(Self {
141 backend: Backend::new()?,
142 state: State::default(),
143 })
144 }
145 pub fn poll(&mut self) -> Vec<Event> {
149 self.backend
150 .poll()
151 .into_iter()
152 .filter_map(|event| self.state.apply(event))
153 .collect()
154 }
155 pub fn gamepad(&self, id: GamepadId) -> Option<&Gamepad> {
156 self.state.pads.get(&id)
157 }
158 pub fn gamepads(&self) -> impl Iterator<Item = (GamepadId, &Gamepad)> {
159 self.state.pads.iter().map(|(&id, pad)| (id, pad))
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 #[test]
167 fn quick_taps_survive_polling_and_disconnect_clears_held_state() {
168 let mut state = State::default();
169 let id = GamepadId(0);
170 state.apply(Update::Connected(id, "pad".into()));
171 let events: Vec<_> = [
172 EventType::ButtonPressed(Button::South),
173 EventType::ButtonReleased(Button::South),
174 ]
175 .into_iter()
176 .filter_map(|event| state.apply(Update::Input(Event { id, event })))
177 .collect();
178 assert_eq!(
179 events.len(),
180 2,
181 "a complete tap between frames must remain actionable"
182 );
183 assert!(!state.pads[&id].is_pressed(Button::South));
184 state.apply(Update::Input(Event {
185 id,
186 event: EventType::ButtonPressed(Button::DPadLeft),
187 }));
188 assert!(state.pads[&id].is_pressed(Button::DPadLeft));
189 state.apply(Update::Input(Event {
190 id,
191 event: EventType::Disconnected,
192 }));
193 assert!(state.pads.is_empty());
194 assert!(
195 state
196 .apply(Update::Input(Event {
197 id,
198 event: EventType::ButtonPressed(Button::South)
199 }))
200 .is_none()
201 );
202 let new_id = GamepadId(1);
203 state.apply(Update::Connected(new_id, "pad".into()));
204 assert!(!state.pads[&new_id].is_pressed(Button::DPadLeft));
205 }
206}