Skip to main content

Input

  • The windowed backend pumps OS events into one snapshot per frame. Scripts read it through the input module.
  • Headless runs read neutral state.
  • A replay records the snapshot per tick.

Actions​

Declared in project.toml:

[input.actions]
jump = ["Space", "gamepad:south"]
move_x = ["keys:KeyA,KeyD", "axis:left_x"]
fire = ["mouse:left"]
if input::action_just_pressed("jump") { /* ... */ }
let x = input::action_value("move_x"); // -1 to 1
  • An action takes any number of keys, buttons, sticks or key pairs.
  • action_value runs -1 to 1: a key answers 0 or 1, a stick or keys:KeyA,KeyD the whole range.
  • action_down is held, and action_just_pressed and action_just_released are the edges.
  • A binding names a key by its W3C code (Space, KeyA, ArrowLeft), mouse:left to mouse:forward, gamepad:south, or axis:left_x, with + or - for one direction.
  • input::bind(name, bindings) rebinds an action and saves it to the user data directory. reset_bindings restores the project's bindings.
  • Actions derive from the raw snapshot every frame. A recording holds the keys; a replay derives the actions.

Keyboard and mouse​

if input::key_just_pressed(input::KEY_SPACE) { /* ... */ }
if input::mouse_down(input::MOUSE_BUTTON_LEFT) { /* ... */ }
let (mx, my) = input::mouse_position();

key_down, key_just_pressed and key_just_released cover keys by named constant, and a key's value is its W3C KeyboardEvent.code. KEY_SHIFT, KEY_CONTROL, KEY_ALT and KEY_META answer for either side. The mouse adds position, per-frame delta, five MOUSE_BUTTON_* buttons and scroll delta.

Gamepads​

for pad in input::gamepads() {
if input::gamepad_just_pressed(pad, input::GAMEPAD_BUTTON_SOUTH) { /* ... */ }
let x = input::gamepad_axis(pad, input::GAMEPAD_AXIS_LEFT_X);
}
  • input::gamepads() lists the connected pads. Every query takes a pad id and a named constant.
  • Buttons are GAMEPAD_BUTTON_*, named by position as SDL3 names them.
  • Axes are GAMEPAD_AXIS_*: a stick reads -1 to 1 with up and right positive, and a trigger reads 0 to 1.
  • input::gamepad_name(pad) names the hardware.

Rumble, motion and the touchpad​

let turn = input::gamepad_gyro(pad); // radians per second
let tilt = input::gamepad_acceleration(pad); // g, with gravity in it
for touch in input::gamepad_touches(pad) {
// touch.x and touch.y run 0 to 1 across the surface
}
CallWhat it does
gamepad_rumble(pad, #{ strong, weak, duration })Runs the two motors at 0 to 1 for that many seconds; answers whether it started
gamepad_stop_rumble(pad)Stops it
gamepad_can_rumble(pad)Whether the pad has motors
gamepad_gyro, gamepad_acceleration, gamepad_touchesA PlayStation pad's motion and touchpad, decoded from its HID reports over USB or Bluetooth

A pad with no gyroscope or touchpad reads zero and an empty list, as an absent pad does. All of it is in the recorded snapshot and replays.

Touch​

  • input::touches() returns the fingers on the screen as #{ id, x, y }, oldest first, with touches_just_started() and touches_just_ended() for the edges.
  • A finger is also a left click, so every widget kind works on a phone. emulate_touch_from_mouse makes a held mouse button a finger.
[input]
emulate_mouse_from_touch = true # the default
emulate_touch_from_mouse = false # the default

Touch controls​

A touch_button presses an action while a finger is on it. A touch_stick pushes one action per axis. The game reads the action.

[nodes.touch_stick]
action_x = "move_x"
action_y = "move_y" # 1 pushed away from the player, as a gamepad reads
anchor = "bottom_left"
offset = [130.0, -130.0] # design pixels to the centre, y down
radius = 90.0

[nodes.touch_button]
action = "jump"
anchor = "bottom_right"
shape = "circle"
  • An action bound to Space gains the button as a second source; the furthest from rest wins. An action no manifest declared still answers.
  • anchor and offset place a control in design pixels, inside the safe area. In the editor they sit in the viewport while the game plays.
  • visibility = "touchscreen" is the default: shown on a phone, a touch browser, or a project with emulate_touch_from_mouse. "always" shows them everywhere.
  • A thumb that slides off a button still holds it. recenter = true puts the stick under the thumb that found it, and deadzone holds a resting thumb at zero.
  • Both are components hit-tested inside the tick: they run headless, land in the digest and replay.
  • ui::wants_pointer() says whether a widget took this frame's finger.

Gestures​

if let Some(scale) = input::pinch().get("scale") { zoom *= scale; }
let pan = input::pan(); // #{ x, y }, two fingers or more
if let Some(dx) = input::swipe().get("x") { /* a unit direction */ }
if input::long_press().get("x").is_some() { /* once per hold */ }
CallAnswers
pinch()#{ scale, x, y } while two fingers move apart or together; scale is against last frame
pan()#{ x, y }, the average movement of every finger down
swipe()#{ x, y, speed } on the frame a finger that travelled far enough lifts
long_press()#{ x, y } once, when a still finger has held long enough

Each derives from the recorded fingers and the fixed step, and replays. Each answers empty when nothing is happening. swipe_pixels, long_press_seconds and long_press_slop_pixels in [input] set the thresholds.

The on-screen keyboard​

  • A focused field raises the system keyboard on iOS and Android.
  • input::keyboard_height() is the covered height in pixels from the bottom, read from UIKit, the Android activity or the page, and recorded.
  • avoid_keyboard = true on a root widget measures the bottom of the screen from the top of the keyboard.

Dropped files​

input::dropped_files() returns the paths of files dropped onto the window this frame.

Feeding input from a script​

feed_key, feed_mouse, feed_mouse_button, feed_touch(id, x, y, phase) and feed_action(name, value) put input into the frame as if a device reported it. A test, a showcase or an automation client drives a game with them. A fed frame records and replays like a real one.

Seeing input​

  • The editor's input overlay draws this frame's input: a chip per key and mouse button held, the actions they drive, a cursor at the pointer, a ring from every press.
  • It reads the game's snapshot. During a replay the cursor follows the recording.
  • Enabled from the command palette (Show input overlay), or with --state input at start.
  • The actions come from the game's project.toml. The editor, itself a Balaur project, declares the game's actions when it opens one.

The full surface​

Every function and all named constants, keys, mouse buttons, pad buttons and axes, are in the script modules reference.