use repose_core::input::{GamepadAxis, GamepadButton, GamepadEvent, GamepadId};
pub trait GamepadBackend {
fn poll(&mut self) -> Vec<GamepadEvent>;
fn set_rumble(
&mut self,
_id: GamepadId,
_low_freq: f32,
_high_freq: f32,
_duration_ms: u32,
) -> bool {
false
}
fn stop_rumble(&mut self, _id: GamepadId) {}
fn is_rumble_supported(&self, _id: GamepadId) -> bool {
false
}
}
pub const STICK_DEADZONE: f32 = 0.2;
pub(crate) fn apply_stick_deadzone(v: f32) -> f32 {
if v.abs() < STICK_DEADZONE {
0.0
} else {
v.signum() * (v.abs() - STICK_DEADZONE) / (1.0 - STICK_DEADZONE)
}
}
#[cfg(all(feature = "gamepad", not(target_os = "android")))]
pub struct GilrsBackend {
gilrs: gilrs::Gilrs,
ff_effects: std::collections::HashMap<u32, gilrs::ff::Effect>,
boot_done: bool,
}
#[cfg(all(feature = "gamepad", not(target_os = "android")))]
impl GilrsBackend {
pub fn new() -> Option<Self> {
match gilrs::Gilrs::new() {
Ok(gilrs) => Some(Self {
gilrs,
ff_effects: std::collections::HashMap::new(),
boot_done: false,
}),
Err(e) => {
log::warn!("gamepad: gilrs init failed ({e}); gamepad input disabled");
None
}
}
}
fn map_button(b: gilrs::Button) -> Option<GamepadButton> {
use gilrs::Button as G;
Some(match b {
G::South => GamepadButton::South,
G::East => GamepadButton::East,
G::West => GamepadButton::West,
G::North => GamepadButton::North,
G::Start => GamepadButton::Start,
G::Select => GamepadButton::Select,
G::LeftTrigger => GamepadButton::LeftShoulder,
G::RightTrigger => GamepadButton::RightShoulder,
G::LeftThumb => GamepadButton::LeftStick,
G::RightThumb => GamepadButton::RightStick,
G::DPadUp => GamepadButton::DPadUp,
G::DPadDown => GamepadButton::DPadDown,
G::DPadLeft => GamepadButton::DPadLeft,
G::DPadRight => GamepadButton::DPadRight,
_ => return None,
})
}
fn map_axis(a: gilrs::Axis) -> Option<GamepadAxis> {
use gilrs::Axis as G;
Some(match a {
G::LeftStickX => GamepadAxis::LeftStickX,
G::LeftStickY => GamepadAxis::LeftStickY,
G::RightStickX => GamepadAxis::RightStickX,
G::RightStickY => GamepadAxis::RightStickY,
_ => return None,
})
}
}
#[cfg(all(feature = "gamepad", not(target_os = "android")))]
impl GamepadBackend for GilrsBackend {
fn poll(&mut self) -> Vec<GamepadEvent> {
use gilrs::EventType as E;
let mut out = Vec::new();
while let Some(ev) = self.gilrs.next_event() {
let id = GamepadId(usize::from(ev.id) as u32);
match ev.event {
E::Connected => {
let name = self.gilrs.gamepad(ev.id).name().to_string();
out.push(GamepadEvent::Connected { id, name });
}
E::Disconnected => out.push(GamepadEvent::Disconnected { id }),
E::ButtonPressed(b, _) | E::ButtonRepeated(b, _) => {
if let Some(button) = Self::map_button(b) {
out.push(GamepadEvent::Button {
id,
button,
pressed: true,
});
}
}
E::ButtonReleased(b, _) => {
if let Some(button) = Self::map_button(b) {
out.push(GamepadEvent::Button {
id,
button,
pressed: false,
});
}
}
E::ButtonChanged(b, v, _) => {
let axis = match b {
gilrs::Button::LeftTrigger2 => Some(GamepadAxis::LeftTrigger),
gilrs::Button::RightTrigger2 => Some(GamepadAxis::RightTrigger),
_ => None,
};
if let Some(axis) = axis {
out.push(GamepadEvent::Axis {
id,
axis,
value: v.clamp(0.0, 1.0),
});
}
}
E::AxisChanged(a, v, _) => {
if let Some(axis) = Self::map_axis(a) {
out.push(GamepadEvent::Axis {
id,
axis,
value: apply_stick_deadzone(v),
});
}
}
E::Dropped | E::ForceFeedbackEffectCompleted => {}
_ => {}
}
}
if !self.boot_done {
self.boot_done = true;
let live: Vec<(GamepadId, String)> = self
.gilrs
.gamepads()
.map(|(gid, pad)| (GamepadId(usize::from(gid) as u32), pad.name().to_string()))
.collect();
let mut synth = synthesize_boot(live.into_iter(), &out);
synth.append(&mut out);
out = synth;
}
out
}
fn is_rumble_supported(&self, id: GamepadId) -> bool {
let want = id.0 as usize;
self.gilrs
.gamepads()
.find(|(gid, _)| usize::from(*gid) == want)
.map(|(_, pad)| pad.is_ff_supported())
.unwrap_or(false)
}
fn set_rumble(
&mut self,
id: GamepadId,
low_freq: f32,
high_freq: f32,
duration_ms: u32,
) -> bool {
use gilrs::ff::{BaseEffect, BaseEffectType, EffectBuilder, Repeat, Replay, Ticks};
let low = low_freq.clamp(0.0, 1.0);
let high = high_freq.clamp(0.0, 1.0);
if low <= 0.0 && high <= 0.0 {
self.stop_rumble(id);
return true;
}
let want = id.0 as usize;
let gid = match self
.gilrs
.gamepads()
.find(|(gid, _)| usize::from(*gid) == want)
.map(|(gid, _)| gid)
{
Some(g) => g,
None => return false,
};
if !self
.gilrs
.connected_gamepad(gid)
.map(|p| p.is_ff_supported())
.unwrap_or(false)
{
return false;
}
let duration = Ticks::from_ms(duration_ms.max(1));
let mut builder = EffectBuilder::new();
builder
.add_effect(BaseEffect {
kind: BaseEffectType::Strong {
magnitude: (low * u16::MAX as f32) as u16,
},
scheduling: Replay {
play_for: duration,
..Default::default()
},
..Default::default()
})
.add_effect(BaseEffect {
kind: BaseEffectType::Weak {
magnitude: (high * u16::MAX as f32) as u16,
},
scheduling: Replay {
play_for: duration,
..Default::default()
},
..Default::default()
})
.repeat(Repeat::For(duration))
.gamepads(&[gid]);
match builder.finish(&mut self.gilrs) {
Ok(effect) => {
let _ = effect.play();
self.ff_effects.insert(id.0, effect);
true
}
Err(e) => {
log::warn!("gamepad: rumble failed for pad {} ({e:?})", id.0);
false
}
}
}
fn stop_rumble(&mut self, id: GamepadId) {
if let Some(effect) = self.ff_effects.remove(&id.0) {
let _ = effect.stop();
}
}
}
pub fn create_backend() -> Option<impl GamepadBackend> {
#[cfg(all(feature = "gamepad", target_os = "android"))]
{
AndroidBackend::new()
}
#[cfg(all(feature = "gamepad", not(target_os = "android")))]
{
GilrsBackend::new()
}
#[cfg(not(feature = "gamepad"))]
{
None::<NoBackend>
}
}
#[cfg(all(feature = "gamepad", target_os = "android"))]
pub fn create_android_backend() -> Option<AndroidBackend> {
AndroidBackend::new()
}
#[cfg(all(feature = "gamepad", target_os = "android"))]
pub struct AndroidBackend {
connected: bool,
}
#[cfg(all(feature = "gamepad", target_os = "android"))]
impl AndroidBackend {
pub fn new() -> Option<Self> {
Some(Self { connected: false })
}
pub fn key_button(&mut self, code: u32, pressed: bool) -> Vec<GamepadEvent> {
let Some(button) = android_code_to_button(code) else {
return Vec::new();
};
let mut out = Vec::with_capacity(2);
if !self.connected {
self.connected = true;
out.push(GamepadEvent::Connected {
id: GamepadId(0),
name: "Android controller".to_string(),
});
}
out.push(GamepadEvent::Button {
id: GamepadId(0),
button,
pressed,
});
out
}
}
#[cfg(all(feature = "gamepad", target_os = "android"))]
impl GamepadBackend for AndroidBackend {
fn poll(&mut self) -> Vec<GamepadEvent> {
Vec::new()
}
}
#[cfg(feature = "gamepad")]
pub fn android_code_to_button(code: u32) -> Option<GamepadButton> {
Some(match code {
19 => GamepadButton::DPadUp,
20 => GamepadButton::DPadDown,
21 => GamepadButton::DPadLeft,
22 => GamepadButton::DPadRight,
23 => GamepadButton::South, 96 => GamepadButton::South, 97 => GamepadButton::East, 99 => GamepadButton::West, 100 => GamepadButton::North, 102 => GamepadButton::LeftShoulder, 103 => GamepadButton::RightShoulder, 106 => GamepadButton::LeftStick, 107 => GamepadButton::RightStick, 108 => GamepadButton::Start, 109 => GamepadButton::Select, _ => return None,
})
}
#[cfg(all(feature = "gamepad", not(target_os = "android")))]
fn synthesize_boot(
live: impl Iterator<Item = (GamepadId, String)>,
reported: &[GamepadEvent],
) -> Vec<GamepadEvent> {
live.filter(|(id, _)| {
!reported
.iter()
.any(|ev| matches!(ev, GamepadEvent::Connected { id: eid, .. } if eid == id))
})
.map(|(id, name)| GamepadEvent::Connected { id, name })
.collect()
}
pub struct NoBackend;
impl GamepadBackend for NoBackend {
fn poll(&mut self) -> Vec<GamepadEvent> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stick_deadzone_snaps_and_rescales() {
assert_eq!(apply_stick_deadzone(0.0), 0.0);
assert_eq!(apply_stick_deadzone(0.19), 0.0);
assert_eq!(apply_stick_deadzone(-0.19), 0.0);
assert_eq!(apply_stick_deadzone(1.0), 1.0);
assert_eq!(apply_stick_deadzone(-1.0), -1.0);
let mid = apply_stick_deadzone(0.6);
assert!((mid - 0.5).abs() < 1e-6);
}
#[test]
fn rumble_unsupported_by_default() {
let mut backend = NoBackend;
assert!(!backend.is_rumble_supported(GamepadId(0)));
assert!(!backend.set_rumble(GamepadId(0), 1.0, 1.0, 100));
backend.stop_rumble(GamepadId(0));
}
#[cfg(feature = "gamepad")]
#[test]
fn android_codes_map_to_standard_layout() {
assert_eq!(android_code_to_button(96), Some(GamepadButton::South));
assert_eq!(android_code_to_button(97), Some(GamepadButton::East));
assert_eq!(android_code_to_button(99), Some(GamepadButton::West));
assert_eq!(android_code_to_button(100), Some(GamepadButton::North));
assert_eq!(
android_code_to_button(102),
Some(GamepadButton::LeftShoulder)
);
assert_eq!(android_code_to_button(108), Some(GamepadButton::Start));
assert_eq!(android_code_to_button(19), Some(GamepadButton::DPadUp));
assert_eq!(android_code_to_button(23), Some(GamepadButton::South));
assert_eq!(android_code_to_button(29), None); assert_eq!(android_code_to_button(98), None); assert_eq!(android_code_to_button(110), None); }
#[cfg(all(feature = "gamepad", not(target_os = "android")))]
#[test]
fn boot_synthesis_reports_all_live_devices() {
let live = vec![
(GamepadId(0), "Pad A".to_string()),
(GamepadId(1), "Pad B".to_string()),
];
let synth = synthesize_boot(live.into_iter(), &[]);
assert_eq!(synth.len(), 2);
assert!(matches!(
&synth[0],
GamepadEvent::Connected { id, name }
if *id == GamepadId(0) && name == "Pad A"
));
}
#[cfg(all(feature = "gamepad", not(target_os = "android")))]
#[test]
fn boot_synthesis_skips_self_reported_ids() {
let live = vec![
(GamepadId(0), "Pad A".to_string()),
(GamepadId(1), "Pad B".to_string()),
];
let reported = vec![GamepadEvent::Connected {
id: GamepadId(1),
name: "Pad B".to_string(),
}];
let synth = synthesize_boot(live.into_iter(), &reported);
assert_eq!(synth.len(), 1);
assert!(matches!(
&synth[0],
GamepadEvent::Connected { id, .. } if *id == GamepadId(0)
));
let reported = vec![GamepadEvent::Button {
id: GamepadId(0),
button: GamepadButton::South,
pressed: true,
}];
let live = vec![(GamepadId(0), "Pad A".to_string())];
assert_eq!(synthesize_boot(live.into_iter(), &reported).len(), 1);
}
#[cfg(all(feature = "gamepad", not(target_os = "android")))]
#[test]
fn first_poll_is_empty_without_hardware() {
if let Some(mut backend) = GilrsBackend::new() {
assert!(backend.poll().is_empty());
assert!(backend.poll().is_empty());
}
}
}