diff --git a/TODO.md b/TODO.md index ad86cc9..a38d746 100644 --- a/TODO.md +++ b/TODO.md @@ -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 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: - **No cursor movement within a line.** Backspace and escape only. The arrow keys stay in the diff --git a/src/frontend_akgl.c b/src/frontend_akgl.c index ac83d94..08e5178 100644 --- a/src/frontend_akgl.c +++ b/src/frontend_akgl.c @@ -77,6 +77,21 @@ akerr_ErrorContext *akbasic_frontend_akgl_init(akbasic_AkglFrontend *obj, const &obj->window, &obj->renderer->sdl_renderer), AKGL_ERR_SDL, "Couldn't create the window: %s", SDL_GetError()); 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() * 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; } if ( obj->window != NULL ) { + SDL_StopTextInput(obj->window); SDL_DestroyWindow(obj->window); obj->window = NULL; window = NULL; diff --git a/src/sink_akgl.c b/src/sink_akgl.c index cdebc90..ba4a813 100644 --- a/src/sink_akgl.c +++ b/src/sink_akgl.c @@ -13,6 +13,7 @@ * equivalent until then. */ +#include #include #include @@ -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 - * editing command here -- a script's own GET loop is what wants those. + * No composed text, but a printable keycode: type it anyway. + * + * **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. */ } diff --git a/tests/akgl_frontend.c b/tests/akgl_frontend.c index 66f73cf..5f95d99 100644 --- a/tests/akgl_frontend.c +++ b/tests/akgl_frontend.c @@ -402,6 +402,62 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_line_editor(void) 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. * @@ -478,6 +534,7 @@ int main(void) CATCH(errctx, test_pump_feeds_input()); CATCH(errctx, test_window_close_stops()); CATCH(errctx, test_line_editor()); + CATCH(errctx, test_text_input_is_started()); CATCH(errctx, test_repl_session()); } CLEANUP { stop_frontend();