f56a1eea3b
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>
473 lines
17 KiB
Rust
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]));
|
|
}
|
|
}
|