Start SDL text input: the AKGL build's keyboard was dead

SDL3 has text input off by default and per-window, so without
SDL_StartTextInput() it emits no SDL_EVENT_TEXT_INPUT at all and every
keystroke reaches libakgl's ring with an empty text field. The editor had just
been changed to prefer that composed text over the keycode, so it read every
key as not-a-character: no echo in the window, and nothing on stdout either,
because RUN could never be typed. One cause, both symptoms.

The editor now falls back to the keycode when there is no composed text, so a
host that forgets to start text input gets a worse keyboard rather than none.

The suite missed this because every keyboard test pushes the text-input event
into SDL's queue by hand -- which is what a real keyboard produces, but only
once text input has been started. Synthesising the end of a chain cannot test
the beginning of it. The new test asserts SDL_TextInputActive() directly, and
both halves were checked by reverting each and watching it fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:23:07 -04:00
parent fca9ad4a89
commit 024a33dbba
4 changed files with 115 additions and 2 deletions

19
TODO.md
View File

@@ -568,6 +568,25 @@ which was the sharp end of the old limitation — it used to mean a program with
had to be passed on the command line. Letters are no longer folded to upper case, because there had to be passed on the command line. Letters are no longer folded to upper case, because there
is no longer any reason to. is no longer any reason to.
**The host has to turn text input on**, and forgetting to is how this broke once. SDL3 has it
off by default and per-window, so it is `akbasic_frontend_akgl_init()` that calls
`SDL_StartTextInput()``akgl/controller.h` says as much. Without it SDL emits no
`SDL_EVENT_TEXT_INPUT` at all, every keystroke reaches the ring with an empty `text`, and an
editor that reads that as "not a character" is silently dead: nothing echoes in the window, and
nothing reaches stdout either, because `RUN` can never be typed.
The editor therefore **falls back to the keycode** when a keystroke carries no composed text,
folded to upper case. A worse keyboard is a great deal better than no keyboard, and it means a
host that embeds these adaptors and forgets `SDL_StartTextInput()` gets a usable editor rather
than a dead one. Both halves are asserted in `tests/akgl_frontend.c`, and both were checked by
reverting each in turn and watching the test fail.
Worth knowing about the test suite that missed this: every other keyboard test pushes
`SDL_EVENT_TEXT_INPUT` into SDL's queue by hand, which is what a real keyboard produces — *once
text input has been started*. Synthesising the end of a chain cannot test the beginning of it.
The new test asserts `SDL_TextInputActive()` directly for that reason, and the whole frontend
suite has been run against a real X11 window as well as the dummy driver.
One limit remains, and it is a choice rather than a gap: One limit remains, and it is a choice rather than a gap:
- **No cursor movement within a line.** Backspace and escape only. The arrow keys stay in the - **No cursor movement within a line.** Backspace and escape only. The arrow keys stay in the

View File

