Skip to main content

pebble/
gamepad.rs

1//! Gamepad/controller polling — `GamepadPlugin` inserts [`Gamepads`] as a
2//! resource and ticks it once per frame in `PreUpdate`, exactly like
3//! [`crate::time::TimePlugin`]. `App::new()` already builds this in, so
4//! `Res<Gamepads>` works without registering anything yourself.
5//!
6//! Backend-agnostic, same as [`crate::time`] — nothing here depends on
7//! `pebble::wgpu` or any particular rendering backend.
8
9use std::collections::HashSet;
10
11use crate::{
12    app::SystemStage,
13    ecs::{plugin::Plugin, system::ResMut},
14};
15
16/// Mirrors `gilrs::Button`.
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
18pub enum GamepadButton {
19    South,
20    East,
21    North,
22    West,
23    C,
24    Z,
25    LeftTrigger,
26    LeftTrigger2,
27    RightTrigger,
28    RightTrigger2,
29    Select,
30    Start,
31    Mode,
32    LeftThumb,
33    RightThumb,
34    DPadUp,
35    DPadDown,
36    DPadLeft,
37    DPadRight,
38    Unknown,
39}
40
41impl From<GamepadButton> for gilrs::Button {
42    fn from(value: GamepadButton) -> Self {
43        match value {
44            GamepadButton::South => Self::South,
45            GamepadButton::East => Self::East,
46            GamepadButton::North => Self::North,
47            GamepadButton::West => Self::West,
48            GamepadButton::C => Self::C,
49            GamepadButton::Z => Self::Z,
50            GamepadButton::LeftTrigger => Self::LeftTrigger,
51            GamepadButton::LeftTrigger2 => Self::LeftTrigger2,
52            GamepadButton::RightTrigger => Self::RightTrigger,
53            GamepadButton::RightTrigger2 => Self::RightTrigger2,
54            GamepadButton::Select => Self::Select,
55            GamepadButton::Start => Self::Start,
56            GamepadButton::Mode => Self::Mode,
57            GamepadButton::LeftThumb => Self::LeftThumb,
58            GamepadButton::RightThumb => Self::RightThumb,
59            GamepadButton::DPadUp => Self::DPadUp,
60            GamepadButton::DPadDown => Self::DPadDown,
61            GamepadButton::DPadLeft => Self::DPadLeft,
62            GamepadButton::DPadRight => Self::DPadRight,
63            GamepadButton::Unknown => Self::Unknown,
64        }
65    }
66}
67
68impl From<gilrs::Button> for GamepadButton {
69    fn from(value: gilrs::Button) -> Self {
70        match value {
71            gilrs::Button::South => Self::South,
72            gilrs::Button::East => Self::East,
73            gilrs::Button::North => Self::North,
74            gilrs::Button::West => Self::West,
75            gilrs::Button::C => Self::C,
76            gilrs::Button::Z => Self::Z,
77            gilrs::Button::LeftTrigger => Self::LeftTrigger,
78            gilrs::Button::LeftTrigger2 => Self::LeftTrigger2,
79            gilrs::Button::RightTrigger => Self::RightTrigger,
80            gilrs::Button::RightTrigger2 => Self::RightTrigger2,
81            gilrs::Button::Select => Self::Select,
82            gilrs::Button::Start => Self::Start,
83            gilrs::Button::Mode => Self::Mode,
84            gilrs::Button::LeftThumb => Self::LeftThumb,
85            gilrs::Button::RightThumb => Self::RightThumb,
86            gilrs::Button::DPadUp => Self::DPadUp,
87            gilrs::Button::DPadDown => Self::DPadDown,
88            gilrs::Button::DPadLeft => Self::DPadLeft,
89            gilrs::Button::DPadRight => Self::DPadRight,
90            _ => Self::Unknown,
91        }
92    }
93}
94
95/// Mirrors `gilrs::Axis`.
96#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
97pub enum GamepadAxis {
98    LeftStickX,
99    LeftStickY,
100    LeftZ,
101    RightStickX,
102    RightStickY,
103    RightZ,
104    DPadX,
105    DPadY,
106    Unknown,
107}
108
109impl From<GamepadAxis> for gilrs::Axis {
110    fn from(value: GamepadAxis) -> Self {
111        match value {
112            GamepadAxis::LeftStickX => Self::LeftStickX,
113            GamepadAxis::LeftStickY => Self::LeftStickY,
114            GamepadAxis::LeftZ => Self::LeftZ,
115            GamepadAxis::RightStickX => Self::RightStickX,
116            GamepadAxis::RightStickY => Self::RightStickY,
117            GamepadAxis::RightZ => Self::RightZ,
118            GamepadAxis::DPadX => Self::DPadX,
119            GamepadAxis::DPadY => Self::DPadY,
120            GamepadAxis::Unknown => Self::Unknown,
121        }
122    }
123}
124
125/// Identifies one connected gamepad. Opaque, `Copy` — valid for the whole
126/// lifetime of the [`Gamepads`] resource that handed it out (via
127/// [`Gamepads::ids`]), even across a disconnect/reconnect of a *different*
128/// controller.
129#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
130pub struct GamepadId(gilrs::GamepadId);
131
132/// Every connected gamepad's current state. A self-contained resource —
133/// `Res<Gamepads>`/`ResMut<Gamepads>` — inserted by [`GamepadPlugin`].
134pub struct Gamepads {
135    /// `Mutex`, not a bare `gilrs::Gilrs`: on some platforms (Windows'
136    /// WGI backend in particular) `gilrs::Gilrs` holds a
137    /// `std::sync::mpsc::Receiver`, which is `Send` but not `Sync` — and
138    /// ECS resources must be `Send + Sync` (`hecs::Component`'s bound)
139    /// regardless of the fact that this scheduler only ever touches one
140    /// resource from one thread at a time, never concurrently.
141    gilrs: std::sync::Mutex<gilrs::Gilrs>,
142    /// Rebuilt every [`tick`](Self::tick) by draining `gilrs`'s event
143    /// queue — `gilrs` itself only exposes a raw event stream plus
144    /// continuously-cached "currently held" state, not pre-computed
145    /// this-tick edges the way `WinitInputHelper` gives
146    /// [`Input`](crate::wgpu::window::Input) for free, so `Gamepads`
147    /// computes them by hand instead.
148    pressed_this_tick: HashSet<(GamepadId, GamepadButton)>,
149    released_this_tick: HashSet<(GamepadId, GamepadButton)>,
150}
151
152impl Gamepads {
153    fn new() -> Result<Self, Box<gilrs::Error>> {
154        Ok(Self {
155            gilrs: std::sync::Mutex::new(gilrs::Gilrs::new().map_err(Box::new)?),
156            pressed_this_tick: HashSet::new(),
157            released_this_tick: HashSet::new(),
158        })
159    }
160
161    fn tick(&mut self) {
162        self.pressed_this_tick.clear();
163        self.released_this_tick.clear();
164        let mut gilrs = self.gilrs.lock().unwrap();
165        while let Some(event) = gilrs.next_event() {
166            let id = GamepadId(event.id);
167            match event.event {
168                gilrs::EventType::ButtonPressed(button, _) => {
169                    self.pressed_this_tick.insert((id, button.into()));
170                }
171                gilrs::EventType::ButtonReleased(button, _) => {
172                    self.released_this_tick.insert((id, button.into()));
173                }
174                _ => {}
175            }
176        }
177    }
178
179    /// Every currently connected gamepad.
180    pub fn ids(&self) -> Vec<GamepadId> {
181        self.gilrs.lock().unwrap().gamepads().map(|(id, _)| GamepadId(id)).collect()
182    }
183
184    pub fn is_connected(&self, id: GamepadId) -> bool {
185        self.gilrs.lock().unwrap().connected_gamepad(id.0).is_some()
186    }
187
188    /// True for every tick the button remains held. `false` for a
189    /// disconnected/unknown `id`.
190    pub fn button_held(&self, id: GamepadId, button: GamepadButton) -> bool {
191        self.gilrs.lock().unwrap().connected_gamepad(id.0).is_some_and(|gamepad| gamepad.is_pressed(button.into()))
192    }
193
194    /// True the tick a button goes from "not held" to "held".
195    pub fn button_pressed(&self, id: GamepadId, button: GamepadButton) -> bool {
196        self.pressed_this_tick.contains(&(id, button))
197    }
198
199    /// True the tick a button goes from "held" to "not held".
200    pub fn button_released(&self, id: GamepadId, button: GamepadButton) -> bool {
201        self.released_this_tick.contains(&(id, button))
202    }
203
204    /// Current value of an analog axis, in `-1.0..=1.0`. `0.0` for a
205    /// disconnected/unknown `id`.
206    pub fn axis(&self, id: GamepadId, axis: GamepadAxis) -> f32 {
207        self.gilrs.lock().unwrap().connected_gamepad(id.0).map_or(0.0, |gamepad| gamepad.value(axis.into()))
208    }
209}
210
211fn tick_gamepads(mut gamepads: ResMut<Gamepads>) {
212    gamepads.tick();
213}
214
215/// Registers [`Gamepads`] as a resource and advances it once per frame.
216///
217/// Unlike [`crate::time::TimePlugin`], this is **not** built into
218/// `App::new()` — add it yourself, and enable the `gamepad` Cargo feature.
219/// If no gamepad backend is available on this platform at all (rare —
220/// distinct from "no controller is currently plugged in", which is a
221/// completely normal, always-supported state), `build` logs a
222/// `tracing::error!` and does not insert [`Gamepads`] — take
223/// `Option<Res<Gamepads>>` in systems that need to keep working either way.
224///
225/// `App::new()` already builds this in, so registering it again yourself
226/// (harmless, but unnecessary) does not open a second gamepad backend —
227/// idempotent the same way `TimePlugin` is.
228pub struct GamepadPlugin;
229
230impl GamepadPlugin {
231    pub fn new() -> Self {
232        Self
233    }
234}
235
236impl Default for GamepadPlugin {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242/// Cheap marker inserted before the (expensive, fallible) real work, so a
243/// second `GamepadPlugin::build` call can check "already handled" without
244/// opening a second gamepad backend just to discard it.
245struct GamepadPluginRan;
246
247impl Plugin for GamepadPlugin {
248    fn build(&self, app: &mut crate::prelude::App) {
249        if !app.try_insert_resource(GamepadPluginRan) {
250            return;
251        }
252        match Gamepads::new() {
253            Ok(gamepads) => {
254                app.add_resource(gamepads);
255                app.add_system(SystemStage::PreUpdate, tick_gamepads);
256            }
257            Err(e) => tracing::error!("GamepadPlugin: failed to initialize gamepad backend: {e}"),
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn every_gamepad_button_round_trips_through_the_gilrs_conversion() {
268        let all = [
269            GamepadButton::South,
270            GamepadButton::East,
271            GamepadButton::North,
272            GamepadButton::West,
273            GamepadButton::C,
274            GamepadButton::Z,
275            GamepadButton::LeftTrigger,
276            GamepadButton::LeftTrigger2,
277            GamepadButton::RightTrigger,
278            GamepadButton::RightTrigger2,
279            GamepadButton::Select,
280            GamepadButton::Start,
281            GamepadButton::Mode,
282            GamepadButton::LeftThumb,
283            GamepadButton::RightThumb,
284            GamepadButton::DPadUp,
285            GamepadButton::DPadDown,
286            GamepadButton::DPadLeft,
287            GamepadButton::DPadRight,
288            GamepadButton::Unknown,
289        ];
290        for button in all {
291            let gilrs_button: gilrs::Button = button.into();
292            assert_eq!(GamepadButton::from(gilrs_button), button);
293        }
294    }
295}