# Input

Hands-on walkthrough: [Tutorials — First input action](/docs/tutorials/first-input.md).

## Model

The engine separates device sampling from game actions.

- An `InputDriver` gathers platform keyboard, mouse, and gamepad state.
- `InputSystem.syncFrame` keeps current and previous snapshots.
- `ActionRegistry` loads named bindings from JSON.
- `ActionState` exposes resolved gameplay intent to the world/game.

The public entry point is `src/hikari/src/input/input.zig`. Frame data and key-trigger helpers are in `input_frame.zig`; action binding parsing and evaluation are in `action_binding.zig`, `action_registry.zig`, and `input_actions_json.zig`.

## Frame semantics

At the start of a frame, `syncFrame` copies current device values to their previous slots and asks the driver for a new snapshot. Read helpers can therefore distinguish a held value from transitions without querying the operating system again mid-frame.

Input remains backend-neutral above the driver. Game and scene code read the normalized snapshot/action state; they do not call Cocoa, Win32, Metal, or D3D12 APIs.

### Mouse wheel units

`MouseFrame.wheel_y` is a per-frame signed scroll sample. Its unit depends on `MouseFrame.flags`:

| Flag | Meaning | Typical source |
|------|---------|----------------|
| `mouse_flag_wheel_precise` | Layout points (trackpad stream) | macOS trackpad |
| `mouse_flag_wheel_momentum` | OS inertia after fingers up (no live user sample this frame) | macOS `momentumPhase` |
| neither | Discrete line/notch units (±1 per click) | Windows `WM_MOUSEWHEEL`, macOS mouse wheel |

UI converts with `ui.scroll_physics.wheelDeltaPoints` / `wheelDeltaPointsSens` (precise path applies gain, not `wheel_step`). Momentum must not hold rubber-band open — only user samples do; outward momentum past an edge is ignored and damps residual velocity so spring-back can run. Non-UI consumers (fly camera) should use `wheelLines`.

### Pointer mode (cursor capture)

`PointerMode` is free or captured. Captured hides the OS cursor and reports relative motion through the usual mouse position stream (`mouseDelta` / action `delta_x`/`delta_y` stay valid). Platform backends (macOS CoreGraphics association + Windows raw input) own hide/clip; focus loss always frees.

| Surface | Call |
|---------|------|
| Game SDK | `hi.world().setPointerMode(.captured)` / `.free` |
| Host input | `InputSystem.setPointerMode` |

**Editor play:** Esc soft-unlocks the cursor (docks stay usable) without clearing the game’s requested mode; a left click on the scene viewport re-arms capture. **Editor edit:** temporary capture while fly-camera navigation is active. Standalone games typically capture when look is live and free when a settings/modal UI is open.

## Input contexts

**Code:** `src/hikari/src/input/input_context.zig`.

A context is a named layer saying which actions are readable while it is active. Without one, every reader has to ask "…but is a menu open?" at each call site, and the day someone forgets, the player shoots through the pause screen.

Resolution walks layers **highest priority first** (equal priority → most recently pushed first):

- a layer that **claims** the action → readable;
- an **exclusive** layer that does not claim it → not readable, walk stops;
- falling off the bottom → readable.

So **an empty stack allows everything** and adopting contexts never changes an existing game's input. A non-exclusive layer only *adds* actions (build mode on top of normal control); an exclusive layer masks everything below (pause menu, dialogue, cutscene, text entry).

For each action read, the first matching decision wins:

```mermaid
flowchart TD
    read["Read an action"] --> next{"Another context layer?"}
    next -->|No| allow["Allow the read"]
    next -->|Highest remaining priority| claim{"Layer claims this action?"}
    claim -->|Yes| allow
    claim -->|No| exclusive{"Layer is exclusive?"}
    exclusive -->|Yes| block["Block the read"]
    exclusive -->|No · continue downward| next
```

Storage is fixed and inline in `World` — 8 layers × 24 actions, no allocation, so a push cannot fail for memory reasons mid-frame. Over capacity returns `StackFull` / `ContextTooLarge` rather than truncating; both mean a missing pop or an oversized layer.

| Surface | Call |
|---------|------|
| Game SDK | `w.pushInputContext(.{ .name = "menu", .exclusive = true, .pointer = .free })` / `popInputContext` / `hasInputContext` / `clearInputContexts` / `inputAllows` |
| Kawa | `Input.push_context({ name = "menu", exclusive = true, pointer = "free" })` / `pop_context` / `has_context` / `clear_contexts` / `allows` |

Both surfaces share **one** stack on `World`, so a layer pushed from either gates both. Every action read — `pressed`, `held`, `value`, **and `released`** — passes through it. Releases are gated deliberately: a button released while a menu is open belongs to the menu, and leaking it is how a weapon fires on the frame the pause screen closes.

`pointer` on a layer sets cursor mode while it is the topmost layer declaring one, which is usually the whole reason a menu layer exists. It replaces hand-written "am I in a menu" pointer syncing.

**Raw input needs a binding-less action.** Mouse delta and gamepad axes are not actions, so nothing gates them automatically. Declare a name like `look` in the gameplay layer with no binding and ask `inputAllows("look")` (Kawa: `Input.allows("look")`) before consuming raw look input.

**The stack outlives the scene.** Clear it on scene unload, or a reload stacks a second set of layers on the first.

