use crate::components::GamepadButton;
use crate::input::stick;
const LOOK_CURVE_EXPONENT: f32 = 2.0;
const ACTIVATE_DEFLECTION: f32 = 0.5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PadAxis {
LeftX,
LeftY,
RightX,
RightY,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum PadEvent {
Connected {
pad: u32,
},
Disconnected {
pad: u32,
},
Button {
pad: u32,
button: GamepadButton,
pressed: bool,
},
Axis {
pad: u32,
axis: PadAxis,
value: f32,
},
}
fn bit(button: GamepadButton) -> u32 {
1 << (button as u32)
}
#[derive(Debug, Default)]
pub(crate) struct PadState {
active: Option<u32>,
left: [f32; 2],
right: [f32; 2],
held: u32,
pressed: u32,
}
impl PadState {
pub(crate) fn ingest(&mut self, event: PadEvent) {
match event {
PadEvent::Connected { pad } => {
if self.active.is_none() {
self.active = Some(pad);
}
}
PadEvent::Disconnected { pad } => {
if self.active == Some(pad) {
*self = PadState::default();
}
}
PadEvent::Button {
pad,
button,
pressed,
} => {
if Some(pad) != self.active {
if !pressed {
return;
}
self.switch_to(pad);
}
if pressed {
self.held |= bit(button);
self.pressed |= bit(button);
} else {
self.held &= !bit(button);
}
}
PadEvent::Axis { pad, axis, value } => {
if Some(pad) != self.active {
if value.abs() < ACTIVATE_DEFLECTION {
return;
}
self.switch_to(pad);
}
match axis {
PadAxis::LeftX => self.left[0] = value,
PadAxis::LeftY => self.left[1] = value,
PadAxis::RightX => self.right[0] = value,
PadAxis::RightY => self.right[1] = value,
}
}
}
}
fn switch_to(&mut self, pad: u32) {
*self = PadState::default();
self.active = Some(pad);
}
pub(crate) fn snapshot(&mut self, deadzone: f32) -> PadSnapshot {
let pressed = core::mem::take(&mut self.pressed);
let look = stick::response_curve(
stick::radial_deadzone(self.right, deadzone),
LOOK_CURVE_EXPONENT,
);
PadSnapshot {
move_axis: stick::radial_deadzone(self.left, deadzone),
look_axis: [look[0], -look[1]],
held: self.held,
pressed,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct PadSnapshot {
pub(crate) move_axis: [f32; 2],
pub(crate) look_axis: [f32; 2],
held: u32,
pressed: u32,
}
impl PadSnapshot {
pub(crate) fn held(&self, button: GamepadButton) -> bool {
self.held & bit(button) != 0
}
pub(crate) fn pressed(&self, button: GamepadButton) -> bool {
self.pressed & bit(button) != 0
}
pub(crate) fn first_pressed(&self) -> Option<GamepadButton> {
GamepadButton::ALL
.iter()
.copied()
.find(|&b| self.pressed(b))
}
}
#[derive(Debug, Default)]
pub(crate) struct GamepadSource {
gilrs: Option<gilrs::Gilrs>,
tried: bool,
}
impl GamepadSource {
pub(crate) fn poll(&mut self, state: &mut PadState) {
if !self.tried {
self.tried = true;
match gilrs::Gilrs::new() {
Ok(gilrs) => {
for (id, _) in gilrs.gamepads() {
state.ingest(PadEvent::Connected {
pad: usize::from(id) as u32,
});
}
self.gilrs = Some(gilrs);
}
Err(e) => tracing::warn!("gamepad input unavailable: {e}"),
}
}
let Some(gilrs) = self.gilrs.as_mut() else {
return;
};
while let Some(event) = gilrs.next_event() {
if let Some(pad_event) = convert(&event) {
state.ingest(pad_event);
}
}
}
}
fn convert(event: &gilrs::Event) -> Option<PadEvent> {
let pad = usize::from(event.id) as u32;
Some(match event.event {
gilrs::EventType::ButtonPressed(b, _) => PadEvent::Button {
pad,
button: button_of(b)?,
pressed: true,
},
gilrs::EventType::ButtonReleased(b, _) => PadEvent::Button {
pad,
button: button_of(b)?,
pressed: false,
},
gilrs::EventType::AxisChanged(a, value, _) => PadEvent::Axis {
pad,
axis: axis_of(a)?,
value,
},
gilrs::EventType::Connected => PadEvent::Connected { pad },
gilrs::EventType::Disconnected => PadEvent::Disconnected { pad },
_ => return None,
})
}
fn button_of(button: gilrs::Button) -> Option<GamepadButton> {
Some(match button {
gilrs::Button::South => GamepadButton::South,
gilrs::Button::East => GamepadButton::East,
gilrs::Button::West => GamepadButton::West,
gilrs::Button::North => GamepadButton::North,
gilrs::Button::LeftTrigger => GamepadButton::LeftShoulder,
gilrs::Button::LeftTrigger2 => GamepadButton::LeftTrigger,
gilrs::Button::RightTrigger => GamepadButton::RightShoulder,
gilrs::Button::RightTrigger2 => GamepadButton::RightTrigger,
gilrs::Button::LeftThumb => GamepadButton::LeftStick,
gilrs::Button::RightThumb => GamepadButton::RightStick,
gilrs::Button::DPadUp => GamepadButton::DpadUp,
gilrs::Button::DPadDown => GamepadButton::DpadDown,
gilrs::Button::DPadLeft => GamepadButton::DpadLeft,
gilrs::Button::DPadRight => GamepadButton::DpadRight,
gilrs::Button::Start => GamepadButton::Start,
gilrs::Button::Select => GamepadButton::Select,
_ => return None,
})
}
fn axis_of(axis: gilrs::Axis) -> Option<PadAxis> {
Some(match axis {
gilrs::Axis::LeftStickX => PadAxis::LeftX,
gilrs::Axis::LeftStickY => PadAxis::LeftY,
gilrs::Axis::RightStickX => PadAxis::RightX,
gilrs::Axis::RightStickY => PadAxis::RightY,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn press(pad: u32, button: GamepadButton) -> PadEvent {
PadEvent::Button {
pad,
button,
pressed: true,
}
}
fn release(pad: u32, button: GamepadButton) -> PadEvent {
PadEvent::Button {
pad,
button,
pressed: false,
}
}
#[test]
fn press_sets_held_and_a_one_frame_edge() {
let mut state = PadState::default();
state.ingest(PadEvent::Connected { pad: 0 });
state.ingest(press(0, GamepadButton::South));
let snap = state.snapshot(0.15);
assert!(snap.held(GamepadButton::South));
assert!(snap.pressed(GamepadButton::South));
assert_eq!(snap.first_pressed(), Some(GamepadButton::South));
let snap = state.snapshot(0.15);
assert!(snap.held(GamepadButton::South));
assert!(!snap.pressed(GamepadButton::South));
assert_eq!(snap.first_pressed(), None);
state.ingest(release(0, GamepadButton::South));
assert!(!state.snapshot(0.15).held(GamepadButton::South));
}
#[test]
fn press_between_snapshots_is_not_lost() {
let mut state = PadState::default();
state.ingest(PadEvent::Connected { pad: 0 });
state.ingest(press(0, GamepadButton::West));
state.ingest(release(0, GamepadButton::West));
let snap = state.snapshot(0.15);
assert!(snap.pressed(GamepadButton::West));
assert!(!snap.held(GamepadButton::West));
}
#[test]
fn axes_shape_through_deadzone_and_look_flips_y() {
let mut state = PadState::default();
state.ingest(PadEvent::Connected { pad: 0 });
state.ingest(PadEvent::Axis {
pad: 0,
axis: PadAxis::LeftY,
value: 1.0,
});
state.ingest(PadEvent::Axis {
pad: 0,
axis: PadAxis::RightY,
value: 1.0,
});
let snap = state.snapshot(0.15);
assert!(
(snap.move_axis[1] - 1.0).abs() < 1e-6,
"{:?}",
snap.move_axis
);
assert!(
(snap.look_axis[1] + 1.0).abs() < 1e-6,
"{:?}",
snap.look_axis
);
state.ingest(PadEvent::Axis {
pad: 0,
axis: PadAxis::LeftY,
value: 0.1,
});
assert_eq!(state.snapshot(0.15).move_axis, [0.0, 0.0]);
}
#[test]
fn most_recently_used_pad_wins_and_state_resets_on_switch() {
let mut state = PadState::default();
state.ingest(PadEvent::Connected { pad: 0 });
state.ingest(PadEvent::Connected { pad: 1 });
state.ingest(press(0, GamepadButton::South));
state.ingest(press(1, GamepadButton::North));
let snap = state.snapshot(0.15);
assert!(snap.held(GamepadButton::North));
assert!(!snap.held(GamepadButton::South));
}
#[test]
fn inactive_pad_drift_and_releases_are_ignored() {
let mut state = PadState::default();
state.ingest(PadEvent::Connected { pad: 0 });
state.ingest(PadEvent::Axis {
pad: 0,
axis: PadAxis::LeftY,
value: 1.0,
});
state.ingest(PadEvent::Axis {
pad: 1,
axis: PadAxis::LeftX,
value: 0.2,
});
let snap = state.snapshot(0.15);
assert!(
(snap.move_axis[1] - 1.0).abs() < 1e-6,
"{:?}",
snap.move_axis
);
state.ingest(release(1, GamepadButton::South));
assert!((state.snapshot(0.15).move_axis[1] - 1.0).abs() < 1e-6);
state.ingest(PadEvent::Axis {
pad: 1,
axis: PadAxis::LeftX,
value: 0.9,
});
let snap = state.snapshot(0.15);
assert!(snap.move_axis[0] > 0.5, "{:?}", snap.move_axis);
assert!(snap.move_axis[1].abs() < 1e-6, "{:?}", snap.move_axis);
}
#[test]
fn disconnect_of_the_active_pad_zeroes_everything() {
let mut state = PadState::default();
state.ingest(PadEvent::Connected { pad: 0 });
state.ingest(press(0, GamepadButton::South));
state.ingest(PadEvent::Axis {
pad: 0,
axis: PadAxis::LeftY,
value: 1.0,
});
state.ingest(PadEvent::Disconnected { pad: 0 });
let snap = state.snapshot(0.15);
assert_eq!(snap.move_axis, [0.0, 0.0]);
assert!(!snap.held(GamepadButton::South));
state.ingest(PadEvent::Connected { pad: 1 });
state.ingest(press(1, GamepadButton::East));
state.ingest(PadEvent::Disconnected { pad: 7 });
assert!(state.snapshot(0.15).held(GamepadButton::East));
}
#[test]
fn first_pressed_takes_declaration_order() {
let mut state = PadState::default();
state.ingest(PadEvent::Connected { pad: 0 });
state.ingest(press(0, GamepadButton::Start));
state.ingest(press(0, GamepadButton::South));
assert_eq!(
state.snapshot(0.15).first_pressed(),
Some(GamepadButton::South)
);
}
}