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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
699 lines
25 KiB
C
699 lines
25 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.
|
|
*
|
|
* **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.
|
|
*/
|
|
static void putchar_at(akbasic_AkglSink *state, char c)
|
|
{
|
|
int col = 0;
|
|
|
|
if ( c == '\n' ) {
|
|
newline(state);
|
|
return;
|
|
}
|
|
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
|
|
newline(state);
|
|
}
|
|
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] = ' ';
|
|
}
|
|
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);
|
|
}
|