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)));
}
});
}