Make GRAPHIC select the text plane
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m22s
akbasic CI Build / sanitizers (push) Failing after 4m43s
akbasic CI Build / coverage (push) Failing after 3m43s
akbasic CI Build / akgl_build (push) Failing after 4m48s
akbasic CI Build / mutation_test (push) Failing after 3m31s
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m22s
akbasic CI Build / sanitizers (push) Failing after 4m43s
akbasic CI Build / coverage (push) Failing after 3m43s
akbasic CI Build / akgl_build (push) Failing after 4m48s
akbasic CI Build / mutation_test (push) Failing after 3m31s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||

|
||||
@@ -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
|
||||
```
|
||||
|
||||

|
||||
@@ -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
|
||||
```
|
||||
|
||||

|
||||
@@ -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
|
||||
```
|
||||
|
||||

|
||||
@@ -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
|
||||
```
|
||||
|
||||

|
||||
@@ -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
|
||||
```
|
||||
|
||||

|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user