`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>
677 lines
24 KiB
C
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);
|
|
}
|