Make GRAPHIC select the text plane (#23) #29

Merged
andrew merged 1 commits from 23 into main 2026-08-03 12:04:13 -04:00
13 changed files with 345 additions and 79 deletions

View File

@@ -62,22 +62,28 @@ small number that is not a colour.
### GRAPHIC
`GRAPHIC mode` chooses a screen mode; `GRAPHIC CLR` clears it. Mode 0 is text and
refuses to draw.
`GRAPHIC mode [, clear] [, split]` chooses the display composition; `GRAPHIC CLR`
clears the saved graphics state. `GRAPHIC 0` displays the text plane. `GRAPHIC 1`
selects a full bitmap plane: `PRINT` still updates the retained text screen, but it is
not visible until text mode returns. `GRAPHIC 2` is the explicit split-screen form;
its optional third argument names the first text row, and its default leaves six text
rows at the bottom. `WINDOW` restricts the current text region; it does not make a
full bitmap visible.
Every picture in this chapter is generated by running the listing above it; see
`MAINTENANCE.md` if you are editing one.
### DRAW
```basic requires=akgl screenshot=draw
10 COLOR 1, 8
20 DRAW 1, 20, 180 TO 90, 40 TO 160, 150 TO 230, 20 TO 300, 120
30 COLOR 2, 6
40 DRAW 2, 20, 190 TO 300, 190
50 LOCATE 160, 100
60 COLOR 3, 3
70 DRAW 3
```basic requires=akgl screenshot=draw text=1
10 GRAPHIC 1, 1
20 COLOR 1, 8
30 DRAW 1, 20, 180 TO 90, 40 TO 160, 150 TO 230, 20 TO 300, 120
40 COLOR 2, 6
50 DRAW 2, 20, 190 TO 300, 190
60 LOCATE 160, 100
70 COLOR 3, 3
80 DRAW 3
```
![](images/draw.png)
@@ -88,14 +94,15 @@ picture — plots wherever `LOCATE` left the pixel cursor.
### BOX
```basic requires=akgl screenshot=box
10 COLOR 1, 8
20 BOX 1, 20, 30, 130, 140
30 COLOR 2, 6
40 BOX 2, 180, 30, 290, 140, 30
50 COLOR 3, 3
60 LOCATE 300, 190
70 BOX 3, 20, 160
```basic requires=akgl screenshot=box text=1
10 GRAPHIC 1, 1
20 COLOR 1, 8
30 BOX 1, 20, 30, 130, 140
40 COLOR 2, 6
50 BOX 2, 180, 30, 290, 140, 30
60 COLOR 3, 3
70 LOCATE 300, 190
80 BOX 3, 20, 160
```
![](images/box.png)
@@ -110,13 +117,14 @@ have.
### CIRCLE
```basic requires=akgl screenshot=circle
10 COLOR 1, 8
20 CIRCLE 1, 80, 70, 60, 60
30 COLOR 2, 6
40 CIRCLE 2, 230, 70, 75, 45
50 COLOR 3, 3
60 CIRCLE 3, 160, 140, 130, 50, 90, 270
```basic requires=akgl screenshot=circle text=1
10 GRAPHIC 1, 1
20 COLOR 1, 8
30 CIRCLE 1, 80, 70, 60, 60
40 COLOR 2, 6
50 CIRCLE 2, 230, 70, 75, 45
60 COLOR 3, 3
70 CIRCLE 3, 160, 140, 130, 50, 90, 270
```
![](images/circle.png)
@@ -129,14 +137,15 @@ degree increment, and a large increment is what turns a circle into a polygon.
### PAINT
```basic requires=akgl screenshot=paint
10 COLOR 1, 8
20 CIRCLE 1, 100, 100, 70, 70
30 BOX 1, 180, 50, 290, 150
40 COLOR 2, 6
50 PAINT 2, 100, 100
60 COLOR 3, 3
70 PAINT 3, 230, 100
```basic requires=akgl screenshot=paint text=1
10 GRAPHIC 1, 1
20 COLOR 1, 8
30 CIRCLE 1, 100, 100, 70, 70
40 BOX 1, 180, 50, 290, 150
50 COLOR 2, 6
60 PAINT 2, 100, 100
70 COLOR 3, 3
80 PAINT 3, 230, 100
```
![](images/paint.png)
@@ -153,13 +162,14 @@ coordinates finishes.
### SCALE
```basic requires=akgl screenshot=scale size=640x400
10 COLOR 1, 3
20 BOX 1, 0, 0, 319, 199
30 SCALE 1, 319, 199
40 COLOR 2, 6
50 BOX 2, 0, 0, 319, 199
60 DRAW 2, 0, 0 TO 319, 199
```basic requires=akgl screenshot=scale size=640x400 text=1
10 GRAPHIC 1, 1
20 COLOR 1, 3
30 BOX 1, 0, 0, 319, 199
40 SCALE 1, 319, 199
50 COLOR 2, 6
60 BOX 2, 0, 0, 319, 199
70 DRAW 2, 0, 0 TO 319, 199
```
![](images/scale.png)
@@ -212,15 +222,16 @@ parallel passes; see Chapter 13.
`SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back:
```basic requires=akgl screenshot=shapes
10 COLOR 1, 8
20 CIRCLE 1, 40, 40, 30, 30
30 COLOR 2, 6
40 PAINT 2, 40, 40
50 SSHAPE A$, 8, 8, 72, 72
60 GSHAPE A$, 120, 20
70 GSHAPE A$, 200, 60
80 GSHAPE A$, 120, 120
```basic requires=akgl screenshot=shapes text=1
10 GRAPHIC 1, 1
20 COLOR 1, 8
30 CIRCLE 1, 40, 40, 30, 30
40 COLOR 2, 6
50 PAINT 2, 40, 40
60 SSHAPE A$, 8, 8, 72, 72
70 GSHAPE A$, 120, 20
80 GSHAPE A$, 200, 60
90 GSHAPE A$, 120, 120
```
![](images/shapes.png)
@@ -244,19 +255,11 @@ does with one, but you cannot store it or measure it.
text and the sprites, so a program draws its picture once and it is there on every frame
after. It does not have to redraw it, and it does not have to capture it into a sprite.
What covers it is the text layer, which repaints every row it owns — opaque, every frame,
for a reason `akbasic_sink_akgl_render()` explains — and by default it owns the whole
window. `WINDOW` shrinks it:
```basic norun
10 WINDOW 0, 0, 39, 1
20 GRAPHIC 1, 1
30 COLOR 1, 3
40 BOX 1, 20, 40, 300, 180
```
Two rows of text at the top, the rest of the window for drawing, and the box is still
there a thousand frames later.
`GRAPHIC 1` changes that order: the full bitmap owns the display, while the text layer
keeps its contents off-screen. Returning with `GRAPHIC 0` reveals what `PRINT` wrote.
Use `GRAPHIC 2, clear, split` when a program deliberately needs both planes; its text
rows begin at `split`. `WINDOW` can then narrow that text region further, but it is not
needed to make a full bitmap visible.
**A redraw also has to fit inside one batch.** The host runs a fixed number of source
lines and then presents, and presenting throws the drawing buffer away — so a run of

View File

@@ -116,6 +116,7 @@ typedef struct akbasic_TextSink
akerr_ErrorContext AKERR_NOIGNORE *(*moveto)(struct akbasic_TextSink *self, int col, int row);
akerr_ErrorContext AKERR_NOIGNORE *(*window)(struct akbasic_TextSink *self, int left, int top, int right, int bottom);
akerr_ErrorContext AKERR_NOIGNORE *(*grid)(struct akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh);
akerr_ErrorContext AKERR_NOIGNORE *(*graphic)(struct akbasic_TextSink *self, int mode, int split);
} akbasic_TextSink;
```
@@ -123,8 +124,8 @@ typedef struct akbasic_TextSink
supplies its own and draws into a text layer. `readline` is expected to set `*eof` rather
than block — that is how `INPUT` behaves sanely inside a frame.
**The last three are optional and may be NULL**, which is how `CHAR`, `WINDOW` and
`RWINDOW` know to refuse by name rather than pretending. Supply `grid` if your text layer
**The last four are optional and may be NULL**, which is how `CHAR`, `WINDOW`, `RWINDOW`
and `GRAPHIC` know to refuse by name rather than pretending. Supply `grid` if your text layer
has a character cell: it is the only way a script can find out how big one is, and
without it anything placing a character and a sprite at the same spot has to hardcode a
number measured against your font.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 739 B

After

Width:  |  Height:  |  Size: 735 B

View File

@@ -189,6 +189,10 @@ typedef struct
int columns; /* the character grid the cell size works out to */
int rows;
/* GRAPHIC chooses whether this text plane is visible and where it begins. */
int graphicmode;
int texttop; /* first screen row the current text region owns */
int cursorcol;
int cursorrow;

View File

@@ -82,6 +82,20 @@ typedef struct akbasic_TextSink
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*grid)(struct akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh);
/**
* Select how this sink's text plane participates in a GRAPHIC display mode.
*
* This is optional: a stream has no display plane to hide or split, so the
* stdio sink leaves it NULL. A graphical sink receives the mode after the
* runtime has validated it. @p split is the first text row for a split mode,
* or -1 when BASIC used the C128 default.
*
* @param self The sink.
* @param mode BASIC 7.0 GRAPHIC mode.
* @param split First text row for a split screen, or -1 for the default.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*graphic)(struct akbasic_TextSink *self, int mode, int split);
} akbasic_TextSink;
/** @brief State for the stdio-backed sink. */

