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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user