241 lines
8.0 KiB
C
241 lines
8.0 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 <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);
|
||
|
|
}
|