View File

@@ -155,24 +155,24 @@ static akerr_ErrorContext *draw_line(akbasic_Runtime *obj, double x1, double y1,
akerr_ErrorContext *akbasic_cmd_graphic(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
double args[2];
double args[3];
int count = 0;
int mode = 0;
int split = -1;
akbasic_Color background;
(void)lval; (void)rval;
PASS(errctx, require_graphics(obj, "GRAPHIC"));
PASS(errctx, akbasic_args_numbers(obj, expr, "GRAPHIC", args, 2, &count));
PASS(errctx, akbasic_args_numbers(obj, expr, "GRAPHIC", args, 3, &count));
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "GRAPHIC expected a mode");
mode = (int)args[0];
/*
* BASIC 7.0 numbers five modes plus a CLR. They differ in bitmap resolution
* and in whether the bottom of the screen stays text, neither of which means
* anything against a host's renderer -- so the mode is recorded and only its
* one observable consequence is honoured: mode 0 is text, and text mode does
* not draw. Refusing an out-of-range mode still matters, because that is a
* typo the program author can fix.
* and in whether the bottom of the screen stays text. Resolution belongs to
* the graphics backend, while the latter is a text-sink decision: an AKGL
* sink hides its text plane for a full bitmap and moves it below the split.
* A stream has no plane, so it simply leaves that optional entry point NULL.
*/
FAIL_ZERO_RETURN(errctx, (mode >= 0 && mode <= 5), AKBASIC_ERR_BOUNDS,
"GRAPHIC mode %d out of range (0 to 5)", mode);
@@ -180,11 +180,22 @@ akerr_ErrorContext *akbasic_cmd_graphic(akbasic_Runtime *obj, akbasic_ASTLeaf *e
/* GRAPHIC CLR: drop the saved shapes and go back to text. */
PASS(errctx, obj->graphics->free_shapes(obj->graphics));
PASS(errctx, akbasic_graphics_state_init(&obj->gfx));
if ( obj->sink != NULL && obj->sink->graphic != NULL ) {
PASS(errctx, obj->sink->graphic(obj->sink, 0, -1));
}
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
}
if ( count >= 3 ) {
split = (int)args[2];
FAIL_ZERO_RETURN(errctx, (split >= 0 && split <= 25), AKBASIC_ERR_BOUNDS,
"GRAPHIC split %d out of range (0 to 25)", split);
}
obj->gfx.mode = mode;
if ( obj->sink != NULL && obj->sink->graphic != NULL ) {
PASS(errctx, obj->sink->graphic(obj->sink, mode, split));
}
if ( count >= 2 && args[1] != 0.0 ) {
PASS(errctx, akbasic_graphics_source_color(&obj->gfx, 0, &background));
PASS(errctx, obj->graphics->clear(obj->graphics, background));

View File

@@ -475,7 +475,7 @@ static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top
"The sink has no character grid to window");
maxcols = state->fullwidth / state->cellw;
maxrows = state->fullheight / state->cellh;
maxrows = (state->fullheight / state->cellh) - state->texttop;
if ( left < 0 ) { left = 0; }
if ( top < 0 ) { top = 0; }
if ( right >= maxcols ) { right = maxcols - 1; }
@@ -484,7 +484,7 @@ static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top
"WINDOW asks for no cells at all");
state->x = state->fullx + (left * state->cellw);
state->y = state->fully + (top * state->cellh);
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;
@@ -494,6 +494,78 @@ static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top
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);
@@ -554,6 +626,8 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi
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;
}
@@ -569,6 +643,7 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi
obj->moveto = sink_moveto;
obj->window = sink_window;
obj->grid = sink_grid;
obj->graphic = sink_graphic;
SUCCEED_RETURN(errctx);
}
@@ -584,6 +659,11 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
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

