# 15. Input

libakgl does not have an input system so much as a *binding* system. SDL3 owns events,
keycodes, keymods, gamepad enumeration and text composition, and it documents all of them
in the [SDL3 wiki](https://wiki.libsdl.org/SDL3/) — start with
[SDL_Event](https://wiki.libsdl.org/SDL3/SDL_Event),
[SDL_Keycode](https://wiki.libsdl.org/SDL3/SDL_Keycode) and
[CategoryGamepad](https://wiki.libsdl.org/SDL3/CategoryGamepad). Nothing here replaces any
of it.

What libakgl adds is two things: a table that says *"when event X arrives from device Y
carrying key or button Z, call this handler on this actor"*, and a small ring buffer of
keystrokes for a caller who wants to ask "is there a key waiting" without owning the event
loop.

## Input is pushed, not polled

**The host pumps every SDL event into `akgl_controller_handle_event`.** There is no
`akgl_input_update()` that reads the keyboard state for you, and there is no internal event
loop. Whatever drains the SDL event queue — your `while ( SDL_PollEvent(&e) )`, or SDL's
`SDL_AppEvent` callback — hands each event over, unconditionally.

Unconditionally is the point: **an event nothing binds is not an error.** It returns
success having done nothing, which is what makes the blanket call correct and keeps the
host from having to know which events matter.

```c
#include <SDL3/SDL.h>
#include <akgl/actor.h>
#include <akgl/controller.h>
#include <akgl/error.h>

/*
 * Bind WASD on top of control map 0, which akgl_controller_default has already
 * filled with the four arrow keys and the four D-pad directions.
 *
 * Bindings are appended and scanned in push order, and the first match stops
 * the scan -- so these fire only for keys the eight defaults do not claim.
 * There is no way to remove one; zeroing akgl_controlmaps[id] is the only reset.
 */
akerr_ErrorContext AKERR_NOIGNORE *bind_wasd(int mapid, SDL_KeyboardID kbid)
{
    akgl_Control control;
    PREPARE_ERROR(errctx);

    PASS(errctx, akgl_controller_default(mapid, "player", kbid, 0));

    /* Copied in by value, so one stack local does for every push. */
    SDL_memset(&control, 0x00, sizeof(akgl_Control));
    control.event_on = SDL_EVENT_KEY_DOWN;
    control.event_off = SDL_EVENT_KEY_UP;

    control.key = SDLK_A;
    control.handler_on = &akgl_actor_cmhf_left_on;
    control.handler_off = &akgl_actor_cmhf_left_off;
    PASS(errctx, akgl_controller_pushmap(mapid, &control));

    control.key = SDLK_D;
    control.handler_on = &akgl_actor_cmhf_right_on;
    control.handler_off = &akgl_actor_cmhf_right_off;
    PASS(errctx, akgl_controller_pushmap(mapid, &control));

    SUCCEED_RETURN(errctx);
}

/* Every event, unconditionally. An event nothing binds is success, not a
 * failure, which is what makes the unconditional call correct. */
akerr_ErrorContext AKERR_NOIGNORE *pump(void *appstate, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, akgl_controller_handle_event(appstate, event));
    SUCCEED_RETURN(errctx);
}
```

## Two independent paths out of one call

`akgl_controller_handle_event` does two unrelated things with the event it is given, and
**neither one suppresses the other**:

```text
                 akgl_controller_handle_event(appstate, event)
                                   |
            +----------------------+----------------------+
            |                                             |
   (a) the keystroke ring, FIRST            (b) the control maps, SECOND
            |                                             |
   KEY_DOWN     -> push {key, mod, ""}       for map 0..7 (target != NULL):
   TEXT_INPUT   -> attach text to the          for control 0..31, in push order:
                   newest press, or push         event type == event_on/off ?
                   a text-only entry             device id  == kbid / jsid ?
            |                                    key/button == this binding ?
            |                                       -> call the handler and
   akgl_controller_poll_key()                           STOP the entire scan
   akgl_controller_poll_keystroke()
   akgl_controller_flush_keys()
```

The ring is filled **first**, before the maps are consulted, and *whether or not* a map
claims the key. A key that drives an actor is still a key an embedded interpreter polling
with `akgl_controller_poll_key()` wants to see.

### The control-map scan

```c excerpt=include/akgl/controller.h
/** @brief How many control maps exist -- effectively the local player limit. */
#define AKGL_MAX_CONTROL_MAPS 8
/** @brief Bindings per control map. The default map installed by akgl_controller_default uses 8 of them. */
#define AKGL_MAX_CONTROLS     32
```

Eight maps means up to eight locally controlled players, each on its own keyboard and
gamepad. A map with a `NULL` `target` is skipped entirely — that is how an unused slot is
spelled.

**Maps are scanned in index order, bindings within a map in push order, and the first
match wins and stops the whole scan.** A key bound in two maps only ever fires in the
lower-numbered one. A key bound twice in one map only fires on the earlier push. This is
what makes `akgl_controller_default` followed by your own pushes behave sanely: the
defaults are already in slots 0–7 and your additions land after them.

A match requires **all three** of: the event type equals the binding's `event_on` or
`event_off`; the device id on the event equals the map's `kbid` (keyboard events) or `jsid`
(gamepad events); and the key or button equals the binding's. The device-id half is what
keeps two players on two keyboards apart — `akgl_controller_list_keyboards` logs every
attached keyboard and its id, which is how you find the number to pass.

### Bind device 0 unless you mean a specific device

**A `kbid` or `jsid` of `0` matches every device of that kind**, and it is what a
single-player game wants.

Do not reach for `SDL_GetKeyboards()` to fill it in. That call reports what is *attached*;
the id a key event actually *carries* is chosen by the video backend, and the two need not
agree. On X11 without XInput2 every key event carries `SDL_GLOBAL_KEYBOARD_ID`, which is
`0`, while SDL registers the attached keyboard as `SDL_DEFAULT_KEYBOARD_ID`, which is `1`.
With XInput2 the events carry the physical slave device's `sourceid` and
`SDL_GetKeyboards()[0]` may be the master. Binding `SDL_GetKeyboards()[0]` therefore gives
you a map that matches nothing at all, and controls that silently do nothing.

That is not hypothetical — the sidescroller in [Chapter 19](19-tutorial-sidescroller.md)
shipped exactly that bug, and its smoke test passed anyway because the test called the
handlers instead of dispatching events.

`0` is the right spelling for "any" because SDL documents `which` as `0` when the source is
unknown or virtual, and joystick ids start at 1 — so no real device is `0` and nothing is
given up by spending it.

There is no way to remove a binding. `akgl_controller_pushmap` appends and that is the
whole API; the only reset is zeroing `akgl_controlmaps[id]` yourself. **Calling
`akgl_controller_default` twice on one map leaves sixteen bindings**, and the first eight
are the ones that fire.

### The keystroke ring

```c excerpt=src/controller.c
static void keybuffer_push_key(SDL_Keycode key, SDL_Keymod mod)
{
    int tail = 0;

    if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
	keybuffer_awaiting_text = false;
	return;
    }
    tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
    keybuffer[tail].key = key;
    keybuffer[tail].mod = mod;
    keybuffer[tail].text[0] = '\0';
    keybuffer_count += 1;
    keybuffer_awaiting_text = true;
}
```

Thirty-two entries (`AKGL_CONTROLLER_KEY_BUFFER`), one fixed array in the library's data
segment, **one ring for the whole process** — it is a file-scope static, not per map and
not per actor.

**A full buffer drops the newest keystroke, not the oldest.** A caller reading a line of
input wants the characters that were typed first; overwriting the head would hand it the
tail of what the user typed and silently lose the beginning.

Two pollers drain the same ring, and a keystroke taken by one is not waiting for the
other:

| Call | Hands back | Discards |
|---|---|---|
| `akgl_controller_poll_key(&keycode, &available)` | The SDL keycode, as an `int` | Entries carrying only composed text and no keycode — there is no key to report |
| `akgl_controller_poll_keystroke(&dest, &available)` | `key`, `mod` and the composed `text` | Nothing |
| `akgl_controller_flush_keys()` | — | Everything waiting |

An empty ring is success, not a failure: `available` comes back `false` and the caller
polls again next frame. **Check `available`, not the keycode against 0** — a text-only
entry legitimately has a keycode of 0.

**The `text` field needs `SDL_StartTextInput()` on your window.** SDL reports one keystroke
as two events — the key going down, and then the character it composed to, if it composed
to anything — and it only sends the second while text input is started. Without that call
`key` and `mod` still arrive and `text` is always the empty string. That is not a libakgl
limitation to work around; **it is the only correct way to get a character out of SDL**, and
it is what makes a keyboard layout, a compose key, a dead key and an IME commit work. `"`,
`!`, `(`, `)`, `:` and `;` are all unreachable from a keycode alone, and so is every
lower-case letter. See
[SDL_StartTextInput](https://wiki.libsdl.org/SDL3/SDL_StartTextInput).

An entry with composed text and **no** keycode is not a degenerate case: an input method or
a dead key finishes a character with no key press of its own, and a line editor wants that
character. `akgl_controller_poll_keystroke` reports it; `akgl_controller_poll_key` cannot,
and drops it on the way past.

Text longer than `AKGL_CONTROLLER_KEYSTROKE_TEXT` (8 bytes) — an IME committing a whole
word at once — is truncated **on a code point boundary**, never through the middle of a
UTF-8 sequence.

```c
#include <SDL3/SDL.h>
#include <akgl/controller.h>
#include <akgl/error.h>

/*
 * Drain the keystroke ring into a line buffer, the way an embedded interpreter
 * or a name-entry screen wants it.
 *
 * The `text` field is only populated while SDL text input is started on the
 * window, so a caller that wants punctuation, lower case, dead keys or an IME
 * commit calls SDL_StartTextInput() first. Without it `key` and `mod` still
 * arrive and `text` is always empty.
 */
akerr_ErrorContext AKERR_NOIGNORE *read_line(char *dest, size_t destsize, bool *done)
{
    akgl_Keystroke stroke;
    bool available = false;
    size_t used = 0;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
    FAIL_ZERO_RETURN(errctx, done, AKERR_NULLPOINTER, "done");

    used = SDL_strlen(dest);
    *done = false;

    /* The ATTEMPT block goes outside the loop: CATCH reports failure by
     * break-ing, which would bind to the loop instead of the block. */
    ATTEMPT {
	for ( ;; ) {
	    CATCH(errctx, akgl_controller_poll_keystroke(&stroke, &available));
	    if ( available == false ) {
		break;
	    }
	    if ( stroke.key == SDLK_RETURN ) {
		*done = true;
		break;
	    }
	    if ( (stroke.text[0] != '\0') && ((used + 1) < destsize) ) {
		dest[used] = stroke.text[0];
		used += 1;
		dest[used] = '\0';
	    }
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}
```

The ring is filled on whichever thread pumps events and **is not synchronized**. Poll it
from that thread.

## The default bindings

`akgl_controller_default(controlmapid, actorname, kbid, jsid)` points a map at the actor
registered under `actorname` and pushes **eight** bindings — the four arrow keys and the
four D-pad directions — each wired to the matching `akgl_actor_cmhf_*_on`/`_off` pair:

| Key | Gamepad button | `handler_on` / `handler_off` |
|---|---|---|
| `SDLK_DOWN` | `SDL_GAMEPAD_BUTTON_DPAD_DOWN` | `akgl_actor_cmhf_down_on` / `_down_off` |
| `SDLK_UP` | `SDL_GAMEPAD_BUTTON_DPAD_UP` | `akgl_actor_cmhf_up_on` / `_up_off` |
| `SDLK_LEFT` | `SDL_GAMEPAD_BUTTON_DPAD_LEFT` | `akgl_actor_cmhf_left_on` / `_left_off` |
| `SDLK_RIGHT` | `SDL_GAMEPAD_BUTTON_DPAD_RIGHT` | `akgl_actor_cmhf_right_on` / `_right_off` |

It is the "just give me something that works" path. A game wanting different keys builds
its own bindings with `akgl_controller_pushmap`, as in the WASD example above.

**The default bindings cannot produce a diagonal.** Every `akgl_actor_cmhf_*_on` handler
clears `AKGL_ACTOR_STATE_MOVING_ALL` before setting its own bit, so pressing Left while Up
is held cancels Up rather than adding to it. That is fine for a four-way JRPG and wrong for
anything that wants eight-way movement — and it means the thrust-vector ellipse cap in
[Chapter 14](14-physics.md) is unreachable through these handlers. Eight-way movement wants
your own handlers that `AKGL_BITMASK_ADD` the movement bit without clearing the others.

`akgl_controller_handle_button_down` / `_up` are a different thing again, and are **not**
control-map bindings: they look the actor up by the literal name `"player"` in
`AKGL_REGISTRY_ACTOR` rather than being told which actor to act on, so they only ever drive
one. The `akgl_actor_cmhf_*` handlers are the general form; see
[Chapter 12](12-actors.md).

## Gamepads

**SDL delivers no button events from a gamepad nobody has opened.** `akgl_game_init` calls
`akgl_controller_open_gamepads()` for the devices present at startup; a device plugged in
mid-session arrives as `SDL_EVENT_GAMEPAD_ADDED`, and `akgl_controller_handle_added` opens
it. Route that event (and `SDL_EVENT_GAMEPAD_REMOVED`, to `akgl_controller_handle_removed`)
or hot-plugged pads stay silent. No gamepads attached at all is success, not an error.

`akgl_controller_handle_added` logs the mapping it found, deliberately: **a controller with
no entry in the mapping database produces no button events at all**, which is otherwise
indistinguishable from a broken binding.

Which is why libakgl ships a database. `include/akgl/SDL_GameControllerDB.h` carries
`AKGL_SDL_GAMECONTROLLER_DB_LEN` (2255) mappings from the community
[SDL_GameControllerDB](https://github.com/mdqinc/SDL_GameControllerDB) project, and
`akgl_game_init` feeds every one to `SDL_AddGamepadMapping` before opening anything.

That header is **generated, and tracked on purpose**. It is the offline fallback that keeps
the library buildable when upstream is renamed, rate-limited or unreachable. Never
hand-edit it; regeneration is `cmake --build build --target controllerdb` and nothing else
runs the script. The full contract, including why a failed fetch used to destroy the very
file it exists to protect, is in `AGENTS.md` under "Generated and Vendored Sources".

## Known defects

**A matched binding's handler pointer is not checked.** `akgl_controller_handle_event`
calls `curcontrol->handler_on(...)` the moment the event type, device id and key all match,
with no `NULL` test. A control pushed with a `NULL` `handler_on` or `handler_off` crashes
when its event arrives — and since `akgl_controller_pushmap` copies whatever it is given,
a `memset`-zeroed `akgl_Control` with only `event_on` filled in is a loaded gun. **Set both
handlers on every binding you push**, even if one of them does nothing. There is no
diagnostic for this at any layer.

**`appstate` is required and never read.** Every entry point in this subsystem —
`akgl_controller_handle_event`, `handle_button_down`, `handle_button_up`, `handle_added`,
`handle_removed` — raises `AKERR_NULLPOINTER` on a `NULL` `appstate` and then never looks
at it. Pass any non-`NULL` pointer if your program has no app state.

**The analogue-axis, mouse and pen fields are declared and never consulted.**
`akgl_Control::axis`, `axis_range_min` and `axis_range_max`, and `akgl_ControlMap::mouseid`
and `penid`, are in the structs and `akgl_controller_handle_event` does not read any of
them. Setting them has no effect; analogue sticks, mice and pens do not reach the control
maps at all today. Handle those events yourself before calling
`akgl_controller_handle_event`.

**Key auto-repeat is not filtered.** The ring push tests only `event->type ==
SDL_EVENT_KEY_DOWN` and does not consult `event->key.repeat`, so a held key fills the
32-entry ring at the platform's repeat rate. Check `repeat` in your event pump if you are
polling for discrete presses.

Two defects this chapter used to carry are **fixed** and are recorded here so nobody
re-derives them from the header prose:

- **A negative `controlmapid` is rejected.** Both `akgl_controller_pushmap` and
  `akgl_controller_default` check `controlmapid < 0` as well as the upper bound and raise
  `AKERR_OUTOFBOUNDS`. `tests/controller.c` passes `-1` and `-4096` to each. The
  `@warning` blocks in `include/akgl/controller.h` saying only the upper bound is checked
  are stale; `TODO.md`, "Known and still open" item 11.
- **The `akgl_controller_handle_*` functions exist.** `controller.h` once declared four
  names that were defined under different spellings, so they linked nowhere. Fixed in
  0.5.0, and `scripts/check_api_surface.sh` runs as the `api_surface` test to stop that
  class of drift coming back.

## Statuses

The ones this subsystem raises, with their libakgl meanings in
[Chapter 4](04-errors.md):

| Status | When |
|---|---|
| `AKERR_NULLPOINTER` | A `NULL` `appstate`, `event`, `control`, or poller destination; SDL cannot enumerate or open a device; no actor registered as `"player"` |
| `AKERR_OUTOFBOUNDS` | `controlmapid` outside `0..AKGL_MAX_CONTROL_MAPS - 1`, or the map already holds `AKGL_MAX_CONTROLS` bindings |
| `AKGL_ERR_REGISTRY` | `akgl_controller_default` could not find `actorname` — usually because the actor has not been created yet |
| anything | Whatever the matched binding's handler raises, propagated unchanged |
