use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use gilrs::{Axis, Button, Event, EventType, GamepadId, Gilrs};
use super::common::{ActionRepeater, AxisDirection, InputActions};
use super::{ControllerAction, ControllerEvent, ControllerEventSender};
const GILRS_POLL_INTERVAL: Duration = Duration::from_millis(8);
const GILRS_RETRY_INTERVAL: Duration = Duration::from_millis(500);
const MAX_GILRS_EVENTS_PER_TICK: usize = 64;
const MAX_WORKER_ACTIONS_PER_TICK: usize = 32;
const STICK_DEADZONE: f32 = 0.5;
pub(super) struct ControllerWorker {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl std::fmt::Debug for ControllerWorker {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ControllerWorker")
.field("stop_requested", &self.stop.load(Ordering::Relaxed))
.field("running", &self.handle.is_some())
.finish()
}
}
impl ControllerWorker {
pub(super) fn spawn(sender: ControllerEventSender, context: egui::Context) -> Option<Self> {
let stop = Arc::new(AtomicBool::new(false));
let worker_stop = Arc::clone(&stop);
let handle = thread::Builder::new()
.name("dream-ini-gilrs-controller".to_owned())
.spawn(move || run_worker(&sender, &context, &worker_stop))
.ok()?;
Some(Self {
stop,
handle: Some(handle),
})
}
}
impl Drop for ControllerWorker {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
handle.thread().unpark();
let _ = handle.join();
}
}
}
struct WorkerState {
gilrs: Option<Gilrs>,
gamepads: BTreeMap<usize, GamepadInputState>,
}
impl WorkerState {
fn new() -> Self {
Self {
gilrs: Gilrs::new().ok(),
gamepads: BTreeMap::new(),
}
}
fn poll(&mut self) -> InputActions {
if self.gilrs.is_none() {
self.gilrs = Gilrs::new().ok();
thread::park_timeout(GILRS_RETRY_INTERVAL);
return InputActions::default();
}
let now = Instant::now();
let mut input = InputActions::default();
for _ in 0..MAX_GILRS_EVENTS_PER_TICK {
let event = self.gilrs.as_mut().and_then(Gilrs::next_event);
let Some(event) = event else {
break;
};
input.extend(self.handle_event(event, now));
if input.actions.len() + input.repeat_actions.len() >= MAX_WORKER_ACTIONS_PER_TICK {
break;
}
}
for gamepad in self.gamepads.values_mut() {
input.repeat_actions.extend(gamepad.repeater.poll(now));
if input.actions.len() + input.repeat_actions.len() >= MAX_WORKER_ACTIONS_PER_TICK {
break;
}
}
truncate_input_actions(&mut input, MAX_WORKER_ACTIONS_PER_TICK);
input
}
fn has_gamepads(&self) -> bool {
self.gilrs
.as_ref()
.is_some_and(|gilrs| gilrs.gamepads().any(|(_, gamepad)| gamepad.is_connected()))
}
fn handle_event(&mut self, event: Event, now: Instant) -> InputActions {
match event.event {
EventType::ButtonPressed(button, _) => {
let gamepad = self.gamepad_state(event.id);
button_pressed(button, gamepad, now)
}
EventType::Connected => InputActions::default(),
EventType::Disconnected => {
self.gamepads.remove(&gamepad_key(event.id));
InputActions::released()
}
EventType::ButtonReleased(button, _) => {
if let Some(action) = repeatable_button_action(button) {
let gamepad = self.gamepad_state(event.id);
if remove_held_button(&mut gamepad.held_buttons, button) {
gamepad.repeater.stop(action);
return InputActions::released();
}
}
InputActions::default()
}
EventType::AxisChanged(Axis::LeftStickX, value, _) => {
let gamepad = self.gamepad_state(event.id);
gamepad.axes.left_x.update(
stick_direction(value),
horizontal_action,
&mut gamepad.repeater,
now,
)
}
EventType::AxisChanged(Axis::LeftStickY, value, _) => {
let gamepad = self.gamepad_state(event.id);
gamepad.axes.left_y.update(
stick_direction(value),
vertical_action,
&mut gamepad.repeater,
now,
)
}
EventType::AxisChanged(Axis::RightStickX, value, _) => {
let gamepad = self.gamepad_state(event.id);
gamepad.axes.right_x.update(
stick_direction(value),
preview_horizontal_scroll_action,
&mut gamepad.repeater,
now,
)
}
EventType::AxisChanged(Axis::RightStickY, value, _) => {
let gamepad = self.gamepad_state(event.id);
gamepad.axes.right_y.update(
stick_direction(value),
preview_vertical_scroll_action,
&mut gamepad.repeater,
now,
)
}
_ => InputActions::default(),
}
}
fn gamepad_state(&mut self, id: GamepadId) -> &mut GamepadInputState {
self.gamepads.entry(gamepad_key(id)).or_default()
}
fn sleep_duration(&self) -> Duration {
self.gamepads
.values()
.filter_map(|gamepad| gamepad.repeater.next_repeat())
.min()
.map(|instant| instant.saturating_duration_since(Instant::now()))
.unwrap_or(GILRS_POLL_INTERVAL)
.min(GILRS_POLL_INTERVAL)
}
}
fn gamepad_key(id: GamepadId) -> usize {
usize::from(id)
}
#[derive(Debug, Default)]
struct GamepadInputState {
axes: AxisState,
held_buttons: Vec<Button>,
repeater: ActionRepeater,
}
fn run_worker(sender: &ControllerEventSender, context: &egui::Context, stop: &AtomicBool) {
let mut state = WorkerState::new();
let mut last_availability = state.has_gamepads();
while !stop.load(Ordering::Relaxed) {
let input = state.poll();
let available = state.has_gamepads();
if available != last_availability {
if send_event(sender, ControllerEvent::Available(available)) {
context.request_repaint();
}
last_availability = available;
}
if input.released {
sender.purge_actions();
send_event(sender, ControllerEvent::PurgeQueuedActions);
}
if input.actions.is_empty() && input.repeat_actions.is_empty() {
thread::park_timeout(state.sleep_duration());
continue;
}
let mut sent_action = false;
for action in input.actions {
if send_event(sender, ControllerEvent::Action(action)) {
sent_action = true;
}
}
for action in input.repeat_actions {
if send_event(sender, ControllerEvent::RepeatAction(action)) {
sent_action = true;
}
}
if sent_action {
context.request_repaint();
}
}
}
fn send_event(sender: &ControllerEventSender, event: ControllerEvent) -> bool {
sender.send(event)
}
fn truncate_input_actions(input: &mut InputActions, limit: usize) {
input.actions.truncate(limit);
input
.repeat_actions
.truncate(limit.saturating_sub(input.actions.len()));
}
#[derive(Debug, Default)]
struct AxisState {
left_x: AxisDirection,
left_y: AxisDirection,
right_x: AxisDirection,
right_y: AxisDirection,
}
fn button_pressed(button: Button, gamepad: &mut GamepadInputState, now: Instant) -> InputActions {
if let Some(action) = immediate_button_action(button) {
return InputActions::action(action);
}
repeatable_button_action(button).map_or_else(InputActions::default, |action| {
if gamepad.held_buttons.contains(&button) {
InputActions::default()
} else {
gamepad.held_buttons.push(button);
InputActions::repeatable_press(gamepad.repeater.start(action, now))
}
})
}
fn remove_held_button(held_buttons: &mut Vec<Button>, button: Button) -> bool {
let Some(index) = held_buttons
.iter()
.position(|held_button| *held_button == button)
else {
return false;
};
held_buttons.swap_remove(index);
true
}
fn immediate_button_action(button: Button) -> Option<ControllerAction> {
match button {
Button::South => Some(ControllerAction::Accept),
Button::East => Some(ControllerAction::Secondary),
Button::West => Some(ControllerAction::ClearCurrent),
Button::North => Some(ControllerAction::Space),
Button::Select => Some(ControllerAction::Cancel),
Button::Start => Some(ControllerAction::SelectCurrent),
Button::LeftTrigger => Some(ControllerAction::ToggleHiddenDirectories),
Button::RightTrigger => Some(ControllerAction::PagePreviewDown),
_ => None,
}
}
fn repeatable_button_action(button: Button) -> Option<ControllerAction> {
match button {
Button::DPadUp => Some(ControllerAction::Up),
Button::DPadDown => Some(ControllerAction::Down),
Button::DPadLeft => Some(ControllerAction::Left),
Button::DPadRight => Some(ControllerAction::Right),
_ => None,
}
}
fn stick_direction(value: f32) -> AxisDirection {
if value < -STICK_DEADZONE {
AxisDirection::Negative
} else if value > STICK_DEADZONE {
AxisDirection::Positive
} else {
AxisDirection::Neutral
}
}
fn horizontal_action(direction: AxisDirection) -> Option<ControllerAction> {
match direction {
AxisDirection::Negative => Some(ControllerAction::Left),
AxisDirection::Neutral => None,
AxisDirection::Positive => Some(ControllerAction::Right),
}
}
fn vertical_action(direction: AxisDirection) -> Option<ControllerAction> {
match direction {
AxisDirection::Negative => Some(ControllerAction::Down),
AxisDirection::Neutral => None,
AxisDirection::Positive => Some(ControllerAction::Up),
}
}
fn preview_horizontal_scroll_action(direction: AxisDirection) -> Option<ControllerAction> {
match direction {
AxisDirection::Negative => Some(ControllerAction::ScrollPreviewLeft),
AxisDirection::Neutral => None,
AxisDirection::Positive => Some(ControllerAction::ScrollPreviewRight),
}
}
fn preview_vertical_scroll_action(direction: AxisDirection) -> Option<ControllerAction> {
match direction {
AxisDirection::Negative => Some(ControllerAction::ScrollPreviewDown),
AxisDirection::Neutral => None,
AxisDirection::Positive => Some(ControllerAction::ScrollPreviewUp),
}
}