Use this file to discover all available pages before exploring further.
Keel provides two complementary input surfaces: a stateful InputState poller on app.input for frame-by-frame “is this held right now” checks, and a typed event bus for one-shot reactions. Both are updated by GLFW callbacks on every tick. Edge detection—is_key_pressed and is_key_released—is snapshotted once at the start of each simulation tick via begin_frame(), so a key press is True for exactly one frame.
Use these methods inside any system when you need continuous input (movement, held fire, held menu navigation):
Method
Returns
Notes
app.input.is_key_down(key)
bool
True every frame the key is physically held
app.input.is_key_pressed(key)
bool
True only on the first frame the key goes down
app.input.is_key_released(key)
bool
True only on the first frame the key comes up
import keel@app.system(keel.Phase.UPDATE)def move_player(world, dt): for transform, player in world.query(keel.Transform2D, Player): if app.input.is_key_down(keel.KEY_D): transform['x'] += player['speed'] * dt if app.input.is_key_down(keel.KEY_A): transform['x'] -= player['speed'] * dt if app.input.is_key_down(keel.KEY_W): transform['y'] += player['speed'] * dt if app.input.is_key_down(keel.KEY_S): transform['y'] -= player['speed'] * dt # Jump: one-shot on the frame the key is first pressed. if app.input.is_key_pressed(keel.KEY_SPACE): trigger_jump(transform)
For one-shot reactions that must fire exactly once—quit on Escape, fire a weapon—read KeyEvent off the world bus instead of polling:
KeyEvent is emitted on PRESS and REPEAT actions, but never on RELEASE. To detect when a key comes up, use app.input.is_key_released() — the polling API is the canonical path for keyboard release detection.
import keel@app.system(keel.Phase.UPDATE)def handle_quit(world, dt): for ev in world.read_events(keel.KeyEvent): if ev.key == keel.KEY_ESCAPE and ev.action == keel.PRESS: app.window.close()
# True only on the first frame the button goes down.app.input.is_mouse_button_pressed(keel.MOUSE_BUTTON_LEFT)# True every frame the button is held.app.input.is_mouse_button_down(keel.MOUSE_BUTTON_LEFT)# True only on the first frame the button comes up.app.input.is_mouse_button_released(keel.MOUSE_BUTTON_LEFT)# Current cursor position in window coordinates.x, y = app.input.mouse_position()# Accumulated scroll delta since last call — resets to (0, 0) after each call.dx, dy = app.input.consume_scroll()
Read these from world.read_events(EventType) inside any system:
MouseButtonEvent
Emitted on every button press and release.Fields: button: int, action: int, mods: int
MouseMoveEvent
Emitted whenever the cursor moves inside the window.Fields: x: float, y: float
MouseScrollEvent
Emitted on scroll wheel motion.Fields: x_offset: float, y_offset: float
import keel@app.system(keel.Phase.UPDATE)def handle_mouse(world, dt): # One-shot click detection via events. for ev in world.read_events(keel.MouseButtonEvent): if ev.button == keel.MOUSE_BUTTON_LEFT and ev.action == keel.PRESS: x, y = app.input.mouse_position() print(f"left click at ({x:.0f}, {y:.0f})") # Scroll zoom. for ev in world.read_events(keel.MouseScrollEvent): zoom_camera(ev.y_offset) # Continuous cursor tracking — just poll. cursor_x, cursor_y = app.input.mouse_position() update_crosshair(cursor_x, cursor_y)
Mouse coordinates are in window space: (0, 0) is the top-left corner and y grows downward. This matches the screen-space coordinate system used by TextLabel.
Keel supports up to four gamepads simultaneously (slots 0–3) using GLFW’s standard gamepad mapping API. Call setup_gamepad(app) once at startup; it registers a PRE_UPDATE system that polls GLFW every tick and emits transition events.
@app.system(keel.Phase.UPDATE)def gamepad_move(world, dt): if not gamepad.is_connected(0): return # slot 0 not connected # Analog stick — returns float in [-1.0, 1.0]. x = gamepad.get_axis(0, keel.GAMEPAD_AXIS_LEFT_X) y = gamepad.get_axis(0, keel.GAMEPAD_AXIS_LEFT_Y) # Digital button — returns bool. if gamepad.is_button_down(0, keel.GAMEPAD_BUTTON_A): trigger_jump() # Apply movement. for transform, player in world.query(keel.Transform2D, Player): transform['x'] += x * player['speed'] * dt transform['y'] += y * player['speed'] * dt
Raw value in [-1.0, 1.0]; 0.0 if disconnected or out of range
get_axis returns the raw value from GLFW. A separate 0.05 deadzone is applied only to axis events (GamepadAxisEvent): an event is emitted only when the axis value changes by more than 0.05 since the last emitted value. Polling via get_axis always gives the unfiltered reading.
keel.GAMEPAD_AXIS_LEFT_X # left stick horizontalkeel.GAMEPAD_AXIS_LEFT_Y # left stick verticalkeel.GAMEPAD_AXIS_RIGHT_X # right stick horizontalkeel.GAMEPAD_AXIS_RIGHT_Y # right stick verticalkeel.GAMEPAD_AXIS_LEFT_TRIGGER # left trigger [0.0, 1.0] at rest = -1.0keel.GAMEPAD_AXIS_RIGHT_TRIGGER # right trigger
Emitted on every button press and release.Fields: gamepad_id: int, button: int, action: int (keel.PRESS or keel.RELEASE)
GamepadAxisEvent
Emitted when an axis moves more than 0.05 since the last emission.Fields: gamepad_id: int, axis: int, value: float
@app.system(keel.Phase.UPDATE)def on_gamepad_events(world, dt): for ev in world.read_events(keel.GamepadButtonEvent): if ev.gamepad_id == 0 and ev.button == keel.GAMEPAD_BUTTON_START: if ev.action == keel.PRESS: toggle_pause() for ev in world.read_events(keel.GamepadAxisEvent): if ev.gamepad_id == 0 and ev.axis == keel.GAMEPAD_AXIS_LEFT_X: print(f"left stick X moved to {ev.value:.2f}")
When a gamepad disconnects mid-session, Keel synthesizes RELEASE events for every button that was held at the moment of disconnect. This prevents “stuck input” bugs where your game logic never sees the button come up. Always check gamepad.is_connected(slot) before reading axes — get_axis returns 0.0 for disconnected slots, but your game logic may need to react differently (e.g., pause and prompt to reconnect).