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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
194
src/controller.c
194
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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user