@@ -77,6 +77,21 @@ akerr_ErrorContext *akbasic_frontend_akgl_init(akbasic_AkglFrontend *obj, const
&obj->window, &obj->renderer->sdl_renderer), &obj->window, &obj->renderer->sdl_renderer),
AKGL_ERR_SDL, "Couldn't create the window: %s", SDL_GetError()); AKGL_ERR_SDL, "Couldn't create the window: %s", SDL_GetError());
window = obj->window; window = obj->window;
/*
* Without this SDL sends no SDL_EVENT_TEXT_INPUT at all, every keystroke
* reaches the ring with an empty `text`, and the line editor has nothing to
* type -- akgl/controller.h says so directly. It is off by default in SDL3
* and is per-window, so it is the host's job and this is the host.
*
* Not fatal if it fails: the editor falls back to the keycode, which is a
* worse keyboard rather than no keyboard. Failing to start a window over an
* input method that is not there would be the wrong trade.
*/
if ( !SDL_StartTextInput(obj->window) ) {
SDL_Log("Could not start text input (%s); typing falls back to keycodes",
SDL_GetError());
}
/* /*
* Bind the 2D methods onto the renderer we just made. akgl_render_init2d() * Bind the 2D methods onto the renderer we just made. akgl_render_init2d()
* would do this too, but it creates its own window from the game properties * would do this too, but it creates its own window from the game properties
@@ -260,6 +275,7 @@ void akbasic_frontend_akgl_shutdown(akbasic_AkglFrontend *obj)
obj->renderer->sdl_renderer = NULL; obj->renderer->sdl_renderer = NULL;
} }
if ( obj->window != NULL ) { if ( obj->window != NULL ) {
SDL_StopTextInput(obj->window);
SDL_DestroyWindow(obj->window); SDL_DestroyWindow(obj->window);
obj->window = NULL; obj->window = NULL;
window = NULL; window = NULL;

View File

@@ -13,6 +13,7 @@
* equivalent until then. * equivalent until then.
*/ */
#include <ctype.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@@ -218,8 +219,28 @@ static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *s
} }
/* /*
* No composed text: a cursor key, a function key or a bare modifier. Not an * No composed text, but a printable keycode: type it anyway.
* editing command here -- a script's own GET loop is what wants those. *
* **This fallback is why the editor still works on a host that never called
* SDL_StartTextInput().** SDL emits no SDL_EVENT_TEXT_INPUT until text input
* is started, so without it every keystroke arrives here with an empty
* `text` -- and treating that as "not a character" makes the entire keyboard
* dead, silently, which is exactly what happened once. A worse keyboard is a
* great deal better than no keyboard.
*
* Upper case, because that is all a bare keycode can offer and it is what a
* C128 does. When text input *is* running a printable key always carries
* text, so this never fires and lower case survives.
*/
if ( key->key >= 0x20 && key->key < 0x7f ) {
edit_append(state, (char)toupper((unsigned char)key->key));
echo_line(state);
return;
}
/*
* Neither: a cursor key, a function key or a bare modifier. Not an editing
* command here -- a script's own GET loop is what wants those.
*/ */
} }

View File

@@ -402,6 +402,62 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_line_editor(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @brief The frontend starts SDL text input, and the editor survives it not being on.
*
* **This is the regression test for a bug the rest of this file could not catch.**
* Every other test here pushes SDL_EVENT_TEXT_INPUT into the queue by hand,
* which is what a real keyboard produces -- but only once text input has been
* *started*. SDL3 has it off by default and per-window, so a host that never
* calls SDL_StartTextInput() gets no such events at all, every keystroke reaches
* the ring with an empty `text`, and an editor that treats that as "not a
* character" is silently dead. That is exactly what shipped: no echo in the
* window, and nothing on stdout either, because RUN could never be typed.
*
* Two assertions, because there are two independent things wrong with that:
* the frontend must turn text input on, and the editor must not be helpless if
* it is ever off.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_text_input_is_started(void)
{
PREPARE_ERROR(errctx);
char line[64];
bool eof = true;
PASS(errctx, start_frontend(NULL));
/* The frontend's job: SDL will not emit a text-input event without this. */
TEST_REQUIRE(SDL_TextInputActive(FRONTEND.window),
"the frontend must start SDL text input, or nothing can be typed");
/*
* The editor's job: keystrokes carrying *no* composed text still type,
* folded to upper case, which is all a bare keycode can offer. push_key()
* sends the key-down alone, so this is precisely the shape the broken build
* saw for every single key.
*/
PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));
push_key('r');
push_key('u');
push_key('n');
push_key('\r');
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
TEST_REQUIRE(!eof, "a text-less line is still a line");
TEST_REQUIRE_STR(line, "RUN");
TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "RUN");
/* And a text-less key that is not printable is still not a character. */
push_key(SDLK_F1);
push_key(SDLK_RIGHT);
push_key('a');
push_key('\r');
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
TEST_REQUIRE_STR(line, "A");
stop_frontend();
SUCCEED_RETURN(errctx);
}
/** /**
* @brief A whole REPL session typed at the window, ending with QUIT. * @brief A whole REPL session typed at the window, ending with QUIT.
* *
@@ -478,6 +534,7 @@ int main(void)
CATCH(errctx, test_pump_feeds_input()); CATCH(errctx, test_pump_feeds_input());
CATCH(errctx, test_window_close_stops()); CATCH(errctx, test_window_close_stops());
CATCH(errctx, test_line_editor()); CATCH(errctx, test_line_editor());
CATCH(errctx, test_text_input_is_started());
CATCH(errctx, test_repl_session()); CATCH(errctx, test_repl_session());
} CLEANUP { } CLEANUP {
stop_frontend(); stop_frontend();