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:
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user