Skip to main content

game_toolkit_input/
lib.rs

1//! Input subsystem: keyboard + mouse + gamepads, with held / just-pressed / just-released
2//! semantics.
3//!
4//! Call [`Input::handle_window_event`] for every `winit::event::WindowEvent` and
5//! [`Input::poll_gamepads`] once per frame before `update`, then [`Input::end_frame`] once
6//! per frame after `update` to clear edge state.
7
8#![forbid(unsafe_code)]
9
10use std::collections::{HashMap, HashSet};
11
12pub use winit::keyboard::KeyCode as Key;
13
14use gilrs::ff::{BaseEffect, BaseEffectType, EffectBuilder, Replay, Ticks};
15pub use gilrs::{Axis, Button, GamepadId};
16use gilrs::{Event, Gilrs};
17
18#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
19pub enum MouseButton {
20    Left,
21    Right,
22    Middle,
23    Other(u16),
24}
25
26impl From<winit::event::MouseButton> for MouseButton {
27    fn from(b: winit::event::MouseButton) -> Self {
28        match b {
29            winit::event::MouseButton::Left => MouseButton::Left,
30            winit::event::MouseButton::Right => MouseButton::Right,
31            winit::event::MouseButton::Middle => MouseButton::Middle,
32            winit::event::MouseButton::Back => MouseButton::Other(3),
33            winit::event::MouseButton::Forward => MouseButton::Other(4),
34            winit::event::MouseButton::Other(n) => MouseButton::Other(n),
35        }
36    }
37}
38
39/// Per-controller state, with the same held / just-pressed / just-released model as the
40/// keyboard. Obtain via [`Input::gamepads`] or [`Input::first_gamepad`].
41pub struct Gamepad {
42    id: GamepadId,
43    name: String,
44    connected: bool,
45    held: HashSet<Button>,
46    pressed: HashSet<Button>,
47    released: HashSet<Button>,
48    axes: HashMap<Axis, f32>,
49}
50
51impl Gamepad {
52    fn new(id: GamepadId) -> Self {
53        Self {
54            id,
55            name: String::new(),
56            connected: true,
57            held: HashSet::new(),
58            pressed: HashSet::new(),
59            released: HashSet::new(),
60            axes: HashMap::new(),
61        }
62    }
63
64    pub fn id(&self) -> GamepadId {
65        self.id
66    }
67    pub fn name(&self) -> &str {
68        &self.name
69    }
70    pub fn is_connected(&self) -> bool {
71        self.connected
72    }
73    pub fn button_held(&self, b: Button) -> bool {
74        self.held.contains(&b)
75    }
76    pub fn button_pressed(&self, b: Button) -> bool {
77        self.pressed.contains(&b)
78    }
79    pub fn button_released(&self, b: Button) -> bool {
80        self.released.contains(&b)
81    }
82    /// Axis value in `[-1, 1]` (triggers in `[0, 1]`); `0.0` if never reported.
83    pub fn axis(&self, axis: Axis) -> f32 {
84        self.axes.get(&axis).copied().unwrap_or(0.0)
85    }
86}
87
88#[derive(Default)]
89pub struct Input {
90    keys_held: HashSet<Key>,
91    keys_pressed: HashSet<Key>,
92    keys_released: HashSet<Key>,
93    mouse_held: HashSet<MouseButton>,
94    mouse_pressed: HashSet<MouseButton>,
95    mouse_released: HashSet<MouseButton>,
96    mouse_pos: (f32, f32),
97    mouse_delta: (f32, f32),
98    scroll: (f32, f32),
99    /// `None` when no gamepad backend could initialize (the toolkit keeps running).
100    gilrs: Option<Gilrs>,
101    gamepads: HashMap<GamepadId, Gamepad>,
102    /// Active rumble effects kept alive for their duration (bounded ring).
103    rumble: Vec<gilrs::ff::Effect>,
104}
105
106impl Input {
107    pub fn new() -> Self {
108        let mut me = Self::default();
109        match Gilrs::new() {
110            Ok(g) => {
111                // Seed pads already connected at startup; gilrs only emits `Connected` for
112                // hot-plugs, so without this a present controller is unknown until it moves.
113                for (id, gp) in g.gamepads() {
114                    let mut pad = Gamepad::new(id);
115                    pad.name = gp.name().to_string();
116                    me.gamepads.insert(id, pad);
117                }
118                me.gilrs = Some(g);
119            }
120            Err(e) => log::warn!("gamepad backend unavailable, continuing without it: {e}"),
121        }
122        me
123    }
124
125    /// Iterator over currently connected gamepads.
126    pub fn gamepads(&self) -> impl Iterator<Item = &Gamepad> {
127        self.gamepads.values().filter(|g| g.connected)
128    }
129
130    /// The first connected gamepad, if any. Convenient for single-player input.
131    pub fn first_gamepad(&self) -> Option<&Gamepad> {
132        self.gamepads.values().find(|g| g.connected)
133    }
134
135    /// Look up a connected gamepad by id.
136    pub fn gamepad(&self, id: GamepadId) -> Option<&Gamepad> {
137        self.gamepads.get(&id).filter(|g| g.connected)
138    }
139
140    /// Drain pending gamepad events into per-pad state, tracking hot-plug. Call once per
141    /// frame before `update`.
142    pub fn poll_gamepads(&mut self) {
143        let Some(gilrs) = self.gilrs.as_mut() else {
144            return;
145        };
146        while let Some(Event { id, event, .. }) = gilrs.next_event() {
147            use gilrs::EventType::*;
148            let pad = self.gamepads.entry(id).or_insert_with(|| Gamepad::new(id));
149            match event {
150                Connected => {
151                    pad.connected = true;
152                    pad.name = gilrs.gamepad(id).name().to_string();
153                }
154                Disconnected | Dropped => {
155                    pad.connected = false;
156                    pad.held.clear();
157                    pad.axes.clear();
158                }
159                ButtonPressed(b, _) => {
160                    pad.held.insert(b);
161                    pad.pressed.insert(b);
162                }
163                ButtonReleased(b, _) => {
164                    pad.held.remove(&b);
165                    pad.released.insert(b);
166                }
167                AxisChanged(axis, value, _) => {
168                    pad.axes.insert(axis, value);
169                }
170                _ => {}
171            }
172        }
173    }
174
175    /// Rumble `id` at `magnitude` (`0..=1`) for `duration_ms`. No-op when the pad has no
176    /// force feedback or no backend is available.
177    pub fn set_rumble(&mut self, id: GamepadId, magnitude: f32, duration_ms: u32) {
178        let Some(gilrs) = self.gilrs.as_mut() else {
179            return;
180        };
181        if !gilrs
182            .connected_gamepad(id)
183            .is_some_and(|g| g.is_ff_supported())
184        {
185            return;
186        }
187        let mag = (magnitude.clamp(0.0, 1.0) * f32::from(u16::MAX)) as u16;
188        let effect = EffectBuilder::new()
189            .add_effect(BaseEffect {
190                kind: BaseEffectType::Strong { magnitude: mag },
191                scheduling: Replay {
192                    play_for: Ticks::from_ms(duration_ms),
193                    ..Default::default()
194                },
195                envelope: Default::default(),
196            })
197            .gamepads(&[id])
198            .finish(gilrs);
199        if let Ok(effect) = effect {
200            let _ = effect.play();
201            // Keep the handle alive so the effect is not dropped (and stopped) immediately;
202            // bound the buffer so finished effects are eventually released.
203            self.rumble.push(effect);
204            if self.rumble.len() > 16 {
205                self.rumble.remove(0);
206            }
207        }
208    }
209
210    pub fn key_held(&self, k: Key) -> bool {
211        self.keys_held.contains(&k)
212    }
213    pub fn key_pressed(&self, k: Key) -> bool {
214        self.keys_pressed.contains(&k)
215    }
216    pub fn key_released(&self, k: Key) -> bool {
217        self.keys_released.contains(&k)
218    }
219
220    pub fn mouse_held(&self, b: MouseButton) -> bool {
221        self.mouse_held.contains(&b)
222    }
223    pub fn mouse_pressed(&self, b: MouseButton) -> bool {
224        self.mouse_pressed.contains(&b)
225    }
226    pub fn mouse_released(&self, b: MouseButton) -> bool {
227        self.mouse_released.contains(&b)
228    }
229
230    pub fn mouse_pos(&self) -> (f32, f32) {
231        self.mouse_pos
232    }
233    pub fn mouse_delta(&self) -> (f32, f32) {
234        self.mouse_delta
235    }
236    pub fn scroll(&self) -> (f32, f32) {
237        self.scroll
238    }
239
240    pub fn handle_window_event(&mut self, event: &winit::event::WindowEvent) {
241        use winit::event::{ElementState, MouseScrollDelta, WindowEvent};
242        match event {
243            WindowEvent::KeyboardInput { event, .. } => {
244                let winit::keyboard::PhysicalKey::Code(code) = event.physical_key else {
245                    return;
246                };
247                match event.state {
248                    ElementState::Pressed => {
249                        if self.keys_held.insert(code) {
250                            self.keys_pressed.insert(code);
251                        }
252                    }
253                    ElementState::Released => {
254                        if self.keys_held.remove(&code) {
255                            self.keys_released.insert(code);
256                        }
257                    }
258                }
259            }
260            WindowEvent::MouseInput { state, button, .. } => {
261                let b = MouseButton::from(*button);
262                match state {
263                    ElementState::Pressed => {
264                        if self.mouse_held.insert(b) {
265                            self.mouse_pressed.insert(b);
266                        }
267                    }
268                    ElementState::Released => {
269                        if self.mouse_held.remove(&b) {
270                            self.mouse_released.insert(b);
271                        }
272                    }
273                }
274            }
275            WindowEvent::CursorMoved { position, .. } => {
276                let new = (position.x as f32, position.y as f32);
277                self.mouse_delta.0 += new.0 - self.mouse_pos.0;
278                self.mouse_delta.1 += new.1 - self.mouse_pos.1;
279                self.mouse_pos = new;
280            }
281            WindowEvent::MouseWheel { delta, .. } => match delta {
282                MouseScrollDelta::LineDelta(x, y) => {
283                    self.scroll.0 += x;
284                    self.scroll.1 += y;
285                }
286                MouseScrollDelta::PixelDelta(p) => {
287                    self.scroll.0 += p.x as f32;
288                    self.scroll.1 += p.y as f32;
289                }
290            },
291            WindowEvent::Focused(false) => {
292                self.keys_held.clear();
293                self.mouse_held.clear();
294            }
295            _ => {}
296        }
297    }
298
299    /// Call once per frame after `update` to clear edge / delta state.
300    pub fn end_frame(&mut self) {
301        self.keys_pressed.clear();
302        self.keys_released.clear();
303        self.mouse_pressed.clear();
304        self.mouse_released.clear();
305        self.mouse_delta = (0.0, 0.0);
306        self.scroll = (0.0, 0.0);
307        for pad in self.gamepads.values_mut() {
308            pad.pressed.clear();
309            pad.released.clear();
310        }
311    }
312}