/** * @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 #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. * * **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) - state->texttop; 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 + ((state->texttop + 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); } /** * @brief Apply GRAPHIC's text-plane half without teaching the runtime about SDL. * * Modes 1 and 3 are full bitmaps, so the retained text grid is hidden. Modes 2 * and 4 have a bitmap above and text below. A C128 gives an omitted split its * bottom six rows; an explicit zero means all text, and the host clamps a C128 * row number to however many measured cells its own window has. */ static akerr_ErrorContext *sink_graphic(akbasic_TextSink *self, int mode, int split) { PREPARE_ERROR(errctx); akbasic_AkglSink *state = NULL; int maxcols = 0; int maxrows = 0; FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in graphic"); state = (akbasic_AkglSink *)self->self; FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state"); maxcols = state->fullwidth / state->cellw; maxrows = state->fullheight / state->cellh; state->graphicmode = mode; if ( mode == 0 ) { /* Text mode owns the whole screen again, not the last split's rows. */ state->texttop = 0; state->x = state->fullx; state->y = state->fully; state->width = state->fullwidth; state->height = state->fullheight; state->columns = maxcols; state->rows = maxrows; if ( state->columns > SINK_MAX_COLUMNS - 1 ) { state->columns = SINK_MAX_COLUMNS - 1; } if ( state->rows > SINK_MAX_ROWS ) { state->rows = SINK_MAX_ROWS; } state->cursorcol = 0; state->cursorrow = 0; SUCCEED_RETURN(errctx); } if ( mode != 2 && mode != 4 ) { SUCCEED_RETURN(errctx); } if ( split < 0 ) { split = maxrows - 6; } if ( split < 0 ) { split = 0; } if ( split >= maxrows ) { /* A split below the final row is C128 all-bitmap mode, not a zero-row grid. */ state->graphicmode = 1; SUCCEED_RETURN(errctx); } state->texttop = split; state->x = state->fullx; state->y = state->fully + (split * state->cellh); state->width = state->fullwidth; state->height = (maxrows - split) * state->cellh; state->columns = maxcols; state->rows = maxrows - split; if ( state->columns > SINK_MAX_COLUMNS - 1 ) { state->columns = SINK_MAX_COLUMNS - 1; } if ( state->rows > SINK_MAX_ROWS ) { state->rows = SINK_MAX_ROWS; } 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; state->graphicmode = 0; state->texttop = 0; 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; obj->graphic = sink_graphic; 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 full bitmap owns every display row; the text buffer remains intact. */ if ( state->graphicmode == 1 || state->graphicmode == 3 ) { SUCCEED_RETURN(errctx); } /* * **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); }