use std::cell::RefCell;
use std::time::{Duration, Instant};
use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Replay, Ticks};
use gilrs::{Axis as GAxis, Button as GButton, GamepadId, Gilrs};
struct Pad {
gilrs: Gilrs,
active: Option<GamepadId>,
effects: Vec<(Effect, Instant)>, start_edge: bool, start_prev: bool,
}
thread_local! {
static PAD: RefCell<Option<Pad>> = const { RefCell::new(None) };
}
fn ensure(p: &mut Option<Pad>) {
if p.is_none() {
if let Ok(gilrs) = Gilrs::new() {
let active = gilrs.gamepads().next().map(|(id, _)| id);
*p = Some(Pad {
gilrs,
active,
effects: Vec::new(),
start_edge: false,
start_prev: false,
});
}
}
}
pub fn poll() {
PAD.with(|cell| {
let mut g = cell.borrow_mut();
ensure(&mut g);
let Some(pad) = g.as_mut() else { return };
while let Some(ev) = pad.gilrs.next_event() {
pad.gilrs.update(&ev);
pad.active = Some(ev.id);
}
if pad.active.is_none() {
pad.active = pad.gilrs.gamepads().next().map(|(id, _)| id);
}
let cur_start = pad
.active
.map(|id| pad.gilrs.gamepad(id).is_pressed(GButton::Start))
.unwrap_or(false);
pad.start_edge = cur_start && !pad.start_prev;
pad.start_prev = cur_start;
let now = Instant::now();
pad.effects.retain(|(_, exp)| *exp > now);
});
}
pub fn start_edge() -> bool {
PAD.with(|cell| {
cell.borrow()
.as_ref()
.map(|p| p.start_edge)
.unwrap_or(false)
})
}
pub fn list() -> String {
let mut out = String::new();
PAD.with(|cell| {
let mut g = cell.borrow_mut();
ensure(&mut g);
match g.as_ref() {
Some(pad) => {
let mut n = 0;
for (_, gp) in pad.gilrs.gamepads() {
n += 1;
out.push_str(&format!(
"{n}: \"{}\" connected={} rumble={} map={:?}\n",
gp.name(),
gp.is_connected(),
gp.is_ff_supported(),
gp.mapping_source()
));
}
if n == 0 {
out.push_str(
"(no gamepads seen by gilrs — try the controller's PC/X-input mode)\n",
);
}
},
None => out.push_str("(gilrs failed to initialise)\n"),
}
});
out
}
pub fn any_button() -> bool {
PAD.with(|cell| {
let g = cell.borrow();
let Some(pad) = g.as_ref() else { return false };
let Some(id) = pad.active else { return false };
let gp = pad.gilrs.gamepad(id);
for b in [
GButton::South,
GButton::East,
GButton::West,
GButton::North,
GButton::LeftTrigger,
GButton::RightTrigger,
GButton::LeftTrigger2,
GButton::RightTrigger2,
GButton::Start,
GButton::Select,
GButton::DPadUp,
GButton::DPadDown,
GButton::DPadLeft,
GButton::DPadRight,
] {
if gp.is_pressed(b) {
return true;
}
}
false
})
}
fn btn(name: &str) -> Option<GButton> {
Some(match name {
"a" => GButton::South,
"b" => GButton::East,
"x" => GButton::West,
"y" => GButton::North,
"l1" => GButton::LeftTrigger,
"r1" => GButton::RightTrigger,
"l2" => GButton::LeftTrigger2,
"r2" => GButton::RightTrigger2,
"l3" => GButton::LeftThumb,
"r3" => GButton::RightThumb,
"start" => GButton::Start,
"select" => GButton::Select,
"dup" => GButton::DPadUp,
"ddown" => GButton::DPadDown,
"dleft" => GButton::DPadLeft,
"dright" => GButton::DPadRight,
_ => return None,
})
}
pub fn button(name: &str) -> bool {
let Some(b) = btn(name) else { return false };
PAD.with(|cell| {
let g = cell.borrow();
let Some(pad) = g.as_ref() else { return false };
let Some(id) = pad.active else { return false };
pad.gilrs.gamepad(id).is_pressed(b)
})
}
pub fn axis(name: &str) -> f32 {
let a = match name {
"lx" => GAxis::LeftStickX,
"ly" => GAxis::LeftStickY,
"rx" => GAxis::RightStickX,
"ry" => GAxis::RightStickY,
"l2" => GAxis::LeftZ,
"r2" => GAxis::RightZ,
_ => return 0.0,
};
PAD.with(|cell| {
let g = cell.borrow();
let Some(pad) = g.as_ref() else { return 0.0 };
let Some(id) = pad.active else { return 0.0 };
pad.gilrs.gamepad(id).value(a)
})
}
pub fn rumble(low: f32, high: f32, ms: u32) {
let lo = (low.clamp(0.0, 1.0) * 65535.0) as u16;
let hi = (high.clamp(0.0, 1.0) * 65535.0) as u16;
PAD.with(|cell| {
let mut g = cell.borrow_mut();
ensure(&mut g);
let Some(pad) = g.as_mut() else { return };
let Some(id) = pad.active else { return };
if !pad.gilrs.gamepad(id).is_ff_supported() {
return;
}
let play = Replay { play_for: Ticks::from_ms(ms), ..Default::default() };
let built = EffectBuilder::new()
.add_effect(BaseEffect {
kind: BaseEffectType::Strong { magnitude: hi },
scheduling: play,
..Default::default()
})
.add_effect(BaseEffect {
kind: BaseEffectType::Weak { magnitude: lo },
scheduling: play,
..Default::default()
})
.gamepads(&[id])
.finish(&mut pad.gilrs);
if let Ok(effect) = built {
let _ = effect.play();
pad.effects.push((
effect,
Instant::now() + Duration::from_millis(ms as u64 + 50),
));
}
});
}