hoplite/
input.rs

1//! Input handling for keyboard and mouse events.
2//!
3//! This module provides the [`Input`] struct, which tracks the state of keyboard keys
4//! and mouse buttons across frames. It distinguishes between three states for each input:
5//!
6//! - **Down**: The input is currently held (persists across frames)
7//! - **Pressed**: The input was just pressed this frame (single-frame event)
8//! - **Released**: The input was just released this frame (single-frame event)
9//!
10//! # Frame Lifecycle
11//!
12//! The input system follows a specific frame lifecycle:
13//!
14//! 1. Call [`Input::begin_frame`] at the start of each frame to clear per-frame state
15//! 2. Process window events via [`Input::handle_event`] during the event loop
16//! 3. Query input state using the various accessor methods during update/render
17//!
18//! # Example
19//!
20//! ```ignore
21//! let mut input = Input::new();
22//!
23//! // In your event loop
24//! input.begin_frame();
25//! for event in events {
26//!     input.handle_event(&event);
27//! }
28//!
29//! // In your update logic
30//! if input.key_pressed(KeyCode::Space) {
31//!     // Jump (only triggers once per press)
32//! }
33//! if input.key_down(KeyCode::KeyW) {
34//!     // Move forward (triggers every frame while held)
35//! }
36//! ```
37
38use std::collections::HashSet;
39
40use glam::Vec2;
41use winit::event::{ElementState, MouseButton, WindowEvent};
42use winit::keyboard::{KeyCode, PhysicalKey};
43
44/// Tracks input state for keyboard and mouse across frames.
45///
46/// This struct maintains three categories of state for both keyboard keys and mouse buttons:
47/// - **Down state**: Which inputs are currently held down (persists until released)
48/// - **Pressed state**: Which inputs were pressed this frame (cleared each frame)
49/// - **Released state**: Which inputs were released this frame (cleared each frame)
50///
51/// Additionally tracks mouse position, movement delta, and scroll wheel input.
52///
53/// # Thread Safety
54///
55/// This struct is not thread-safe and should be accessed from a single thread,
56/// typically the main thread that owns the window.
57pub struct Input {
58    /// Keys currently held down.
59    keys_down: HashSet<KeyCode>,
60    /// Keys pressed this frame (cleared at the start of each frame).
61    keys_pressed: HashSet<KeyCode>,
62    /// Keys released this frame (cleared at the start of each frame).
63    keys_released: HashSet<KeyCode>,
64    /// Mouse buttons currently held down.
65    mouse_buttons_down: HashSet<MouseButton>,
66    /// Mouse buttons pressed this frame (cleared at the start of each frame).
67    mouse_buttons_pressed: HashSet<MouseButton>,
68    /// Mouse buttons released this frame (cleared at the start of each frame).
69    mouse_buttons_released: HashSet<MouseButton>,
70    /// Current mouse position in window coordinates (pixels from top-left).
71    mouse_position: Vec2,
72    /// Mouse movement delta accumulated this frame.
73    mouse_delta: Vec2,
74    /// Scroll wheel delta accumulated this frame, normalized to "lines".
75    scroll_delta: Vec2,
76}
77
78impl Default for Input {
79    fn default() -> Self {
80        Self {
81            keys_down: HashSet::new(),
82            keys_pressed: HashSet::new(),
83            keys_released: HashSet::new(),
84            mouse_buttons_down: HashSet::new(),
85            mouse_buttons_pressed: HashSet::new(),
86            mouse_buttons_released: HashSet::new(),
87            mouse_position: Vec2::ZERO,
88            mouse_delta: Vec2::ZERO,
89            scroll_delta: Vec2::ZERO,
90        }
91    }
92}
93
94impl Input {
95    /// Creates a new input tracker with all state cleared.
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Resets per-frame input state.
101    ///
102    /// This must be called at the start of each frame, before processing any window events.
103    /// It clears the "pressed" and "released" states for keys and mouse buttons,
104    /// as well as the mouse movement and scroll deltas.
105    ///
106    /// The "down" states are preserved, as they represent inputs that are still held.
107    pub fn begin_frame(&mut self) {
108        self.keys_pressed.clear();
109        self.keys_released.clear();
110        self.mouse_buttons_pressed.clear();
111        self.mouse_buttons_released.clear();
112        self.mouse_delta = Vec2::ZERO;
113        self.scroll_delta = Vec2::ZERO;
114    }
115
116    /// Processes a window event and updates input state accordingly.
117    ///
118    /// This method handles the following event types:
119    /// - [`WindowEvent::KeyboardInput`]: Updates key down/pressed/released state
120    /// - [`WindowEvent::MouseInput`]: Updates mouse button down/pressed/released state
121    /// - [`WindowEvent::CursorMoved`]: Updates mouse position and accumulates movement delta
122    /// - [`WindowEvent::MouseWheel`]: Accumulates scroll delta (normalized to lines)
123    ///
124    /// Other event types are ignored.
125    ///
126    /// # Key Press Detection
127    ///
128    /// Keys are only marked as "pressed" on the first event when transitioning from
129    /// released to pressed. Held keys that generate repeat events will not trigger
130    /// additional "pressed" states.
131    pub fn handle_event(&mut self, event: &WindowEvent) {
132        match event {
133            WindowEvent::KeyboardInput { event, .. } => {
134                if let PhysicalKey::Code(key) = event.physical_key {
135                    match event.state {
136                        ElementState::Pressed => {
137                            if !self.keys_down.contains(&key) {
138                                self.keys_pressed.insert(key);
139                            }
140                            self.keys_down.insert(key);
141                        }
142                        ElementState::Released => {
143                            self.keys_down.remove(&key);
144                            self.keys_released.insert(key);
145                        }
146                    }
147                }
148            }
149            WindowEvent::MouseInput { state, button, .. } => match state {
150                ElementState::Pressed => {
151                    if !self.mouse_buttons_down.contains(button) {
152                        self.mouse_buttons_pressed.insert(*button);
153                    }
154                    self.mouse_buttons_down.insert(*button);
155                }
156                ElementState::Released => {
157                    self.mouse_buttons_down.remove(button);
158                    self.mouse_buttons_released.insert(*button);
159                }
160            },
161            WindowEvent::CursorMoved { position, .. } => {
162                let new_pos = Vec2::new(position.x as f32, position.y as f32);
163                self.mouse_delta += new_pos - self.mouse_position;
164                self.mouse_position = new_pos;
165            }
166            WindowEvent::MouseWheel { delta, .. } => {
167                let d = match delta {
168                    winit::event::MouseScrollDelta::LineDelta(x, y) => Vec2::new(*x, *y),
169                    winit::event::MouseScrollDelta::PixelDelta(pos) => {
170                        Vec2::new(pos.x as f32, pos.y as f32) / 120.0
171                    }
172                };
173                self.scroll_delta += d;
174            }
175            _ => {}
176        }
177    }
178
179    /// Returns `true` if the key is currently held down.
180    ///
181    /// This returns `true` for every frame that the key remains pressed,
182    /// making it suitable for continuous actions like movement.
183    #[inline]
184    pub fn key_down(&self, key: KeyCode) -> bool {
185        self.keys_down.contains(&key)
186    }
187
188    /// Returns `true` if the key was pressed this frame.
189    ///
190    /// This only returns `true` for the single frame when the key transitions
191    /// from released to pressed, making it suitable for discrete actions like jumping.
192    #[inline]
193    pub fn key_pressed(&self, key: KeyCode) -> bool {
194        self.keys_pressed.contains(&key)
195    }
196
197    /// Returns `true` if the key was released this frame.
198    ///
199    /// This only returns `true` for the single frame when the key transitions
200    /// from pressed to released.
201    #[inline]
202    pub fn key_released(&self, key: KeyCode) -> bool {
203        self.keys_released.contains(&key)
204    }
205
206    /// Returns `true` if the mouse button is currently held down.
207    ///
208    /// This returns `true` for every frame that the button remains pressed.
209    #[inline]
210    pub fn mouse_down(&self, button: MouseButton) -> bool {
211        self.mouse_buttons_down.contains(&button)
212    }
213
214    /// Returns `true` if the mouse button was pressed this frame.
215    ///
216    /// This only returns `true` for the single frame when the button transitions
217    /// from released to pressed.
218    #[inline]
219    pub fn mouse_pressed(&self, button: MouseButton) -> bool {
220        self.mouse_buttons_pressed.contains(&button)
221    }
222
223    /// Returns `true` if the mouse button was released this frame.
224    ///
225    /// This only returns `true` for the single frame when the button transitions
226    /// from pressed to released.
227    #[inline]
228    pub fn mouse_released(&self, button: MouseButton) -> bool {
229        self.mouse_buttons_released.contains(&button)
230    }
231
232    /// Returns the current mouse position in window coordinates.
233    ///
234    /// The position is measured in pixels from the top-left corner of the window's
235    /// client area. The value is updated whenever a [`WindowEvent::CursorMoved`] event
236    /// is processed.
237    #[inline]
238    pub fn mouse_position(&self) -> Vec2 {
239        self.mouse_position
240    }
241
242    /// Returns the accumulated mouse movement delta for this frame.
243    ///
244    /// The delta represents the total mouse movement since the last call to
245    /// [`begin_frame`](Self::begin_frame). Positive X is rightward, positive Y is downward.
246    #[inline]
247    pub fn mouse_delta(&self) -> Vec2 {
248        self.mouse_delta
249    }
250
251    /// Returns the accumulated scroll wheel delta for this frame.
252    ///
253    /// The delta is normalized to "lines" (typically one notch of a scroll wheel).
254    /// For pixel-based scroll events (e.g., touchpad), the value is divided by 120
255    /// to approximate line-based scrolling.
256    ///
257    /// - `x`: Horizontal scroll (positive = right)
258    /// - `y`: Vertical scroll (positive = down, though this varies by platform)
259    #[inline]
260    pub fn scroll_delta(&self) -> Vec2 {
261        self.scroll_delta
262    }
263
264    /// Handles raw mouse motion from device events.
265    ///
266    /// This is called for `DeviceEvent::MouseMotion` events, which provide
267    /// raw mouse movement independent of cursor position. This is essential
268    /// for FPS-style camera controls when the cursor is locked (grabbed),
269    /// as `CursorMoved` events stop reporting movement when the cursor
270    /// hits window boundaries.
271    ///
272    /// # Arguments
273    ///
274    /// * `dx` - Horizontal movement (positive = right)
275    /// * `dy` - Vertical movement (positive = down)
276    pub fn handle_raw_mouse_motion(&mut self, dx: f32, dy: f32) {
277        self.mouse_delta += Vec2::new(dx, dy);
278    }
279}