# 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::<ActionOverrides>(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::<ActionOverrides>("input.bindings");

// User remap → write into Settings → export to disk.
actions.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyW)]);
*settings.get_mut::<ActionOverrides>("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::<ActionOverrides>("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::<ActionOverrides>("input.bindings");
*settings.get_mut::<ActionOverrides>("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.
