Skip to main content

mittens_engine/engine/
user_input.rs

1//! Input handling (winit -> engine state).
2//!
3//! Goal: keep `Windowing` focused on window lifecycle + rendering, while `UserInput`
4//! owns interpreting window events into a small, reusable `InputState`.
5
6use std::collections::HashSet;
7
8use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
9use winit::keyboard::{Key, NamedKey};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum TextInputFrameEvent {
13    InsertText(String),
14    Backspace,
15    DeleteForward,
16    MoveCaretLeft,
17    MoveCaretRight,
18}
19
20/// Snapshot of user input.
21///
22/// This is intentionally minimal for now, but it already supports:
23/// - current key/button state (`down`)
24/// - per-frame transitions (`pressed`/`released`)
25/// - cursor position and wheel delta
26/// - mouse movement delta
27#[derive(Default, Debug, Clone)]
28pub struct InputState {
29    pub keys_down: HashSet<Key>,
30    pub keys_pressed: HashSet<Key>,
31    pub keys_released: HashSet<Key>,
32
33    pub mouse_down: HashSet<MouseButton>,
34    pub mouse_pressed: HashSet<MouseButton>,
35    pub mouse_released: HashSet<MouseButton>,
36
37    /// Cursor position in physical pixels (as reported by winit).
38    pub cursor_pos: Option<(f32, f32)>,
39
40    /// Previous cursor position (updated at `begin_frame`).
41    prev_cursor_pos: Option<(f32, f32)>,
42
43    /// Mouse movement delta since last frame (current - previous).
44    mouse_movement: (f32, f32),
45
46    /// Derived mouse drag state (active when a button is held while the cursor moves).
47    mouse_dragging: bool,
48    mouse_drag_delta: (f32, f32),
49
50    /// Accumulated wheel delta since last `begin_frame`.
51    pub wheel_delta: (f32, f32),
52
53    text_input_events: Vec<TextInputFrameEvent>,
54}
55
56impl InputState {
57    /// Called at the start of a render/update frame.
58    ///
59    /// Important: this does **not** clear edge-triggered sets (`pressed`/`released`).
60    /// Those are cleared at `end_frame` so events delivered before `RedrawRequested`
61    /// are still visible to systems during `Universe::update`.
62    pub fn start_frame(&mut self) {
63        // Update mouse movement delta.
64        self.mouse_movement = match (self.cursor_pos, self.prev_cursor_pos) {
65            (Some((cx, cy)), Some((px, py))) => (cx - px, cy - py),
66            _ => (0.0, 0.0),
67        };
68        self.prev_cursor_pos = self.cursor_pos;
69
70        // Derive drag state from buttons + movement.
71        let any_button_down = !self.mouse_down.is_empty();
72        let moved = self.mouse_movement.0 != 0.0 || self.mouse_movement.1 != 0.0;
73        self.mouse_dragging = any_button_down && moved;
74        self.mouse_drag_delta = if self.mouse_dragging {
75            self.mouse_movement
76        } else {
77            (0.0, 0.0)
78        };
79    }
80
81    /// Clears edge-triggered sets at the end of a frame.
82    pub fn end_frame(&mut self) {
83        self.keys_pressed.clear();
84        self.keys_released.clear();
85        self.mouse_pressed.clear();
86        self.mouse_released.clear();
87        self.wheel_delta = (0.0, 0.0);
88        self.text_input_events.clear();
89    }
90
91    #[inline]
92    pub fn key_down(&self, key: &Key) -> bool {
93        self.keys_down.contains(key)
94    }
95
96    #[inline]
97    pub fn key_pressed(&self, key: &Key) -> bool {
98        self.keys_pressed.contains(key)
99    }
100
101    #[inline]
102    pub fn key_released(&self, key: &Key) -> bool {
103        self.keys_released.contains(key)
104    }
105
106    /// Returns the mouse movement delta (dx, dy) since the last frame.
107    /// Returns (0, 0) if cursor position is not available.
108    #[inline]
109    pub fn mouse_movement(&self) -> (f32, f32) {
110        self.mouse_movement
111    }
112
113    /// Whether the user is currently dragging the mouse (button held + cursor moved this frame).
114    #[inline]
115    pub fn mouse_dragging(&self) -> bool {
116        self.mouse_dragging
117    }
118
119    /// Mouse drag delta (dx, dy) in pixels for this frame.
120    #[inline]
121    pub fn mouse_drag_delta(&self) -> (f32, f32) {
122        self.mouse_drag_delta
123    }
124
125    /// Whether the given mouse button is currently dragging (that button is held + cursor moved
126    /// this frame).
127    #[inline]
128    pub fn mouse_dragging_button(&self, button: MouseButton) -> bool {
129        self.mouse_down.contains(&button)
130            && (self.mouse_movement.0 != 0.0 || self.mouse_movement.1 != 0.0)
131    }
132
133    /// Mouse drag delta (dx, dy) in pixels for this frame, gated to the given button.
134    #[inline]
135    pub fn mouse_drag_delta_button(&self, button: MouseButton) -> (f32, f32) {
136        if self.mouse_dragging_button(button) {
137            self.mouse_movement
138        } else {
139            (0.0, 0.0)
140        }
141    }
142
143    #[inline]
144    pub fn text_input_events(&self) -> &[TextInputFrameEvent] {
145        &self.text_input_events
146    }
147}
148
149/// Stateful input event processor.
150#[derive(Default, Debug, Clone)]
151pub struct UserInput {
152    state: InputState,
153}
154
155impl UserInput {
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    pub fn state(&self) -> &InputState {
161        &self.state
162    }
163
164    pub fn state_mut(&mut self) -> &mut InputState {
165        &mut self.state
166    }
167
168    pub fn start_frame(&mut self) {
169        self.state.start_frame();
170    }
171
172    pub fn end_frame(&mut self) {
173        self.state.end_frame();
174    }
175
176    /// Feed a winit event into this input handler.
177    ///
178    /// Returns `true` if the event was recognized/consumed as input.
179    pub fn handle_window_event(&mut self, event: &WindowEvent) -> bool {
180        match event {
181            WindowEvent::KeyboardInput { event, .. } => {
182                fn normalize_key(key: &Key) -> Key {
183                    match key {
184                        // Treat ASCII letters case-insensitively by storing the lowercase form.
185                        // This makes WASD/QE work regardless of Shift state.
186                        Key::Character(s) => {
187                            if s.len() == 1 {
188                                let c = s.chars().next().unwrap_or('\0');
189                                if c.is_ascii_alphabetic() {
190                                    return Key::Character(
191                                        c.to_ascii_lowercase().to_string().into(),
192                                    );
193                                }
194                            }
195                            Key::Character(s.clone())
196                        }
197                        _ => key.clone(),
198                    }
199                }
200
201                let key = normalize_key(&event.logical_key);
202                match event.state {
203                    ElementState::Pressed => {
204                        let was_down = self.state.keys_down.contains(&key);
205                        self.state.keys_down.insert(key.clone());
206                        if !was_down {
207                            self.state.keys_pressed.insert(key);
208                        }
209                        match &event.logical_key {
210                            Key::Named(NamedKey::Backspace) => {
211                                self.state
212                                    .text_input_events
213                                    .push(TextInputFrameEvent::Backspace);
214                            }
215                            Key::Named(NamedKey::Delete) => {
216                                self.state
217                                    .text_input_events
218                                    .push(TextInputFrameEvent::DeleteForward);
219                            }
220                            Key::Named(NamedKey::ArrowLeft) => {
221                                self.state
222                                    .text_input_events
223                                    .push(TextInputFrameEvent::MoveCaretLeft);
224                            }
225                            Key::Named(NamedKey::ArrowRight) => {
226                                self.state
227                                    .text_input_events
228                                    .push(TextInputFrameEvent::MoveCaretRight);
229                            }
230                            _ => {}
231                        }
232                        if let Some(text) = event.text.as_ref() {
233                            let filtered: String =
234                                text.chars().filter(|c| !c.is_control()).collect();
235                            if !filtered.is_empty() {
236                                self.state
237                                    .text_input_events
238                                    .push(TextInputFrameEvent::InsertText(filtered));
239                            }
240                        }
241                    }
242                    ElementState::Released => {
243                        self.state.keys_down.remove(&key);
244                        self.state.keys_released.insert(key);
245                    }
246                }
247                true
248            }
249
250            WindowEvent::MouseInput { state, button, .. } => {
251                match state {
252                    ElementState::Pressed => {
253                        let was_down = self.state.mouse_down.contains(button);
254                        self.state.mouse_down.insert(*button);
255                        if !was_down {
256                            self.state.mouse_pressed.insert(*button);
257                        }
258                    }
259                    ElementState::Released => {
260                        self.state.mouse_down.remove(button);
261                        self.state.mouse_released.insert(*button);
262                    }
263                }
264                true
265            }
266
267            WindowEvent::CursorMoved { position, .. } => {
268                self.state.cursor_pos = Some((position.x as f32, position.y as f32));
269                true
270            }
271
272            WindowEvent::MouseWheel { delta, .. } => {
273                let (dx, dy) = match delta {
274                    MouseScrollDelta::LineDelta(x, y) => (*x, *y),
275                    MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
276                };
277                self.state.wheel_delta.0 += dx;
278                self.state.wheel_delta.1 += dy;
279                true
280            }
281
282            _ => false,
283        }
284    }
285}