View File

@@ -109,5 +109,6 @@ akerr_ErrorContext *akbasic_sink_init_stdio(akbasic_TextSink *obj, akbasic_Stdio
obj->moveto = NULL;
obj->window = NULL;
obj->grid = NULL;
obj->graphic = NULL;
SUCCEED_RETURN(errctx);
}

View File

@@ -143,6 +143,22 @@ static akerr_ErrorContext *tee_window(akbasic_TextSink *self, int left, int top,
SUCCEED_RETURN(errctx);
}
/** @brief Forward GRAPHIC's display-mode decision to the half with a display. */
static akerr_ErrorContext *tee_graphic(akbasic_TextSink *self, int mode, int split)
{
PREPARE_ERROR(errctx);
akbasic_TeeSink *state = NULL;
PASS(errctx, tee_state(self, &state));
if ( state->primary != NULL && state->primary->graphic != NULL ) {
PASS(errctx, state->primary->graphic(state->primary, mode, split));
}
if ( state->mirror != NULL && state->mirror->graphic != NULL ) {
PASS(errctx, state->mirror->graphic(state->mirror, mode, split));
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Report the grid of whichever half has one.
*
@@ -202,6 +218,7 @@ akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink
obj->moveto = NULL;
obj->window = NULL;
obj->grid = NULL;
obj->graphic = NULL;
/*
* Offered only when a half can actually do it, so CHAR's refusal against a
* stdio-only driver still reads correctly through a tee.
@@ -221,5 +238,9 @@ akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink
(mirror != NULL && mirror->grid != NULL) ) {
obj->grid = tee_grid;
}
if ( (primary != NULL && primary->graphic != NULL) ||
(mirror != NULL && mirror->graphic != NULL) ) {
obj->graphic = tee_graphic;
}
SUCCEED_RETURN(errctx);
}

View File

@@ -1173,6 +1173,78 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_drawing_layer_persists(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief GRAPHIC selects whether the AKGL text plane covers the drawing layer.
*
* `PRINT` always writes into the retained grid. In a full bitmap mode the grid
* is simply not composited; returning to mode zero reveals it. Split mode moves
* the text region below the requested row, leaving the drawing visible above.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_graphic_text_composition(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
int fullrows = 0;
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, akgl_renderer, font,
TARGET_SIZE, TARGET_SIZE));
PASS(errctx, akbasic_runtime_init(&RUNTIME, &AKGLSINK));
PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, akgl_renderer));
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, NULL));
PASS(errctx, akbasic_runtime_load(&RUNTIME,
"10 GRAPHIC 1, 1\n"
"20 COLOR 1, 3\n"
"30 DRAW 1, 40, 40\n"
"40 PRINT \"HIDDEN\"\n"));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_graphics_akgl_begin(&GRAPHICS));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
PASS(errctx, akbasic_graphics_akgl_end(&GRAPHICS));
TEST_REQUIRE_INT(AKGLSINKSTATE.graphicmode, 1);
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "HIDDEN");
PASS(errctx, clear_target());
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the full-bitmap frame back");
TEST_REQUIRE(pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
"GRAPHIC 1 must show DRAW even after PRINT wrote the hidden text grid");
SDL_DestroySurface(shot);
PASS(errctx, AKGLSINK.graphic(&AKGLSINK, 0, -1));
PASS(errctx, clear_target());
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the text frame back");
TEST_REQUIRE(!pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
"GRAPHIC 0 must put the retained text plane back over the drawing");
SDL_DestroySurface(shot);
PASS(errctx, AKGLSINK.clear(&AKGLSINK));
fullrows = TARGET_SIZE / AKGLSINKSTATE.cellh;
PASS(errctx, AKGLSINK.graphic(&AKGLSINK, 2, 3));
TEST_REQUIRE_INT(AKGLSINKSTATE.y, 3 * AKGLSINKSTATE.cellh);
TEST_REQUIRE_INT(AKGLSINKSTATE.rows, fullrows - 3);
PASS(errctx, AKGLSINK.writeln(&AKGLSINK, "BOTTOM"));
PASS(errctx, clear_target());
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the split-screen frame back");
TEST_REQUIRE(pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
"GRAPHIC 2 must leave the graphics region above its text rows visible");
SDL_DestroySurface(shot);
PASS(errctx, AKGLSINK.graphic(&AKGLSINK, 0, -1));
TEST_REQUIRE_INT(AKGLSINKSTATE.texttop, 0);
TEST_REQUIRE_INT(AKGLSINKSTATE.rows, fullrows);
akbasic_graphics_akgl_shutdown(&GRAPHICS);
SUCCEED_RETURN(errctx);
}
/**
* @brief The contact says which way to push out and how far.
*
@@ -1339,6 +1411,7 @@ int main(void)
CATCH(errctx, test_static_geometry());
CATCH(errctx, test_contact_geometry());
CATCH(errctx, test_drawing_layer_persists());
CATCH(errctx, test_graphic_text_composition());
} CLEANUP {
if ( font != NULL ) {
TTF_CloseFont(font);

View File

@@ -34,6 +34,22 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
SUCCEED_RETURN(errctx);
}
/** @brief The values delivered through GRAPHIC's optional text-plane hook. */
static int GRAPHIC_CALLS = 0;
static int GRAPHIC_MODE = 0;
static int GRAPHIC_SPLIT = 0;
static akerr_ErrorContext AKERR_NOIGNORE *record_graphic(akbasic_TextSink *self, int mode, int split)
{
PREPARE_ERROR(errctx);
(void)self;
GRAPHIC_CALLS += 1;
GRAPHIC_MODE = mode;
GRAPHIC_SPLIT = split;
SUCCEED_RETURN(errctx);
}
/** @brief The white of palette index 2, which most of these draw with. */
#define WHITE "#ffffff"
/** @brief The red of palette index 3. */
@@ -323,6 +339,21 @@ static void test_graphic(void)
TEST_REQUIRE_STR(MOCK.log, "");
harness_stop();
/* The third argument reaches a graphical text sink as the first text row. */
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
GRAPHIC_CALLS = 0;
HARNESS_SINK.graphic = record_graphic;
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT, NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 GRAPHIC 2, 1, 19\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_INT(GRAPHIC_CALLS, 1);
TEST_REQUIRE_INT(GRAPHIC_MODE, 2);
TEST_REQUIRE_INT(GRAPHIC_SPLIT, 19);
harness_stop();
/* GRAPHIC CLR drops the saved shapes and resets the whole state. */
TEST_REQUIRE_OK(run_program("10 COLOR 1, 3\n20 GRAPHIC 5\n"));
TEST_REQUIRE_STR(MOCK.log, "freeshapes\n");

View File

@@ -47,6 +47,9 @@ static akerr_ErrorContext AKERR_NOIGNORE *broken_clear(akbasic_TextSink *self)
static int MOVETOS = 0;
static int WINDOWS = 0;
static int WINDOW_ARGS[4] = { 0, 0, 0, 0 };
static int GRAPHICS = 0;
static int GRAPHIC_MODE = 0;
static int GRAPHIC_SPLIT = 0;
static akerr_ErrorContext AKERR_NOIGNORE *counting_moveto(akbasic_TextSink *self, int col, int row)
{
@@ -70,6 +73,17 @@ static akerr_ErrorContext AKERR_NOIGNORE *counting_window(akbasic_TextSink *self
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext AKERR_NOIGNORE *counting_graphic(akbasic_TextSink *self, int mode, int split)
{
PREPARE_ERROR(errctx);
(void)self;
GRAPHICS += 1;
GRAPHIC_MODE = mode;
GRAPHIC_SPLIT = split;
SUCCEED_RETURN(errctx);
}
int main(void)
{
akbasic_TextSink primary;
@@ -163,11 +177,15 @@ int main(void)
gridded.clear = broken_clear;
gridded.moveto = counting_moveto;
gridded.window = counting_window;
gridded.grid = NULL;
gridded.graphic = counting_graphic;
MOVETOS = 0;
WINDOWS = 0;
GRAPHICS = 0;
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &gridded, NULL));
TEST_REQUIRE(tee.moveto != NULL, "a tee with a gridded half must offer moveto");
TEST_REQUIRE(tee.window != NULL, "a tee with a gridded half must offer window");
TEST_REQUIRE(tee.graphic != NULL, "a tee with a graphical half must offer graphic");
TEST_REQUIRE_OK(tee.moveto(&tee, 3, 4));
TEST_REQUIRE_OK(tee.window(&tee, 0, 20, 39, 24));
TEST_REQUIRE_INT(MOVETOS, 1);
@@ -176,6 +194,10 @@ int main(void)
TEST_REQUIRE_INT(WINDOW_ARGS[1], 20);
TEST_REQUIRE_INT(WINDOW_ARGS[2], 39);
TEST_REQUIRE_INT(WINDOW_ARGS[3], 24);
TEST_REQUIRE_OK(tee.graphic(&tee, 2, 19));
TEST_REQUIRE_INT(GRAPHICS, 1);
TEST_REQUIRE_INT(GRAPHIC_MODE, 2);
TEST_REQUIRE_INT(GRAPHIC_SPLIT, 19);
/* The gridded half being first is wired the same way round. */
WINDOWS = 0;
@@ -193,6 +215,10 @@ int main(void)
broken.writeln = broken_write;
broken.readline = NULL;
broken.clear = broken_clear;
broken.moveto = NULL;
broken.window = NULL;
broken.grid = NULL;
broken.graphic = NULL;
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &broken, NULL));
TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_IO);
TEST_REQUIRE_STATUS(tee.writeln(&tee, "X"), AKERR_IO);

View File

@@ -197,16 +197,17 @@ static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int
}
PASS(errctx, akbasic_runtime_load(&RUNTIME, SOURCE));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
/* The same layer bracket and composition order the standalone frontend uses. */
PASS(errctx, akbasic_graphics_akgl_begin(&GRAPHICS));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
PASS(errctx, akbasic_graphics_akgl_end(&GRAPHICS));
/*
* The drawing verbs are immediate and are already on the target. The text
* grid and the sprites are not: both are redrawn from state the interpreter
* keeps, which in the standalone frontend happens once a frame. There is no
* frame here, so this is it -- and the order is the frontend's, grid first
* and sprites over it, because that ordering is what a program written
* against the real host will have assumed.
* The drawing layer, text grid and sprites all redraw from retained state.
* Keep this in the frontend's order so a documentation image exercises the
* same GRAPHIC composition users run rather than a convenient direct path.
*/
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
if ( grid ) {
PASS(errctx, akbasic_sink_akgl_render(&SINK));
}