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:
@@ -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