Files
akbasic/include/akbasic/akgl.h
Tachikoma 3342f2b569
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
Make GRAPHIC select the text plane
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 08:08:34 -04:00

732 lines
34 KiB
C

/**
* @file akgl.h
* @brief The libakgl-backed implementations of the sink and the three devices.
*
* Everything declared here lives in the separate `akbasic_akgl` target, which is
* the only part of this project that links SDL. The core library builds and its
* whole test suite runs on a machine with no SDL on it; that is why the records
* these initializers populate are plain function-pointer structs and why this
* header is the only one that includes a libakgl header.
*
* **The interpreter owns no window, no renderer and no event loop.** Every one
* of these takes something the host already created and draws or plays through
* it. None of them creates a device, and none of them pumps events.
*
* Each initializer calls akgl_error_init() first. It reserves libakgl's 256-262
* status band and names every AKGL_ERR_* code; akgl_game_init() calls it as its
* first statement, but a program driving subsystems directly -- which is exactly
* what an embedded interpreter does -- never goes through akgl_game_init() and
* has to call it itself. Skip it and every AKGL_ERR_* that reaches a stack trace
* prints "Unknown Error". It is idempotent, so a host that already called it
* loses nothing.
*/
#ifndef _AKBASIC_AKGL_H_
#define _AKBASIC_AKGL_H_
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/collision.h>
#include <akgl/renderer.h>
#include <akgl/sprite.h>
#include <akgl/ui.h>
#include <akgl/version.h>
/*
* **0.5.0 was a hard floor, API as well as ABI**, and it is worth keeping the
* reason: it namespaced every exported symbol, so the spellings this target
* compiles against did not exist before it. `akgl_render_bind2d` is
* `akgl_render_2d_bind`, `akgl_sprite_sheet_coords_for_frame` is
* `akgl_spritesheet_coords_for_frame`, and the `renderer`, `camera` and `window`
* globals carry the prefix -- with `_akgl_renderer` and `_akgl_camera` now
* `akgl_default_renderer` and `akgl_default_camera`.
*
* That rename was not cosmetic and libakgl's TODO.md says why at length: an
* exported global called `renderer` collided with a test's own variable of the
* same name, the executable's definition preempted the library's, and every
* texture load in that suite failed while the suite reported success.
*
* **0.6.0 and 0.7.0 broke nothing here**, and the floor moves anyway. 0.6.0 is
* three arcade-physics fixes and a `physics.max_timestep` property this target
* does not use; 0.7.0 reports failures libakstdlib's wrappers were already
* catching, and takes libakerror 2.0.1 so it can drop the exit-status trap its
* own suites needed -- the same defect `include/akbasic/error.h` guards for this
* band, fixed at the source rather than worked around twice.
*
* **0.8.0 broke nothing here either**, and is the largest of the three: a whole
* collision subsystem -- shapes, pooled proxies, a pluggable broad phase, a
* narrowphase answering with a contact -- none of which this target called on the
* day the floor moved. It is compatible by luck rather than by design, which is
* the argument for moving the floor rather than deciding for ourselves that a
* minor release was really compatible.
*
* Two things about it a reader here has to know. `akgl_Actor` grew fields, so
* its `sizeof` changed and a translation unit compiled against a 0.7 `actor.h`
* writes `renderfunc` and `actorData` at the wrong offsets -- and
* `src/sprite_akgl.c` writes exactly those two. The symptom is a jump through a
* garbage pointer on the first frame, which is precisely the case this guard
* exists to turn into a compile error. And libakgl's reserved status band grew
* from five codes to six with `AKGL_ERR_COLLISION`, taking it to 256 to 261 --
* 0.9.0 moved it again, below. `MAINTENANCE.md`'s coordinated range map and
* `docs/15-error-codes.md` both carry the current band.
*
* **0.9.0 is the first one that is additive on its face**, and the floor moves
* anyway. It is the `akgl_ui` subsystem -- a vendored clay, an arena, menus,
* HUDs and dialogs -- reached through a new `akgl/ui.h`, plus three new entry
* points in `akgl/draw.h` (`akgl_draw_filled_rounded_rect`, `akgl_draw_arc`,
* `akgl_draw_set_clip`). Every header this target already compiled against --
* `actor.h`, `sprite.h`, `renderer.h`, `text.h`, `registry.h`, `heap.h` -- is
* byte-identical to 0.8.0, so unlike 0.8.0 there is no `sizeof` to get wrong.
* The status band grew again regardless: `AKGL_ERR_UI` makes seven codes and
* libakgl now owns 256 to 262.
*
* `src/ui_akgl.c` reaches the widget helpers -- `akgl_ui_dialog`,
* `akgl_ui_label`, `akgl_ui_menu` and the frame bracket -- and nothing else.
* The raw clay route is deliberately not exposed to a BASIC program; see
* `docs/19-user-interface.md`.
*
* It also collided with the build rather than the code. 0.9.0 moved eight test
* property calls from `set_tests_properties` to `set_property(TEST ...)`, which
* errors on a test name that does not exist where the old command was silent --
* and this repository suppresses `add_test()` while it pulls its dependencies
* in, so those names do not exist. Eight configure failures, fixed by shadowing
* `set_property` in its `TEST` form too; `MAINTENANCE.md`'s embedding section
* carries the detail.
*
* The soname carries MAJOR.MINOR while the major is 0, so 0.5, 0.6 and 0.7 are
* different ABIs *by declaration* -- libakgl's versioning policy says a 0.x minor
* bump may break, and the soname is built to match. The floor moves with every
* one of them, because the alternative is deciding for ourselves which of
* libakgl's minor releases were really compatible, and that is exactly the
* judgement the soname exists to take away from us.
*
* What it catches is the case the soname cannot: a build against 0.6 headers
* that happens to find a 0.7 library, or the reverse. Refused here rather than
* at link time, because a missing symbol names a function and this names the
* release.
*/
#if !AKGL_VERSION_AT_LEAST(0, 9, 0)
#error "akbasic's libakgl adaptors require libakgl 0.9.0 or later"
#endif
#include <akbasic/audio.h>
#include <akbasic/graphics.h>
#include <akbasic/input.h>
#include <akbasic/sink.h>
#include <akbasic/sprite.h>
#include <akbasic/ui.h>
/**
* @brief One full cursor blink in milliseconds, half on and half off.
*
* Half a second, which is about what a Commodore does and is slow enough to read
* under.
*/
#define AKBASIC_SINK_CURSOR_BLINK_MS 500
/** @brief How many saved SSHAPE regions the graphics backend will hold at once. */
#define AKBASIC_AKGL_MAX_SHAPES 16
/**
* @brief One frame of the host's own loop, borrowed by the sink's line editor.
*
* The sink has to be able to wait for a typed line without owning an event loop,
* and those two requirements only meet in one place: the host hands over a
* callback that does exactly one frame -- pump events, redraw, present -- and
* the editor calls it between keystrokes. The loop is still the host's; the
* editor just borrows it a frame at a time.
*
* Setting @p running false is how the host says the window closed. The editor
* reports end of input rather than raising, because that is what running out of
* input means everywhere else in sink.h.
*
* @param self Whatever the host passed to akbasic_sink_akgl_set_pump().
* @param running Set false to stop waiting; the editor then reports EOF.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
typedef akerr_ErrorContext AKERR_NOIGNORE *(*akbasic_AkglPump)(void *self, bool *running);
/**
* @brief State for the libakgl-backed text sink.
*
* The cursor, the wrap and the scroll live here rather than in the interpreter:
* everything in the reference's basicruntime_graphics.go except Write and
* Println, which are the sink interface itself.
*/
typedef struct
{
akgl_RenderBackend *renderer;
TTF_Font *font;
SDL_Color color;
/**
* What a text cell is erased to before its glyphs are drawn. Opaque black
* by default, which is a Commodore console and is what makes the text layer
* *authoritative* over the rows it occupies rather than transparent over
* them -- see akbasic_sink_akgl_render().
*/
SDL_Color background;
int x; /* pixel origin of the text area */
int y;
int width; /* pixel size of the text area */
int height;
/*
* The whole drawable area, kept so WINDOW can grow back out to it. Without
* it a window could only ever shrink, since `x`/`width` have by then been
* overwritten with the current window's.
*/
int fullx;
int fully;
int fullwidth;
int fullheight;
int cellw; /* one character cell, measured from the font */
int cellh;
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;
/*
* The scrollback the sink redraws every frame. A fixed grid rather than a
* list of lines: the interpreter allocates nothing, and neither does this.
*/
char text[64][256];
/**
* Milliseconds for one full cursor blink, half on and half off. Zero holds
* the cursor solid, which is what a test wanting a deterministic frame sets.
*/
uint64_t cursorperiodms;
/*
* The line editor. Nothing here is live except while readline is running,
* and `editing` is what says so -- render() draws a cursor only then, and
* the scroll adjusts `editrow` only then.
*/
akbasic_AkglPump pump;
void *pumpself;
bool editing;
int editrow; /* where the line being typed starts */
int editcol;
int editlen; /* characters accepted so far */
int echolen; /* characters currently drawn, so a backspace */
/* knows how much to erase */
char editline[256];
} akbasic_AkglSink;
/**
* @brief State for the libakgl-backed graphics backend.
*
* The shape pool is why this exists at all. SSHAPE hands the BASIC program a
* handle rather than the pixels -- see TODO.md section 5 -- and these are the
* surfaces those handles refer to.
*/
typedef struct
{
akgl_RenderBackend *renderer;
SDL_Surface *shapes[AKBASIC_AKGL_MAX_SHAPES];
int shapecount;
/**
* The drawing layer: a texture the drawing verbs render into and the frame
* composites, so a drawing stays on screen without the program redrawing it.
*
* **Without this a drawing lasts exactly one frame.** The verbs are
* immediate and go to the renderer's back buffer, SDL double-buffers, and
* the frontend never clears -- so what a program drew is gone the moment the
* frame it was issued in is presented. The only way to keep a picture was to
* capture it with `SSHAPE` and install it as a sprite, which is what
* `examples/breakout/sprites/breakout.bas` spends two of its eight sprites
* doing and what TODO.md section 9 item 9 was filed about.
*
* NULL until the first drawing verb runs. A program that never draws pays
* for no texture, which matters: this is the size of the window.
*/
SDL_Texture *layer;
/**
* What the target was before the layer was made current, so it can be put
* back. The frontend brackets the whole step phase rather than each verb --
* one pair of SDL_SetRenderTarget calls a frame instead of one per DRAW.
*/
SDL_Texture *savedtarget;
bool layeractive;
} akbasic_AkglGraphics;
/**
* @brief State for the libakgl-backed sprite backend.
*
* One libakgl actor per BASIC sprite, plus the sheet, sprite and character each
* actor needs -- built by hand rather than loaded from a sprite document, since
* a Commodore sprite has no animation, no frame list and no state map to
* describe. Everything is pooled by libakgl; what is held here are the pointers
* and the one texture per slot that this backend owns and must destroy.
*
* `graphics` is the graphics backend's state, borrowed so `SPRSAV A$, n` can
* resolve a handle SSHAPE minted. The two devices are separate records on
* purpose -- a host may supply one and withhold the other -- so this is a
* pointer that may be NULL rather than an assumption that both exist.
*/
typedef struct
{
akgl_RenderBackend *renderer;
akbasic_AkglGraphics *graphics;
akgl_SpriteSheet *sheets[AKBASIC_MAX_SPRITES];
akgl_Sprite *sprites[AKBASIC_MAX_SPRITES];
akgl_Character *characters[AKBASIC_MAX_SPRITES];
akgl_Actor *actors[AKBASIC_MAX_SPRITES];
/** Per-axis expansion, which an actor's single `scale` cannot carry. */
bool xexpand[AKBASIC_MAX_SPRITES];
bool yexpand[AKBASIC_MAX_SPRITES];
/**
* One collision proxy per slot, claimed once at init and held for the life
* of the backend.
*
* Claimed up front rather than per scan for two reasons. The pool is shared
* with whatever host this interpreter is embedded in -- a game with its own
* shaped actors draws from the same #AKGL_MAX_HEAP_COLLISION_PROXY -- so
* running out is a real possibility, and it should be an init-time failure
* naming the pool rather than a collision scan that starts refusing halfway
* through a game. And a proxy carries a *copy* of its shape, so there is
* nothing to rebuild per scan beyond the position.
*
* These are not registered with any partitioner. akgl_collision_test()
* takes two positioned proxies and needs no world, which is what lets eight
* sprites be tested pairwise without paying for a broad phase that eight
* objects would not repay.
*/
akgl_CollisionProxy *proxies[AKBASIC_MAX_SPRITES];
/**
* The shape each proxy is synced from.
*
* Kept here rather than read back off the proxy because
* akgl_collision_proxy_sync() wants a live shape to copy from, and because
* the shape is derived from the sprite's frame and expansion bits every scan
* -- a sprite whose picture or expansion changed has a different box, and
* the cheapest correct thing is to rebuild it rather than track what
* invalidates it.
*/
akgl_CollisionShape shapes[AKBASIC_MAX_SPRITES];
/**
* The rectangle each proxy was last synced from, so a scan can tell whether
* there is anything to sync. See sync_proxy() in src/sprite_akgl.c for why
* this is compared rather than flagged.
*/
SDL_FRect syncedbox[AKBASIC_MAX_SPRITES];
/**
* SPRHIT's shape per slot, as the device was told it.
*
* A second copy of what akbasic_Sprite already carries, and deliberately: a
* device is told about changes and never asked what they were
* (include/akbasic/sprite.h), so the backend has to be able to answer from
* its own state rather than reaching back into the runtime it does not have
* a pointer to.
*/
int shapekind[AKBASIC_MAX_SPRITES];
float32_t shapex1[AKBASIC_MAX_SPRITES];
float32_t shapey1[AKBASIC_MAX_SPRITES];
float32_t shapex2[AKBASIC_MAX_SPRITES];
float32_t shapey2[AKBASIC_MAX_SPRITES];
bool shapeexplicit[AKBASIC_MAX_SPRITES];
/**
* SOLID's static geometry: a proxy and a shape per registered rectangle.
*
* **Not registered with any partitioner, and there is no
* `akgl_CollisionWorld` here at all.** libakgl's uniform grid keeps its cell
* heads, its cell size and its origin in file-scope statics
* (`deps/libakgl/src/collision_grid.c`), so it is one index per process --
* and `akgl_collision_world_init()` ends in a `reset()` that memsets those
* heads *and* calls `akgl_heap_init_collision_cells()`. An interpreter
* embedded in a game that has its own collision world would destroy every
* registration that game had made, on the first `SOLID` a script ran.
*
* So the geometry is indexed here, by an ordinary array, and pairs go
* straight to `akgl_collision_test()` -- which takes two positioned proxies
* and needs no world. At sixty-four rectangles that is the right answer
* anyway: libakgl's own measurements put a naive all-pairs sweep at 0.7% of
* a frame at sixty-four objects, and a broad phase is bookkeeping that only
* repays itself well above that.
*/
akgl_CollisionProxy *solidproxies[AKBASIC_MAX_SOLIDS];
akgl_CollisionShape solidshapes[AKBASIC_MAX_SOLIDS];
SDL_FRect solidbox[AKBASIC_MAX_SOLIDS];
bool solidactive[AKBASIC_MAX_SOLIDS];
/**
* What the last scan saw, so an unchanged scan can answer without redoing
* the work.
*
* The scan runs at the top of every interpreter step -- up to 256 times a
* rendered frame -- and its inputs are the sprites' boxes, which sprite
* slots are collidable, and the static geometry. A sprite moves at most once
* in that time. If none of those changed, the answer cannot have changed
* either, and eight rectangle comparisons say so far more cheaply than
* recomputing it.
*
* This is what makes static geometry affordable at all: eight sprites
* against sixty-four rectangles is five hundred and twelve tests, which is
* fine once a frame and is not fine two hundred and fifty-six times.
*/
SDL_FRect lastbox[AKBASIC_MAX_SPRITES];
bool lastlive[AKBASIC_MAX_SPRITES];
uint16_t lastmask;
uint16_t lastsolidmask;
bool lastvalid;
/**
* The deepest contact each sprite was in when the scan last ran.
*
* Kept as a side effect of the pass that computes the masks, because that is
* the only place the information exists -- `akgl_collision_test()` produces
* it and a caller that discards it cannot get it back. Deepest wins, because
* the deepest overlap is the one a program has to undo.
*/
akbasic_Contact contacts[AKBASIC_MAX_SPRITES];
bool hascontact[AKBASIC_MAX_SPRITES];
} akbasic_AkglSprites;
/**
* @brief Point a text sink at a renderer and a font the host already has.
*
* The character grid is derived by measuring one glyph with akgl_text_measure(),
* which is the direct equivalent of the reference's font.SizeUTF8("A") -- and
* which did not exist in libakgl until 42b60f7. A font that is not monospaced
* still works; the grid is then sized by whatever "A" happens to measure, and
* proportional glyphs simply do not line up in columns.
*
* @param obj Object to initialize, inspect, or modify.
* @param state Storage for the sink's own state; must outlive the sink.
* @param renderer The renderer the host already initialized; not created here.
* @param font An open font; not opened or closed here.
* @param w Width in pixels of the text area.
* @param h Height in pixels of the text area.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any pointer argument is NULL.
* @throws AKBASIC_ERR_VALUE When the font measures a zero-width cell, which would divide by zero.
* @throws AKBASIC_ERR_BOUNDS When the area is too small for even one character.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h);
/**
* @brief Draw whatever the sink currently holds.
*
* Separate from the sink's write path because the interpreter does not own the
* frame: a host calls this when it is drawing, not when the script happens to
* PRINT. A driver that only wants stdout never calls it at all.
*
* @param obj The sink to draw.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKGL_ERR_SDL When the renderer refuses the text.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_akgl_render(akbasic_TextSink *obj);
/**
* @brief Give the sink a line editor by lending it one frame of the host's loop.
*
* Without this, readline reports end of input immediately: a drawn text layer is
* not a source of lines, and a sink that blocked on the keyboard with no way to
* pump events would deadlock the process on its first INPUT. With it, readline
* collects keystrokes from libakgl's ring -- the one the host's own event pump
* fills -- echoes them into the grid, and returns the line on Return.
*
* **It is a real keyboard as of libakgl 0.3.0.** The ring carries the composed
* UTF-8 text SDL worked out from the keystroke, so shifted characters, a
* keyboard layout, a compose key and a dead key all do what the person typing
* expects -- including the double quote, without which a BASIC string literal
* cannot be typed at all. Until 0.3.0 the ring reported a keycode and nothing
* else, so none of that was reachable and letters were folded to upper case;
* that was libakgl API-gap item 10, filed from here.
*
* Composed text is taken over the keycode wherever there is any, which is what
* makes the layout work: on an AZERTY keyboard the key SDL calls `SDLK_Q`
* composes to "a", and "a" is what the program should see.
*
* Editing is Backspace to erase one character and Escape to abandon the line.
* There is no cursor movement within the line: the ring reports the arrow keys,
* but a script's own GET loop wants them.
*
* @param obj The sink to give an editor to.
* @param pump One frame of the host's loop, or NULL to take the editor away.
* @param self Passed back to @p pump untouched.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL or carries no sink state.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_akgl_set_pump(akbasic_TextSink *obj, akbasic_AkglPump pump, void *self);
/**
* @brief Point a graphics backend at a renderer the host already has.
* @param obj Object to initialize, inspect, or modify.
* @param state Storage for the shape pool; must outlive the backend.
* @param renderer The renderer the host already initialized; not created here.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any argument is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer);
/**
* @brief Make the drawing layer current, so what the program draws is kept.
*
* Called by a host **before** running the interpreter's steps, and paired with
* akbasic_graphics_akgl_end(). Between the two, every drawing verb renders into
* a texture that survives the frame instead of into a back buffer that does not.
*
* Bracketing the whole step phase rather than each verb is deliberate: it is one
* pair of SDL_SetRenderTarget calls a frame instead of one per `DRAW`, and it is
* also what makes `SSHAPE` read back what the program has just drawn rather than
* whatever the previous frame left.
*
* A host that never calls these gets the old behaviour -- drawings live one
* frame -- which is what `tools/screenshot.c` wants, since it presents nothing.
*
* @param obj The graphics backend, or NULL to do nothing.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL When the layer texture cannot be created or made current.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_akgl_begin(akbasic_GraphicsBackend *obj);
/**
* @brief Put the previous render target back. Pairs with akbasic_graphics_akgl_begin().
*
* @param obj The graphics backend, or NULL to do nothing.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL When the previous target cannot be restored.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_akgl_end(akbasic_GraphicsBackend *obj);
/**
* @brief Draw the layer onto the frame, under the text and the sprites.
*
* Called once a frame by a host, before the sink renders. Does nothing when no
* program has drawn anything, so a text-only program composites nothing.
*
* @param obj The graphics backend, or NULL to do nothing.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL When the layer cannot be drawn.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_akgl_render(akbasic_GraphicsBackend *obj);
/**
* @brief Is the drawing layer currently the render target?
*
* A host needs this because SDL refuses to present while a target is current,
* and the sink's line editor calls a host's pump from *inside* a step -- when
* the layer is on -- while the frame loop calls it between steps, when it is
* not. Answering the question is what lets one pump serve both.
*
* @param obj The graphics backend, or NULL for false.
*/
bool akbasic_graphics_akgl_layer_active(akbasic_GraphicsBackend *obj);
/** @brief Destroy the layer texture. Safe on a backend that never made one. */
void akbasic_graphics_akgl_shutdown(akbasic_GraphicsBackend *obj);
/**
* @brief Point a sprite backend at a renderer the host already has.
*
* Requires akgl_heap_init() and akgl_registry_init() to have run -- every
* akgl_*_initialize ends in a registry write -- and requires the global `camera`
* to point somewhere, because akgl_actor_render() dereferences it without
* checking. akgl_game_init() does all three; a host that skips it, which is
* every host embedding this interpreter, does them itself.
* src/frontend_akgl.c is the worked example.
*
* @param obj Object to initialize, inspect, or modify.
* @param state Storage for the sprite pool; must outlive the backend.
* @param renderer The renderer the host already initialized; not created here.
* @param graphics The graphics backend's state, so SPRSAV can resolve an SSHAPE handle; may be NULL.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj`, `state` or `renderer` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic_AkglSprites *state, akgl_RenderBackend *renderer, akbasic_AkglGraphics *graphics);
/**
* @brief Draw every defined, enabled sprite.
*
* Separate from the verbs for the same reason akbasic_sink_akgl_render() is: the
* interpreter does not own the frame. A host calls this when it is drawing.
*
* The actors are swept directly rather than through akgl_registry_iterate_actor(),
* which ends in FINISH_NORETURN -- an unhandled error inside it terminates the
* process, and goal 3 forbids anything in this library doing that. Sweeping our
* own eight also leaves a host's own actors alone.
*
* @param obj The backend to draw.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL or carries no state.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_akgl_render(akbasic_SpriteBackend *obj);
/**
* @brief Release every texture the sprite backend created.
*
* The actors, sprites, sheets and characters are libakgl's pooled objects and go
* back with akgl_heap_init(); the SDL textures behind the sheets are this
* backend's own and are not anybody else's to free.
*
* @param obj The backend to tear down. NULL is not an error.
*/
void akbasic_sprite_akgl_shutdown(akbasic_SpriteBackend *obj);
/**
* @brief Wire an audio backend to libakgl's tone generator.
*
* Calls akgl_audio_init(), which opens an SDL audio device. A host that owns its
* own audio pipeline can call akgl_audio_init() itself beforehand -- it is
* idempotent in the sense that mattered upstream: the voice table works whether
* or not a device is open.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKGL_ERR_SDL When no audio device can be opened.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_audio_init_akgl(akbasic_AudioBackend *obj);
/**
* @brief Wire an input backend to libakgl's keystroke ring.
*
* Reads only. The ring is filled by akgl_controller_handle_event(), which the
* *host* calls as it pumps SDL events -- so a script gets keystrokes without the
* interpreter owning the event loop.
*
* Worth knowing before lending this to a script: 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. A host that cares either
* withholds the input backend or supplies a filtered one of its own.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_input_init_akgl(akbasic_InputBackend *obj);
/**
* @brief State for the libakgl-backed UI backend.
*
* **This one is retained where the others are not, and that is the point.**
* libakgl's UI is immediate mode: widgets are declared inside a frame bracket
* and clay borrows their text until the bracket closes. A BASIC program declares
* a menu once and expects it to survive several hundred frames. So the verbs
* write here, and akbasic_ui_akgl_render() re-declares the lot once a frame from
* what it finds. The text is copied on the way in for the same reason.
*
* Element ids are generated once at init -- "menu1", "hud1" and so on -- because
* clay identifies an element by a string that has to be the same every frame for
* hover and scroll state to survive between them.
*/
typedef struct
{
akgl_RenderBackend *renderer;
char fontname[64]; /* registry name this loaded the UI font under */
uint16_t fontid; /* what clay calls it */
bool ready; /* akgl_ui_init succeeded and shutdown has not run */
/*
* The one style, and whether UISTYLE has set it. When it has not, every
* widget is handed NULL and gets libakgl's default rather than a copy of it
* made here -- so "never styled" and "styled back to the default" stay the
* same thing.
*/
akgl_UiStyle style;
bool styled;
char dialogtext[AKBASIC_MAX_STRING_LENGTH];
bool dialogopen;
char labelid[AKBASIC_UI_MAX_LABELS][8];
char labeltext[AKBASIC_UI_MAX_LABELS][AKBASIC_MAX_STRING_LENGTH];
int labelanchor[AKBASIC_UI_MAX_LABELS];
bool labelset[AKBASIC_UI_MAX_LABELS];
char menuid[AKBASIC_UI_MAX_MENUS][8];
char menuitems[AKBASIC_UI_MAX_MENUS][AKBASIC_UI_MAX_MENU_ITEMS][AKBASIC_MAX_STRING_LENGTH];
akgl_UiMenu menus[AKBASIC_UI_MAX_MENUS];
} akbasic_AkglUi;
/**
* @brief Bring the UI subsystem up and point a UI backend at it.
*
* Loads @p fontpath into the font registry under a name of its own and registers
* that name with clay. **It calls akgl_registry_init_font(), which
* akgl_registry_init() deliberately does not** -- nothing else in this
* repository has needed the font registry, because the text sink opens its font
* with a bare TTF_OpenFont.
*
* @warning akgl_ui_init() is process-global and refuses a second call. One UI
* per process: two runtimes in one process share it.
*
* @param obj Object to initialize, inspect, or modify.
* @param state Storage for the retained widget set; must outlive the backend.
* @param renderer The renderer the host already initialized; not created here.
* @param fontpath Font file the widgets draw with. Required.
* @param fontsize Point size to load it at.
* @param width Layout width, in pixels. Usually the window's.
* @param height Layout height, in pixels.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj`, `state`, `renderer` or `fontpath` is NULL.
* @throws AKBASIC_ERR_VALUE When `fontsize`, `width` or `height` is not positive.
* @throws AKGL_ERR_UI When the subsystem is already up, or clay refuses.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_ui_init_akgl(akbasic_UiBackend *obj, akbasic_AkglUi *state, akgl_RenderBackend *renderer, const char *fontpath, int fontsize, int width, int height);
/**
* @brief Declare and draw the whole retained widget set, once, for this frame.
*
* Separate from the verbs for the same reason akbasic_sprite_akgl_render() is:
* the interpreter does not own the frame. A host calls this when it is drawing,
* after everything else -- the UI goes on top.
*
* A frame that fails still closes its bracket, so one bad frame is one bad frame
* rather than a wedged subsystem.
*
* @param obj The backend to draw.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL or carries no state.
* @throws AKGL_ERR_UI When clay reported a layout error.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_ui_akgl_render(akbasic_UiBackend *obj);
/**
* @brief Offer one SDL event to the UI, and say whether it was taken.
*
* The mouse goes to the subsystem, which never consumes a keystroke. The
* keyboard and gamepad go to each menu that has entries, and those **do**
* consume Up, Down and Return -- so a host must route this before it feeds the
* keystroke ring, and a menu that is up owns those keys. Retiring the menu gives
* them back.
*
* @param obj The backend to offer the event to.
* @param event The event; not modified.
* @param consumed Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any argument is NULL, or `obj` carries no state.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_ui_akgl_handle_event(akbasic_UiBackend *obj, SDL_Event *event, bool *consumed);
/**
* @brief Take the UI subsystem down and unload the font it loaded.
*
* Idempotent, and never fails: it is called from teardown paths that are already
* unwinding. Call it before the renderer goes.
*
* @param obj The backend to shut down; NULL is a no-op.
*/
void akbasic_ui_akgl_shutdown(akbasic_UiBackend *obj);
#endif // _AKBASIC_AKGL_H_