Files
akbasic/src/sink_akgl.c
Andrew Kesterson 694b446ce4 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

677 lines
24 KiB
C

/**
* @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 <ctype.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akgl/controller.h>
#include <akgl/draw.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;
}
/*
* 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;
}
}
}
/** @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);
}
/**
* @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;
int erasedto = 0;
int endedat = 0;
state->cursorrow = state->editrow;
state->cursorcol = state->editcol;
for ( i = 0; i < state->echolen; i++ ) {
putchar_at(state, ' ');
}
erasedto = state->cursorrow;
state->cursorrow = state->editrow;
state->cursorcol = state->editcol;
for ( i = 0; i < state->editlen; i++ ) {
putchar_at(state, state->editline[i]);
}
endedat = state->cursorrow;
state->echolen = state->editlen;
/*
* A line that got shorter can leave whole rows holding nothing but the
* spaces the erase pass just wrote -- which is what backspacing back across
* a wrap does to the row below. Truncate them, so such a row reports itself
* *empty* rather than merely blank-looking.
*
* That distinction is load-bearing rather than tidiness: render() decides
* what to erase and redraw from whether a row has text, and a row of spaces
* is text. Left alone it would be repainted forever, and anything the
* program had drawn underneath it would stay erased.
*/
for ( i = endedat + 1; i <= erasedto && i < SINK_MAX_ROWS; i++ ) {
state->text[i][0] = '\0';
}
}
/** @brief Append one byte to the line being typed, if there is room for it. */
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';
}
/**
* @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.
*
* 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.
*/
static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted)
{
size_t i = 0;
if ( key->key == SDLK_RETURN || key->key == SDLK_KP_ENTER ||
key->key == '\r' || key->key == '\n' ) {
*submitted = true;
return;
}
if ( key->key == SDLK_BACKSPACE || key->key == '\b' || key->key == 0x7f ) {
if ( state->editlen > 0 ) {
/*
* 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.
*/
state->editlen -= 1;
state->editline[state->editlen] = '\0';
echo_line(state);
}
return;
}
if ( key->key == SDLK_ESCAPE || key->key == 0x1b ) {
state->editlen = 0;
state->editline[0] = '\0';
echo_line(state);
return;
}
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);
return;
}
/*
* 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.
*/
}
/**
* @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);
akgl_Keystroke key;
bool submitted = false;
bool available = false;
bool running = true;
while ( !submitted ) {
/*
* 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));
if ( available ) {
edit_key(state, &key, &submitted);
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);
}
static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
"NULL argument in akgl sink readline");
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';
/*
* 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.
*/
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);
}
strncpy(dest, state->editline, len - 1);
dest[len - 1] = '\0';
newline(state);
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);
}
/**
* @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.
*/
/**
* @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);
}
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);
}
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;
/* Opaque, not transparent: the text layer owns the rows it draws. */
state->cursorperiodms = AKBASIC_SINK_CURSOR_BLINK_MS;
state->background.r = 0x00;
state->background.g = 0x00;
state->background.b = 0x00;
state->background.a = 0xff;
state->x = 0;
state->y = 0;
state->width = w;
state->height = h;
/* 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;
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;
obj->moveto = sink_moveto;
obj->window = sink_window;
obj->grid = sink_grid;
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");
/*
* **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.
*
* 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.
*
* 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.
*
* 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++ ) {
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));
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)));
}
/*
* 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.
*/
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));
}
}
}
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;
SUCCEED_RETURN(errctx);
}