From a3eada1b3f82ab1e8365844cc1cf5a95e58cb34c Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Fri, 31 Jul 2026 12:52:38 -0400 Subject: [PATCH] Carry modifiers and composed text in the keystroke ring The ring held a keycode and dropped the rest of the event, so a line editor built on akgl_controller_poll_key() could not reach a single shifted character: no double quote, no colon, no parenthesis, and no lower case. The modifier state was discarded two layers below the caller, and polling SDL_GetModState() at read time cannot recover it -- the key is long released by then, which is the point of a ring. akgl_Keystroke carries the keycode, the modifiers and the UTF-8 text SDL composed, and akgl_controller_poll_keystroke() hands back all three. akgl_controller_poll_key() is unchanged for its callers: a game asking whether the up arrow was pressed already gets what it wants. The text comes from SDL_EVENT_TEXT_INPUT, which is the only correct way to get a character out of SDL, and is attached to the press still waiting for it -- tracked with a flag rather than by "whatever entry is newest", so a press dropped by a full buffer cannot hand its text to an older key. Text with no press behind it (an IME commit, a dead key) is buffered on its own with keycode 0, and the keycode-only poller discards those on the way past. Oversized text is cut on a code point boundary, and a sequence the string ends in the middle of is dropped rather than read past its terminator. Co-Authored-By: Claude Opus 5 (1M context) --- include/akgl/controller.h | 68 ++++++++++++- src/controller.c | 194 ++++++++++++++++++++++++++++++++++---- tests/controller.c | 166 ++++++++++++++++++++++++++++++++ 3 files changed, 406 insertions(+), 22 deletions(-) diff --git a/include/akgl/controller.h b/include/akgl/controller.h index 0efebbf..bcee29e 100644 --- a/include/akgl/controller.h +++ b/include/akgl/controller.h @@ -12,9 +12,16 @@ * bound in two maps only fires once, in the lower-numbered one. * * Alongside that, and independent of it, every key press is pushed into a small - * ring buffer that akgl_controller_poll_key() drains. That serves a caller that - * wants "is there a key waiting" without owning an event loop, and it sees - * keys whether or not a control map also claimed them. + * ring buffer that akgl_controller_poll_key() and akgl_controller_poll_keystroke() + * drain. That serves a caller that wants "is there a key waiting" without owning + * an event loop, and it sees keys whether or not a control map also claimed them. + * + * The two pollers read the same buffer and differ in what they hand back. A game + * asking "was the up arrow pressed" wants a keycode and nothing else, which is + * akgl_controller_poll_key(). A line editor asking "what did the user type" needs + * the modifier state and the character SDL composed -- there is no `"` keycode, + * and no keycode at all for a dead key or an IME commit -- which is + * akgl_controller_poll_keystroke(). */ #ifndef _CONTROLLER_H_ @@ -39,6 +46,23 @@ */ #define AKGL_CONTROLLER_KEY_BUFFER 32 +/** + * @brief Bytes of composed text one buffered keystroke can carry, NUL included. + * + * A UTF-8 code point is at most four bytes, so this holds one character and its + * terminator with room to spare. An input event carrying more than fits -- an + * IME committing a whole word at once -- is truncated on a code point boundary + * rather than split through the middle of one. + */ +#define AKGL_CONTROLLER_KEYSTROKE_TEXT 8 + +/** @brief One buffered keystroke: which key, which modifiers, and what it typed. */ +typedef struct { + SDL_Keycode key; /**< The keycode, or 0 for an entry that carries only composed text -- an IME commit, or a character finished by a dead key. */ + SDL_Keymod mod; /**< Modifier state when the key went down. 0 on a text-only entry, whose text already reflects them. */ + char text[AKGL_CONTROLLER_KEYSTROKE_TEXT]; /**< What the keystroke composed to, UTF-8. The empty string for a key that produces no character, such as an arrow or a bare modifier. */ +} akgl_Keystroke; + /** @brief Maps one SDL input to pressed and released callbacks. */ typedef struct { uint32_t event_on; /**< SDL event type that fires `handler_on`, e.g. `SDL_EVENT_KEY_DOWN`. */ @@ -85,7 +109,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void); * * The entry point for the whole subsystem -- call it for every event the host * pumps. A key press is recorded in the poll buffer first, whether or not any - * map wants it; then the maps are scanned in index order and, within a map, + * map wants it, and an `SDL_EVENT_TEXT_INPUT` is attached to the press it + * belongs to; then the maps are scanned in index order and, within a map, * bindings in the order they were pushed. The **first** binding whose event type * and device id and button/key all match wins, and the scan stops there. * @@ -244,6 +269,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void); * was typed first is what is read first. This runs on whichever thread pumps * events; it is not synchronized. * + * Entries that carry only composed text and no keycode are discarded on the way + * past rather than reported as keycode 0: this form is for a caller that acts on + * keys, and there is no key to report. A caller that wants those characters uses + * akgl_controller_poll_keystroke() instead. + * * @param keycode Receives the SDL keycode, or 0 when nothing was waiting. * Required -- the return value is the error context. * @param available Receives `true` when a keystroke was taken, `false` when the @@ -256,6 +286,36 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void); */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available); +/** + * @brief Take the oldest waiting keystroke whole: key, modifiers and text. + * + * akgl_controller_poll_key() with nothing thrown away. A line editor needs all + * three: the keycode to recognise Backspace and Return, the modifier state to + * tell Ctrl-C from a `c`, and the composed text because a keycode cannot express + * what a shifted key produces on the user's own layout. `"`, `!`, `(`, `)`, `:` + * and `;` are all unreachable from a keycode alone, and so is every lower-case + * letter. + * + * The text is what SDL composed, taken from the `SDL_EVENT_TEXT_INPUT` that + * follows the key press -- the only correct way to get a character out of SDL, + * and what makes a keyboard layout, a compose key and a dead key work. **SDL + * only sends those events while text input is started**, so a host that wants + * the `text` field populated calls `SDL_StartTextInput()` on its window first. + * Without it, `key` and `mod` still arrive and `text` is always empty. + * + * The two pollers drain the same buffer, so a keystroke taken by one is not + * waiting for the other. + * + * @param dest Receives the keystroke. Required. Written only when a + * keystroke was waiting; zeroed otherwise, so `key` is 0 and + * `text` is the empty string. + * @param available Receives `true` when a keystroke was taken, `false` when the + * buffer was empty. Required. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER If @p dest or @p available is `NULL`. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available); + /** * @brief Discard every keystroke waiting in the buffer. * diff --git a/src/controller.c b/src/controller.c index 61d0652..9485482 100644 --- a/src/controller.c +++ b/src/controller.c @@ -13,36 +13,173 @@ akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS]; /* - * Keystrokes waiting for akgl_controller_poll_key(). Filled by - * akgl_controller_handle_event() before it consults the control maps, so a key - * that also drives an actor is still delivered to a polling caller. + * Keystrokes waiting for akgl_controller_poll_key() and + * akgl_controller_poll_keystroke(). Filled by akgl_controller_handle_event() + * before it consults the control maps, so a key that also drives an actor is + * still delivered to a polling caller. * * head is the next slot to read, count is how many are waiting. Both index a * fixed array rather than a queue object, which is the whole point: a host that * never polls cannot make this grow. */ -static SDL_Keycode keybuffer[AKGL_CONTROLLER_KEY_BUFFER]; +static akgl_Keystroke keybuffer[AKGL_CONTROLLER_KEY_BUFFER]; static int keybuffer_head = 0; static int keybuffer_count = 0; +/* + * Whether the newest entry is a key press that has not been given its composed + * text yet. SDL reports one keystroke as two events -- the key going down, and + * then the text it composed to, if it composed to anything -- so the text that + * arrives next belongs to the press that arrived last. Without this flag a + * press dropped by a full buffer would hand its text to whatever older press + * happened to still be sitting at the end of the ring. + */ +static bool keybuffer_awaiting_text = false; + /** - * @brief Record one keystroke, discarding it if the buffer is already full. + * @brief Length in bytes of the UTF-8 sequence @p src begins with, or 0. + * + * 0 both for a byte that cannot start a sequence and for a sequence the string + * ends in the middle of. SDL should hand over neither, and reading past a + * terminator to find out otherwise is not a risk worth taking for input that + * arrives from a keyboard driver. + * + * @param src UTF-8 text, NUL terminated. Required. + */ +static size_t utf8_sequence_length(const char *src) +{ + unsigned char lead = (unsigned char)src[0]; + size_t len = 0; + size_t i = 0; + + if ( lead < 0x80 ) { + return 1; + } else if ( (lead & 0xe0) == 0xc0 ) { + len = 2; + } else if ( (lead & 0xf0) == 0xe0 ) { + len = 3; + } else if ( (lead & 0xf8) == 0xf0 ) { + len = 4; + } else { + return 0; + } + for ( i = 1; i < len; i++ ) { + // A NUL fails this too, which is what stops the scan at the end of a + // truncated string instead of past it. + if ( ((unsigned char)src[i] & 0xc0) != 0x80 ) { + return 0; + } + } + return len; +} + +/** + * @brief Copy as much of @p src as fits in @p dest without splitting a character. + * + * SDL hands over whatever the platform composed, which for an input method can + * be several characters at once, and the ring entry holds one. Truncating on a + * byte boundary would leave a partial UTF-8 sequence that nothing downstream + * can render, so this stops at the last code point that fits whole. + * + * @param dest Receives the prefix, always NUL terminated. Required. + * @param destsize Bytes available in @p dest, terminator included. + * @param src UTF-8 text to copy. Required. + */ +static void copy_utf8_prefix(char *dest, size_t destsize, const char *src) +{ + size_t used = 0; + size_t len = 0; + + dest[0] = '\0'; + while ( src[used] != '\0' ) { + len = utf8_sequence_length(&src[used]); + if ( len == 0 ) { + // Not valid UTF-8 from here on. Keep what is already known good. + break; + } + if ( (used + len) >= destsize ) { + break; + } + memcpy(&dest[used], &src[used], len); + used += len; + } + dest[used] = '\0'; +} + +/** + * @brief Record one key press, discarding it if the buffer is already full. * * Dropping the newest rather than overwriting the oldest is deliberate. A * caller reading a line of input wants the characters that were typed first; * overwriting would hand it the tail of what the user typed and silently lose * the head. + * + * The entry starts with no text. Whatever the press composes to arrives as a + * separate event and is attached by keybuffer_attach_text(). */ -static void keybuffer_push(SDL_Keycode key) +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; + keybuffer[tail].key = key; + keybuffer[tail].mod = mod; + keybuffer[tail].text[0] = '\0'; keybuffer_count += 1; + keybuffer_awaiting_text = true; +} + +/** + * @brief Give composed text to the press it belongs to, or buffer it alone. + * + * An entry with no keycode is not a degenerate case to be avoided: 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(), which has no keycode to give, discards it. + */ +static void keybuffer_attach_text(const char *text) +{ + int newest = 0; + + if ( (keybuffer_awaiting_text == true) && (keybuffer_count > 0) ) { + newest = (keybuffer_head + keybuffer_count - 1) % AKGL_CONTROLLER_KEY_BUFFER; + copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text); + keybuffer_awaiting_text = false; + return; + } + if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) { + return; + } + newest = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER; + keybuffer[newest].key = 0; + keybuffer[newest].mod = 0; + copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text); + keybuffer_count += 1; +} + +/** + * @brief Take the oldest entry, whatever it carries. + * + * @param dest Receives the entry. Required. + * @return `true` when an entry was taken, `false` when the buffer was empty. + */ +static bool keybuffer_take(akgl_Keystroke *dest) +{ + if ( keybuffer_count == 0 ) { + return false; + } + memcpy(dest, &keybuffer[keybuffer_head], sizeof(akgl_Keystroke)); + keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER; + keybuffer_count -= 1; + if ( keybuffer_count == 0 ) { + // Nothing is left for a text event to attach itself to. + keybuffer_awaiting_text = false; + } + return true; } akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void) @@ -102,7 +239,12 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even // key an interpreter polling with akgl_controller_poll_key() wants to see, // and the scan below returns as soon as a binding claims the event. if ( event->type == SDL_EVENT_KEY_DOWN ) { - keybuffer_push(event->key.key); + keybuffer_push_key(event->key.key, event->key.mod); + } else if ( event->type == SDL_EVENT_TEXT_INPUT ) { + // The character SDL composed for the press recorded a moment ago. A + // keycode cannot express it: the layout, the shift state, a compose key + // and a dead key all have their say between the two events. + keybuffer_attach_text(event->text.text); } ATTEMPT { @@ -481,22 +623,37 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available) { + akgl_Keystroke keystroke; + PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination"); FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination"); - if ( keybuffer_count == 0 ) { - // An empty buffer is the ordinary case, not a failure: the caller is - // asking whether a key is waiting, and the answer is no. - *keycode = 0; - *available = false; - SUCCEED_RETURN(errctx); + // An empty buffer is the ordinary case, not a failure: the caller is asking + // whether a key is waiting, and the answer is no. + *keycode = 0; + *available = false; + while ( keybuffer_take(&keystroke) == true ) { + if ( keystroke.key == 0 ) { + // Composed text with no key behind it. This form has no way to + // report it, so it is dropped rather than handed back as key 0. + continue; + } + *keycode = (int)keystroke.key; + *available = true; + break; } + SUCCEED_RETURN(errctx); +} - *keycode = (int)keybuffer[keybuffer_head]; - *available = true; - keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER; - keybuffer_count -= 1; +akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination"); + FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination"); + + memset(dest, 0x00, sizeof(akgl_Keystroke)); + *available = keybuffer_take(dest); SUCCEED_RETURN(errctx); } @@ -505,5 +662,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void) PREPARE_ERROR(errctx); keybuffer_head = 0; keybuffer_count = 0; + keybuffer_awaiting_text = false; SUCCEED_RETURN(errctx); } diff --git a/tests/controller.c b/tests/controller.c index bfd8186..af0fa64 100644 --- a/tests/controller.c +++ b/tests/controller.c @@ -88,6 +88,26 @@ static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which event->key.key = key; } +/** @brief Build a synthetic keyboard event carrying modifier state. */ +static void make_key_event_mod(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key, SDL_Keymod mod) +{ + make_key_event(event, type, which, key); + event->key.mod = mod; +} + +/** + * @brief Build the text input event SDL sends after a key press composes. + * + * @p text is not copied by SDL or by the library at this point, so callers pass + * a string literal. + */ +static void make_text_event(SDL_Event *event, const char *text) +{ + memset(event, 0x00, sizeof(SDL_Event)); + event->type = SDL_EVENT_TEXT_INPUT; + event->text.text = text; +} + /** @brief Build a synthetic gamepad button event. */ static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button) { @@ -682,6 +702,151 @@ akerr_ErrorContext *test_controller_poll_key_overflow(void) SUCCEED_RETURN(e); } +akerr_ErrorContext *test_controller_poll_keystroke(void) +{ + PREPARE_ERROR(errctx); + SDL_Event event; + akgl_Keystroke keystroke; + int keycode = -1; + bool available = true; + + ATTEMPT { + reset_control_maps(); + CATCH(errctx, akgl_controller_flush_keys()); + + // An empty buffer answers "nothing waiting" and zeroes the destination. + memset(&keystroke, 0xff, sizeof(akgl_Keystroke)); + TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), + "polling an empty keystroke buffer"); + TEST_ASSERT(errctx, available == false, + "polling an empty buffer reported a keystroke was available"); + TEST_ASSERT(errctx, keystroke.key == 0, + "polling an empty buffer left a keycode of %d behind", (int)keystroke.key); + TEST_ASSERT(errctx, keystroke.text[0] == '\0', + "polling an empty buffer left text behind"); + + // The case the whole thing exists for: a shifted key. The keycode is + // the unshifted one, so the composed character is the only place the + // double quote can come from. + make_key_event_mod(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_APOSTROPHE, SDL_KMOD_LSHIFT); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + make_text_event(&event, "\""); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + + TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), + "polling for a shifted key"); + TEST_ASSERT(errctx, available == true, "a shifted key press never reached the buffer"); + TEST_ASSERT(errctx, keystroke.key == SDLK_APOSTROPHE, + "the keystroke reported keycode %d, expected %d", + (int)keystroke.key, (int)SDLK_APOSTROPHE); + TEST_ASSERT(errctx, (keystroke.mod & SDL_KMOD_SHIFT) != 0, + "the keystroke lost its shift state (mod %d)", (int)keystroke.mod); + TEST_ASSERT(errctx, strcmp(keystroke.text, "\"") == 0, + "the keystroke composed to \"%s\", expected a double quote", keystroke.text); + + // A key that composes to nothing still arrives, with empty text -- that + // is how a line editor recognises Backspace and the arrows. + CATCH(errctx, akgl_controller_flush_keys()); + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), + "polling for a non-printing key"); + TEST_ASSERT(errctx, keystroke.key == SDLK_LEFT, + "a non-printing key reported keycode %d, expected %d", + (int)keystroke.key, (int)SDLK_LEFT); + TEST_ASSERT(errctx, keystroke.text[0] == '\0', + "a non-printing key composed to \"%s\", expected nothing", keystroke.text); + + // Text with no key press behind it -- an input method, or a character + // finished by a dead key -- is buffered on its own rather than dropped. + CATCH(errctx, akgl_controller_flush_keys()); + make_text_event(&event, "e"); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), + "polling for composed text with no key press"); + TEST_ASSERT(errctx, available == true, "composed text with no key press was dropped"); + TEST_ASSERT(errctx, keystroke.key == 0, + "text with no key press reported keycode %d, expected 0", (int)keystroke.key); + TEST_ASSERT(errctx, strcmp(keystroke.text, "e") == 0, + "text with no key press composed to \"%s\", expected \"e\"", keystroke.text); + TEST_ASSERT(errctx, keystroke.mod == 0, + "a text-only entry reported modifiers %d; the character it carries already " + "reflects them", (int)keystroke.mod); + + // The keycode form has nowhere to report that entry, so it discards it + // on the way past rather than handing back a keystroke with no key. + CATCH(errctx, akgl_controller_flush_keys()); + make_text_event(&event, "e"); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available), + "polling by keycode past a text-only entry"); + TEST_ASSERT(errctx, available == true, "the key behind a text-only entry was lost"); + TEST_ASSERT(errctx, keycode == SDLK_B, + "polling by keycode returned %d, expected %d", keycode, (int)SDLK_B); + + // Both pollers drain the same buffer. + CATCH(errctx, akgl_controller_flush_keys()); + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_C); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available)); + TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available), + "polling by keycode after the same key was taken as a keystroke"); + TEST_ASSERT(errctx, available == false, "one keystroke was delivered to both pollers"); + + // Text that arrives after its press has already been drained becomes an + // entry of its own instead of being attached to somebody else's key. + CATCH(errctx, akgl_controller_flush_keys()); + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_D); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available)); + make_text_event(&event, "d"); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), + "polling for text that outlived its key press"); + TEST_ASSERT(errctx, available == true, "text that outlived its key press was dropped"); + TEST_ASSERT(errctx, keystroke.key == 0, + "text that outlived its key press was attached to keycode %d", + (int)keystroke.key); + + // More text than an entry holds is cut on a character boundary, never + // through the middle of one. Each of these is two bytes, so three fit + // in the eight-byte field and the fourth does not. + CATCH(errctx, akgl_controller_flush_keys()); + make_text_event(&event, "\xce\xb1\xce\xb1\xce\xb1\xce\xb1"); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), + "polling for text longer than one entry holds"); + TEST_ASSERT(errctx, strcmp(keystroke.text, "\xce\xb1\xce\xb1\xce\xb1") == 0, + "oversized text was truncated to \"%s\", expected three whole characters", + keystroke.text); + + // A sequence the string ends in the middle of is dropped rather than + // copied by reading past the terminator to find its other half. + CATCH(errctx, akgl_controller_flush_keys()); + make_text_event(&event, "a\xce"); + CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); + TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), + "polling for text that ends mid-character"); + TEST_ASSERT(errctx, strcmp(keystroke.text, "a") == 0, + "a truncated character came back as \"%s\", expected just the whole one", + keystroke.text); + + TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, + akgl_controller_poll_keystroke(NULL, &available), + "polling into a NULL keystroke"); + TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, + akgl_controller_poll_keystroke(&keystroke, NULL), + "polling into a NULL availability flag"); + } CLEANUP { + reset_control_maps(); + IGNORE(akgl_controller_flush_keys()); + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + int main(void) { PREPARE_ERROR(errctx); @@ -711,6 +876,7 @@ int main(void) CATCH(errctx, test_controller_device_enumeration()); CATCH(errctx, test_controller_poll_key()); CATCH(errctx, test_controller_poll_key_overflow()); + CATCH(errctx, test_controller_poll_keystroke()); } CLEANUP { SDL_Quit(); } PROCESS(errctx) {