Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
/**
|
|
|
|
|
* @file sink_akgl.c
|
|
|
|
|
* @brief The libakgl-backed text sink: where PRINT goes when a host is drawing.
|
|
|
|
|
*
|
|
|
|
|
* Implements the section 1.5 vtable over akgl_text_measure() and
|
|
|
|
|
* akgl_text_rendertextat(). Owns the cursor, the wrap and the scroll --
|
|
|
|
|
* everything in the reference's basicruntime_graphics.go except Write and
|
|
|
|
|
* Println, which are the sink interface itself.
|
|
|
|
|
*
|
|
|
|
|
* The measurement call is new in libakgl 42b60f7 and is the reason this file can
|
|
|
|
|
* exist at all: the reference derives its character grid from
|
|
|
|
|
* font.SizeUTF8("A") (basicruntime.go:96) and there was no akgl_text_*
|
|
|
|
|
* equivalent until then.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-07-31 14:23:07 -04:00
|
|
|
#include <ctype.h>
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
|
2026-07-31 11:21:39 -04:00
|
|
|
#include <akgl/controller.h>
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
#include <akgl/draw.h>
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
#include <akgl/error.h>
|
|
|
|
|
#include <akgl/text.h>
|
|
|
|
|
|
|
|
|
|
#include <akbasic/akgl.h>
|
|
|
|
|
#include <akbasic/error.h>
|
|
|
|
|
|
|
|
|
|
/** @brief Rows of scrollback the sink keeps. Matches the `text` array in akgl.h. */
|
|
|
|
|
#define SINK_MAX_ROWS 64
|
|
|
|
|
/** @brief Bytes per row, including the terminator. */
|
|
|
|
|
#define SINK_MAX_COLUMNS 256
|
|
|
|
|
|
|
|
|
|
/** @brief Scroll the grid up by one row, dropping the top one. */
|
|
|
|
|
static void scroll(akbasic_AkglSink *state)
|
|
|
|
|
{
|
|
|
|
|
int row = 0;
|
|
|
|
|
|
|
|
|
|
for ( row = 0; row < SINK_MAX_ROWS - 1; row++ ) {
|
|
|
|
|
memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS);
|
|
|
|
|
}
|
|
|
|
|
memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS);
|
|
|
|
|
if ( state->cursorrow > 0 ) {
|
|
|
|
|
state->cursorrow -= 1;
|
|
|
|
|
}
|
2026-07-31 11:21:39 -04:00
|
|
|
/*
|
|
|
|
|
* A line being typed is anchored to a row, and that row just moved. Follow
|
|
|
|
|
* it, or a backspace erases somebody else's text. Clamped at zero: a line
|
|
|
|
|
* long enough to scroll its own start off the top redraws from the top row,
|
|
|
|
|
* which is cosmetically wrong and is the only place this can be seen.
|
|
|
|
|
*/
|
|
|
|
|
if ( state->editing ) {
|
|
|
|
|
state->editrow -= 1;
|
|
|
|
|
if ( state->editrow < 0 ) {
|
|
|
|
|
state->editrow = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @brief Move to the start of the next row, scrolling if that runs off the end. */
|
|
|
|
|
static void newline(akbasic_AkglSink *state)
|
|
|
|
|
{
|
|
|
|
|
state->cursorcol = 0;
|
|
|
|
|
state->cursorrow += 1;
|
|
|
|
|
if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) {
|
|
|
|
|
scroll(state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Put one character at the cursor, wrapping and scrolling as needed.
|
|
|
|
|
*
|
|
|
|
|
* Wrapping happens here, on the character grid, rather than being left to
|
|
|
|
|
* SDL_ttf's own wraplength. The cursor has to end up somewhere definite: a
|
|
|
|
|
* program that PRINTs a long string and then PRINTs again expects the second one
|
|
|
|
|
* to start on the row after the first one ended, and only the code that placed
|
|
|
|
|
* the characters knows which row that is.
|
|
|
|
|
*/
|
|
|
|
|
static void putchar_at(akbasic_AkglSink *state, char c)
|
|
|
|
|
{
|
|
|
|
|
if ( c == '\n' ) {
|
|
|
|
|
newline(state);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
|
|
|
|
|
newline(state);
|
|
|
|
|
}
|
|
|
|
|
state->text[state->cursorrow][state->cursorcol] = c;
|
|
|
|
|
state->cursorcol += 1;
|
|
|
|
|
state->text[state->cursorrow][state->cursorcol] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglSink *state = NULL;
|
|
|
|
|
size_t i = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in akgl sink write");
|
|
|
|
|
state = (akbasic_AkglSink *)self->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"akgl sink has no state");
|
|
|
|
|
for ( i = 0; text[i] != '\0'; i++ ) {
|
|
|
|
|
putchar_at(state, text[i]);
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglSink *state = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, sink_write(self, text));
|
|
|
|
|
state = (akbasic_AkglSink *)self->self;
|
|
|
|
|
newline(state);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 11:21:39 -04:00
|
|
|
/**
|
|
|
|
|
* @brief Redraw the line being typed, erasing whatever a longer one left behind.
|
|
|
|
|
*
|
|
|
|
|
* Two passes over the grid rather than one clever one. The first blanks what is
|
|
|
|
|
* already drawn and the second draws the current text, which leaves the cursor
|
|
|
|
|
* exactly where the text ends without anybody having to reproduce putchar_at's
|
|
|
|
|
* wrapping arithmetic a second time. Nothing is rendered here -- the grid is
|
|
|
|
|
* memory, and the host draws it when it draws its frame -- so the cost is a
|
|
|
|
|
* couple of memcpy-sized loops per keystroke.
|
|
|
|
|
*/
|
|
|
|
|
static void echo_line(akbasic_AkglSink *state)
|
|
|
|
|
{
|
|
|
|
|
int i = 0;
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
int erasedto = 0;
|
|
|
|
|
int endedat = 0;
|
2026-07-31 11:21:39 -04:00
|
|
|
|
|
|
|
|
state->cursorrow = state->editrow;
|
|
|
|
|
state->cursorcol = state->editcol;
|
|
|
|
|
for ( i = 0; i < state->echolen; i++ ) {
|
|
|
|
|
putchar_at(state, ' ');
|
|
|
|
|
}
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
erasedto = state->cursorrow;
|
|
|
|
|
|
2026-07-31 11:21:39 -04:00
|
|
|
state->cursorrow = state->editrow;
|
|
|
|
|
state->cursorcol = state->editcol;
|
|
|
|
|
for ( i = 0; i < state->editlen; i++ ) {
|
|
|
|
|
putchar_at(state, state->editline[i]);
|
|
|
|
|
}
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
endedat = state->cursorrow;
|
2026-07-31 11:21:39 -04:00
|
|
|
state->echolen = state->editlen;
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* 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';
|
|
|
|
|
}
|
2026-07-31 11:21:39 -04:00
|
|
|
}
|
|
|
|
|
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
/** @brief Append one byte to the line being typed, if there is room for it. */
|
|
|
|
|
static void edit_append(akbasic_AkglSink *state, char c)
|
|
|
|
|
{
|
|
|
|
|
if ( state->editlen >= (int)sizeof(state->editline) - 1 ) {
|
|
|
|
|
/* Full. Dropped silently, exactly as a C128's 80-character limit does. */
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
state->editline[state->editlen] = c;
|
|
|
|
|
state->editlen += 1;
|
|
|
|
|
state->editline[state->editlen] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 11:21:39 -04:00
|
|
|
/**
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
* @brief Fold one keystroke into the line being typed.
|
|
|
|
|
*
|
|
|
|
|
* **The composed text is preferred over the keycode wherever there is any**, and
|
|
|
|
|
* that is the whole reason libakgl 0.3.0's akgl_Keystroke exists. A keycode
|
|
|
|
|
* cannot express a shifted character, a keyboard layout, a compose key or a dead
|
|
|
|
|
* key; SDL has already worked all of that out by the time the ring sees it, and
|
|
|
|
|
* `text` is the answer. Before 0.3.0 this took a bare keycode, folded letters to
|
|
|
|
|
* upper case, and could not type a double quote -- which meant a BASIC string
|
|
|
|
|
* literal could not be typed at the window at all.
|
2026-07-31 11:21:39 -04:00
|
|
|
*
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
* The keycode is still what identifies the editing keys, because Return,
|
|
|
|
|
* Backspace and Escape are keys rather than characters and several of them
|
|
|
|
|
* compose to text SDL would otherwise hand straight through.
|
2026-07-31 11:21:39 -04:00
|
|
|
*/
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted)
|
2026-07-31 11:21:39 -04:00
|
|
|
{
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
size_t i = 0;
|
|
|
|
|
|
|
|
|
|
if ( key->key == SDLK_RETURN || key->key == SDLK_KP_ENTER ||
|
|
|
|
|
key->key == '\r' || key->key == '\n' ) {
|
2026-07-31 11:21:39 -04:00
|
|
|
*submitted = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
if ( key->key == SDLK_BACKSPACE || key->key == '\b' || key->key == 0x7f ) {
|
2026-07-31 11:21:39 -04:00
|
|
|
if ( state->editlen > 0 ) {
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
/*
|
|
|
|
|
* One *byte* at a time, which is wrong for a multi-byte character and
|
|
|
|
|
* is left that way deliberately: every character a BASIC program can
|
|
|
|
|
* hold is one byte (section 1.2's inline string), so a multi-byte
|
|
|
|
|
* character cannot survive being submitted anyway. Erasing what was
|
|
|
|
|
* accepted is consistent with that.
|
|
|
|
|
*/
|
2026-07-31 11:21:39 -04:00
|
|
|
state->editlen -= 1;
|
|
|
|
|
state->editline[state->editlen] = '\0';
|
|
|
|
|
echo_line(state);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
if ( key->key == SDLK_ESCAPE || key->key == 0x1b ) {
|
2026-07-31 11:21:39 -04:00
|
|
|
state->editlen = 0;
|
|
|
|
|
state->editline[0] = '\0';
|
|
|
|
|
echo_line(state);
|
|
|
|
|
return;
|
|
|
|
|
}
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
|
|
|
|
|
if ( key->text[0] != '\0' ) {
|
|
|
|
|
for ( i = 0; key->text[i] != '\0' && i < sizeof(key->text); i++ ) {
|
|
|
|
|
/*
|
|
|
|
|
* Printable ASCII only. The grid is a byte per cell and a value's
|
|
|
|
|
* string is a fixed 256 bytes, so a multi-byte character has nowhere
|
|
|
|
|
* to go -- dropping it is honest where storing half of it is not.
|
|
|
|
|
*/
|
|
|
|
|
if ( (unsigned char)key->text[i] >= 0x20 && (unsigned char)key->text[i] < 0x7f ) {
|
|
|
|
|
edit_append(state, key->text[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
echo_line(state);
|
2026-07-31 11:21:39 -04:00
|
|
|
return;
|
|
|
|
|
}
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
|
|
|
|
|
/*
|
2026-07-31 14:23:07 -04:00
|
|
|
* 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.
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
*/
|
2026-07-31 11:21:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Collect keystrokes until a line is submitted or the host stops.
|
|
|
|
|
*
|
|
|
|
|
* Its own function because it is a loop: CATCH and the _BREAK macros expand to a
|
|
|
|
|
* C break, which inside a loop would escape only the loop and leave the rest of
|
|
|
|
|
* an ATTEMPT running with an error pending. PASS only in here, and the caller
|
|
|
|
|
* wraps this one call in the ATTEMPT that owns the cleanup.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *edit_loop(akbasic_AkglSink *state, bool *eof)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
akgl_Keystroke key;
|
2026-07-31 11:21:39 -04:00
|
|
|
bool submitted = false;
|
|
|
|
|
bool available = false;
|
|
|
|
|
bool running = true;
|
|
|
|
|
|
|
|
|
|
while ( !submitted ) {
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
/*
|
|
|
|
|
* poll_keystroke rather than poll_key: the same ring, with the modifier
|
|
|
|
|
* state and the composed text still attached. poll_key is the reduced
|
|
|
|
|
* view, and it is what GET and GETKEY still use -- a script asking "was
|
|
|
|
|
* the up arrow pressed" wants a keycode, not the empty string.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akgl_controller_poll_keystroke(&key, &available));
|
2026-07-31 11:21:39 -04:00
|
|
|
if ( available ) {
|
Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go
with them: the CMake block that declared libakgl's vendored dependencies by
hand, the akgl/actor.h include in three files, and the six vtable pointers
assigned by hand in two more, now akgl_render_bind2d().
Two gaps were capabilities rather than inconveniences, and both are now real:
The line editor takes the composed UTF-8 text the ring carries in preference
to the keycode, so shifted characters, keyboard layouts, compose keys and dead
keys all work. A double quote can be typed, which means a BASIC string literal
can be typed -- the sharp end of the old limitation. Letters are no longer
folded to upper case.
SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3
sweeps once rather than oscillating and TODO.md section 5 says so. A backend
with no sweep still refuses the swept note and plays the held one.
The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by
temporarily demanding 0.4.0 and watching it fire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:25:14 -04:00
|
|
|
edit_key(state, &key, &submitted);
|
2026-07-31 11:21:39 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* Nothing waiting: hand the frame back to the host, which pumps the
|
|
|
|
|
* events that fill the ring this loop is reading. Skipping the pump
|
|
|
|
|
* while keys are available is what keeps a paste or a fast typist from
|
|
|
|
|
* costing one frame per character.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, state->pump(state->pumpself, &running));
|
|
|
|
|
if ( !running ) {
|
|
|
|
|
*eof = true;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
2026-07-31 11:21:39 -04:00
|
|
|
akbasic_AkglSink *state = NULL;
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in akgl sink readline");
|
2026-07-31 11:21:39 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (len > 1), AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"Read buffer of %zu bytes is too small", len);
|
|
|
|
|
state = (akbasic_AkglSink *)self->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"akgl sink has no state");
|
|
|
|
|
|
|
|
|
|
*eof = false;
|
|
|
|
|
dest[0] = '\0';
|
|
|
|
|
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
/*
|
2026-07-31 11:21:39 -04:00
|
|
|
* No pump means no host loop to borrow, and a sink that sat on the keyboard
|
|
|
|
|
* without one would deadlock the process on its first INPUT. Report end of
|
|
|
|
|
* input instead -- the contract sink.h states, and what INPUT already
|
|
|
|
|
* handles.
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
*/
|
2026-07-31 11:21:39 -04:00
|
|
|
if ( state->pump == NULL ) {
|
|
|
|
|
*eof = true;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state->editing = true;
|
|
|
|
|
state->editrow = state->cursorrow;
|
|
|
|
|
state->editcol = state->cursorcol;
|
|
|
|
|
state->editlen = 0;
|
|
|
|
|
state->echolen = 0;
|
|
|
|
|
state->editline[0] = '\0';
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, edit_loop(state, eof));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
/* Whatever happened, stop drawing a cursor over a line nobody is typing. */
|
|
|
|
|
state->editing = false;
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
if ( *eof ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
}
|
2026-07-31 11:21:39 -04:00
|
|
|
strncpy(dest, state->editline, len - 1);
|
|
|
|
|
dest[len - 1] = '\0';
|
|
|
|
|
newline(state);
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *sink_clear(akbasic_TextSink *self)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglSink *state = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL sink in akgl sink clear");
|
|
|
|
|
state = (akbasic_AkglSink *)self->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"akgl sink has no state");
|
|
|
|
|
memset(state->text, 0, sizeof(state->text));
|
|
|
|
|
state->cursorcol = 0;
|
|
|
|
|
state->cursorrow = 0;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
int cellw = 0;
|
|
|
|
|
int cellh = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in sink_init_akgl");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL renderer in sink_init_akgl: the host creates it, not the sink");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (font != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL font in sink_init_akgl");
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Before anything else in libakgl. akgl_game_init() would have done it, but
|
|
|
|
|
* an embedded interpreter drives subsystems directly and never calls that --
|
|
|
|
|
* and a code raised before this runs carries no name into its stack trace.
|
|
|
|
|
* Idempotent, so a host that already called it loses nothing.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akgl_error_init());
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The character grid, measured rather than assumed. Direct equivalent of the
|
|
|
|
|
* reference's font.SizeUTF8("A"), and it needs no renderer -- which is why
|
|
|
|
|
* the sink can size itself before a frame has ever been drawn.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akgl_text_measure(font, "A", &cellw, &cellh));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (cellw > 0 && cellh > 0), AKBASIC_ERR_VALUE,
|
|
|
|
|
"Font measures a %dx%d character cell, which cannot be a grid",
|
|
|
|
|
cellw, cellh);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (w >= cellw && h >= cellh), AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"A %dx%d text area has no room for a %dx%d character",
|
|
|
|
|
w, h, cellw, cellh);
|
|
|
|
|
|
|
|
|
|
memset(state, 0, sizeof(*state));
|
|
|
|
|
state->renderer = renderer;
|
|
|
|
|
state->font = font;
|
|
|
|
|
state->color.r = 0xff;
|
|
|
|
|
state->color.g = 0xff;
|
|
|
|
|
state->color.b = 0xff;
|
|
|
|
|
state->color.a = 0xff;
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
/* Opaque, not transparent: the text layer owns the rows it draws. */
|
Own every pixel each frame, and bring the cursor back as a blinking block
The blinking backspace was not the backspace. The sink skipped rows it
believed were already clear, tracked in a drawn[] array, which assumes a frame
inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame
inherits the one two back: a row erased once is clean in one buffer and dirty
in the other, and presenting alternates between them. The first character of a
backspaced wrap flickered forever.
It reproduces on X11 and never under the dummy driver or a software renderer,
which is why the suite stayed green through two rounds of fixing it. The sink
now repaints every row it owns every frame -- a frame either owns every pixel
it presents or inherits pixels it cannot reason about.
Confirmed on real hardware with the reported input: the wrapped tail's row
reads zero across five successive captures.
The cursor returns as a blinking block, half a second per cycle, off SDL's
clock. It sits in the cell after the text rather than under it, which is what
made the underscore unreadable, and it wraps to the next row when a line
exactly fills one -- where the next character actually lands.
The new regression test paints stale pixels by hand, which is what a swapped-in
buffer hands back, so the case is covered without real hardware. Every new
assertion was checked by reverting the fix and watching it fail.
Recorded honestly in TODO.md section 5: the same buffer swap means the
graphics verbs were never reliable across frames either. A one-shot DRAW lands
in one buffer and the next present shows the other. Making that work needs a
persistent surface, which is its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
|
|
|
state->cursorperiodms = AKBASIC_SINK_CURSOR_BLINK_MS;
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
state->background.r = 0x00;
|
|
|
|
|
state->background.g = 0x00;
|
|
|
|
|
state->background.b = 0x00;
|
|
|
|
|
state->background.a = 0xff;
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
state->x = 0;
|
|
|
|
|
state->y = 0;
|
|
|
|
|
state->width = w;
|
|
|
|
|
state->height = h;
|
|
|
|
|
state->cellw = cellw;
|
|
|
|
|
state->cellh = cellh;
|
|
|
|
|
state->columns = w / cellw;
|
|
|
|
|
state->rows = h / cellh;
|
|
|
|
|
if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
|
|
|
|
|
state->columns = SINK_MAX_COLUMNS - 1;
|
|
|
|
|
}
|
|
|
|
|
if ( state->rows > SINK_MAX_ROWS ) {
|
|
|
|
|
state->rows = SINK_MAX_ROWS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj->self = state;
|
|
|
|
|
obj->write = sink_write;
|
|
|
|
|
obj->writeln = sink_writeln;
|
|
|
|
|
obj->readline = sink_readline;
|
|
|
|
|
obj->clear = sink_clear;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglSink *state = NULL;
|
|
|
|
|
int row = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL sink in sink_akgl_render");
|
|
|
|
|
state = (akbasic_AkglSink *)obj->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"akgl sink has no state");
|
|
|
|
|
|
|
|
|
|
/*
|
Own every pixel each frame, and bring the cursor back as a blinking block
The blinking backspace was not the backspace. The sink skipped rows it
believed were already clear, tracked in a drawn[] array, which assumes a frame
inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame
inherits the one two back: a row erased once is clean in one buffer and dirty
in the other, and presenting alternates between them. The first character of a
backspaced wrap flickered forever.
It reproduces on X11 and never under the dummy driver or a software renderer,
which is why the suite stayed green through two rounds of fixing it. The sink
now repaints every row it owns every frame -- a frame either owns every pixel
it presents or inherits pixels it cannot reason about.
Confirmed on real hardware with the reported input: the wrapped tail's row
reads zero across five successive captures.
The cursor returns as a blinking block, half a second per cycle, off SDL's
clock. It sits in the cell after the text rather than under it, which is what
made the underscore unreadable, and it wraps to the next row when a line
exactly fills one -- where the next character actually lands.
The new regression test paints stale pixels by hand, which is what a swapped-in
buffer hands back, so the case is covered without real hardware. Every new
assertion was checked by reverting the fix and watching it fail.
Recorded honestly in TODO.md section 5: the same buffer swap means the
graphics verbs were never reliable across frames either. A one-shot DRAW lands
in one buffer and the next present shows the other. Making that work needs a
persistent surface, which is its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
|
|
|
* **Repaint every row of the text area, every frame.** Not just the rows
|
|
|
|
|
* that changed -- that was tried, with a `drawn[]` array marking which rows
|
|
|
|
|
* had carried glyphs, and it is wrong on real hardware.
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
*
|
Own every pixel each frame, and bring the cursor back as a blinking block
The blinking backspace was not the backspace. The sink skipped rows it
believed were already clear, tracked in a drawn[] array, which assumes a frame
inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame
inherits the one two back: a row erased once is clean in one buffer and dirty
in the other, and presenting alternates between them. The first character of a
backspaced wrap flickered forever.
It reproduces on X11 and never under the dummy driver or a software renderer,
which is why the suite stayed green through two rounds of fixing it. The sink
now repaints every row it owns every frame -- a frame either owns every pixel
it presents or inherits pixels it cannot reason about.
Confirmed on real hardware with the reported input: the wrapped tail's row
reads zero across five successive captures.
The cursor returns as a blinking block, half a second per cycle, off SDL's
clock. It sits in the cell after the text rather than under it, which is what
made the underscore unreadable, and it wraps to the next row when a line
exactly fills one -- where the next character actually lands.
The new regression test paints stale pixels by hand, which is what a swapped-in
buffer hands back, so the case is covered without real hardware. Every new
assertion was checked by reverting the fix and watching it fail.
Recorded honestly in TODO.md section 5: the same buffer swap means the
graphics verbs were never reliable across frames either. A one-shot DRAW lands
in one buffer and the next present shows the other. Making that work needs a
persistent surface, which is its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
|
|
|
* SDL_RenderPresent swaps buffers. What a frame inherits is not the frame
|
|
|
|
|
* before it, it is the frame *two* before it, and on some backends it is
|
|
|
|
|
* undefined. So a row erased once is erased in one buffer and still dirty in
|
|
|
|
|
* the other, and presenting alternates between them: the row blinks in and
|
|
|
|
|
* out forever. That is exactly what a backspace across a line wrap looked
|
|
|
|
|
* like -- the first character of the wrapped tail flickering after the rest
|
|
|
|
|
* had gone. It reproduces on X11 and never under the dummy driver or a
|
|
|
|
|
* software renderer, which is why the suite was green.
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
*
|
Own every pixel each frame, and bring the cursor back as a blinking block
The blinking backspace was not the backspace. The sink skipped rows it
believed were already clear, tracked in a drawn[] array, which assumes a frame
inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame
inherits the one two back: a row erased once is clean in one buffer and dirty
in the other, and presenting alternates between them. The first character of a
backspaced wrap flickered forever.
It reproduces on X11 and never under the dummy driver or a software renderer,
which is why the suite stayed green through two rounds of fixing it. The sink
now repaints every row it owns every frame -- a frame either owns every pixel
it presents or inherits pixels it cannot reason about.
Confirmed on real hardware with the reported input: the wrapped tail's row
reads zero across five successive captures.
The cursor returns as a blinking block, half a second per cycle, off SDL's
clock. It sits in the cell after the text rather than under it, which is what
made the underscore unreadable, and it wraps to the next row when a line
exactly fills one -- where the next character actually lands.
The new regression test paints stale pixels by hand, which is what a swapped-in
buffer hands back, so the case is covered without real hardware. Every new
assertion was checked by reverting the fix and watching it fail.
Recorded honestly in TODO.md section 5: the same buffer swap means the
graphics verbs were never reliable across frames either. A one-shot DRAW lands
in one buffer and the next present shows the other. Making that work needs a
persistent surface, which is its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
|
|
|
* There is no cheaper correct answer available here. A frame either owns
|
|
|
|
|
* every pixel it presents or it inherits pixels it cannot reason about.
|
|
|
|
|
*
|
|
|
|
|
* Row by row rather than one fill over the whole area, still: the two are
|
|
|
|
|
* equivalent in cost here and the row loop keeps the text layer's ownership
|
|
|
|
|
* of exactly `rows * cellh` pixels obvious, which matters when the area is
|
|
|
|
|
* smaller than the window.
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
*
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
* 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.
|
|
|
|
|
*
|
|
|
|
|
* Wrapping is off at the draw -- a zero wraplength -- because the grid above
|
|
|
|
|
* already decided where every line ends. Letting SDL_ttf wrap again would
|
|
|
|
|
* put characters somewhere the cursor does not think they are.
|
|
|
|
|
*/
|
|
|
|
|
for ( row = 0; row < state->rows; row++ ) {
|
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>
2026-07-31 15:03:37 -04:00
|
|
|
SDL_FRect cells;
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
Own every pixel each frame, and bring the cursor back as a blinking block
The blinking backspace was not the backspace. The sink skipped rows it
believed were already clear, tracked in a drawn[] array, which assumes a frame
inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame
inherits the one two back: a row erased once is clean in one buffer and dirty
in the other, and presenting alternates between them. The first character of a
backspaced wrap flickered forever.
It reproduces on X11 and never under the dummy driver or a software renderer,
which is why the suite stayed green through two rounds of fixing it. The sink
now repaints every row it owns every frame -- a frame either owns every pixel
it presents or inherits pixels it cannot reason about.
Confirmed on real hardware with the reported input: the wrapped tail's row
reads zero across five successive captures.
The cursor returns as a blinking block, half a second per cycle, off SDL's
clock. It sits in the cell after the text rather than under it, which is what
made the underscore unreadable, and it wraps to the next row when a line
exactly fills one -- where the next character actually lands.
The new regression test paints stale pixels by hand, which is what a swapped-in
buffer hands back, so the case is covered without real hardware. Every new
assertion was checked by reverting the fix and watching it fail.
Recorded honestly in TODO.md section 5: the same buffer swap means the
graphics verbs were never reliable across frames either. A one-shot DRAW lands
in one buffer and the next present shows the other. Making that work needs a
persistent surface, which is its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
|
|
|
if ( state->text[row][0] == '\0' ) {
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akgl_text_rendertextat(state->font, state->text[row],
|
|
|
|
|
state->color, 0,
|
|
|
|
|
state->x,
|
|
|
|
|
state->y + (row * state->cellh)));
|
|
|
|
|
}
|
2026-07-31 11:21:39 -04:00
|
|
|
|
|
|
|
|
/*
|
Own every pixel each frame, and bring the cursor back as a blinking block
The blinking backspace was not the backspace. The sink skipped rows it
believed were already clear, tracked in a drawn[] array, which assumes a frame
inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame
inherits the one two back: a row erased once is clean in one buffer and dirty
in the other, and presenting alternates between them. The first character of a
backspaced wrap flickered forever.
It reproduces on X11 and never under the dummy driver or a software renderer,
which is why the suite stayed green through two rounds of fixing it. The sink
now repaints every row it owns every frame -- a frame either owns every pixel
it presents or inherits pixels it cannot reason about.
Confirmed on real hardware with the reported input: the wrapped tail's row
reads zero across five successive captures.
The cursor returns as a blinking block, half a second per cycle, off SDL's
clock. It sits in the cell after the text rather than under it, which is what
made the underscore unreadable, and it wraps to the next row when a line
exactly fills one -- where the next character actually lands.
The new regression test paints stale pixels by hand, which is what a swapped-in
buffer hands back, so the case is covered without real hardware. Every new
assertion was checked by reverting the fix and watching it fail.
Recorded honestly in TODO.md section 5: the same buffer swap means the
graphics verbs were never reliable across frames either. A one-shot DRAW lands
in one buffer and the next present shows the other. Making that work needs a
persistent surface, which is its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
|
|
|
* The cursor: a filled block at the cursor cell, blinking.
|
|
|
|
|
*
|
|
|
|
|
* A block rather than the reference's underscore glyph (drawCursor,
|
|
|
|
|
* basicruntime_graphics.go:33). The underscore sat *under* the text being
|
|
|
|
|
* typed and made it hard to read, which is what got it removed; a block
|
|
|
|
|
* occupies the cell after the text instead of the space beneath it.
|
|
|
|
|
*
|
|
|
|
|
* The clock is SDL's rather than the host's. Everywhere else in this
|
|
|
|
|
* interpreter the host owns the clock, because the library owns no loop and
|
|
|
|
|
* must not block -- but this is an adaptor that already links SDL, the
|
|
|
|
|
* blink is cosmetic, and threading a timestamp through the sink interface to
|
|
|
|
|
* animate a cursor would be a poor trade. `cursorperiodms` of zero holds it
|
|
|
|
|
* solid, which is what makes a frame deterministic for a test.
|
2026-07-31 11:21:39 -04:00
|
|
|
*/
|
Own every pixel each frame, and bring the cursor back as a blinking block
The blinking backspace was not the backspace. The sink skipped rows it
believed were already clear, tracked in a drawn[] array, which assumes a frame
inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame
inherits the one two back: a row erased once is clean in one buffer and dirty
in the other, and presenting alternates between them. The first character of a
backspaced wrap flickered forever.
It reproduces on X11 and never under the dummy driver or a software renderer,
which is why the suite stayed green through two rounds of fixing it. The sink
now repaints every row it owns every frame -- a frame either owns every pixel
it presents or inherits pixels it cannot reason about.
Confirmed on real hardware with the reported input: the wrapped tail's row
reads zero across five successive captures.
The cursor returns as a blinking block, half a second per cycle, off SDL's
clock. It sits in the cell after the text rather than under it, which is what
made the underscore unreadable, and it wraps to the next row when a line
exactly fills one -- where the next character actually lands.
The new regression test paints stale pixels by hand, which is what a swapped-in
buffer hands back, so the case is covered without real hardware. Every new
assertion was checked by reverting the fix and watching it fail.
Recorded honestly in TODO.md section 5: the same buffer swap means the
graphics verbs were never reliable across frames either. A one-shot DRAW lands
in one buffer and the next present shows the other. Making that work needs a
persistent surface, which is its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
|
|
|
if ( state->editing ) {
|
|
|
|
|
SDL_FRect cell;
|
|
|
|
|
bool visible = true;
|
|
|
|
|
|
|
|
|
|
if ( state->cursorperiodms > 0 ) {
|
|
|
|
|
visible = ((SDL_GetTicks() % state->cursorperiodms) <
|
|
|
|
|
(state->cursorperiodms / 2));
|
|
|
|
|
}
|
|
|
|
|
if ( visible ) {
|
|
|
|
|
int curcol = state->cursorcol;
|
|
|
|
|
int currow = state->cursorrow;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A line that exactly fills a row leaves the cursor one past the last
|
|
|
|
|
* column, because putchar_at only wraps when the *next* character
|
|
|
|
|
* arrives. Drawn there it would be off the right edge and invisible.
|
|
|
|
|
* Show it where the next character will actually land instead, which
|
|
|
|
|
* is also where a C128 puts it.
|
|
|
|
|
*/
|
|
|
|
|
if ( curcol >= state->columns ) {
|
|
|
|
|
curcol = 0;
|
|
|
|
|
currow += 1;
|
|
|
|
|
}
|
|
|
|
|
if ( currow < state->rows ) {
|
|
|
|
|
cell.x = (float)(state->x + (curcol * state->cellw));
|
|
|
|
|
cell.y = (float)(state->y + (currow * state->cellh));
|
|
|
|
|
cell.w = (float)state->cellw;
|
|
|
|
|
cell.h = (float)state->cellh;
|
|
|
|
|
PASS(errctx, akgl_draw_filled_rect(state->renderer, &cell, state->color));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-31 11:21:39 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_sink_akgl_set_pump(akbasic_TextSink *obj, akbasic_AkglPump pump, void *self)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglSink *state = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL sink in sink_akgl_set_pump");
|
|
|
|
|
state = (akbasic_AkglSink *)obj->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"akgl sink has no state");
|
|
|
|
|
|
|
|
|
|
state->pump = pump;
|
|
|
|
|
state->pumpself = self;
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:53 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|