1use std::collections::HashMap;
7
8use crate::gamepad::{radial_deadzone, valid_deadzone};
9use crate::{GamepadId, GamepadSnapshot, Stick};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum StickUpdate {
14 OtherPad,
16 NotFinite,
18 Neutral,
20 Moved,
22}
23
24#[derive(Clone, Debug)]
41pub struct ActivePad {
42 dead_zone: f32,
43 active: Option<GamepadId>,
44 sticks: [(f32, f32); 2],
45 waiting: HashMap<GamepadId, [bool; 2]>,
47 released: bool,
49}
50
51impl ActivePad {
52 #[must_use]
55 pub fn new(dead_zone: f32) -> Option<Self> {
56 valid_deadzone(dead_zone).then(|| Self {
57 dead_zone,
58 active: None,
59 sticks: [(0.0, 0.0); 2],
60 waiting: HashMap::new(),
61 released: false,
62 })
63 }
64
65 #[must_use]
67 pub const fn active(&self) -> Option<GamepadId> {
68 self.active
69 }
70
71 pub fn activate(&mut self, id: GamepadId) -> bool {
74 if self.active.is_some_and(|active| active != id) {
75 return false;
76 }
77 self.active = Some(id);
78 true
79 }
80
81 pub fn release_until_neutral(&mut self) {
84 self.sticks = [(0.0, 0.0); 2];
85 self.waiting.clear();
86 self.released = true;
87 }
88
89 pub fn submit(&mut self, id: GamepadId, snapshot: &GamepadSnapshot) -> StickUpdate {
92 if self.active.is_some_and(|active| active != id) {
93 return StickUpdate::OtherPad;
94 }
95 if !snapshot.is_finite() {
96 return StickUpdate::NotFinite;
97 }
98 let mut sticks = [Stick::Left, Stick::Right]
99 .map(|stick| radial_deadzone(snapshot.stick(stick), self.dead_zone));
100 if self.released {
101 self.waiting.entry(id).or_insert([true; 2]);
102 }
103 if let Some(waiting) = self.waiting.get_mut(&id) {
104 for (stick, waits) in sticks.iter_mut().zip(waiting) {
105 *waits &= *stick != (0.0, 0.0);
106 if *waits {
107 *stick = (0.0, 0.0);
108 }
109 }
110 }
111 let moved = sticks.iter().any(|&stick| stick != (0.0, 0.0));
112 if self.active.is_none() && !moved {
113 return StickUpdate::Neutral;
114 }
115 self.active = Some(id);
116 self.sticks = sticks;
117 if moved {
118 StickUpdate::Moved
119 } else {
120 StickUpdate::Neutral
121 }
122 }
123
124 pub fn disconnect(&mut self, id: GamepadId) -> bool {
128 if self.active != Some(id) {
129 return false;
130 }
131 self.waiting.insert(id, [true; 2]);
132 self.active = None;
133 self.sticks = [(0.0, 0.0); 2];
134 true
135 }
136
137 #[must_use]
140 pub const fn stick(&self, stick: Stick) -> (f32, f32) {
141 match stick {
142 Stick::Left => self.sticks[0],
143 Stick::Right => self.sticks[1],
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::{PadAxis, PadKind};
152
153 const DEAD: f32 = 0.2;
154
155 fn pushed(left: (f32, f32), right: (f32, f32)) -> GamepadSnapshot {
156 let mut snapshot = GamepadSnapshot::neutral(PadKind::Xbox);
157 snapshot.axes[PadAxis::LeftX as usize] = left.0;
158 snapshot.axes[PadAxis::LeftY as usize] = left.1;
159 snapshot.axes[PadAxis::RightX as usize] = right.0;
160 snapshot.axes[PadAxis::RightY as usize] = right.1;
161 snapshot
162 }
163
164 fn close(a: (f32, f32), b: (f32, f32)) -> bool {
165 (a.0 - b.0).abs() < 1e-6 && (a.1 - b.1).abs() < 1e-6
166 }
167
168 const REST: (f32, f32) = (0.0, 0.0);
169
170 #[test]
173 fn the_first_pad_to_move_holds_the_sticks() {
174 let mut pad = ActivePad::new(DEAD).unwrap();
175 let (one, two) = (GamepadId(1), GamepadId(2));
176 assert_eq!(
177 pad.submit(two, &pushed((0.1, 0.1), REST)),
178 StickUpdate::Neutral
179 );
180 assert_eq!(
181 pad.active(),
182 None,
183 "a pad inside its dead zone takes nothing"
184 );
185 assert_eq!(
186 pad.submit(one, &pushed((0.6, 0.0), REST)),
187 StickUpdate::Moved
188 );
189 assert_eq!(pad.active(), Some(one));
190 assert!(
191 close(pad.stick(Stick::Left), (0.5, 0.0)),
192 "rescaled: {:?}",
193 pad.stick(Stick::Left)
194 );
195 assert_eq!(
196 pad.submit(two, &pushed((1.0, 0.0), REST)),
197 StickUpdate::OtherPad
198 );
199 assert!(!pad.activate(two));
200 assert!(pad.disconnect(one));
201 assert_eq!(pad.stick(Stick::Left), REST);
202 assert!(pad.activate(two));
203 }
204
205 #[test]
208 fn a_released_stick_waits_until_it_has_rested() {
209 let mut pad = ActivePad::new(DEAD).unwrap();
210 let one = GamepadId(1);
211 let both = pushed((0.0, 1.0), (1.0, 0.0));
212 assert_eq!(pad.submit(one, &both), StickUpdate::Moved);
213 pad.release_until_neutral();
214 assert_eq!(pad.stick(Stick::Left), REST);
215 assert_eq!(
216 pad.submit(one, &both),
217 StickUpdate::Neutral,
218 "both still held"
219 );
220 assert_eq!(
221 pad.submit(one, &pushed(REST, (1.0, 0.0))),
222 StickUpdate::Neutral
223 );
224 assert_eq!(
225 pad.submit(one, &both),
226 StickUpdate::Moved,
227 "the left one rested"
228 );
229 assert!(close(pad.stick(Stick::Left), (0.0, 1.0)));
230 assert_eq!(pad.stick(Stick::Right), REST, "the right one never did");
231
232 assert!(pad.disconnect(one));
233 let newcomer = GamepadId(2);
234 assert_eq!(pad.submit(newcomer, &both), StickUpdate::Neutral);
235 assert_eq!(
236 pad.submit(newcomer, &pushed(REST, REST)),
237 StickUpdate::Neutral
238 );
239 assert_eq!(pad.submit(newcomer, &both), StickUpdate::Moved);
240 }
241
242 #[test]
245 fn a_returning_pad_must_rest_first() {
246 let mut pad = ActivePad::new(DEAD).unwrap();
247 let one = GamepadId(1);
248 let held = pushed((1.0, 0.0), REST);
249 assert_eq!(pad.submit(one, &held), StickUpdate::Moved);
250 assert!(pad.disconnect(one));
251 assert!(!pad.disconnect(one), "it holds nothing now");
252 assert_eq!(pad.submit(one, &held), StickUpdate::Neutral);
253 assert_eq!(pad.submit(one, &pushed(REST, REST)), StickUpdate::Neutral);
254 assert_eq!(pad.submit(one, &held), StickUpdate::Moved);
255 }
256
257 #[test]
259 fn bad_dead_zones_and_snapshots_are_refused() {
260 assert!(ActivePad::new(1.0).is_none());
261 assert!(ActivePad::new(-0.1).is_none());
262 assert!(ActivePad::new(f32::NAN).is_none());
263 let mut pad = ActivePad::new(DEAD).unwrap();
264 let broken = pushed((f32::NAN, 0.0), REST);
265 assert_eq!(pad.submit(GamepadId(1), &broken), StickUpdate::NotFinite);
266 assert_eq!(pad.active(), None);
267 }
268}