//! The per-frame [`InputState`] — keyboard, mouse, cursor, and scroll with //! edge detection. The module-level documentation lives in //! [`crate::input`](super); this file is the implementation. use std::collections::HashSet; use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent}; use winit::keyboard::{KeyCode, PhysicalKey}; use crate::math::Vec2; /// Pixels-per-line factor used to normalize trackpad pixel scroll deltas into /// the same units as wheel-notch [`MouseScrollDelta::LineDelta`]. Matches the /// convention the editor's orbit-camera zoom already uses, so behavior is /// consistent whether the user has a mouse wheel or a touchpad. const SCROLL_PIXELS_PER_LINE: f32 = 40.0; /// Per-frame snapshot of keyboard, mouse, and pointer state. /// /// Built up across the frame from raw events and queried by game / editor /// code. All edge sets (pressed / released, mouse delta, scroll) are cleared /// by [`end_frame`](Self::end_frame); held state and cursor position persist /// across frames. #[derive(Debug, Default, Clone)] pub struct InputState { keys_held: HashSet, keys_pressed: HashSet, keys_released: HashSet, mouse_held: HashSet, mouse_pressed: HashSet, mouse_released: HashSet, cursor: Option, mouse_delta: Vec2, scroll: Vec2, } impl InputState { /// A new state with nothing pressed and no cursor known. pub fn new() -> Self { Self::default() } // --- Queries: keyboard ------------------------------------------------- /// `true` if `key` became pressed this frame (edge — true for exactly the /// frame of the key-down, regardless of OS auto-repeat). pub fn pressed(&self, key: KeyCode) -> bool { self.keys_pressed.contains(&key) } /// `true` if `key` was released this frame (edge — true for exactly the /// frame of the key-up). pub fn released(&self, key: KeyCode) -> bool { self.keys_released.contains(&key) } /// `true` if `key` is currently held down (state — true every frame until /// the key-up arrives). pub fn held(&self, key: KeyCode) -> bool { self.keys_held.contains(&key) } /// All currently-held keys. Useful for debug overlays. pub fn keys_held(&self) -> impl Iterator + '_ { self.keys_held.iter().copied() } // --- Queries: mouse ---------------------------------------------------- /// `true` if `button` became pressed this frame (edge). pub fn mouse_pressed(&self, button: MouseButton) -> bool { self.mouse_pressed.contains(&button) } /// `true` if `button` was released this frame (edge). pub fn mouse_released(&self, button: MouseButton) -> bool { self.mouse_released.contains(&button) } /// `true` if `button` is currently held down (state). pub fn mouse_held(&self, button: MouseButton) -> bool { self.mouse_held.contains(&button) } /// All currently-held mouse buttons. pub fn mouse_buttons_held(&self) -> impl Iterator + '_ { self.mouse_held.iter().copied() } /// Current cursor position in physical pixels, or `None` if the cursor /// has not entered the window yet (or just left it). pub fn cursor(&self) -> Option { self.cursor } /// Cursor movement since the last [`end_frame`](Self::end_frame), in /// physical pixels. The first cursor event of a session (or after a /// [`CursorLeft`](WindowEvent::CursorLeft)) seeds the position **without** /// producing a delta, so consumers never see a phantom jump on the first /// frame the cursor appears. pub fn mouse_delta(&self) -> Vec2 { self.mouse_delta } /// Scroll accumulated since the last [`end_frame`](Self::end_frame), in /// line-equivalent units (pixel deltas are divided by a fixed pixels-per- /// line constant so wheels and touchpads report on the same scale). pub fn scroll(&self) -> Vec2 { self.scroll } // --- Event pump -------------------------------------------------------- /// Folds one raw [`WindowEvent`] into the state. /// /// Non-input events (resize, redraw, focus, …) are ignored, so the runner /// can pump every event without filtering. Auto-repeat key-down events /// from the OS do not re-fire the [`pressed`](Self::pressed) edge: a held /// key only produces an edge on the first down. pub fn handle_event(&mut self, event: &WindowEvent) { match event { WindowEvent::KeyboardInput { event, .. } => { if let PhysicalKey::Code(code) = event.physical_key { match event.state { ElementState::Pressed => self.press_key(code), ElementState::Released => self.release_key(code), } } } WindowEvent::MouseInput { state, button, .. } => match state { ElementState::Pressed => self.press_mouse(*button), ElementState::Released => self.release_mouse(*button), }, WindowEvent::CursorMoved { position, .. } => { self.set_cursor(Vec2::new(position.x as f32, position.y as f32)); } WindowEvent::CursorLeft { .. } => self.forget_cursor(), WindowEvent::MouseWheel { delta, .. } => match delta { MouseScrollDelta::LineDelta(x, y) => self.add_scroll(*x, *y), MouseScrollDelta::PixelDelta(p) => self.add_scroll( p.x as f32 / SCROLL_PIXELS_PER_LINE, p.y as f32 / SCROLL_PIXELS_PER_LINE, ), }, WindowEvent::Focused(false) => self.release_all_held(), _ => {} } } // --- Synthesized mutators (used by both handle_event and tests) -------- /// Records that `key` was pressed. The [`pressed`](Self::pressed) edge /// fires only when the key was not already held, so OS auto-repeat does /// not retrigger one-shot actions. pub fn press_key(&mut self, key: KeyCode) { if self.keys_held.insert(key) { self.keys_pressed.insert(key); } } /// Records that `key` was released. The [`released`](Self::released) /// edge fires whether or not the key was previously tracked as held — /// the OS occasionally sends a release without a matching press (e.g. /// the window gained focus mid-press). pub fn release_key(&mut self, key: KeyCode) { self.keys_held.remove(&key); self.keys_released.insert(key); } /// Records that `button` was pressed (with the same edge semantics as /// [`press_key`]). pub fn press_mouse(&mut self, button: MouseButton) { if self.mouse_held.insert(button) { self.mouse_pressed.insert(button); } } /// Records that `button` was released. pub fn release_mouse(&mut self, button: MouseButton) { self.mouse_held.remove(&button); self.mouse_released.insert(button); } /// Sets the cursor position. The delta is accumulated **only** relative /// to a previously-known cursor; the very first set (or the first set /// after a [`CursorLeft`](WindowEvent::CursorLeft) event) seeds the /// position without contributing to [`mouse_delta`](Self::mouse_delta). pub fn set_cursor(&mut self, position: Vec2) { if let Some(prev) = self.cursor { self.mouse_delta += position - prev; } self.cursor = Some(position); } /// Adds a raw mouse delta in physical pixels. Useful for relative-motion /// sources (`DeviceEvent::MouseMotion`, future pointer-lock) and for tests. pub fn add_mouse_delta(&mut self, dx: f32, dy: f32) { self.mouse_delta += Vec2::new(dx, dy); } /// Adds a scroll increment in line-equivalent units. pub fn add_scroll(&mut self, x: f32, y: f32) { self.scroll += Vec2::new(x, y); } // --- Frame boundary ---------------------------------------------------- /// Clears per-frame edge state and accumulated deltas; held state and /// cursor position persist. The runner calls this after game logic has /// read the edges for the current frame. pub fn end_frame(&mut self) { self.keys_pressed.clear(); self.keys_released.clear(); self.mouse_pressed.clear(); self.mouse_released.clear(); self.mouse_delta = Vec2::ZERO; self.scroll = Vec2::ZERO; } /// Forgets the cursor anchor so the next [`set_cursor`](Self::set_cursor) /// re-seeds without producing a phantom delta. The event pump calls this /// on [`CursorLeft`](WindowEvent::CursorLeft); the public exposure lets /// hosts that drive `InputState` directly (e.g. tests, or a future /// pointer-lock toggle) re-anchor without simulating a window event. pub fn forget_cursor(&mut self) { self.cursor = None; } /// Releases every currently-held key and mouse button (firing each /// `released` edge once). The event pump calls this when the window /// loses focus, since the OS will never deliver the matching releases /// for keys held at that moment, and stuck-key bugs would otherwise /// follow the window across alt-tab cycles. pub fn release_all_held(&mut self) { for key in self.keys_held.drain() { self.keys_released.insert(key); } for button in self.mouse_held.drain() { self.mouse_released.insert(button); } } } #[cfg(test)] mod tests { use super::*; #[test] fn key_press_sets_edge_and_state() { let mut input = InputState::new(); input.press_key(KeyCode::Space); assert!(input.pressed(KeyCode::Space)); assert!(input.held(KeyCode::Space)); assert!(!input.released(KeyCode::Space)); } #[test] fn end_frame_clears_edges_but_not_held() { let mut input = InputState::new(); input.press_key(KeyCode::Space); input.end_frame(); assert!(!input.pressed(KeyCode::Space), "edge must clear"); assert!(input.held(KeyCode::Space), "state must persist"); } #[test] fn key_release_sets_edge_and_clears_held() { let mut input = InputState::new(); input.press_key(KeyCode::KeyA); input.end_frame(); input.release_key(KeyCode::KeyA); assert!(input.released(KeyCode::KeyA)); assert!(!input.held(KeyCode::KeyA)); assert!(!input.pressed(KeyCode::KeyA)); } #[test] fn os_auto_repeat_does_not_refire_pressed_edge() { let mut input = InputState::new(); input.press_key(KeyCode::KeyW); input.end_frame(); // pressed edge consumed // The OS resends Pressed for the same key while it's held. input.press_key(KeyCode::KeyW); assert!( !input.pressed(KeyCode::KeyW), "auto-repeat must not retrigger pressed" ); assert!(input.held(KeyCode::KeyW)); } #[test] fn release_without_prior_press_still_emits_edge() { // The OS occasionally delivers a release with no matching press (e.g. // window focused mid-press). The released edge still fires so consumers // can react. let mut input = InputState::new(); input.release_key(KeyCode::Escape); assert!(input.released(KeyCode::Escape)); assert!(!input.held(KeyCode::Escape)); } #[test] fn pressed_and_released_in_same_frame_both_fire() { // Within a single frame a quick tap should register both edges so // logic that wants a "click on release" pattern is reachable from // the synthesized input path. let mut input = InputState::new(); input.press_key(KeyCode::Enter); input.release_key(KeyCode::Enter); assert!(input.pressed(KeyCode::Enter)); assert!(input.released(KeyCode::Enter)); assert!(!input.held(KeyCode::Enter)); } #[test] fn mouse_button_edges_parallel_keyboard() { let mut input = InputState::new(); input.press_mouse(MouseButton::Left); assert!(input.mouse_pressed(MouseButton::Left)); assert!(input.mouse_held(MouseButton::Left)); input.end_frame(); assert!(!input.mouse_pressed(MouseButton::Left)); assert!(input.mouse_held(MouseButton::Left)); input.release_mouse(MouseButton::Left); assert!(input.mouse_released(MouseButton::Left)); assert!(!input.mouse_held(MouseButton::Left)); } #[test] fn first_cursor_move_produces_no_delta() { let mut input = InputState::new(); input.set_cursor(Vec2::new(100.0, 200.0)); assert_eq!(input.mouse_delta(), Vec2::ZERO); assert_eq!(input.cursor(), Some(Vec2::new(100.0, 200.0))); } #[test] fn subsequent_cursor_moves_accumulate_delta() { let mut input = InputState::new(); input.set_cursor(Vec2::new(100.0, 200.0)); input.set_cursor(Vec2::new(110.0, 195.0)); input.set_cursor(Vec2::new(115.0, 190.0)); // (110-100) + (115-110), (195-200) + (190-195) = (15, -10) assert_eq!(input.mouse_delta(), Vec2::new(15.0, -10.0)); } #[test] fn end_frame_resets_delta_but_preserves_cursor() { let mut input = InputState::new(); input.set_cursor(Vec2::new(0.0, 0.0)); input.set_cursor(Vec2::new(10.0, 10.0)); input.end_frame(); assert_eq!(input.mouse_delta(), Vec2::ZERO); assert_eq!(input.cursor(), Some(Vec2::new(10.0, 10.0))); // Next move accumulates from the persisted cursor, not from zero. input.set_cursor(Vec2::new(13.0, 11.0)); assert_eq!(input.mouse_delta(), Vec2::new(3.0, 1.0)); } #[test] fn add_mouse_delta_layers_on_top_of_cursor_motion() { let mut input = InputState::new(); input.set_cursor(Vec2::new(0.0, 0.0)); input.set_cursor(Vec2::new(5.0, 0.0)); input.add_mouse_delta(2.0, 3.0); // e.g. raw DeviceEvent motion assert_eq!(input.mouse_delta(), Vec2::new(7.0, 3.0)); } #[test] fn scroll_accumulates_and_resets() { let mut input = InputState::new(); input.add_scroll(0.0, 1.0); input.add_scroll(0.0, 2.5); assert_eq!(input.scroll(), Vec2::new(0.0, 3.5)); input.end_frame(); assert_eq!(input.scroll(), Vec2::ZERO); } #[test] fn focus_loss_via_handle_event_releases_held() { let mut input = InputState::new(); input.press_key(KeyCode::KeyW); input.press_mouse(MouseButton::Left); input.end_frame(); // Focused(false) is one of the WindowEvent variants with no DeviceId, // so the routing through handle_event itself is exercised here. input.handle_event(&WindowEvent::Focused(false)); assert!(!input.held(KeyCode::KeyW), "key must not stay stuck"); assert!(!input.mouse_held(MouseButton::Left)); assert!(input.released(KeyCode::KeyW)); assert!(input.mouse_released(MouseButton::Left)); } #[test] fn release_all_held_drops_state_and_fires_edges() { let mut input = InputState::new(); input.press_key(KeyCode::KeyW); input.press_key(KeyCode::ShiftLeft); input.press_mouse(MouseButton::Right); input.end_frame(); input.release_all_held(); assert!(!input.held(KeyCode::KeyW)); assert!(!input.held(KeyCode::ShiftLeft)); assert!(!input.mouse_held(MouseButton::Right)); assert!(input.released(KeyCode::KeyW)); assert!(input.released(KeyCode::ShiftLeft)); assert!(input.mouse_released(MouseButton::Right)); } #[test] fn forget_cursor_resets_anchor_so_next_move_has_no_delta() { let mut input = InputState::new(); input.set_cursor(Vec2::new(0.0, 0.0)); input.set_cursor(Vec2::new(10.0, 10.0)); input.end_frame(); input.forget_cursor(); assert!(input.cursor().is_none()); // First move back in reseeds without contributing a delta. input.set_cursor(Vec2::new(200.0, 50.0)); assert_eq!(input.mouse_delta(), Vec2::ZERO); assert_eq!(input.cursor(), Some(Vec2::new(200.0, 50.0))); } #[test] fn handle_event_ignores_unrelated_window_events() { // These three WindowEvent variants don't carry a DeviceId, so they // can be constructed in tests — the routing through handle_event is // exercised end-to-end here. let mut input = InputState::new(); input.press_key(KeyCode::Space); input.handle_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new( 800, 600, ))); input.handle_event(&WindowEvent::CloseRequested); input.handle_event(&WindowEvent::RedrawRequested); assert!(input.pressed(KeyCode::Space)); assert!(input.held(KeyCode::Space)); } #[test] fn keys_held_iterates_currently_held_keys() { let mut input = InputState::new(); input.press_key(KeyCode::KeyW); input.press_key(KeyCode::KeyA); input.release_key(KeyCode::KeyA); let held: HashSet = input.keys_held().collect(); assert_eq!(held, HashSet::from([KeyCode::KeyW])); } }