# Input Stage 7 reference for `oxide_engine::input` — the engine's input abstraction. Three layers live here today: - **Piece 1: [`InputState`](#raw-input-state)** — the raw per-frame snapshot (keyboard / mouse / cursor / scroll, with edge detection). - **Piece 2: [`Binding`] + [`ActionMap`](#named-actions-and-remapping)** — named actions (e.g. `"Jump"`) bound to one or more physical inputs, with defaults, runtime remapping, and RON-persistable user overrides. - **Piece 3: [`AxisBinding`] + [`Axis2DBinding`](#directional-axes)** — 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. The editor pieces (flythrough camera, bindings preferences page, transform gizmos) layer on top of these. For the raw [`WindowEvent`](windowing.md) vocabulary the runner pumps from, see [windowing.md](windowing.md). ## Raw input state ### Why a separate layer Game code wants three distinct things from a physical key: - **The press edge** — fires *once* on the frame a key first goes down. A jump fires here. - **The release edge** — fires *once* on the frame a key comes back up. A charged shot fires here. - **The held state** — true every frame between press and release. A sprint modifier reads this. Reading these straight off `WindowEvent::KeyboardInput` is doable but error- prone: OS key auto-repeat re-sends `Pressed` on every repeat, focus loss can leave keys "held" with no matching release, and a `CursorMoved` carries no delta unless the consumer remembers the previous position. `InputState` solves all of that in one place, and its semantics are unit-tested. ## How the runner uses it The windowing [`run`](windowing.md) loop owns one `InputState` and: 1. Pumps every incoming [`WindowEvent`](windowing.md) into it via `InputState::handle_event` **before** any callback sees the event, so `ctx.input()` in `WindowApp::event` already reflects the event being delivered. 2. Calls `WindowApp::update` — game logic reads `ctx.input()` to query the accumulated state for the frame. 3. After `update` returns, calls `InputState::end_frame` to roll edges and per-frame deltas off. Held state and the cursor anchor persist. The result: in `update`, edges describe what happened "since the previous frame" and held state is "right now". ## Reading input from a `WindowApp` ```rust use oxide_engine::prelude::*; use oxide_engine::winit::event::MouseButton; use oxide_engine::winit::keyboard::KeyCode; #[derive(Default)] struct MyApp; impl WindowApp for MyApp { fn update(&mut self, ctx: &mut AppCtx<'_>) { let input = ctx.input(); if input.pressed(KeyCode::Space) { // Fires once, on the frame Space went down. } if input.held(KeyCode::ShiftLeft) { // True every frame Shift is down. } if input.released(KeyCode::Escape) { ctx.request_exit(); } // Right-drag pans by the mouse delta accumulated this frame. if input.mouse_held(MouseButton::Right) { let _delta = input.mouse_delta(); // physical pixels } // Scroll is in line-equivalent units (touchpad pixels are normalized // so wheels and trackpads report on the same scale). let _zoom_amount = input.scroll().y; } } ``` ## Edge semantics, in detail `InputState` keeps three sets per device (held / pressed / released) and applies these rules: - `press_key(k)` — if `k` was **not** already held, both `held` and `pressed` add it. If it was already held (OS auto-repeat), `pressed` is unchanged. The one-shot press edge fires exactly once per real keypress. - `release_key(k)` — `held` removes `k`; `released` adds `k`. The release edge fires whether or not the key was previously tracked as held, so the occasional "release without matching press" the OS delivers (focus changes, alt-tab) still produces a usable signal. - `end_frame()` — clears `pressed` and `released` (and the per-frame mouse delta + scroll). `held` and the cursor anchor are untouched. - `WindowEvent::Focused(false)` — every currently-held key and mouse button is force-released (released-edge fires for each), so a key held when the user alt-tabbed away cannot remain stuck after the window comes back. Mouse buttons mirror the keyboard rules exactly. Cursor + delta and scroll use the same end-of-frame reset. ## Cursor and mouse delta Cursor position is stored as physical pixels relative to the window. The **delta** is the sum of the segment vectors between `set_cursor` calls *within the frame*, not the gross displacement from the first event. The first `set_cursor` after construction (or after `forget_cursor` / `WindowEvent::CursorLeft`) seeds the anchor without contributing to the delta — so the first frame the cursor enters the window never produces a phantom jump. ```text Frame 1: cursor enters at (100, 100) → delta = (0, 0) Frame 2: moves (100,100)→(105,98)→(108,95) → delta = (8, -5) Frame 3: no movement → delta = (0, 0) (cursor still at (108, 95)) ``` `add_mouse_delta(dx, dy)` exists for relative-motion sources that don't go through `CursorMoved` (a future `DeviceEvent::MouseMotion` pump, a pointer-lock toggle, or a synthesized test). It layers on top of the cursor-based delta. ## Scroll Scroll is reported in **line-equivalent units**: wheel notches arrive as `LineDelta` and pass through unchanged; trackpad pixel deltas are divided by a fixed pixels-per-line constant (40) so a touchpad gesture and a wheel notch produce comparable numbers. ## Testing inputs directly The mutator API (`press_key`, `release_mouse`, `set_cursor`, `add_mouse_delta`, `add_scroll`, `forget_cursor`, `release_all_held`) is the same path `handle_event` uses, and is intentionally public. Tests should call it directly rather than try to fabricate `WindowEvent`s — winit 0.30's `DeviceId` cannot be constructed outside a real event loop, so most input variants are unreachable from synthesized events. The mutators are unit-tested and exercised end-to-end by `stage7` integration tests in the [`tests`](../tests) crate. ```rust use oxide_engine::prelude::*; use oxide_engine::winit::keyboard::KeyCode; let mut input = InputState::new(); input.press_key(KeyCode::Space); assert!(input.pressed(KeyCode::Space)); assert!(input.held(KeyCode::Space)); input.end_frame(); assert!(!input.pressed(KeyCode::Space)); assert!(input.held(KeyCode::Space)); ``` ## Named actions and remapping `InputState` answers "is `KeyCode::Space` down?". Game code shouldn't ask that question: physical keys are user-settings territory, and querying them directly couples gameplay to a fixed keyboard layout. `ActionMap` adds the indirection — game code asks "is `\"Jump\"` engaged?" and the map resolves it to whatever the user (or the program's default) has bound. ### The data model An action carries two binding lists: - **`defaults`** — the bindings registered from code at startup. They never change at runtime. - **`current`** — the bindings actually queried each frame. Initially a clone of `defaults`; remapped by the settings screen; restored by the "Restore defaults" button. Persistence saves only `current`. On reload, the program first registers actions from code (defaults reappear from source), then applies the saved overrides on top. Actions that vanished from code never break an old settings file — they're silently skipped. ### Setting up actions ```rust use oxide_engine::prelude::*; use oxide_engine::winit::event::MouseButton; use oxide_engine::winit::keyboard::KeyCode; let mut actions = ActionMap::new(); actions .register("Jump", [Binding::Key(KeyCode::Space)]) .register( "Sprint", [ Binding::Key(KeyCode::ShiftLeft), Binding::Key(KeyCode::ShiftRight), ], ) .register("Fire", [Binding::Mouse(MouseButton::Left)]); ``` Multi-bind on either axis is supported: an action can list several bindings (the `Sprint` example), and one physical key can drive several actions (e.g. `Space` → both `"Jump"` and `"Confirm"`). ### Querying actions ```rust # use oxide_engine::prelude::*; # use oxide_engine::winit::keyboard::KeyCode; # let mut actions = ActionMap::new(); # actions.register("Jump", [Binding::Key(KeyCode::Space)]); # let input = InputState::new(); if actions.action_pressed("Jump", &input) { // Fires once, on the frame Jump becomes engaged. } if actions.action_held("Jump", &input) { // True every frame Jump is engaged (at least one binding held). } if actions.action_released("Jump", &input) { // Fires once, when the last engaged binding releases. } ``` Action edges have **hysteresis at the action level**, not the binding level: pressing a second binding while the action is already engaged does not retrigger `action_pressed`, and releasing one binding while another is still held does not fire `action_released`. The edge fires only on the action's transition between engaged and disengaged. (See the [`action_pressed`](../engine/src/input/action.rs) rustdoc for the precise definition.) Querying an unregistered action returns `false` everywhere — never a panic — so typo'd action names are graceful. ### Runtime remap ```rust # use oxide_engine::prelude::*; # use oxide_engine::winit::keyboard::KeyCode; # let mut actions = ActionMap::new(); # actions.register("Jump", [Binding::Key(KeyCode::Space)]); // A bindings preferences page calls these — game code is untouched. actions.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyW)]); actions.add_binding("Jump", Binding::Key(KeyCode::Space)); // restore as alt actions.remove_binding("Jump", Binding::Key(KeyCode::KeyW)); actions.clear_bindings("Jump"); // make Jump temporarily unbindable actions.restore_defaults("Jump"); // ↩ user's defaults actions.restore_all_defaults(); // ↩ everything ``` ### Persistence via the Stage-6 settings framework `ActionOverrides` is the serializable projection of an `ActionMap`'s current bindings, and it derives `Default + Serialize + Deserialize` so it plugs straight into `Settings::register::(name)` — no framework code changes needed. The whole cycle: ```rust use oxide_engine::prelude::*; use oxide_engine::winit::keyboard::KeyCode; // One-time setup at startup. let mut actions = ActionMap::new(); actions.register("Jump", [Binding::Key(KeyCode::Space)]); let mut settings = Settings::new(); settings.register::("input.bindings"); // User remap → write into Settings → export to disk. actions.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyW)]); *settings.get_mut::("input.bindings").unwrap() = actions.overrides(); let on_disk = settings.export(); // RON map, persist however you like // On the next launch, after re-registering defaults from code: settings.import(&on_disk); actions.apply_overrides(settings.get::("input.bindings").unwrap()); // Jump is now bound to W again. ``` The `apply_overrides` step is order-independent with respect to which actions the file knows about: unknown names are skipped, and registered actions absent from the file keep their defaults. ## Directional axes Buttons answer "is this engaged?". Movement and camera control want a **direction with magnitude**. `AxisBinding` (1D, returns `f32`) and `Axis2DBinding` (2D, returns `Vec2`) compose direction sets of `Binding`s into those values. They live in the same [`ActionMap`] as button actions but in **separate name spaces**, so `"Move"` can be a 2D axis and `"MoveSlower"` a button without conflict — and a `"Move"` button can coexist with a `"Move"` axis if a project wants it to. ### 1D axes An `AxisBinding` is a pair of binding sets (one for the +1 direction, one for −1). Any binding held on a side contributes a full unit; if both sides are held simultaneously they cancel to 0 — a "soft brake" the player gets for free. ```rust use oxide_engine::prelude::*; use oxide_engine::winit::keyboard::KeyCode; let mut actions = ActionMap::new(); actions.register_axis( "MoveX", AxisBinding::new( [Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)], ), ); let mut input = InputState::new(); input.press_key(KeyCode::KeyD); assert_eq!(actions.axis("MoveX", &input), 1.0); ``` Multiple bindings on the same direction do **not** stack (`D` and `→` both held still reads as `1.0`, not `2.0`) — the axis reports direction, not accumulated input. ### 2D axes `Axis2DBinding::new(right, left, up, down)` composes four direction sets into a `Vec2`. Diagonals are intentionally **not normalized** — a game that wants unit-length movement normalizes at the call site; a game that wants diagonal-faster gets it for free. The cancel-on-both rule applies independently on each axis. ```rust # use oxide_engine::prelude::*; # use oxide_engine::winit::keyboard::KeyCode; let mut actions = ActionMap::new(); actions.register_axis_2d( "Move", 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::KeyW); input.press_key(KeyCode::KeyA); let v = actions.axis_2d("Move", &input); assert_eq!(v, oxide_engine::math::Vec2::new(-1.0, 1.0)); // If you want unit-length: `if v != Vec2::ZERO { v.normalize() } else { v }` ``` ### Remap, restore, and persistence `set_axis_bindings` / `set_axis_2d_bindings` swap the current bindings without renaming the action. `restore_axis_defaults` / `restore_axis_2d_defaults` revert to the code-defined bindings. `restore_all_defaults` covers every action across all three kinds in one call. [`ActionOverrides`] carries axis overrides alongside button overrides in three sub-maps. The settings round-trip is identical to the button case — `ActionOverrides` is the same settings-section type: ```rust # use oxide_engine::prelude::*; # let mut actions = ActionMap::new(); # let mut settings = Settings::new(); settings.register::("input.bindings"); *settings.get_mut::("input.bindings").unwrap() = actions.overrides(); let on_disk = settings.export(); // axes, axes_2d, and buttons all persist ``` Older settings files written before axes existed (i.e. with no `axes` or `axes_2d` field in the RON) load cleanly — the missing sub-maps deserialize as empty, and registered axes keep their code-defined defaults. ## Status and what's next - **Piece 1 (✅).** Raw `InputState` + runner integration. - **Piece 2 (✅).** Named button action mapping with defaults, multi-bind in either direction, runtime remap, and RON persistence through the Stage-6 settings framework. - **Piece 3 (✅ — this section).** 1D and 2D directional axes composed from `Binding` direction sets, sharing `ActionMap` storage and the same `ActionOverrides` persistence payload. - **Editor pieces (planned).** Flythrough camera using the action map, bindings page in Preferences contributed via the Stage-6 extension API, transform gizmos.