# 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 #include #include #include /* * 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 #include #include /* * 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 |