Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
137
src/audio_akgl.c
Normal file
137
src/audio_akgl.c
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @file audio_akgl.c
|
||||
* @brief Wires the audio backend record to libakgl's tone generator.
|
||||
*
|
||||
* Another thin adaptor. The one thing in here worth reading is the waveform
|
||||
* mapping: akbasic_Waveform and akgl_AudioWaveform currently agree on every
|
||||
* value, and the mapping is still written out rather than cast across, because
|
||||
* two enumerations agreeing by accident is not a contract and a future SOUND
|
||||
* waveform number would break the coincidence silently.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/audio.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
/**
|
||||
* @brief Map a BASIC waveform onto libakgl's.
|
||||
*
|
||||
* @param waveform The BASIC-side selection.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKBASIC_ERR_BOUNDS When the waveform is not one of the four SOUND defines.
|
||||
*/
|
||||
static akerr_ErrorContext *to_akgl_waveform(akbasic_Waveform waveform, akgl_AudioWaveform *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
switch ( waveform ) {
|
||||
case AKBASIC_WAVE_TRIANGLE:
|
||||
*dest = AKGL_AUDIO_WAVE_TRIANGLE;
|
||||
break;
|
||||
case AKBASIC_WAVE_SAWTOOTH:
|
||||
*dest = AKGL_AUDIO_WAVE_SAWTOOTH;
|
||||
break;
|
||||
case AKBASIC_WAVE_SQUARE:
|
||||
*dest = AKGL_AUDIO_WAVE_SQUARE;
|
||||
break;
|
||||
case AKBASIC_WAVE_NOISE:
|
||||
*dest = AKGL_AUDIO_WAVE_NOISE;
|
||||
break;
|
||||
default:
|
||||
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "No such waveform %d", (int)waveform);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_tone(akbasic_AudioBackend *self, int voice, double hz, int ms)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
FAIL_ZERO_RETURN(errctx, (ms >= 0), AKERR_VALUE, "Negative note duration %d", ms);
|
||||
PASS(errctx, akgl_audio_tone(voice, (float32_t)hz, (uint32_t)ms));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_stop(akbasic_AudioBackend *self, int voice)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, akgl_audio_stop(voice));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_waveform(akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_AudioWaveform mapped = AKGL_AUDIO_WAVE_SQUARE;
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, to_akgl_waveform(waveform, &mapped));
|
||||
PASS(errctx, akgl_audio_waveform(voice, mapped));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_envelope(akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
FAIL_ZERO_RETURN(errctx, (attack >= 0 && decay >= 0 && release >= 0), AKERR_VALUE,
|
||||
"Negative envelope stage: %d/%d/%d", attack, decay, release);
|
||||
PASS(errctx, akgl_audio_envelope(voice, (uint32_t)attack, (uint32_t)decay,
|
||||
(float32_t)sustain, (uint32_t)release));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_volume(akbasic_AudioBackend *self, double level)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, akgl_audio_volume((float32_t)level));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_voice_active(akbasic_AudioBackend *self, int voice, bool *active)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, akgl_audio_voice_active(voice, active));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_audio_init_akgl(akbasic_AudioBackend *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||||
"NULL backend in audio_init_akgl");
|
||||
PASS(errctx, akgl_error_init());
|
||||
|
||||
/*
|
||||
* Opens an SDL audio device. A host that owns its own audio pipeline can
|
||||
* skip this entirely and pull samples with akgl_audio_mix() instead -- the
|
||||
* voice table works either way, which is the design upstream chose and the
|
||||
* reason its own suite can be deterministic.
|
||||
*/
|
||||
PASS(errctx, akgl_audio_init());
|
||||
|
||||
obj->self = NULL; /* the voice table is libakgl's, not ours */
|
||||
obj->tone = snd_tone;
|
||||
obj->stop = snd_stop;
|
||||
obj->waveform = snd_waveform;
|
||||
obj->envelope = snd_envelope;
|
||||
obj->volume = snd_volume;
|
||||
obj->voice_active = snd_voice_active;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
244
src/graphics_akgl.c
Normal file
244
src/graphics_akgl.c
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* @file graphics_akgl.c
|
||||
* @brief Wires the graphics backend record to libakgl's immediate-mode drawing.
|
||||
*
|
||||
* A thin adaptor and deliberately so: the interesting decisions about what a
|
||||
* BASIC graphics verb means live in src/runtime_graphics.c, and everything here
|
||||
* is coordinate and colour conversion plus the shape pool that SSHAPE hands out
|
||||
* handles into.
|
||||
*
|
||||
* The akgl_draw_* family is new in libakgl 42b60f7. Every one of its entry
|
||||
* points takes the akgl_RenderBackend the host already initialized rather than
|
||||
* reaching for a global, which is exactly what goal 3 needs.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
/** @brief The colour conversion, which is the whole impedance mismatch. */
|
||||
static SDL_Color to_sdl(akbasic_Color color)
|
||||
{
|
||||
SDL_Color out;
|
||||
|
||||
out.r = color.r;
|
||||
out.g = color.g;
|
||||
out.b = color.b;
|
||||
out.a = color.a;
|
||||
return out;
|
||||
}
|
||||
|
||||
/** @brief Recover the backend's own state, or say that it has none. */
|
||||
static akerr_ErrorContext *state_of(akbasic_GraphicsBackend *self, akbasic_AkglGraphics **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in akgl graphics backend");
|
||||
*dest = (akbasic_AkglGraphics *)self->self;
|
||||
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
|
||||
"akgl graphics backend has no state");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
PASS(errctx, akgl_draw_point(state->renderer, (float32_t)x, (float32_t)y, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
PASS(errctx, akgl_draw_line(state->renderer, (float32_t)x1, (float32_t)y1,
|
||||
(float32_t)x2, (float32_t)y2, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Turn two opposite corners into the rect SDL wants.
|
||||
*
|
||||
* BASIC gives two corners in whatever order the program felt like; SDL_FRect is
|
||||
* an origin and a size and does nothing useful with a negative one.
|
||||
*/
|
||||
static SDL_FRect corners_to_rect(double x1, double y1, double x2, double y2)
|
||||
{
|
||||
SDL_FRect rect;
|
||||
|
||||
rect.x = (float32_t)((x1 < x2) ? x1 : x2);
|
||||
rect.y = (float32_t)((y1 < y2) ? y1 : y2);
|
||||
rect.w = (float32_t)((x1 < x2) ? (x2 - x1) : (x1 - x2));
|
||||
rect.h = (float32_t)((y1 < y2) ? (y2 - y1) : (y1 - y2));
|
||||
return rect;
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
SDL_FRect rect;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
rect = corners_to_rect(x1, y1, x2, y2);
|
||||
PASS(errctx, akgl_draw_rect(state->renderer, &rect, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
SDL_FRect rect;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
rect = corners_to_rect(x1, y1, x2, y2);
|
||||
PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
/*
|
||||
* AKERR_OUTOFBOUNDS out of here means the fill exhausted its span stack and
|
||||
* left the region partly filled. It is passed straight through: PAINT is
|
||||
* where that is turned into something a BASIC program can see, because PAINT
|
||||
* is the only thing that knows a program is watching.
|
||||
*/
|
||||
PASS(errctx, akgl_draw_flood_fill(state->renderer, x, y, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_clear(akbasic_GraphicsBackend *self, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
SDL_FRect rect;
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx,
|
||||
SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, &w, &h),
|
||||
AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
/*
|
||||
* A filled rectangle over the output rather than SDL_RenderClear, because
|
||||
* clearing is the host's prerogative: a game that draws a background and
|
||||
* then hands the renderer to a script does not expect GRAPHIC to wipe it to
|
||||
* a colour of the script's choosing outside the area the script owns. This
|
||||
* covers exactly the render output and nothing beyond it.
|
||||
*/
|
||||
rect.x = 0.0f;
|
||||
rect.y = 0.0f;
|
||||
rect.w = (float32_t)w;
|
||||
rect.h = (float32_t)h;
|
||||
PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
SDL_Rect region;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER,
|
||||
"NULL handle in save_shape");
|
||||
FAIL_ZERO_RETURN(errctx, (state->shapecount < AKBASIC_AKGL_MAX_SHAPES),
|
||||
AKBASIC_ERR_DEVICE,
|
||||
"SSHAPE pool is full at %d saved regions; GRAPHIC CLR releases them",
|
||||
AKBASIC_AKGL_MAX_SHAPES);
|
||||
|
||||
region.x = (x1 < x2) ? x1 : x2;
|
||||
region.y = (y1 < y2) ? y1 : y2;
|
||||
region.w = (x1 < x2) ? (x2 - x1) : (x1 - x2);
|
||||
region.h = (y1 < y2) ? (y2 - y1) : (y1 - y2);
|
||||
|
||||
/*
|
||||
* NULL through dest asks akgl_draw_copy_region to allocate the surface. That
|
||||
* is the one allocation in this file and it belongs to the pool below, which
|
||||
* is bounded -- so it is a fixed ceiling on memory, not an open one.
|
||||
*/
|
||||
state->shapes[state->shapecount] = NULL;
|
||||
PASS(errctx, akgl_draw_copy_region(state->renderer, ®ion,
|
||||
&state->shapes[state->shapecount]));
|
||||
*handle = state->shapecount;
|
||||
state->shapecount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < state->shapecount),
|
||||
AKBASIC_ERR_BOUNDS, "No saved shape %d", handle);
|
||||
FAIL_ZERO_RETURN(errctx, (state->shapes[handle] != NULL), AKBASIC_ERR_STATE,
|
||||
"Saved shape %d has been released", handle);
|
||||
PASS(errctx, akgl_draw_paste_region(state->renderer, state->shapes[handle],
|
||||
(float32_t)x, (float32_t)y));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_free_shapes(akbasic_GraphicsBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
int i = 0;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
for ( i = 0; i < state->shapecount; i++ ) {
|
||||
if ( state->shapes[i] != NULL ) {
|
||||
SDL_DestroySurface(state->shapes[i]);
|
||||
state->shapes[i] = NULL;
|
||||
}
|
||||
}
|
||||
state->shapecount = 0;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in graphics_init_akgl");
|
||||
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
|
||||
"NULL renderer in graphics_init_akgl: the host creates it, not this");
|
||||
PASS(errctx, akgl_error_init());
|
||||
|
||||
memset(state, 0, sizeof(*state));
|
||||
state->renderer = renderer;
|
||||
|
||||
obj->self = state;
|
||||
obj->point = gfx_point;
|
||||
obj->line = gfx_line;
|
||||
obj->rect = gfx_rect;
|
||||
obj->filled_rect = gfx_filled_rect;
|
||||
obj->paint = gfx_paint;
|
||||
obj->clear = gfx_clear;
|
||||
obj->save_shape = gfx_save_shape;
|
||||
obj->paste_shape = gfx_paste_shape;
|
||||
obj->free_shapes = gfx_free_shapes;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
69
src/input_akgl.c
Normal file
69
src/input_akgl.c
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file input_akgl.c
|
||||
* @brief Wires the input backend record to libakgl's keystroke ring.
|
||||
*
|
||||
* The thinnest of the four adaptors, and the one whose *shape* carries the
|
||||
* design: akgl_controller_poll_key() reads a ring that
|
||||
* akgl_controller_handle_event() fills as the **host** pumps SDL events. The
|
||||
* interpreter therefore answers "is there a key waiting" without owning an event
|
||||
* loop, which is the whole of goal 3 for input.
|
||||
*
|
||||
* The caveat a host needs, repeated here because this is the file that causes
|
||||
* it: that ring is process-global and the host's own control maps read the same
|
||||
* events. A script sitting in a GET loop drains keystrokes the game will then
|
||||
* never see. Withhold the backend, or supply a filtered one.
|
||||
*/
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* 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, so on its own it does not compile. Not a
|
||||
* workaround anybody should copy -- filed upstream against libakgl, whose own
|
||||
* style rules call for self-contained headers. Delete this note and the include
|
||||
* when controller.h includes what it uses.
|
||||
*/
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
static akerr_ErrorContext *in_poll_key(akbasic_InputBackend *self, int *keycode, bool *available)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
/*
|
||||
* An empty ring comes back as success with available false, both here and
|
||||
* upstream. Passed through unchanged: GET reports it as the empty string,
|
||||
* which is ordinary BASIC rather than a failure.
|
||||
*/
|
||||
PASS(errctx, akgl_controller_poll_key(keycode, available));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *in_flush_keys(akbasic_InputBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, akgl_controller_flush_keys());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_input_init_akgl(akbasic_InputBackend *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||||
"NULL backend in input_init_akgl");
|
||||
PASS(errctx, akgl_error_init());
|
||||
|
||||
obj->self = NULL; /* the ring is libakgl's, not ours */
|
||||
obj->poll_key = in_poll_key;
|
||||
obj->flush_keys = in_flush_keys;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
240
src/sink_akgl.c
Normal file
240
src/sink_akgl.c
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* @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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/text.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
/** @brief Rows of scrollback the sink keeps. Matches the `text` array in akgl.h. */
|
||||
#define SINK_MAX_ROWS 64
|
||||
/** @brief Bytes per row, including the terminator. */
|
||||
#define SINK_MAX_COLUMNS 256
|
||||
|
||||
/** @brief Scroll the grid up by one row, dropping the top one. */
|
||||
static void scroll(akbasic_AkglSink *state)
|
||||
{
|
||||
int row = 0;
|
||||
|
||||
for ( row = 0; row < SINK_MAX_ROWS - 1; row++ ) {
|
||||
memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS);
|
||||
}
|
||||
memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS);
|
||||
if ( state->cursorrow > 0 ) {
|
||||
state->cursorrow -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Move to the start of the next row, scrolling if that runs off the end. */
|
||||
static void newline(akbasic_AkglSink *state)
|
||||
{
|
||||
state->cursorcol = 0;
|
||||
state->cursorrow += 1;
|
||||
if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) {
|
||||
scroll(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Put one character at the cursor, wrapping and scrolling as needed.
|
||||
*
|
||||
* Wrapping happens here, on the character grid, rather than being left to
|
||||
* SDL_ttf's own wraplength. The cursor has to end up somewhere definite: a
|
||||
* program that PRINTs a long string and then PRINTs again expects the second one
|
||||
* to start on the row after the first one ended, and only the code that placed
|
||||
* the characters knows which row that is.
|
||||
*/
|
||||
static void putchar_at(akbasic_AkglSink *state, char c)
|
||||
{
|
||||
if ( c == '\n' ) {
|
||||
newline(state);
|
||||
return;
|
||||
}
|
||||
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
|
||||
newline(state);
|
||||
}
|
||||
state->text[state->cursorrow][state->cursorcol] = c;
|
||||
state->cursorcol += 1;
|
||||
state->text[state->cursorrow][state->cursorcol] = '\0';
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSink *state = NULL;
|
||||
size_t i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in akgl sink write");
|
||||
state = (akbasic_AkglSink *)self->self;
|
||||
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
||||
"akgl sink has no state");
|
||||
for ( i = 0; text[i] != '\0'; i++ ) {
|
||||
putchar_at(state, text[i]);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSink *state = NULL;
|
||||
|
||||
PASS(errctx, sink_write(self, text));
|
||||
state = (akbasic_AkglSink *)self->self;
|
||||
newline(state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in akgl sink readline");
|
||||
/*
|
||||
* A drawn text layer is not a source of lines. INPUT through a graphics sink
|
||||
* wants a line editor built on the keystroke ring, which is its own piece of
|
||||
* work and is not done: see TODO.md section 3. Until it exists this reports
|
||||
* end of input rather than pretending to have read something.
|
||||
*
|
||||
* EOF rather than an error, because that is the contract sink.h states:
|
||||
* running off the end of input is how RUNSTREAM mode finishes normally, and
|
||||
* INPUT already handles it.
|
||||
*/
|
||||
if ( len > 0 ) {
|
||||
dest[0] = '\0';
|
||||
}
|
||||
*eof = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *sink_clear(akbasic_TextSink *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSink *state = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER,
|
||||
"NULL sink in akgl sink clear");
|
||||
state = (akbasic_AkglSink *)self->self;
|
||||
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
||||
"akgl sink has no state");
|
||||
memset(state->text, 0, sizeof(state->text));
|
||||
state->cursorcol = 0;
|
||||
state->cursorrow = 0;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int cellw = 0;
|
||||
int cellh = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in sink_init_akgl");
|
||||
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
|
||||
"NULL renderer in sink_init_akgl: the host creates it, not the sink");
|
||||
FAIL_ZERO_RETURN(errctx, (font != NULL), AKERR_NULLPOINTER,
|
||||
"NULL font in sink_init_akgl");
|
||||
|
||||
/*
|
||||
* Before anything else in libakgl. akgl_game_init() would have done it, but
|
||||
* an embedded interpreter drives subsystems directly and never calls that --
|
||||
* and a code raised before this runs carries no name into its stack trace.
|
||||
* Idempotent, so a host that already called it loses nothing.
|
||||
*/
|
||||
PASS(errctx, akgl_error_init());
|
||||
|
||||
/*
|
||||
* The character grid, measured rather than assumed. Direct equivalent of the
|
||||
* reference's font.SizeUTF8("A"), and it needs no renderer -- which is why
|
||||
* the sink can size itself before a frame has ever been drawn.
|
||||
*/
|
||||
PASS(errctx, akgl_text_measure(font, "A", &cellw, &cellh));
|
||||
FAIL_ZERO_RETURN(errctx, (cellw > 0 && cellh > 0), AKBASIC_ERR_VALUE,
|
||||
"Font measures a %dx%d character cell, which cannot be a grid",
|
||||
cellw, cellh);
|
||||
FAIL_ZERO_RETURN(errctx, (w >= cellw && h >= cellh), AKBASIC_ERR_BOUNDS,
|
||||
"A %dx%d text area has no room for a %dx%d character",
|
||||
w, h, cellw, cellh);
|
||||
|
||||
memset(state, 0, sizeof(*state));
|
||||
state->renderer = renderer;
|
||||
state->font = font;
|
||||
state->color.r = 0xff;
|
||||
state->color.g = 0xff;
|
||||
state->color.b = 0xff;
|
||||
state->color.a = 0xff;
|
||||
state->x = 0;
|
||||
state->y = 0;
|
||||
state->width = w;
|
||||
state->height = h;
|
||||
state->cellw = cellw;
|
||||
state->cellh = cellh;
|
||||
state->columns = w / cellw;
|
||||
state->rows = h / cellh;
|
||||
if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
|
||||
state->columns = SINK_MAX_COLUMNS - 1;
|
||||
}
|
||||
if ( state->rows > SINK_MAX_ROWS ) {
|
||||
state->rows = SINK_MAX_ROWS;
|
||||
}
|
||||
|
||||
obj->self = state;
|
||||
obj->write = sink_write;
|
||||
obj->writeln = sink_writeln;
|
||||
obj->readline = sink_readline;
|
||||
obj->clear = sink_clear;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSink *state = NULL;
|
||||
int row = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||||
"NULL sink in sink_akgl_render");
|
||||
state = (akbasic_AkglSink *)obj->self;
|
||||
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
||||
"akgl sink has no state");
|
||||
|
||||
/*
|
||||
* A loop, so no CATCH and no _BREAK macros: they expand to a C break, which
|
||||
* would escape the loop with an error still pending. PASS only.
|
||||
*
|
||||
* Wrapping is off at the draw -- a zero wraplength -- because the grid above
|
||||
* already decided where every line ends. Letting SDL_ttf wrap again would
|
||||
* put characters somewhere the cursor does not think they are.
|
||||
*/
|
||||
for ( row = 0; row < state->rows; row++ ) {
|
||||
if ( state->text[row][0] == '\0' ) {
|
||||
continue;
|
||||
}
|
||||
PASS(errctx, akgl_text_rendertextat(state->font, state->text[row],
|
||||
state->color, 0,
|
||||
state->x,
|
||||
state->y + (row * state->cellh)));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
Reference in New Issue
Block a user