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:
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user