## Action maps

Authored actions always live at `configs/input_actions.json` (URI
`config://input_actions.json`, kind `com.hikari.config.input_actions`, schema
version `2`). The authored/staged filesystem document wins over its AssetStore
blob. There is no path in startup policy.

The authored document is the action schema: it decides which action ids exist
and supplies every default binding. Runtime rebinding never replaces it. User
changes live separately under the product's writable root as sparse
`configs/input_bindings.json` overrides (kind
`com.hikari.config.input_bindings`, version `1`). Only ids still present in the
authored registry are applied; removed ids are ignored and omitted from the
next save, while newly authored ids immediately receive their defaults.

An untouched session creates no user file. `setActionBinding` marks the live
registry dirty only when the value changes, and shutdown writes only bindings
that differ from authored defaults. Returning a binding to its authored value
therefore removes its override; when none remain, the user file is deleted.
Legacy per-user `input_actions.json` snapshots are deliberately ignored.

### Authored format

Bindings name controls rather than exposing runtime bit masks. Fields are sparse:
omit an unused device or a value that should keep its default.

```json
{
  "kind": "com.hikari.config.input_actions",
  "version": 2,
  "actions": [
    {
      "id": "move_forward",
      "bindings": {
        "keys": ["w", "up"],
        "gamepad_buttons": ["dpad_up"]
      }
    },
    {
      "id": "move_x",
      "bindings": {
        "negative_keys": ["a", "left"],
        "positive_keys": ["d", "right"],
        "gamepad_axis": "left_x"
      }
    },
    {
      "id": "fire",
      "bindings": {
        "mouse_buttons": ["left"],
        "gamepad_buttons": ["r1"]
      }
    }
  ]
}
```

The accepted names are:

| Field | Values |
|------|--------|
| `keys`, `negative_keys`, `positive_keys` | every letter `a`–`z`, every digit `0`–`9`, `f1`–`f12`, `tab`, `enter`, `space`, `escape`, `up`, `down`, `left`, `right`, `shift`, `alt`, `primary_modifier`, `backspace`, `delete`, `home`, `end`. `alt` (Option on macOS) is only seen as a held modifier: Alt/Option chords are never captured from the OS. Punctuation, page up/down, insert and caps lock are not bindable — the key mask is a 64-bit word shared with the native shims and one bit is left |
| `gamepad_buttons` | `a`, `b`, `x`, `y`, `l1`, `r1`, `l3`, `r3`, `menu`, `options`, `home`, `dpad_up`, `dpad_down`, `dpad_left`, `dpad_right` |
| `mouse_buttons` | `left`, `right`, `middle` |
| `gamepad_axis` | `left_x`, `left_y`, `right_x`, `right_y`, `left_trigger`, `right_trigger` |
| `mouse_axis` | `delta_x`, `delta_y`, `wheel_y` |

Each named array is OR-combined. `negative_keys` subtract one and
`positive_keys` add one, which makes a scalar action such as `move_x` directly
usable with `actionValue`. `axis_scale` defaults to `1`, `deadzone` to `0.15`,
and `player` to `0`. A binding-less action is simply `"bindings": {}`.

Version 1 numeric-mask documents are intentionally rejected. Hikari is
unreleased, so authored defaults should be converted to the named version 2
format instead of carrying a legacy reader.

### Per-user override format

The engine writes this document; projects do not ship one:

```json
{
  "kind": "com.hikari.config.input_bindings",
  "version": 1,
  "bindings": [
    {
      "action": "move_forward",
      "binding": { "keys": ["up"] }
    }
  ]
}
```

## Editor input ownership

Editor retained UI and viewport navigation have separate ownership. Interactive editor panels consume their own input first; a hovered panel, including its wheel gesture, must not also navigate the viewport camera. `EditorViewportController` owns edit-camera movement through its narrow view-provider interface (`scene_input_blocked` + press-origin latches).

In Play, game UI and entity scripts receive **scene-local** dimensions and remapped input inside the viewport (`World.gameInputFrame`):

| Pointer mode | Sample outside scene rect |
|--------------|---------------------------|
| **Free** | Buttons, wheel, and `inside` cleared — dock/inspector clicks never reach `mouse_left` / look scripts |
| **Captured** | Motion and buttons kept (virtual aim point is unbounded; chrome is already inert) |

Do not re-implement "am I over chrome?" in game scripts — the remapped frame is the ownership answer.

## Text entry

UI text fields read printable characters from `KeyboardFrame.text` each frame. Focus, caret, Backspace/Delete, and Left/Right live in the UI layer; the platform only fills the text buffer and key bits.

Desktop platforms stream physical keyboard characters into that buffer. Consoles that cannot type directly open a native virtual keyboard through a short text-input session on `InputDriver`:

- `beginTextInput` / `endTextInput` — edge-triggered when any focused editable field wants text
- Session hosts call `SessionCore.syncTextInput` once per frame after all UI (game + editor) has run
- Virtual-keyboard results still land in `KeyboardFrame.text` (streaming or bulk inject on dismiss)

Do not add a second text path in UI code. Platform SDKs own soft keyboards; UI owns buffers and caret.

## Extending input

Add a platform device implementation behind `InputDriver`, normalize its state into `InputState`, and expose bindings/actions through the shared action layer. Do not make scene code platform-aware or build a second action-resolution path in a driver.
