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 #[must_use]
74 pub fn name(&self) -> &str {
75 &self.name
76 }
77 #[must_use]
78 pub fn is_pressed(&self, button: Button) -> bool {
79 self.pressed.contains(&button)
80 }
81 #[must_use]
82 pub fn value(&self, axis: Axis) -> f32 {
83 self.axes.get(&axis).copied().unwrap_or(0.0)
84 }
85}
86
87pub(crate) enum Update {
88 Connected(GamepadId, String),
89 Input(Event),
90}
91
92#[derive(Default)]
93struct State {
94 pads: BTreeMap<GamepadId, Gamepad>,
95}
96impl State {
97 fn apply(&mut self, update: Update) -> Option<Event> {
98 let event = match update {
99 Update::Connected(id, name) => {
100 self.pads.insert(
101 id,
102 Gamepad {
103 name,
104 pressed: HashSet::new(),
105 axes: HashMap::new(),
106 },
107 );
108 return Some(Event {
109 id,
110 event: EventType::Connected,
111 });
112 }
113 Update::Input(event) => event,
114 };
115 if event.event == EventType::Disconnected {
116 self.pads.remove(&event.id)?;
117 } else {
118 let pad = self.pads.get_mut(&event.id)?;
120 match event.event {
121 EventType::ButtonPressed(button) => {
122 if !pad.pressed.insert(button) {
125 return None;
126 }
127 }
128 EventType::ButtonReleased(button) => {
129 if !pad.pressed.remove(&button) {
132 return None;
133 }
134 }
135 EventType::AxisChanged(axis, value) => {
136 pad.axes.insert(axis, value);
137 }
138 _ => {}
139 }
140 }
141 Some(event)
142 }
143}
144
145pub struct Gamepads {
146 backend: Backend,
147 state: State,
148}
149impl Gamepads {
150 pub fn new() -> Result<Self, String> {
151 Ok(Self {
152 backend: Backend::new()?,
153 state: State::default(),
154 })
155 }
156 pub fn poll(&mut self) -> Vec<Event> {
160 self.backend
161 .poll()
162 .into_iter()
163 .filter_map(|event| self.state.apply(event))
164 .collect()
165 }
166 #[must_use]
167 pub fn gamepad(&self, id: GamepadId) -> Option<&Gamepad> {
168 self.state.pads.get(&id)
169 }
170 pub fn gamepads(&self) -> impl Iterator<Item = (GamepadId, &Gamepad)> {
171 self.state.pads.iter().map(|(&id, pad)| (id, pad))
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 #[test]
179 fn quick_taps_survive_polling_and_disconnect_clears_held_state() {
180 let mut state = State::default();
181 let id = GamepadId(0);
182 state.apply(Update::Connected(id, "pad".into()));
183 let events: Vec<_> = [
184 EventType::ButtonPressed(Button::South),
185 EventType::ButtonReleased(Button::South),
186 ]
187 .into_iter()
188 .filter_map(|event| state.apply(Update::Input(Event { id, event })))
189 .collect();
190 assert_eq!(
191 events.len(),
192 2,
193 "a complete tap between frames must remain actionable"
194 );
195 assert!(!state.pads[&id].is_pressed(Button::South));
196 state.apply(Update::Input(Event {
197 id,
198 event: EventType::ButtonPressed(Button::DPadLeft),
199 }));
200 assert!(state.pads[&id].is_pressed(Button::DPadLeft));
201 state.apply(Update::Input(Event {
202 id,
203 event: EventType::Disconnected,
204 }));
205 assert!(state.pads.is_empty());
206 assert!(
207 state
208 .apply(Update::Input(Event {
209 id,
210 event: EventType::ButtonPressed(Button::South)
211 }))
212 .is_none()
213 );
214 let new_id = GamepadId(1);
215 state.apply(Update::Connected(new_id, "pad".into()));
216 assert!(!state.pads[&new_id].is_pressed(Button::DPadLeft));
217 }
218
219 #[test]
223 fn an_unmatched_release_is_dropped() {
224 let mut state = State::default();
225 let id = GamepadId(0);
226 state.apply(Update::Connected(id, "pad".into()));
227 state.apply(Update::Input(Event {
228 id,
229 event: EventType::ButtonPressed(Button::East),
230 }));
231 let release = |button| {
232 Update::Input(Event {
233 id,
234 event: EventType::ButtonReleased(button),
235 })
236 };
237 assert!(state.apply(release(Button::South)).is_none());
238 assert!(state.pads[&id].is_pressed(Button::East));
239 assert!(!state.pads[&id].is_pressed(Button::South));
240 assert!(state.apply(release(Button::East)).is_some());
241 assert!(!state.pads[&id].is_pressed(Button::East));
242 }
243
244 #[test]
248 fn a_repeated_press_is_reported_once() {
249 let mut state = State::default();
250 let id = GamepadId(0);
251 state.apply(Update::Connected(id, "pad".into()));
252 let events: Vec<_> = [
253 EventType::ButtonPressed(Button::South),
254 EventType::ButtonPressed(Button::South),
255 EventType::ButtonReleased(Button::South),
256 ]
257 .into_iter()
258 .filter_map(|event| state.apply(Update::Input(Event { id, event })))
259 .map(|event| event.event)
260 .collect();
261 assert_eq!(
262 events,
263 [
264 EventType::ButtonPressed(Button::South),
265 EventType::ButtonReleased(Button::South),
266 ]
267 );
268 assert!(!state.pads[&id].is_pressed(Button::South));
269 }
270}