use std::collections::HashMap;
use crate::gamepad::{radial_deadzone, valid_deadzone};
use crate::{GamepadId, GamepadSnapshot, Stick};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StickUpdate {
OtherPad,
NotFinite,
Neutral,
Moved,
}
#[derive(Clone, Debug)]
pub struct ActivePad {
dead_zone: f32,
active: Option<GamepadId>,
sticks: [(f32, f32); 2],
waiting: HashMap<GamepadId, [bool; 2]>,
released: bool,
}
impl ActivePad {
#[must_use]
pub fn new(dead_zone: f32) -> Option<Self> {
valid_deadzone(dead_zone).then(|| Self {
dead_zone,
active: None,
sticks: [(0.0, 0.0); 2],
waiting: HashMap::new(),
released: false,
})
}
#[must_use]
pub const fn active(&self) -> Option<GamepadId> {
self.active
}
pub fn activate(&mut self, id: GamepadId) -> bool {
if self.active.is_some_and(|active| active != id) {
return false;
}
self.active = Some(id);
true
}
pub fn release_until_neutral(&mut self) {
self.sticks = [(0.0, 0.0); 2];
self.waiting.clear();
self.released = true;
}
pub fn submit(&mut self, id: GamepadId, snapshot: &GamepadSnapshot) -> StickUpdate {
if self.active.is_some_and(|active| active != id) {
return StickUpdate::OtherPad;
}
if !snapshot.is_finite() {
return StickUpdate::NotFinite;
}
let mut sticks = [Stick::Left, Stick::Right]
.map(|stick| radial_deadzone(snapshot.stick(stick), self.dead_zone));
if self.released {
self.waiting.entry(id).or_insert([true; 2]);
}
if let Some(waiting) = self.waiting.get_mut(&id) {
for (stick, waits) in sticks.iter_mut().zip(waiting) {
*waits &= *stick != (0.0, 0.0);
if *waits {
*stick = (0.0, 0.0);
}
}
}
let moved = sticks.iter().any(|&stick| stick != (0.0, 0.0));
if self.active.is_none() && !moved {
return StickUpdate::Neutral;
}
self.active = Some(id);
self.sticks = sticks;
if moved {
StickUpdate::Moved
} else {
StickUpdate::Neutral
}
}
pub fn disconnect(&mut self, id: GamepadId) -> bool {
if self.active != Some(id) {
return false;
}
self.waiting.insert(id, [true; 2]);
self.active = None;
self.sticks = [(0.0, 0.0); 2];
true
}
#[must_use]
pub const fn stick(&self, stick: Stick) -> (f32, f32) {
match stick {
Stick::Left => self.sticks[0],
Stick::Right => self.sticks[1],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PadAxis, PadKind};
const DEAD: f32 = 0.2;
fn pushed(left: (f32, f32), right: (f32, f32)) -> GamepadSnapshot {
let mut snapshot = GamepadSnapshot::neutral(PadKind::Xbox);
snapshot.axes[PadAxis::LeftX as usize] = left.0;
snapshot.axes[PadAxis::LeftY as usize] = left.1;
snapshot.axes[PadAxis::RightX as usize] = right.0;
snapshot.axes[PadAxis::RightY as usize] = right.1;
snapshot
}
fn close(a: (f32, f32), b: (f32, f32)) -> bool {
(a.0 - b.0).abs() < 1e-6 && (a.1 - b.1).abs() < 1e-6
}
const REST: (f32, f32) = (0.0, 0.0);
#[test]
fn the_first_pad_to_move_holds_the_sticks() {
let mut pad = ActivePad::new(DEAD).unwrap();
let (one, two) = (GamepadId(1), GamepadId(2));
assert_eq!(
pad.submit(two, &pushed((0.1, 0.1), REST)),
StickUpdate::Neutral
);
assert_eq!(
pad.active(),
None,
"a pad inside its dead zone takes nothing"
);
assert_eq!(
pad.submit(one, &pushed((0.6, 0.0), REST)),
StickUpdate::Moved
);
assert_eq!(pad.active(), Some(one));
assert!(
close(pad.stick(Stick::Left), (0.5, 0.0)),
"rescaled: {:?}",
pad.stick(Stick::Left)
);
assert_eq!(
pad.submit(two, &pushed((1.0, 0.0), REST)),
StickUpdate::OtherPad
);
assert!(!pad.activate(two));
assert!(pad.disconnect(one));
assert_eq!(pad.stick(Stick::Left), REST);
assert!(pad.activate(two));
}
#[test]
fn a_released_stick_waits_until_it_has_rested() {
let mut pad = ActivePad::new(DEAD).unwrap();
let one = GamepadId(1);
let both = pushed((0.0, 1.0), (1.0, 0.0));
assert_eq!(pad.submit(one, &both), StickUpdate::Moved);
pad.release_until_neutral();
assert_eq!(pad.stick(Stick::Left), REST);
assert_eq!(
pad.submit(one, &both),
StickUpdate::Neutral,
"both still held"
);
assert_eq!(
pad.submit(one, &pushed(REST, (1.0, 0.0))),
StickUpdate::Neutral
);
assert_eq!(
pad.submit(one, &both),
StickUpdate::Moved,
"the left one rested"
);
assert!(close(pad.stick(Stick::Left), (0.0, 1.0)));
assert_eq!(pad.stick(Stick::Right), REST, "the right one never did");
assert!(pad.disconnect(one));
let newcomer = GamepadId(2);
assert_eq!(pad.submit(newcomer, &both), StickUpdate::Neutral);
assert_eq!(
pad.submit(newcomer, &pushed(REST, REST)),
StickUpdate::Neutral
);
assert_eq!(pad.submit(newcomer, &both), StickUpdate::Moved);
}
#[test]
fn a_returning_pad_must_rest_first() {
let mut pad = ActivePad::new(DEAD).unwrap();
let one = GamepadId(1);
let held = pushed((1.0, 0.0), REST);
assert_eq!(pad.submit(one, &held), StickUpdate::Moved);
assert!(pad.disconnect(one));
assert!(!pad.disconnect(one), "it holds nothing now");
assert_eq!(pad.submit(one, &held), StickUpdate::Neutral);
assert_eq!(pad.submit(one, &pushed(REST, REST)), StickUpdate::Neutral);
assert_eq!(pad.submit(one, &held), StickUpdate::Moved);
}
#[test]
fn bad_dead_zones_and_snapshots_are_refused() {
assert!(ActivePad::new(1.0).is_none());
assert!(ActivePad::new(-0.1).is_none());
assert!(ActivePad::new(f32::NAN).is_none());
let mut pad = ActivePad::new(DEAD).unwrap();
let broken = pushed((f32::NAN, 0.0), REST);
assert_eq!(pad.submit(GamepadId(1), &broken), StickUpdate::NotFinite);
assert_eq!(pad.active(), None);
}
}