/** * @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 #include #include #include #include #include #include /** @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); }