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.
|
Land a character written past a short row's terminator
A grid row is a NUL-terminated string, and `putchar_at()` wrote the character,
advanced and terminated -- so `CHAR 1, 40, 1, "#"` on an otherwise empty row
stored the `#` at column 40 with `text[1][0]` still `'\0'`, and the render loop,
which stops at the terminator, drew nothing at all.
The silent nothing is the trap. The write succeeded, the cursor moved, the
stdout mirror showed the character, and only the window stayed blank -- so the
program looked right everywhere except where it mattered. The documented
truncation on the way *back* is fine and is unaffected.
`putchar_at()` now fills the gap with spaces before storing.
**Padded there rather than in `sink_moveto()`**, which TODO.md proposed: this way
`moveto` stays read-only -- the objection that entry raised against its own
suggestion -- and a row is only ever padded when a character actually arrives.
The pad fills from the terminator rather than replacing it. The buffer is not
cleared between rows, so replacing only the terminator would expose the tail of
whatever longer row used to be there: writing "ABCDEFGHIJ", truncating it to
"ABCX", then writing at column 7 must give "ABCX Z" and not "ABCX FGZ".
tests/akgl_backends.c asserts all three cases, and the middle one keeps the
truncation pinned.
TODO.md section 6 item 32, struck.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:13:42 -04:00
|
|
|
*
|
|
|
|
|
* **A row is a NUL-terminated string, so a write past its end pads.** Without
|
|
|
|
|
* that, `CHAR 1, 40, 1, "#"` on an otherwise empty row stored the `#` at column
|
|
|
|
|
* 40 with `text[1][0]` still `'\0'` -- and the render loop, which stops at the
|
|
|
|
|
* terminator, drew nothing at all. The write succeeded, the cursor moved, the
|
|
|
|
|
* stdout mirror showed the character, and the window stayed blank. That silent
|
|
|
|
|
* nothing is the trap; the documented truncation on the way *back* is fine and
|
|
|
|
|
* is unaffected. TODO.md section 6 item 32.
|
|
|
|
|
*
|
|
|
|
|
* The padding is spaces rather than whatever was there: the buffer is not
|
|
|
|
|
* cleared between rows, so the gap holds the tail of some longer row that used
|
|
|
|
|
* to be here.
|
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 void putchar_at(akbasic_AkglSink *state, char c)
|
|
|
|
|
{
|
Land a character written past a short row's terminator
A grid row is a NUL-terminated string, and `putchar_at()` wrote the character,
advanced and terminated -- so `CHAR 1, 40, 1, "#"` on an otherwise empty row
stored the `#` at column 40 with `text[1][0]` still `'\0'`, and the render loop,
which stops at the terminator, drew nothing at all.
The silent nothing is the trap. The write succeeded, the cursor moved, the
stdout mirror showed the character, and only the window stayed blank -- so the
program looked right everywhere except where it mattered. The documented
truncation on the way *back* is fine and is unaffected.
`putchar_at()` now fills the gap with spaces before storing.
**Padded there rather than in `sink_moveto()`**, which TODO.md proposed: this way
`moveto` stays read-only -- the objection that entry raised against its own
suggestion -- and a row is only ever padded when a character actually arrives.
The pad fills from the terminator rather than replacing it. The buffer is not
cleared between rows, so replacing only the terminator would expose the tail of
whatever longer row used to be there: writing "ABCDEFGHIJ", truncating it to
"ABCX", then writing at column 7 must give "ABCX Z" and not "ABCX FGZ".
tests/akgl_backends.c asserts all three cases, and the middle one keeps the
truncation pinned.
TODO.md section 6 item 32, struck.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:13:42 -04:00
|
|
|
int col = 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
|
|
|
if ( c == '\n' ) {
|
|
|
|
|
newline(state);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
|
|
|
|
|
newline(state);
|
|
|
|
|
}
|
Land a character written past a short row's terminator
A grid row is a NUL-terminated string, and `putchar_at()` wrote the character,
advanced and terminated -- so `CHAR 1, 40, 1, "#"` on an otherwise empty row
stored the `#` at column 40 with `text[1][0]` still `'\0'`, and the render loop,
which stops at the terminator, drew nothing at all.
The silent nothing is the trap. The write succeeded, the cursor moved, the
stdout mirror showed the character, and only the window stayed blank -- so the
program looked right everywhere except where it mattered. The documented
truncation on the way *back* is fine and is unaffected.
`putchar_at()` now fills the gap with spaces before storing.
**Padded there rather than in `sink_moveto()`**, which TODO.md proposed: this way
`moveto` stays read-only -- the objection that entry raised against its own
suggestion -- and a row is only ever padded when a character actually arrives.
The pad fills from the terminator rather than replacing it. The buffer is not
cleared between rows, so replacing only the terminator would expose the tail of
whatever longer row used to be there: writing "ABCDEFGHIJ", truncating it to
"ABCX", then writing at column 7 must give "ABCX Z" and not "ABCX FGZ".
tests/akgl_backends.c asserts all three cases, and the middle one keeps the
truncation pinned.
TODO.md section 6 item 32, struck.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:13:42 -04:00
|
|
|
for ( col = 0; col < state->cursorcol; col++ ) {
|
|
|
|
|
if ( state->text[state->cursorrow][col] == '\0' ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for ( ; col < state->cursorcol; col++ ) {
|
|
|
|
|
state->text[state->cursorrow][col] = ' ';
|
|
|
|
|
}
|
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->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);
|
|
|
|
|
}
|
|
|
|
|
|
Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.
Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.
Writing the tests turned up eight defects nobody had listed. Seven are fixed:
IF A = 2 THEN was a parse error; only == worked
IF ... AND ... was a parse error, because a condition parsed as one relation
IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
EXIT before any NEXT restarted the program and exhausted the variable pool
READ never found a DATA line above it, and swallowed the lines between
PRINT 2 + 2 at the prompt was filed as program text instead of answering
a short read discarded its bytes, so COPY produced empty files
every verb taking an argument list said "peek() returned nil token!" on none
The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.
Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.
Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.
94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
|
|
|
/**
|
|
|
|
|
* @brief Put the cursor at a character cell, for CHAR.
|
|
|
|
|
*
|
|
|
|
|
* Clamped rather than refused: a program that asks for a column past the edge of
|
|
|
|
|
* a window it cannot measure has made an ordinary mistake, and a C128 clamps too.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *sink_moveto(akbasic_TextSink *self, int col, int row)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglSink *state = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in moveto");
|
|
|
|
|
state = (akbasic_AkglSink *)self->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
|
|
|
|
|
|
|
|
|
|
if ( col < 0 ) { col = 0; }
|
|
|
|
|
if ( row < 0 ) { row = 0; }
|
|
|
|
|
if ( col >= state->columns ) { col = state->columns - 1; }
|
|
|
|
|
if ( row >= state->rows ) { row = state->rows - 1; }
|
|
|
|
|
state->cursorcol = col;
|
|
|
|
|
state->cursorrow = row;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Constrain the text area to a rectangle of cells, for WINDOW.
|
|
|
|
|
*
|
|
|
|
|
* The sink already drives everything -- wrap, scroll, cursor -- from `x`, `y`,
|
|
|
|
|
* `columns` and `rows`, so a window is those four fields and nothing else. The
|
|
|
|
|
* cell size does not change, which is what keeps a windowed program's text the
|
|
|
|
|
* same size as an unwindowed one's.
|
|
|
|
|
*
|
|
|
|
|
* The full-screen geometry is remembered so a later WINDOW can grow back out;
|
|
|
|
|
* without it each call could only ever shrink.
|
|
|
|
|
*/
|
Let a program ask how big the text grid is
`RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows
or the cell size -- so anything placing a character *and* a sprite at the same
spot had to hardcode a number measured by hand against whatever font the host
loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the
one thing in that listing that breaks on a different font or window.
**`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.**
`RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns.
`RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter
has neither -- refused by name, because answering 0 would be a plausible lie,
which is worse than a refusal that says why.
The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own
dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about
the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a
column count stops being right the moment a program calls `WINDOW`.
Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns,
rows, cell width, cell height -- implemented by the akgl sink and forwarded by
the tee, in the shape `moveto` and `window` already had. NULL everywhere else,
so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_
stdio()` clears it for the same reason it now clears the other two.
Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50
columns by 37 rows -- the three numbers the Breakout listing had written out as
constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not.
tests/console_verbs.c drives the answers through a stand-in sink with a grid,
since the harness sink is stdio and has none; tests/graphics_verbs.c covers the
new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=`
block in docs/10-embedding.md moves with the header, which is `docs_examples`
doing its job.
TODO.md section 6 item 31's second half, struck.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:05:17 -04:00
|
|
|
/**
|
|
|
|
|
* @brief Report the character grid, for RWINDOW and RGR.
|
|
|
|
|
*
|
|
|
|
|
* The four numbers a program cannot otherwise learn. Columns and rows are the
|
|
|
|
|
* *current* window rather than the whole screen, which is what RWINDOW means on
|
|
|
|
|
* a C128 and what a program placing text actually needs; the cell size is
|
|
|
|
|
* unaffected by windowing, which is what keeps a windowed program's text the
|
|
|
|
|
* same size as an unwindowed one's.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *sink_grid(akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglSink *state = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in grid");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (columns != NULL && rows != NULL && cellw != NULL && cellh != NULL),
|
|
|
|
|
AKERR_NULLPOINTER, "NULL destination in grid");
|
|
|
|
|
state = (akbasic_AkglSink *)self->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state->cellw > 0 && state->cellh > 0), AKBASIC_ERR_STATE,
|
|
|
|
|
"The sink has no character grid to measure");
|
|
|
|
|
|
|
|
|
|
*columns = state->columns;
|
|
|
|
|
*rows = state->rows;
|
|
|
|
|
*cellw = state->cellw;
|
|
|
|
|
*cellh = state->cellh;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.
Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.
Writing the tests turned up eight defects nobody had listed. Seven are fixed:
IF A = 2 THEN was a parse error; only == worked
IF ... AND ... was a parse error, because a condition parsed as one relation
IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
EXIT before any NEXT restarted the program and exhausted the variable pool
READ never found a DATA line above it, and swallowed the lines between
PRINT 2 + 2 at the prompt was filed as program text instead of answering
a short read discarded its bytes, so COPY produced empty files
every verb taking an argument list said "peek() returned nil token!" on none
The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.
Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.
Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.
94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
|
|
|
static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top, int right, int bottom)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglSink *state = NULL;
|
|
|
|
|
int maxcols = 0;
|
|
|
|
|
int maxrows = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in window");
|
|
|
|
|
state = (akbasic_AkglSink *)self->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state->cellw > 0 && state->cellh > 0), AKBASIC_ERR_STATE,
|
|
|
|
|
"The sink has no character grid to window");
|
|
|
|
|
|
|
|
|
|
maxcols = state->fullwidth / state->cellw;
|
|
|
|
|
maxrows = state->fullheight / state->cellh;
|
|
|
|
|
if ( left < 0 ) { left = 0; }
|
|
|
|
|
if ( top < 0 ) { top = 0; }
|
|
|
|
|
if ( right >= maxcols ) { right = maxcols - 1; }
|
|
|
|
|
if ( bottom >= maxrows ) { bottom = maxrows - 1; }
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (right >= left && bottom >= top), AKBASIC_ERR_VALUE,
|
|
|
|
|
"WINDOW asks for no cells at all");
|
|
|
|
|
|
|
|
|
|
state->x = state->fullx + (left * state->cellw);
|
|
|
|
|
state->y = state->fully + (top * state->cellh);
|
|
|
|
|
state->columns = (right - left) + 1;
|
|
|
|
|
state->rows = (bottom - top) + 1;
|
|
|
|
|
state->width = state->columns * state->cellw;
|
|
|
|
|
state->height = state->rows * state->cellh;
|
|
|
|
|
state->cursorcol = 0;
|
|
|
|
|
state->cursorrow = 0;
|
|
|
|
|
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
|
|
|
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;
|
Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.
Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.
Writing the tests turned up eight defects nobody had listed. Seven are fixed:
IF A = 2 THEN was a parse error; only == worked
IF ... AND ... was a parse error, because a condition parsed as one relation
IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
EXIT before any NEXT restarted the program and exhausted the variable pool
READ never found a DATA line above it, and swallowed the lines between
PRINT 2 + 2 at the prompt was filed as program text instead of answering
a short read discarded its bytes, so COPY produced empty files
every verb taking an argument list said "peek() returned nil token!" on none
The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.
Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.
Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.
94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
|
|
|
/* Remembered so WINDOW can grow back out to the whole area. */
|
|
|
|
|
state->fullx = state->x;
|
|
|
|
|
state->fully = state->y;
|
|
|
|
|
state->fullwidth = w;
|
|
|
|
|
state->fullheight = 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
|
|
|
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;
|
Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.
Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.
Writing the tests turned up eight defects nobody had listed. Seven are fixed:
IF A = 2 THEN was a parse error; only == worked
IF ... AND ... was a parse error, because a condition parsed as one relation
IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
EXIT before any NEXT restarted the program and exhausted the variable pool
READ never found a DATA line above it, and swallowed the lines between
PRINT 2 + 2 at the prompt was filed as program text instead of answering
a short read discarded its bytes, so COPY produced empty files
every verb taking an argument list said "peek() returned nil token!" on none
The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.
Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.
Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.
94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
|
|
|
obj->moveto = sink_moveto;
|
|
|
|
|
obj->window = sink_window;
|
Let a program ask how big the text grid is
`RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows
or the cell size -- so anything placing a character *and* a sprite at the same
spot had to hardcode a number measured by hand against whatever font the host
loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the
one thing in that listing that breaks on a different font or window.
**`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.**
`RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns.
`RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter
has neither -- refused by name, because answering 0 would be a plausible lie,
which is worse than a refusal that says why.
The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own
dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about
the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a
column count stops being right the moment a program calls `WINDOW`.
Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns,
rows, cell width, cell height -- implemented by the akgl sink and forwarded by
the tee, in the shape `moveto` and `window` already had. NULL everywhere else,
so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_
stdio()` clears it for the same reason it now clears the other two.
Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50
columns by 37 rows -- the three numbers the Breakout listing had written out as
constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not.
tests/console_verbs.c drives the answers through a stand-in sink with a grid,
since the harness sink is stdio and has none; tests/graphics_verbs.c covers the
new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=`
block in docs/10-embedding.md moves with the header, which is `docs_examples`
doing its job.
TODO.md section 6 item 31's second half, struck.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:05:17 -04:00
|
|
|
obj->grid = sink_grid;
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|