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}