Devices

One cycle of keyboard, text, mouse and gamepad input.

App.Input contains the host's observation as input.devices. A snapshot is pure data and grants no device authority.

Two kinds of information are in it, and each field says which it is. A state sample is the latest value at the cycle boundary: a held key, the pointer position, a gamepad axis. An interval event is something that happened since the previous input: a press, a release, a typed character, a wheel notch. On a desktop host the keyboard, mouse-button, wheel and text events are recorded from the window system's own event callbacks, so an event that reaches the process is in the next input or reported as an overflow, never silently lost, with a latency of at most one cycle. Gamepad buttons are the exception: raylib polls them with no callback, so their edges are sampled and a press-and-release between two polls is lost.

The interval events come in two views. The packed bits behind key_pressed, mouse.button_pressed and friends are the coalesced view: at least one press, at least one release, per key or button. events is the record: every event in delivery order, with count, order across sources and the pointer position of every click preserved up to a stated capacity. Read the bits for simple held-or-pressed logic; read the list when order or count matters -- a double tap, a drag that ended and began again inside one frame, text editing where a keystroke has to land between two characters.

Tests can start from Devices.none and set only the relevant state with the with_key_* and with_mouse_* methods.

Devices.none.with_key_pressed(KeySpace)
none : Snapshot

A neutral snapshot with the host's own packed list lengths, for tests.

empty and none return the same result for every query -- nothing held, nothing typed, pointer at the origin, no gamepad connected. They differ in what they are made of. empty's packed lists are empty, which is all a model seed needs and costs nothing. none's are the lengths the host actually samples: 349 key bytes, 7 mouse-button bytes, 4 gamepad availability bytes, 4 x 18 gamepad button bytes, and 4 x 6 axes. That is what makes it writable, so the with_key_* and with_mouse_* methods have somewhere to put a bit.

devices = Devices.none.with_key_pressed(KeyEscape)

Seed a model with empty; build a test's input from none.

empty : Snapshot

A snapshot in which nothing is pressed, nothing was typed, the pointer is at the origin, and no gamepad is connected.

Use this to initialize models before the first device snapshot is sampled. Devices.none is the same observation with the host's packed list lengths, which is what a test that wants to say "SPACE was pressed" needs.

Snapshot

:= {
    keys : List(U8),
    text_input : List(U32),
    text_input_overflow : Bool,
    events : List(Event),
    events_overflow : Bool,
    gamepads : Snapshot,
    mouse : Snapshot,
}

Everything the host sampled from the input devices for one cycle.

Reach for the methods rather than indexing the packed lists directly: input.key_pressed(KeyW), input.mouse.position(), input.gamepad(One).

key_down : Snapshot, Key -> Bool

Check whether a key is currently held. Receiver form: input.key_down(KeyW).

key_up : Snapshot, Key -> Bool

Check whether a key is currently up. Receiver form: input.key_up(KeyW).

key_pressed : Snapshot, Key -> Bool

Check whether a key was pressed during this input interval. Receiver form: input.key_pressed(KeyW). Static Keys.key_pressed(input, KeyW) remains available; combine singular queries with or when checking alternatives.

key_released : Snapshot, Key -> Bool

Check whether a key was released during this input interval. Receiver form: input.key_released(KeyW). Static Keys.key_released(input, KeyW) remains.

gamepad : Snapshot, Id -> [Connected(View), Disconnected]

Resolve a gamepad slot in this input's sampled snapshot. The returned pad is snapshot-scoped; query it now rather than retaining it in the model.

with_key_down : Snapshot, Key -> Snapshot

Say that one key is held, as Devices.none.with_key_down(KeyW).

Each with_key_* states the complete state of the key it names, replacing whatever that key had. Different keys are independent bytes, so these compose: Devices.none.with_key_pressed(KeySpace).with_key_down(KeyLeftShift).

Only a snapshot with the host's packed list lengths can hold these. Start from Devices.none, not Devices.empty.

with_key_pressed : Snapshot, Key -> Snapshot

Say that one key went down this cycle, as Devices.none.with_key_pressed(KeySpace).

A key pressed this cycle is also held, which is how the host packs it, so this satisfies key_down as well as key_pressed.

with_key_released : Snapshot, Key -> Snapshot

Say that one key came up this cycle, as Devices.none.with_key_released(KeySpace).

A key released this cycle is no longer held, which is how the host packs it, so this satisfies key_released and key_up.

with_mouse_position : Snapshot, { x : F32, y : F32 } -> Snapshot

Place the pointer, as Devices.none.with_mouse_position({ x: 40, y: 12 }).

with_mouse_delta : Snapshot, { x : F32, y : F32 } -> Snapshot

Say how far the pointer moved this cycle, which is what input.mouse.delta() reports.

with_mouse_wheel : Snapshot, { x : F32, y : F32 } -> Snapshot

Say how far the wheel moved this cycle, which is what input.mouse.wheel_delta() reports.

wheel is set to the vertical movement, matching how the host samples a one-dimensional wheel alongside the two-axis one.

with_mouse_button_down : Snapshot, Button -> Snapshot

Say that one mouse button is held. The with_mouse_button_* family states the complete state of the button it names, exactly as the with_key_* family does.

with_mouse_button_pressed : Snapshot, Button -> Snapshot

Say that one mouse button went down this cycle. A button pressed this cycle is also held.

with_mouse_button_released : Snapshot, Button -> Snapshot

Say that one mouse button came up this cycle. A button released this cycle is no longer held.

with_text_input : Snapshot, List(U32) -> Snapshot

Say that these codepoints were typed this cycle, in order, as Devices.none.with_text_input([72, 105]) for typing "Hi".

As the host does, at most 32 codepoints are kept; a longer burst keeps the first 32 and sets text_input_overflow. The key bits and the event record are not derived from it: a test that wants them to agree states them too, as the host would have.

with_text_input_overflow : Snapshot, Bool -> Snapshot

Say whether typed text was cut at the interval's capacity, which is what text_input_overflow reports.

with_events : Snapshot, List(Event) -> Snapshot

State the interval's event record, as Devices.none.with_events([KeyPressed(KeySpace), KeyReleased(KeySpace)]).

The bits are not derived from it: a test that wants both views to agree states both, as the host would have. Unlike the with_key_* methods this works on Devices.empty too, since a list has no fixed length to fit.

with_events_overflow : Snapshot, Bool -> Snapshot

Say whether the event record was cut at its capacity, which is what events_overflow reports.

Event : [
    KeyPressed(Key),
    KeyReleased(Key),
    ButtonPressed(Button, { x : F32, y : F32 }),
    ButtonReleased(Button, { x : F32, y : F32 }),
    Wheel({ x : F32, y : F32 }),
    Text(U32),
]

One input event, as the window system delivered it.

KeyPressed and KeyReleased are physical key edges; auto-repeat is not an event. ButtonPressed and ButtonReleased include the pointer position where the click happened, in the same logical coordinates as mouse.position(). Wheel is one scroll event's offsets, which mouse.wheel_delta() sums. Text is one typed codepoint, following the active keyboard layout, which is why it is separate from the key edge that produced it.