Input
The application owns three input devices and lends them to your states: app->keyboard(),
app->gamepads() and app->mouse(). All three are read once at the start of every step, before
any state updates (The tick), and you ask them
questions from update().
Held, pressed and released
Section titled “Held, pressed and released”Every device answers the same three questions about a key or button:
| Question | Meaning |
|---|---|
held(...) |
Down now. |
pressed(...) |
Down now, and not down at the previous step. True for one step per press. |
released(...) |
Up now, and down at the previous step. |
Use held for anything continuous (moving, charging, scrolling) and pressed for anything that
happens once per press (jumping, confirming, opening a menu). Because each step compares against
the one before it, a state pushed by a press does not see that same press: the button was already
down at the previous step.
Keyboard
Section titled “Keyboard”Keys are named by position, not by the character printed on them. Key::w, Key::a,
Key::s and Key::d are the block under the left hand on any layout, which is what a movement
binding wants. The minimal sample reads arrows and WASD together:
{
{ direction.x -= 1.0f; } if (keyboard.held(Key::right) || keyboard.held(Key::d)) { direction.x += 1.0f; } if (keyboard.held(Key::up) || keyboard.held(Key::w)) { direction.y -= 1.0f; } if (keyboard.held(Key::down) || keyboard.held(Key::s)) { direction.y += 1.0f; }
return direction;}samples/minimal/states/hello_state.cpp, lines 46–68 at 862e08b
Typed text is separate. keyboard.typed() returns what the player typed during the step, as
UTF-8, with the keyboard layout, dead keys and input methods already applied by the system. It is
valid until the next step, so copy it to keep it. Use it for a name-entry box, never for
movement.
When the window loses focus, every held key is treated as released, so a character does not walk on by itself after an alt-tab.
Gamepads
Section titled “Gamepads”Gamepads has four slots, numbered 0 to 3, and answers held, pressed and released with a
slot and a GamepadButton. The full state of a slot, sticks and triggers included, is
state(slot).
Do not check whether a pad is connected before reading it. An empty slot reads as a neutral
pad with nothing pressed, and pressed is false unless the slot was occupied at both steps. So
reading a pad that is not there costs nothing and does nothing, and a game that reads keyboard
and pad side by side works with either. The minimal sample’s quit check:
connected(slot), just_connected(slot) and just_disconnected(slot) are there for when the
answer matters to the game, such as showing “Player 2, press Start”.
Sticks are raw, from −1 to 1 on each axis, with +y pointing down like every other
coordinate in the engine. Choosing a deadzone is the game’s decision, made once with
apply_deadzone(stick, deadzone). It is circular, and rescales what is left so a small push
outside the deadzone moves slowly rather than jumping to the threshold:
keyboard_direction(keyboard);if (move.length_squared() > 1.0f){ move.normalize();}this->position_ += move * (move_speed * dt);samples/minimal/states/hello_state.cpp, lines 151–157 at 862e08b
The minimal sample adds the keyboard’s direction to the stick’s and clamps the sum, so both work at once and neither is the “real” device.
Triggers are read as numbers from 0 to 1, or as buttons with trigger_held, trigger_pressed
and trigger_released, each taking the threshold that counts as pulled.
Binding tables
Section titled “Binding tables”Labrador has no action-mapping layer: what each key does is your game’s, written in your game. For more than a handful of bindings, a table keeps it in one place. LineSweeper has nine for the keyboard and nine for the pad:
constexpr Binding bindings[] = { { Key::right, button_right }, { Key::down, button_soft_drop }, { Key::space, button_hard_drop }, { Key::up, button_rotate_clockwise }, { Key::x, button_rotate_clockwise }, { Key::z, button_rotate_anticlockwise }, { Key::c, button_hold }, { Key::shift, button_hold },};samples/linesweeper/states/play_state.cpp, lines 45–55 at 862e08b
and reads them into one byte per step, which is the whole of what its rules see:
for (const Binding& binding : bindings){ if (keyboard.held(binding.key)) { input |= binding.button; }}
// OR, AND NO connected() AROUND IT. The byte tick() gets does not say// which device set a bit and must not - a player holding left on the// d-pad while tapping Z on the keyboard is one input, and the// recording of it is one byte either way. An empty slot reads as a// neutral state rather than a stale one (gamepad.h), so this loop// contributes nothing when there is no pad and needs no guard to.//// Slot 0, spelt here rather than tracked, because this is a// one-player game. Which pad is which player is a decision the engine// deliberately leaves to a game (gamepads.h), and this is the whole of// this game's.for (const PadBinding& binding : pad_bindings){ if (pads.held(0, binding.pad)) { input |= binding.button; }}
return input;samples/linesweeper/states/play_state.cpp, lines 174–201 at 862e08b
LineSweeper reads held rather than pressed for these, and works out the presses inside its
rules by comparing each step’s byte with the last. That keeps a recorded match replayable: the
recording carries its own presses. The
sample’s design notes
explain the choice.
Directions for menus
Section titled “Directions for menus”Menus want “up, down, left, right” rather than a stick vector, with a repeat when a direction is
held. pad_direction(state) turns a pad’s d-pad and left stick into a Direction, and
DirectionRepeat turns a held direction into presses: one at once, another after a delay, then
one every interval. LineSweeper’s pause menu combines the keyboard and the pad, then repeats:
{
// The keyboard first and held, not pressed: the repeat is // DirectionRepeat's job now, so every source below reports what is // being pushed and nothing computes an edge of its own. Two devices // disagreeing is not a case - a hand is on one of them. { return Direction::up; }
if (keyboard.held(Key::down)) { return Direction::down; }
// Slot 0, spelt here rather than tracked, because this is a one-player // game - the same decision read_input() makes in play_state.cpp and // for the same reason.}samples/linesweeper/states/pause_state.cpp, lines 183–205 at 862e08b
this->repeat_.update(this->held_direction(), dt);samples/linesweeper/states/pause_state.cpp, lines 233–234 at 862e08b
When a menu opens while a direction is already held, call repeat.start_held(direction) in
init(): the held direction then has to be let go before it counts, so the cursor does not jump
before the menu is on screen. The Menus guide continues from here.
Mouse answers held, pressed and released for MouseButton::left, right, middle, x1
and x2, and also reports position() in window pixels, motion() since the last step, and
wheel() and wheel_horizontal() in notches. While a button is held the mouse keeps reporting
movement outside the window, so a drag is one gesture.
Development documentation (unreleased). Built from Labrador 862e08b of 2026-10-08.