Skip to main content

crcbl_input/
active_pad.rs

1//! One pad's sticks, for a game reading analog movement and look outside the
2//! [`crate::ActionMap`]: the pad that moved first keeps the sticks, a dead
3//! zone filters them, and after a menu or a lost focus nothing moves until
4//! each stick has been let go.
5
6use std::collections::HashMap;
7
8use crate::gamepad::{radial_deadzone, valid_deadzone};
9use crate::{GamepadId, GamepadSnapshot, Stick};
10
11/// What [`ActivePad::submit`] did with a snapshot.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum StickUpdate {
14    /// Another pad holds the sticks; this one was ignored.
15    OtherPad,
16    /// An axis was not finite; the snapshot was ignored.
17    NotFinite,
18    /// Both sticks read zero after the dead zone and any release.
19    Neutral,
20    /// A stick reads something.
21    Moved,
22}
23
24/// The pad whose sticks drive the game, its filtered sticks, and the
25/// let-go rule.
26///
27/// - **The first pad to move a stick past the dead zone holds them**, or the
28///   first to be [`activate`](Self::activate)d by a button, and keeps them
29///   until it [`disconnect`](Self::disconnect)s: a second pad cannot take
30///   over mid-move.
31/// - **Each stick reads through a scaled radial dead zone**
32///   ([`crate::radial_deadzone`]): zero inside it, rescaled from its edge to
33///   full throw.
34/// - **[`release_until_neutral`](Self::release_until_neutral)** — on opening a
35///   menu, on losing focus — zeroes the sticks and keeps each stick of each
36///   pad at zero until that stick has come back inside the dead zone, so a
37///   stick still held as the menu closes does not walk the player off. Every
38///   pad first seen after a release is held to the same rule, and so is a
39///   pad that disconnected while it held the sticks, when it returns.
40#[derive(Clone, Debug)]
41pub struct ActivePad {
42    dead_zone: f32,
43    active: Option<GamepadId>,
44    sticks: [(f32, f32); 2],
45    /// Per pad, per stick (left, right): whether it is still waiting to rest.
46    waiting: HashMap<GamepadId, [bool; 2]>,
47    /// Whether pads not yet seen must rest first: set by a release.
48    released: bool,
49}
50
51impl ActivePad {
52    /// No pad holding the sticks, with `dead_zone` in `0.0..1.0`; `None`
53    /// outside it.
54    #[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    /// The pad holding the sticks.
66    #[must_use]
67    pub const fn active(&self) -> Option<GamepadId> {
68        self.active
69    }
70
71    /// Gives `id` the sticks, as a button press on it does, unless another
72    /// pad holds them. Returns whether `id` holds them now.
73    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    /// Zeroes the sticks and holds every stick of every pad at zero until it
82    /// has rested.
83    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    /// Reads `snapshot` from pad `id`: see [`StickUpdate`]. A pad that moves
90    /// while none holds the sticks takes them.
91    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    /// Takes the sticks from `id` if it holds them, zeroing them; when it
125    /// returns, its sticks must rest before they drive again. Returns whether
126    /// it held them.
127    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    /// A stick as the holding pad last reported it, filtered: `(x, y)`, +Y
138    /// up, inside the unit disc.
139    #[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    /// **The first pad to move keeps the sticks until it leaves**, through
171    /// the dead zone, and a button press cannot steal them either.
172    #[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    /// **After a release, a held stick stays at zero until it rests**, each
206    /// stick on its own, and a pad first seen after it waits too.
207    #[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    /// **A pad that leaves while holding the sticks must let go when it
243    /// returns**, release or none.
244    #[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    /// Bad input is refused rather than read.
258    #[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}