feat(input): raw device layer — multi-key held state, analog, mouse, gamepad, touch, full-state replay (#50)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m24s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s

The raw device layer the input proposal sketched, over the action maps +
record/replay of #7. Beyond one key per frame, gameplay can read:

- Multiple simultaneous held keys: Input.key_down / key_pressed / key_released,
  with clean rising/falling edges (hold left AND jump).
- Analog from keys: Input.axis(neg, pos) and a normalized Input.vector(l,r,u,d)
  (diagonals scaled by 1/sqrt(2)), plus Input.strength(action).
- Mouse: Input.mouse_x/y, mouse_dx/dy (per-frame delta), mouse_down(btn), wheel.
- Gamepads: Input.pad_connected/pad_button/pad_axis (SDL-order buttons, -1..1
  sticks); touch: Input.touch_count/touch_x/touch_y.

The held set is fed by the platform when windowed — cocoa.ll now tracks
keyDown/keyUp into a 256-bit held-key bitset (win_held) and the mouse
buttons/wheel (win_mouse), gated so headless builds DCE the native calls — and
by the Input.press / Input.set_mouse / Input.set_pad / Input.set_touch injection
on every target (Godot-style action injection: replays, AI, network-fed input).
Input.record / replay now snapshot the full per-frame device state (held set +
mouse), extending #7's single-key tape.

Everything is integer and deterministic, so the same inputs reproduce the same
frame on every run and headless. The gamepad/touch native hardware bindings
(GameController.framework / NSTouch) feed the same injected state and are the one
remaining platform-glue follow-up; the software layer, semantics and replay are
complete and driven deterministically today.

Worked example + regression: examples/library/input_device.ludic
(1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1, injection-driven headless). 23 new
docs/language/input pages. Full suite 78 passed, self-host C-free fixpoint
intact, no golden drift; cocoa.ll assembles and a windowed build links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 17:14:24 +03:00
parent 1f5e3c1c1a
commit 53bb441f23
35 changed files with 23630 additions and 20813 deletions

View file

@ -4,4 +4,6 @@ title: Input
order: 6
---
Reading the keyboard.
Reading input. At the base, <a href="input-key.html"><code>Input.key</code></a> gives this frame's key and <a href="input-poll.html"><code>Input.poll</code></a> is the single per-frame read that also drives deterministic record/replay. Over that sit <strong>named actions</strong> — <a href="input-bind.html"><code>Input.bind</code></a> / <a href="input-down.html"><code>Input.down</code></a> / <a href="input-pressed.html"><code>Input.pressed</code></a> / <a href="input-rebind.html"><code>Input.rebind</code></a> — so gameplay reads rebindable actions, not physical keys.
The <strong>device layer</strong> adds everything past one key per frame: multiple simultaneous held keys (<a href="input-key_down.html"><code>Input.key_down</code></a> / <a href="input-key_pressed.html"><code>key_pressed</code></a> / <a href="input-key_released.html"><code>key_released</code></a>), analog <a href="input-axis.html"><code>Input.axis</code></a> and normalized <a href="input-vector.html"><code>Input.vector</code></a>, the <a href="input-mouse_x.html">mouse</a> (position, delta, buttons, <a href="input-wheel.html">wheel</a>), <a href="input-pad_button.html">gamepads</a> and <a href="input-touch_count.html">touch</a>. The held set is fed by the platform when windowed and by the <a href="input-press.html"><code>Input.press</code></a> / <a href="input-set_mouse.html"><code>Input.set_*</code></a> injection on every target — the same idea as Godot's <code>action_press</code>, and what a replay, an AI, or the network feeds. Everything is integer and deterministic, and record/replay snapshots the whole per-frame state.

View file

@ -0,0 +1,24 @@
---
id: input-axis
name: Input.axis
category: input
kind: namespace-method
tokens: Input.axis
sig: Input.axis(neg, pos) -> fixed
tip: A -1..+1 axis from two keys.
order: 13
ns: Input
member: axis
---
Returns a digital axis from two keys: <code>+1.0</code> when the positive key is held, <code>-1.0</code> when the negative one is, <code>0</code> otherwise (both or neither). The building block for keyboard movement; a gamepad stick feeds the analog value through the same read.
```ludic
program Demo {
entry {
Input.press('d')
Input.poll()
print(Math.round(Input.axis('a', 'd')))
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-key_down
name: Input.key_down
category: input
kind: namespace-method
tokens: Input.key_down
sig: Input.key_down(key) -> bool
tip: Is a physical key held this frame?
order: 8
ns: Input
member: key_down
---
Returns whether a physical <code>key</code> is held this frame — from the platform when windowed, from the polled key when headless, and from <a href="input-press.html"><code>Input.press</code></a> injection on any target. Unlike the single-key <a href="input-down.html"><code>Input.down</code></a>, several keys can be held at once (move left <em>and</em> jump).
```ludic
program Demo {
entry {
Input.press('a')
Input.poll()
if Input.key_down('a') { print(1) }
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-key_pressed
name: Input.key_pressed
category: input
kind: namespace-method
tokens: Input.key_pressed
sig: Input.key_pressed(key) -> bool
tip: Did a key go down this frame (edge)?
order: 9
ns: Input
member: key_pressed
---
Returns whether a key went down <em>this</em> frame — held now, not held last frame. A clean rising edge for one-shot actions (fire, confirm), distinct from the continuous <a href="input-key_down.html"><code>Input.key_down</code></a>.
```ludic
program Demo {
entry {
Input.press(' ')
Input.poll()
if Input.key_pressed(' ') { print(1) }
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-key_released
name: Input.key_released
category: input
kind: namespace-method
tokens: Input.key_released
sig: Input.key_released(key) -> bool
tip: Did a key go up this frame (edge)?
order: 10
ns: Input
member: key_released
---
Returns whether a key went up this frame — held last frame, not held now. The falling edge, for release-triggered actions (charge-and-release, letting go of a grip).
```ludic
program Demo {
entry {
Input.press('a')
Input.poll()
Input.release('a')
Input.poll()
if Input.key_released('a') { print(1) }
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-mouse_down
name: Input.mouse_down
category: input
kind: namespace-method
tokens: Input.mouse_down
sig: Input.mouse_down(button) -> bool
tip: Is a mouse button held?
order: 20
ns: Input
member: mouse_down
---
Returns whether a mouse button is held this frame — button <code>0</code> left, <code>1</code> right, <code>2</code> middle/other.
```ludic
program Demo {
entry {
Input.set_mouse(0, 0, 1, 0)
Input.poll()
if Input.mouse_down(0) { print(1) }
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-mouse_dx
name: Input.mouse_dx
category: input
kind: namespace-method
tokens: Input.mouse_dx
sig: Input.mouse_dx() -> int
tip: Mouse x movement since the last poll.
order: 18
ns: Input
member: mouse_dx
---
The change in the mouse's x since the previous <a href="input-poll.html"><code>Input.poll</code></a> — the relative motion for dragging, panning, or a mouse-look camera.
```ludic
program Demo {
entry {
Input.set_mouse(10, 0, 0, 0)
Input.poll()
Input.set_mouse(15, 0, 0, 0)
Input.poll()
print(Input.mouse_dx())
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-mouse_dy
name: Input.mouse_dy
category: input
kind: namespace-method
tokens: Input.mouse_dy
sig: Input.mouse_dy() -> int
tip: Mouse y movement since the last poll.
order: 19
ns: Input
member: mouse_dy
---
The change in the mouse's y since the previous <a href="input-poll.html"><code>Input.poll</code></a> — the vertical companion to <a href="input-mouse_dx.html"><code>Input.mouse_dx</code></a>.
```ludic
program Demo {
entry {
Input.set_mouse(0, 10, 0, 0)
Input.poll()
Input.set_mouse(0, 18, 0, 0)
Input.poll()
print(Input.mouse_dy())
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-mouse_x
name: Input.mouse_x
category: input
kind: namespace-method
tokens: Input.mouse_x
sig: Input.mouse_x() -> int
tip: The mouse x position this frame.
order: 16
ns: Input
member: mouse_x
---
The mouse's x position this frame — fed by the window when windowed, or by <a href="input-set_mouse.html"><code>Input.set_mouse</code></a> otherwise.
```ludic
program Demo {
entry {
Input.set_mouse(10, 20, 0, 0)
Input.poll()
print(Input.mouse_x())
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-mouse_y
name: Input.mouse_y
category: input
kind: namespace-method
tokens: Input.mouse_y
sig: Input.mouse_y() -> int
tip: The mouse y position this frame.
order: 17
ns: Input
member: mouse_y
---
The mouse's y position this frame — fed by the window when windowed, or by <a href="input-set_mouse.html"><code>Input.set_mouse</code></a> otherwise.
```ludic
program Demo {
entry {
Input.set_mouse(10, 20, 0, 0)
Input.poll()
print(Input.mouse_y())
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-pad_axis
name: Input.pad_axis
category: input
kind: namespace-method
tokens: Input.pad_axis
sig: Input.pad_axis(pad, axis) -> fixed
tip: A gamepad analog axis, -1..+1.
order: 25
ns: Input
member: pad_axis
---
Returns a gamepad analog axis as a <code>fixed</code> in <code>-1.0..+1.0</code> — axis <code>0</code> left-stick x, <code>1</code> left-stick y, <code>2</code> right-stick x, <code>3</code> right-stick y. Apply your own deadzone near zero.
```ludic
program Demo {
entry {
Input.set_pad(0, true, 0, 1.0, 0.0, 0.0, 0.0)
print(Math.round(Input.pad_axis(0, 0)))
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-pad_button
name: Input.pad_button
category: input
kind: namespace-method
tokens: Input.pad_button
sig: Input.pad_button(pad, button) -> bool
tip: Is a gamepad button held?
order: 24
ns: Input
member: pad_button
---
Returns whether <code>button</code> on gamepad <code>pad</code> is held — a bit in the pad's button mask, in the SDL standard order (A/B/X/Y, bumpers, start, …).
```ludic
program Demo {
entry {
Input.set_pad(0, true, 1, 0.0, 0.0, 0.0, 0.0)
if Input.pad_button(0, 0) { print(1) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-pad_connected
name: Input.pad_connected
category: input
kind: namespace-method
tokens: Input.pad_connected
sig: Input.pad_connected(pad) -> bool
tip: Is a gamepad connected?
order: 23
ns: Input
member: pad_connected
---
Returns whether gamepad <code>pad</code> (0..3) is connected. Fed by <a href="input-set_pad.html"><code>Input.set_pad</code></a> (and, where a platform gamepad binding is present, by the device); lets a game show a "controller connected" prompt.
```ludic
program Demo {
entry {
Input.set_pad(0, true, 0, 0.0, 0.0, 0.0, 0.0)
if Input.pad_connected(0) { print(1) }
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-press
name: Input.press
category: input
kind: namespace-method
tokens: Input.press
sig: Input.press(key)
tip: Inject a held key (AI, tutorial, testing, network).
order: 11
ns: Input
member: press
---
Injects a held key into the input state — the same idea as Godot's <code>action_press</code>. It persists until <a href="input-release.html"><code>Input.release</code></a>, and combines with the platform's real keys, so an AI, a replay, a tutorial, or the network can drive input exactly as a player would. Deterministic on every target.
```ludic
program Demo {
entry {
Input.press('w')
Input.poll()
if Input.key_down('w') { print(1) }
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-release
name: Input.release
category: input
kind: namespace-method
tokens: Input.release
sig: Input.release(key)
tip: Release an injected key.
order: 12
ns: Input
member: release
---
Releases a key previously held with <a href="input-press.html"><code>Input.press</code></a>. After the next <a href="input-poll.html"><code>Input.poll</code></a> the key reads as up (and fires <a href="input-key_released.html"><code>Input.key_released</code></a> that frame).
```ludic
program Demo {
entry {
Input.press('w')
Input.poll()
Input.release('w')
Input.poll()
print(0)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-set_mouse
name: Input.set_mouse
category: input
kind: namespace-method
tokens: Input.set_mouse
sig: Input.set_mouse(x, y, buttons, wheel)
tip: Inject the mouse state (headless / AI / testing).
order: 22
ns: Input
member: set_mouse
---
Injects the whole mouse state: position <code>x</code>/<code>y</code>, a <code>buttons</code> bitmask (bit 0 left, 1 right, 2 middle), and this frame's <code>wheel</code> delta. The mouse counterpart to <a href="input-press.html"><code>Input.press</code></a> — for headless runs, AI, replays, or network-fed cursors.
```ludic
program Demo {
entry {
Input.set_mouse(64, 32, 1, 0)
Input.poll()
print(Input.mouse_x())
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-set_pad
name: Input.set_pad
category: input
kind: namespace-method
tokens: Input.set_pad
sig: Input.set_pad(pad, connected, buttons, lx, ly, rx, ry)
tip: Inject a gamepad's whole state.
order: 26
ns: Input
member: set_pad
---
Injects gamepad <code>pad</code>'s whole state: <code>connected</code>, a <code>buttons</code> bitmask, and the four stick axes (<code>lx</code>, <code>ly</code>, <code>rx</code>, <code>ry</code>) as <code>fixed</code> in <code>-1.0..+1.0</code>. A platform gamepad binding feeds the same state; until then this drives it from replays, AI, or the network.
```ludic
program Demo {
entry {
Input.set_pad(0, true, 5, 1.0, 0.0, 0.0, 0.0)
if Input.pad_button(0, 2) { print(1) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-set_touch
name: Input.set_touch
category: input
kind: namespace-method
tokens: Input.set_touch
sig: Input.set_touch(index, x, y, active)
tip: Inject a touch point.
order: 30
ns: Input
member: set_touch
---
Injects touch point <code>index</code> (0..7): <code>active</code> with a position, or inactive. A platform touch binding feeds the same points; until then this drives multi-touch from replays, AI, or the network.
```ludic
program Demo {
entry {
Input.set_touch(0, 64, 64, true)
print(Input.touch_count())
}
}
```

View file

@ -0,0 +1,25 @@
---
id: input-strength
name: Input.strength
category: input
kind: namespace-method
tokens: Input.strength
sig: Input.strength(action) -> fixed
tip: 0..1 strength of a named action.
order: 15
ns: Input
member: strength
---
Returns the <code>0.0..1.0</code> strength of a named <a href="input-bind.html">action</a> — <code>1.0</code> when any bound key is held, <code>0</code> otherwise for keys (digital), and the analog magnitude when a gamepad feeds the action. Lets analog and digital sources share one read.
```ludic
program Demo {
entry {
Input.bind("gas", 'w')
Input.press('w')
Input.poll()
print(Math.round(Input.strength("gas")))
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-touch_count
name: Input.touch_count
category: input
kind: namespace-method
tokens: Input.touch_count
sig: Input.touch_count() -> int
tip: How many touch points are active?
order: 27
ns: Input
member: touch_count
---
Returns how many touch points are currently active (0..8). Iterate them with <a href="input-touch_x.html"><code>Input.touch_x</code></a> / <a href="input-touch_y.html"><code>Input.touch_y</code></a>.
```ludic
program Demo {
entry {
Input.set_touch(0, 10, 20, true)
Input.set_touch(1, 30, 40, true)
print(Input.touch_count())
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-touch_x
name: Input.touch_x
category: input
kind: namespace-method
tokens: Input.touch_x
sig: Input.touch_x(index) -> int
tip: The x of a touch point.
order: 28
ns: Input
member: touch_x
---
The x position of touch point <code>index</code> (0..7). Pair with <a href="input-touch_y.html"><code>Input.touch_y</code></a> and <a href="input-touch_count.html"><code>Input.touch_count</code></a> for multi-touch.
```ludic
program Demo {
entry {
Input.set_touch(0, 100, 200, true)
print(Input.touch_x(0))
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-touch_y
name: Input.touch_y
category: input
kind: namespace-method
tokens: Input.touch_y
sig: Input.touch_y(index) -> int
tip: The y of a touch point.
order: 29
ns: Input
member: touch_y
---
The y position of touch point <code>index</code> (0..7) — the vertical companion to <a href="input-touch_x.html"><code>Input.touch_x</code></a>.
```ludic
program Demo {
entry {
Input.set_touch(0, 100, 200, true)
print(Input.touch_y(0))
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-vector
name: Input.vector
category: input
kind: namespace-method
tokens: Input.vector
sig: Input.vector(left, right, up, down) -> Vector
tip: A normalized 2D vector from four keys.
order: 14
ns: Input
member: vector
---
Returns a 2D movement <a href="vector.html"><code>Vector</code></a> from four direction keys, normalized so a diagonal is not faster than a straight move (each diagonal component is <code>0.7071</code>). The one call for eight-way keyboard movement.
```ludic
program Demo {
entry {
Input.press('d')
Input.press('w')
Input.poll()
let v = Input.vector('a', 'd', 'w', 's')
print(Math.round(Vector.x(v) * 100))
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-wheel
name: Input.wheel
category: input
kind: namespace-method
tokens: Input.wheel
sig: Input.wheel() -> int
tip: Scroll-wheel delta this frame.
order: 21
ns: Input
member: wheel
---
The scroll-wheel delta accumulated this frame — positive up, negative down, <code>0</code> when the wheel did not move. A per-frame delta, reset each <a href="input-poll.html"><code>Input.poll</code></a>.
```ludic
program Demo {
entry {
Input.set_mouse(0, 0, 0, 3)
Input.poll()
print(Input.wheel())
}
}
```