Files
Oxide/engine/src/input/state.rs
T
Homer Simpson f56a1eea3b Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:02 +02:00

473 lines
17 KiB
Rust

//! 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<KeyCode>,
keys_pressed: HashSet<KeyCode>,
keys_released: HashSet<KeyCode>,
mouse_held: HashSet<MouseButton>,
mouse_pressed: HashSet<MouseButton>,
mouse_released: HashSet<MouseButton>,
cursor: Option<Vec2>,
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<Item = KeyCode> + '_ {
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<Item = MouseButton> + '_ {
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<Vec2> {
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<KeyCode> = input.keys_held().collect();
assert_eq!(held, HashSet::from([KeyCode::KeyW]));
}
}