/** * @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 /* * akgl/actor.h before akgl/controller.h, and it is not optional: controller.h * declares two handler function pointers taking an akgl_Actor * and includes * nothing that declares the type. Filed upstream; see the same note in * src/input_akgl.c, which is where it was found first. */ #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; } /* * 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; state->cursorrow = state->editrow; state->cursorcol = state->editcol; for ( i = 0; i < state->echolen; i++ ) { putchar_at(state, ' '); } state->cursorrow = state->editrow; state->cursorcol = state->editcol; for ( i = 0; i < state->editlen; i++ ) { putchar_at(state, state->editline[i]); } state->echolen = state->editlen; } /** * @brief Fold one keycode into the line being typed. * * The keycodes are SDL's, and an unshifted printable key carries its own ASCII * value, which is why the range test below is all the translation there is. * Shifted characters are unreachable: the ring carries no modifier state. See * akbasic_sink_akgl_set_pump() in akgl.h. */ static void edit_key(akbasic_AkglSink *state, int keycode, bool *submitted) { if ( keycode == '\r' || keycode == '\n' ) { *submitted = true; return; } if ( keycode == '\b' || keycode == 0x7f ) { if ( state->editlen > 0 ) { state->editlen -= 1; state->editline[state->editlen] = '\0'; echo_line(state); } return; } if ( keycode == 0x1b ) { state->editlen = 0; state->editline[0] = '\0'; echo_line(state); return; } if ( keycode < 0x20 || keycode > 0x7e ) { /* A cursor or function key. Not an editing command here; a script's own * GET loop is what wants those. */ return; } 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] = (char)toupper(keycode); state->editlen += 1; state->editline[state->editlen] = '\0'; echo_line(state); } /** * @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); bool submitted = false; bool available = false; bool running = true; int keycode = 0; while ( !submitted ) { PASS(errctx, akgl_controller_poll_key(&keycode, &available)); if ( available ) { edit_key(state, keycode, &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); } 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))); } /* * The cursor, drawn only while a line is being typed. The reference draws it * the same way, as a literal underscore glyph (drawCursor, * basicruntime_graphics.go:33), rather than as a filled rectangle -- which * means it needs no draw primitive and cannot be a different shape from the * text it sits in. */ if ( state->editing ) { PASS(errctx, akgl_text_rendertextat(state->font, "_", state->color, 0, state->x + (state->cursorcol * state->cellw), state->y + (state->cursorrow * state->cellh))); } 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); }