Files
akbasic/src/sink_akgl.c

241 lines
8.0 KiB
C
Raw Normal View History

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.
*/
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#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;
}
}
/** @brief Move to the start of the next row, scrolling if that runs off the end. */
static void newline(akbasic_AkglSink *state)
{
state->cursorcol = 0;
state->cursorrow += 1;
if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) {
scroll(state);
}
}
/**
* @brief Put one character at the cursor, wrapping and scrolling as needed.
*
* Wrapping happens here, on the character grid, rather than being left to
* SDL_ttf's own wraplength. The cursor has to end up somewhere definite: a
* program that PRINTs a long string and then PRINTs again expects the second one
* to start on the row after the first one ended, and only the code that placed
* the characters knows which row that is.
*/
static void putchar_at(akbasic_AkglSink *state, char c)
{
if ( c == '\n' ) {
newline(state);
return;
}
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
newline(state);
}
state->text[state->cursorrow][state->cursorcol] = c;
state->cursorcol += 1;
state->text[state->cursorrow][state->cursorcol] = '\0';
}
static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
size_t i = 0;
FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
"NULL argument in akgl sink write");
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state");
for ( i = 0; text[i] != '\0'; i++ ) {
putchar_at(state, text[i]);
}
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
PASS(errctx, sink_write(self, text));
state = (akbasic_AkglSink *)self->self;
newline(state);
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
"NULL argument in akgl sink readline");
/*
* A drawn text layer is not a source of lines. INPUT through a graphics sink
* wants a line editor built on the keystroke ring, which is its own piece of
* work and is not done: see TODO.md section 3. Until it exists this reports
* end of input rather than pretending to have read something.
*
* EOF rather than an error, because that is the contract sink.h states:
* running off the end of input is how RUNSTREAM mode finishes normally, and
* INPUT already handles it.
*/
if ( len > 0 ) {
dest[0] = '\0';
}
*eof = true;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *sink_clear(akbasic_TextSink *self)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER,
"NULL sink in akgl sink clear");
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state");
memset(state->text, 0, sizeof(state->text));
state->cursorcol = 0;
state->cursorrow = 0;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h)
{
PREPARE_ERROR(errctx);
int cellw = 0;
int cellh = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
"NULL argument in sink_init_akgl");
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
"NULL renderer in sink_init_akgl: the host creates it, not the sink");
FAIL_ZERO_RETURN(errctx, (font != NULL), AKERR_NULLPOINTER,
"NULL font in sink_init_akgl");
/*
* Before anything else in libakgl. akgl_game_init() would have done it, but
* an embedded interpreter drives subsystems directly and never calls that --
* and a code raised before this runs carries no name into its stack trace.
* Idempotent, so a host that already called it loses nothing.
*/
PASS(errctx, akgl_error_init());
/*
* The character grid, measured rather than assumed. Direct equivalent of the
* reference's font.SizeUTF8("A"), and it needs no renderer -- which is why
* the sink can size itself before a frame has ever been drawn.
*/
PASS(errctx, akgl_text_measure(font, "A", &cellw, &cellh));
FAIL_ZERO_RETURN(errctx, (cellw > 0 && cellh > 0), AKBASIC_ERR_VALUE,
"Font measures a %dx%d character cell, which cannot be a grid",
cellw, cellh);
FAIL_ZERO_RETURN(errctx, (w >= cellw && h >= cellh), AKBASIC_ERR_BOUNDS,
"A %dx%d text area has no room for a %dx%d character",
w, h, cellw, cellh);
memset(state, 0, sizeof(*state));
state->renderer = renderer;
state->font = font;
state->color.r = 0xff;
state->color.g = 0xff;
state->color.b = 0xff;
state->color.a = 0xff;
state->x = 0;
state->y = 0;
state->width = w;
state->height = h;
state->cellw = cellw;
state->cellh = cellh;
state->columns = w / cellw;
state->rows = h / cellh;
if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
state->columns = SINK_MAX_COLUMNS - 1;
}
if ( state->rows > SINK_MAX_ROWS ) {
state->rows = SINK_MAX_ROWS;
}
obj->self = state;
obj->write = sink_write;
obj->writeln = sink_writeln;
obj->readline = sink_readline;
obj->clear = sink_clear;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
int row = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
"NULL sink in sink_akgl_render");
state = (akbasic_AkglSink *)obj->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state");
/*
* 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++ ) {
if ( state->text[row][0] == '\0' ) {
continue;
}
PASS(errctx, akgl_text_rendertextat(state->font, state->text[row],
state->color, 0,
state->x,
state->y + (row * state->cellh)));
}
SUCCEED_RETURN(errctx);
}