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>
This commit is contained in:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit 9eead719b0
157 changed files with 47270 additions and 2 deletions
File diff suppressed because it is too large Load Diff
+212
View File
@@ -0,0 +1,212 @@
//! [`AxisBinding`] and [`Axis2DBinding`] — directional inputs composed from
//! [`Binding`]s into floats and [`Vec2`]s.
//!
//! A 1D axis pairs a "positive" binding set with a "negative" binding set;
//! each direction held contributes ±1. If both directions are held the
//! contributions cancel and the axis reads 0 — a "soft brake" any third-
//! person camera or twin-stick character controller needs out of the box.
//! Each direction supports several bindings (a WASD axis can also accept
//! arrow keys), and the same physical key can appear in many axes' direction
//! sets.
//!
//! A 2D axis is just a pair of 1D axes (X then Y). Diagonals are
//! intentionally **not** normalized at this layer — some games want
//! Quake-style diagonal speedup, others want unit-length input. Whichever
//! convention a game wants, applying it once at the call site is clearer
//! than having to undo a default at every site that disagrees.
use serde::{Deserialize, Serialize};
use crate::math::Vec2;
use super::{Binding, InputState};
/// One direction of an axis — typically positive (right / forward / up) or
/// negative (left / back / down) — bound to one or more physical inputs.
/// Any binding held contributes a full unit; multiple held bindings on the
/// same direction do not stack.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AxisBinding {
/// Bindings that pull the axis toward +1.
pub positive: Vec<Binding>,
/// Bindings that pull the axis toward -1.
pub negative: Vec<Binding>,
}
impl AxisBinding {
/// A new axis with the given direction binding lists.
pub fn new(
positive: impl IntoIterator<Item = Binding>,
negative: impl IntoIterator<Item = Binding>,
) -> Self {
Self {
positive: positive.into_iter().collect(),
negative: negative.into_iter().collect(),
}
}
/// Evaluates the axis against `input`. Returns -1, 0, or +1 (the
/// directions OR'd together — multiple held bindings on the same side
/// don't stack).
pub fn value(&self, input: &InputState) -> f32 {
let pos = self.positive.iter().any(|b| b.held(input));
let neg = self.negative.iter().any(|b| b.held(input));
match (pos, neg) {
(true, false) => 1.0,
(false, true) => -1.0,
// Both held → mutual cancel; neither → idle. Same result.
_ => 0.0,
}
}
}
/// A 2D axis composed of two [`AxisBinding`]s (X and Y).
///
/// Output is the unmodified vector `(x.value, y.value)` — diagonals are
/// `(±1, ±1)`, magnitude √2. Normalize at the call site if your game wants
/// unit-length movement.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Axis2DBinding {
/// The X (right left) axis.
pub x: AxisBinding,
/// The Y (up down) axis.
pub y: AxisBinding,
}
impl Axis2DBinding {
/// A 2D axis from four direction binding lists in the usual order
/// (`right`, `left`, `up`, `down`).
pub fn new(
right: impl IntoIterator<Item = Binding>,
left: impl IntoIterator<Item = Binding>,
up: impl IntoIterator<Item = Binding>,
down: impl IntoIterator<Item = Binding>,
) -> Self {
Self {
x: AxisBinding::new(right, left),
y: AxisBinding::new(up, down),
}
}
/// Evaluates the axis against `input`, returning the raw `(x, y)` value
/// without normalization.
pub fn value(&self, input: &InputState) -> Vec2 {
Vec2::new(self.x.value(input), self.y.value(input))
}
}
#[cfg(test)]
mod tests {
use super::*;
use winit::keyboard::KeyCode;
fn ad_axis() -> AxisBinding {
AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)])
}
#[test]
fn idle_axis_is_zero() {
let input = InputState::new();
assert_eq!(ad_axis().value(&input), 0.0);
}
#[test]
fn positive_direction_returns_plus_one() {
let mut input = InputState::new();
input.press_key(KeyCode::KeyD);
assert_eq!(ad_axis().value(&input), 1.0);
}
#[test]
fn negative_direction_returns_minus_one() {
let mut input = InputState::new();
input.press_key(KeyCode::KeyA);
assert_eq!(ad_axis().value(&input), -1.0);
}
#[test]
fn both_directions_held_cancel_to_zero() {
let mut input = InputState::new();
input.press_key(KeyCode::KeyA);
input.press_key(KeyCode::KeyD);
assert_eq!(
ad_axis().value(&input),
0.0,
"left+right held simultaneously must read as idle"
);
}
#[test]
fn multi_bindings_on_same_direction_do_not_stack() {
// WASD + arrow keys both contribute, but holding two positives is
// still +1 (not +2). The axis is a directional indicator, not an
// accumulator.
let axis = AxisBinding::new(
[
Binding::Key(KeyCode::KeyD),
Binding::Key(KeyCode::ArrowRight),
],
[
Binding::Key(KeyCode::KeyA),
Binding::Key(KeyCode::ArrowLeft),
],
);
let mut input = InputState::new();
input.press_key(KeyCode::KeyD);
input.press_key(KeyCode::ArrowRight);
assert_eq!(axis.value(&input), 1.0);
}
#[test]
fn axis_2d_returns_vector_components_independently() {
let axis = Axis2DBinding::new(
[Binding::Key(KeyCode::KeyD)],
[Binding::Key(KeyCode::KeyA)],
[Binding::Key(KeyCode::KeyW)],
[Binding::Key(KeyCode::KeyS)],
);
let mut input = InputState::new();
input.press_key(KeyCode::KeyD);
input.press_key(KeyCode::KeyW);
assert_eq!(axis.value(&input), Vec2::new(1.0, 1.0));
input.release_key(KeyCode::KeyD);
input.press_key(KeyCode::KeyA);
// Now A + W held.
assert_eq!(axis.value(&input), Vec2::new(-1.0, 1.0));
}
#[test]
fn axis_2d_diagonal_is_unnormalized() {
// Diagonals are (±1, ±1) — caller normalizes if it cares.
let axis = Axis2DBinding::new(
[Binding::Key(KeyCode::KeyD)],
[Binding::Key(KeyCode::KeyA)],
[Binding::Key(KeyCode::KeyW)],
[Binding::Key(KeyCode::KeyS)],
);
let mut input = InputState::new();
input.press_key(KeyCode::KeyD);
input.press_key(KeyCode::KeyW);
let v = axis.value(&input);
assert!(
(v.length() - 2_f32.sqrt()).abs() < 1e-6,
"diagonal must be sqrt(2), got {}",
v.length()
);
}
#[test]
fn axis_ron_round_trip() {
let axis = Axis2DBinding::new(
[Binding::Key(KeyCode::KeyD)],
[Binding::Key(KeyCode::KeyA)],
[Binding::Key(KeyCode::KeyW)],
[Binding::Key(KeyCode::KeyS)],
);
let s = ron::to_string(&axis).unwrap();
let parsed: Axis2DBinding = ron::from_str(&s).unwrap();
assert_eq!(parsed, axis);
}
}
+147
View File
@@ -0,0 +1,147 @@
//! [`Binding`] — one physical input that can drive a named action.
//!
//! A binding is the smallest unit an [`ActionMap`](super::ActionMap) maps
//! action names to. The enum is intentionally small (keys and mouse buttons
//! today; gamepad / pointer-axis variants will be added without breaking
//! existing serialized maps as long as new variants are appended).
use serde::{Deserialize, Serialize};
use winit::event::MouseButton;
use winit::keyboard::KeyCode;
use super::InputState;
/// One physical input that can be bound to a named action.
///
/// Two bindings compare equal only if they refer to the exact same physical
/// input — the enum derives `Hash`/`Eq` so a `HashSet<Binding>` can be used
/// to deduplicate a key's contribution to multiple actions without
/// allocating per-action sets.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Binding {
/// A keyboard key, identified by layout-independent physical position
/// (the same `KeyCode` an [`InputState`] query takes).
Key(KeyCode),
/// A mouse button.
Mouse(MouseButton),
}
impl Binding {
/// `true` if this binding's `pressed` edge fired in `input` this frame.
pub fn pressed(&self, input: &InputState) -> bool {
match *self {
Binding::Key(k) => input.pressed(k),
Binding::Mouse(b) => input.mouse_pressed(b),
}
}
/// `true` if this binding's `released` edge fired in `input` this frame.
pub fn released(&self, input: &InputState) -> bool {
match *self {
Binding::Key(k) => input.released(k),
Binding::Mouse(b) => input.mouse_released(b),
}
}
/// `true` if this binding is currently held down in `input`.
pub fn held(&self, input: &InputState) -> bool {
match *self {
Binding::Key(k) => input.held(k),
Binding::Mouse(b) => input.mouse_held(b),
}
}
/// `true` if this binding was held *going into* this frame — i.e. it was
/// held continuously from before the current frame's events arrived.
/// Used by [`ActionMap`](super::ActionMap) to recover prior-frame state
/// from the current frame's snapshot alone, without storing a previous
/// `InputState`.
///
/// Derivation: a binding was held before the frame iff it is currently
/// held or was released this frame (either way it was down going in),
/// **except** when it was also pressed this frame — a same-frame tap
/// goes idle → pressed → released, so it was not held going in.
pub(crate) fn held_before_frame(&self, input: &InputState) -> bool {
(self.held(input) || self.released(input)) && !self.pressed(input)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_binding_routes_to_keyboard_queries() {
let mut input = InputState::new();
let b = Binding::Key(KeyCode::Space);
input.press_key(KeyCode::Space);
assert!(b.pressed(&input));
assert!(b.held(&input));
assert!(!b.released(&input));
input.end_frame();
assert!(!b.pressed(&input));
assert!(b.held(&input));
input.release_key(KeyCode::Space);
assert!(b.released(&input));
assert!(!b.held(&input));
}
#[test]
fn mouse_binding_routes_to_mouse_queries() {
let mut input = InputState::new();
let b = Binding::Mouse(MouseButton::Right);
input.press_mouse(MouseButton::Right);
assert!(b.pressed(&input));
assert!(b.held(&input));
input.end_frame();
input.release_mouse(MouseButton::Right);
assert!(b.released(&input));
assert!(!b.held(&input));
}
#[test]
fn held_before_frame_distinguishes_press_release_tap() {
let b = Binding::Key(KeyCode::KeyJ);
// Idle → pressed this frame. Not held before.
let mut input = InputState::new();
input.press_key(KeyCode::KeyJ);
assert!(!b.held_before_frame(&input));
// Held continuously. Held before.
let mut input = InputState::new();
input.press_key(KeyCode::KeyJ);
input.end_frame();
assert!(b.held_before_frame(&input));
// Held → released this frame. Held before.
let mut input = InputState::new();
input.press_key(KeyCode::KeyJ);
input.end_frame();
input.release_key(KeyCode::KeyJ);
assert!(b.held_before_frame(&input));
// Same-frame tap (idle → pressed → released). Not held before.
let mut input = InputState::new();
input.press_key(KeyCode::KeyJ);
input.release_key(KeyCode::KeyJ);
assert!(!b.held_before_frame(&input));
}
#[test]
fn ron_round_trip_preserves_key_and_mouse_variants() {
let bindings = vec![
Binding::Key(KeyCode::Space),
Binding::Mouse(MouseButton::Left),
Binding::Key(KeyCode::ShiftLeft),
];
let s = ron::to_string(&bindings).unwrap();
let parsed: Vec<Binding> = ron::from_str(&s).unwrap();
assert_eq!(parsed, bindings);
}
}
+72
View File
@@ -0,0 +1,72 @@
//! Per-frame input — raw state, edges, and remappable named actions.
//!
//! Stage 7 builds the engine's input abstraction in three layers:
//!
//! 1. [`InputState`] (piece 1) — the per-frame snapshot of keyboard, mouse,
//! cursor, and scroll, with `pressed` / `released` edge detection and a
//! persistent `held` state. The windowing runner pumps raw `WindowEvent`s
//! into it and clears edges between frames; game/editor code reads it via
//! [`AppCtx::input`](crate::window::AppCtx::input).
//! 2. [`Binding`] + [`ActionMap`] (piece 2) — named actions like `"Jump"`
//! bound to one or more physical inputs, each carrying a **default**
//! binding and a (possibly remapped) **current** binding. Game code
//! queries actions by name, so a user-facing remap never touches game
//! code. Current bindings round-trip through RON for persistence
//! (typically via the [`Settings`](crate::settings::Settings) framework).
//! 3. [`AxisBinding`] + [`Axis2DBinding`] (piece 3) — directional inputs
//! composed from `Binding` direction sets (e.g. `WASD` → `Vec2 "Move"`),
//! stored alongside button actions in the same [`ActionMap`] and
//! persisted through the same [`ActionOverrides`] payload.
//!
//! # Why edges and state are tracked separately
//!
//! Game logic typically wants three distinct things from a physical input:
//! the moment it became pressed (a jump fires once on key-down, never on
//! subsequent frames while held), the moment it was released (a charged
//! shot fires on key-up), and whether it is currently down (a sprint key
//! accelerates while held). Tracking all three explicitly makes the
//! semantics robust against OS key auto-repeat — a held key produces a
//! single `pressed` edge no matter how many times the OS re-sends the
//! event — and avoids the per-callsite bookkeeping every action would
//! otherwise need.
//!
//! # Quick reference
//!
//! ```
//! use oxide_engine::input::{ActionMap, Binding, InputState};
//! use oxide_engine::winit::keyboard::KeyCode;
//!
//! let mut input = InputState::new();
//! input.press_key(KeyCode::Space);
//! assert!(input.pressed(KeyCode::Space)); // edge — true only this frame
//! assert!(input.held(KeyCode::Space)); // state — true while held
//!
//! // Layer named actions on top — game code never names the physical key.
//! let mut actions = ActionMap::new();
//! actions.register("Jump", [Binding::Key(KeyCode::Space)]);
//! assert!(actions.action_pressed("Jump", &input));
//!
//! input.end_frame();
//! assert!(!input.pressed(KeyCode::Space)); // edge cleared
//! assert!(input.held(KeyCode::Space)); // held persists
//! ```
//!
//! # Synthesized-event API
//!
//! The mutators on [`InputState`] (`press_key`, `release_mouse`,
//! `set_cursor`, `add_mouse_delta`, `add_scroll`, `forget_cursor`,
//! `release_all_held`) are the same path `handle_event` uses, and are
//! intentionally public so tests can drive input directly without
//! constructing `winit` events (winit 0.30's `DeviceId` cannot be
//! fabricated outside an event loop, so most `WindowEvent` variants are
//! unreachable from synthesized events).
mod action;
mod axis;
mod binding;
mod state;
pub use action::{ActionMap, ActionOverrides};
pub use axis::{Axis2DBinding, AxisBinding};
pub use binding::Binding;
pub use state::InputState;
+472
View File
@@ -0,0 +1,472 @@
//! 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]));
}
}