Erase what the text layer drew: three GUI bugs, one cause

The sink painted glyphs on top of whatever was already on the renderer and
never erased anything, and the host deliberately does not clear the frame so a
DRAW survives. The grid was always correct; the screen kept the previous
frame. That surfaced as three separate-looking faults:

  - a backspaced character stayed on screen, including across a line wrap
  - a scroll left the old rows behind, "papered over with garbage"
  - the editing cursor smeared an underscore along every column it passed
    through, which read as an underline under the text being typed

render() now fills each row it is about to draw, and each row that has emptied
since it last drew, with the background first. Row by row rather than one
clear over the whole area: the text layer is authoritative over the rows it
occupies and leaves every other pixel alone, so a picture behind the text
loses only the strips the text uses instead of all of it.

echo_line() also truncates rows that hold nothing but the spaces its own erase
pass wrote, which is what backspacing back across a wrap leaves behind. A row
of spaces is text as far as render() is concerned, so it would have been
repainted forever and kept erasing whatever was under it.

The cursor glyph is removed outright, as asked. The smearing was the erase bug
rather than the cursor, so one could come back and behave -- a block would
read better than an underscore.

Four pixel-level tests, because grid assertions could never have caught any of
this. Each was checked by reverting the fix and watching it fail; two of them
were vacuous when first written and are noted as such where they are fixed.
The real-keyboard test also now rubs out a character and scrolls forty lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:03:37 -04:00
parent cfdde907df
commit acd20eed9a
5 changed files with 400 additions and 25 deletions

View File

@@ -20,6 +20,7 @@
#include <akerror.h>
#include <akgl/controller.h>
#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/text.h>
@@ -131,18 +132,38 @@ static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text
static void echo_line(akbasic_AkglSink *state)
{
int i = 0;
int erasedto = 0;
int endedat = 0;
state->cursorrow = state->editrow;
state->cursorcol = state->editcol;
for ( i = 0; i < state->echolen; i++ ) {
putchar_at(state, ' ');
}
erasedto = state->cursorrow;
state->cursorrow = state->editrow;
state->cursorcol = state->editcol;
for ( i = 0; i < state->editlen; i++ ) {
putchar_at(state, state->editline[i]);
}
endedat = state->cursorrow;
state->echolen = state->editlen;
/*
* A line that got shorter can leave whole rows holding nothing but the
* spaces the erase pass just wrote -- which is what backspacing back across
* a wrap does to the row below. Truncate them, so such a row reports itself
* *empty* rather than merely blank-looking.
*
* That distinction is load-bearing rather than tidiness: render() decides
* what to erase and redraw from whether a row has text, and a row of spaces
* is text. Left alone it would be repainted forever, and anything the
* program had drawn underneath it would stay erased.
*/
for ( i = endedat + 1; i <= erasedto && i < SINK_MAX_ROWS; i++ ) {
state->text[i][0] = '\0';
}
}
/** @brief Append one byte to the line being typed, if there is room for it. */
@@ -395,6 +416,11 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi
state->color.g = 0xff;
state->color.b = 0xff;
state->color.a = 0xff;
/* Opaque, not transparent: the text layer owns the rows it draws. */
state->background.r = 0x00;
state->background.g = 0x00;
state->background.b = 0x00;
state->background.a = 0xff;
state->x = 0;
state->y = 0;
state->width = w;
@@ -431,6 +457,24 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
"akgl sink has no state");
/*
* **Erase each row before drawing it.** Without this the sink paints glyphs
* on top of whatever the previous frame left, and the host deliberately does
* not clear the frame either -- §5 deviation 26, so that a DRAW survives
* past the frame it was issued in. The grid was always right and the screen
* kept the last frame, which showed up as three separate-looking bugs: a
* backspaced character that would not go away, a scroll that appeared to
* overwrite the screen with garbage, and the editing cursor smearing an
* underscore along every column it passed through.
*
* Row by row rather than one clear over the whole text area, and that is the
* deliberate part: the text layer is authoritative over the rows it occupies
* and leaves every other pixel alone. A program that draws a picture and
* prints one line loses one line's strip of it, the way a C128's text plane
* sits over its bitmap -- rather than losing the picture.
*
* `drawn` is what makes a row that has *emptied* get erased. Once it is
* cleared there is nothing left in the grid to say it ever held anything.
*
* A loop, so no CATCH and no _BREAK macros: they expand to a C break, which
* would escape the loop with an error still pending. PASS only.
*
@@ -439,7 +483,20 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
* put characters somewhere the cursor does not think they are.
*/
for ( row = 0; row < state->rows; row++ ) {
if ( state->text[row][0] == '\0' ) {
bool hastext = (state->text[row][0] != '\0');
SDL_FRect cells;
if ( !hastext && !state->drawn[row] ) {
continue;
}
cells.x = (float)state->x;
cells.y = (float)(state->y + (row * state->cellh));
cells.w = (float)state->width;
cells.h = (float)state->cellh;
PASS(errctx, akgl_draw_filled_rect(state->renderer, &cells, state->background));
state->drawn[row] = hastext;
if ( !hastext ) {
continue;
}
PASS(errctx, akgl_text_rendertextat(state->font, state->text[row],
@@ -449,18 +506,13 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
}
/*
* The cursor, drawn only while a line is being typed. The reference draws it
* the same way, as a literal underscore glyph (drawCursor,
* basicruntime_graphics.go:33), rather than as a filled rectangle -- which
* means it needs no draw primitive and cannot be a different shape from the
* text it sits in.
* No cursor glyph. The reference draws one as a literal underscore
* (drawCursor, basicruntime_graphics.go:33) and this did too, until it turned
* out to sit *under* the text being typed and make it hard to read. Removed
* on request; §5 records it, and the smearing that made it unbearable is
* fixed by the erase above rather than by the removal -- so a cursor could
* come back and behave, if one is wanted.
*/
if ( state->editing ) {
PASS(errctx, akgl_text_rendertextat(state->font, "_",
state->color, 0,
state->x + (state->cursorcol * state->cellw),
state->y + (state->cursorrow * state->cellh)));
}
SUCCEED_RETURN(errctx);
}