415 lines
14 KiB
C
415 lines
14 KiB
C
|
|
/**
|
||
|
|
* @file ui_akgl.c
|
||
|
|
* @brief Wires the UI backend record to libakgl's clay-backed widget helpers.
|
||
|
|
*
|
||
|
|
* The three helpers -- akgl_ui_dialog, akgl_ui_label and akgl_ui_menu -- and the
|
||
|
|
* frame bracket around them. Nothing here touches clay directly: a BASIC program
|
||
|
|
* gets the widgets, not the layout engine, and `CLAY()` blocks are the route a
|
||
|
|
* game written in C takes instead.
|
||
|
|
*
|
||
|
|
* **This file is where immediate mode meets a language that is not.** libakgl
|
||
|
|
* wants the widget set declared inside a bracket every frame and borrows the
|
||
|
|
* text until the bracket closes; a BASIC program says `MENU 1, "START"` on line
|
||
|
|
* 100 and expects it up on line 900. So the record's entry points are setters
|
||
|
|
* that copy into akbasic_AkglUi, and akbasic_ui_akgl_render() replays the whole
|
||
|
|
* set once a frame. The verbs never see a frame and the frame never sees a BASIC
|
||
|
|
* string.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <akgl/error.h>
|
||
|
|
#include <akgl/registry.h>
|
||
|
|
#include <akgl/text.h>
|
||
|
|
#include <akgl/ui.h>
|
||
|
|
|
||
|
|
#include <akbasic/akgl.h>
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Registry name the UI font is loaded under.
|
||
|
|
*
|
||
|
|
* Its own name rather than sharing the sink's, because the sink does not use the
|
||
|
|
* font registry at all -- it holds a `TTF_Font *` it opened itself -- and because
|
||
|
|
* a size is baked into a registered handle, so the UI's font is a different
|
||
|
|
* entry even when it is the same file.
|
||
|
|
*/
|
||
|
|
#define UI_FONT_NAME "akbasic.ui"
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Fail the build if the two menu ceilings ever disagree.
|
||
|
|
*
|
||
|
|
* include/akbasic/ui.h restates AKGL_UI_MENU_MAX_ITEMS rather than including an
|
||
|
|
* akgl header, which is correct and is also exactly the kind of restatement that
|
||
|
|
* rots. A mismatch would overflow akgl_UiMenu::items, so it is a negative array
|
||
|
|
* size here rather than a memory error at frame one.
|
||
|
|
*/
|
||
|
|
typedef char ui_menu_ceilings_agree[
|
||
|
|
(AKBASIC_UI_MAX_MENU_ITEMS == AKGL_UI_MENU_MAX_ITEMS) ? 1 : -1];
|
||
|
|
|
||
|
|
/** @brief Recover the backend's own state, or say that it has none. */
|
||
|
|
static akerr_ErrorContext *state_of(akbasic_UiBackend *self, akbasic_AkglUi **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in akgl UI backend");
|
||
|
|
*dest = (akbasic_AkglUi *)self->self;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"akgl UI backend has no state");
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief The style every widget draws with, or NULL for libakgl's own default. */
|
||
|
|
static akgl_UiStyle *look_of(akbasic_AkglUi *state)
|
||
|
|
{
|
||
|
|
return state->styled ? &state->style : NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ------------------------------------------------- the record's setters --- */
|
||
|
|
|
||
|
|
static akerr_ErrorContext *ui_dialog(akbasic_UiBackend *self, const char *text)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(self, &state));
|
||
|
|
if ( text == NULL || text[0] == '\0' ) {
|
||
|
|
state->dialogopen = false;
|
||
|
|
state->dialogtext[0] = '\0';
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
strncpy(state->dialogtext, text, sizeof(state->dialogtext) - 1);
|
||
|
|
state->dialogtext[sizeof(state->dialogtext) - 1] = '\0';
|
||
|
|
state->dialogopen = true;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *ui_label(akbasic_UiBackend *self, int slot, int anchor, const char *text)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(self, &state));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (slot >= 0 && slot < AKBASIC_UI_MAX_LABELS),
|
||
|
|
AKERR_OUTOFBOUNDS, "HUD slot %d is outside the backend's range", slot);
|
||
|
|
if ( text == NULL ) {
|
||
|
|
state->labelset[slot] = false;
|
||
|
|
state->labeltext[slot][0] = '\0';
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
strncpy(state->labeltext[slot], text, sizeof(state->labeltext[slot]) - 1);
|
||
|
|
state->labeltext[slot][sizeof(state->labeltext[slot]) - 1] = '\0';
|
||
|
|
state->labelanchor[slot] = anchor;
|
||
|
|
state->labelset[slot] = true;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *ui_menu(akbasic_UiBackend *self, int slot, const char *const *items, int count)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(self, &state));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (slot >= 0 && slot < AKBASIC_UI_MAX_MENUS),
|
||
|
|
AKERR_OUTOFBOUNDS, "MENU slot %d is outside the backend's range", slot);
|
||
|
|
FAIL_ZERO_RETURN(errctx, (count >= 0 && count <= AKBASIC_UI_MAX_MENU_ITEMS),
|
||
|
|
AKERR_OUTOFBOUNDS, "MENU entry count %d is outside 0..%d",
|
||
|
|
count, AKBASIC_UI_MAX_MENU_ITEMS);
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (count > 0 && items == NULL), AKERR_NULLPOINTER,
|
||
|
|
"MENU was given %d entries and no list", count);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Redefining resets the selection and the latch. The entries have just
|
||
|
|
* changed meaning, so an index into the old list is not worth carrying over
|
||
|
|
* and an unread activation of an entry that no longer exists is worse.
|
||
|
|
*/
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
FAIL_ZERO_RETURN(errctx, (items[i] != NULL), AKERR_NULLPOINTER,
|
||
|
|
"MENU entry %d is NULL", i + 1);
|
||
|
|
strncpy(state->menuitems[slot][i], items[i], sizeof(state->menuitems[slot][i]) - 1);
|
||
|
|
state->menuitems[slot][i][sizeof(state->menuitems[slot][i]) - 1] = '\0';
|
||
|
|
state->menus[slot].items[i] = state->menuitems[slot][i];
|
||
|
|
}
|
||
|
|
for ( i = count; i < AKBASIC_UI_MAX_MENU_ITEMS; i++ ) {
|
||
|
|
state->menus[slot].items[i] = NULL;
|
||
|
|
}
|
||
|
|
state->menus[slot].count = count;
|
||
|
|
state->menus[slot].selected = 0;
|
||
|
|
state->menus[slot].activated = false;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *ui_menu_state(akbasic_UiBackend *self, int slot, int *selected, bool *activated, bool clear)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(self, &state));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (selected != NULL && activated != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL destination in menu_state");
|
||
|
|
FAIL_ZERO_RETURN(errctx, (slot >= 0 && slot < AKBASIC_UI_MAX_MENUS),
|
||
|
|
AKERR_OUTOFBOUNDS, "MENU slot %d is outside the backend's range", slot);
|
||
|
|
|
||
|
|
*selected = (int)state->menus[slot].selected;
|
||
|
|
*activated = state->menus[slot].activated;
|
||
|
|
if ( clear ) {
|
||
|
|
state->menus[slot].activated = false;
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *ui_style(akbasic_UiBackend *self, akbasic_Color *fill, akbasic_Color *edge, akbasic_Color *ink, double padding, double radius)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(self, &state));
|
||
|
|
/*
|
||
|
|
* No colours means the library's default, and the way to say that is to stop
|
||
|
|
* having a style rather than to copy libakgl's values into ours. Copying them
|
||
|
|
* would make UISTYLE-with-no-arguments a snapshot of whatever the default was
|
||
|
|
* on the day this was written.
|
||
|
|
*/
|
||
|
|
if ( fill == NULL || edge == NULL || ink == NULL ) {
|
||
|
|
state->styled = false;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
state->style.fill = to_sdl(*fill);
|
||
|
|
state->style.edge = to_sdl(*edge);
|
||
|
|
state->style.ink = to_sdl(*ink);
|
||
|
|
state->style.padding = (float32_t)padding;
|
||
|
|
state->style.corner_radius = (float32_t)radius;
|
||
|
|
state->style.fontid = state->fontid;
|
||
|
|
state->styled = true;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *ui_clear(akbasic_UiBackend *self)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(self, &state));
|
||
|
|
state->dialogopen = false;
|
||
|
|
state->dialogtext[0] = '\0';
|
||
|
|
state->styled = false;
|
||
|
|
for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) {
|
||
|
|
state->labelset[i] = false;
|
||
|
|
state->labeltext[i][0] = '\0';
|
||
|
|
}
|
||
|
|
for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
|
||
|
|
state->menus[i].count = 0;
|
||
|
|
state->menus[i].selected = 0;
|
||
|
|
state->menus[i].activated = false;
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ------------------------------------------------------------- the frame --- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Declare every retained widget into the open frame.
|
||
|
|
*
|
||
|
|
* Its own function because it is three loops, and CATCH inside one escapes only
|
||
|
|
* the loop -- PASS is the only thing that may appear in here. The caller CATCHes
|
||
|
|
* this single call, which is what lets it close the bracket on the way out.
|
||
|
|
*
|
||
|
|
* Order is deliberate: labels, then the dialog, then the menus. A menu is the
|
||
|
|
* thing the player is being asked to act on, so nothing else should be able to
|
||
|
|
* overlap it.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *declare_widgets(akbasic_AkglUi *state)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akgl_UiStyle *look = look_of(state);
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) {
|
||
|
|
if ( state->labelset[i] ) {
|
||
|
|
PASS(errctx, akgl_ui_label(state->labelid[i], state->labeltext[i],
|
||
|
|
(akgl_UiAnchor)state->labelanchor[i], look));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ( state->dialogopen ) {
|
||
|
|
PASS(errctx, akgl_ui_dialog("dialog", state->dialogtext, look));
|
||
|
|
}
|
||
|
|
for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
|
||
|
|
if ( state->menus[i].count > 0 ) {
|
||
|
|
state->menus[i].style = look;
|
||
|
|
PASS(errctx, akgl_ui_menu(&state->menus[i]));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_ui_akgl_render(akbasic_UiBackend *obj)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
bool opened = false;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(obj, &state));
|
||
|
|
if ( !state->ready ) {
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_ui_frame_begin());
|
||
|
|
opened = true;
|
||
|
|
CATCH(errctx, declare_widgets(state));
|
||
|
|
/*
|
||
|
|
* Cleared before frame_end rather than after, because frame_end closes
|
||
|
|
* the bracket whether it succeeds or fails -- so a CLEANUP that closed it
|
||
|
|
* again would be closing somebody else's next frame.
|
||
|
|
*/
|
||
|
|
opened = false;
|
||
|
|
CATCH(errctx, akgl_ui_frame_end(state->renderer));
|
||
|
|
} CLEANUP {
|
||
|
|
if ( opened ) {
|
||
|
|
IGNORE(akgl_ui_frame_end(state->renderer));
|
||
|
|
}
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_ui_akgl_handle_event(akbasic_UiBackend *obj, SDL_Event *event, bool *consumed)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(obj, &state));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (event != NULL && consumed != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in ui_akgl_handle_event");
|
||
|
|
*consumed = false;
|
||
|
|
if ( !state->ready ) {
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* The subsystem first: it is mouse-only and never consumes a keystroke, so
|
||
|
|
* this is safe whatever else is going on.
|
||
|
|
*/
|
||
|
|
PASS(errctx, akgl_ui_handle_event(state, event, consumed));
|
||
|
|
if ( *consumed ) {
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Then each menu that has entries -- and these *do* take Up, Down and
|
||
|
|
* Return. That is why the loop is over menus with a count rather than over
|
||
|
|
* all four: a program with no menu up leaves the cursor keys and Return to
|
||
|
|
* the line editor, which is what makes the REPL usable at all.
|
||
|
|
*/
|
||
|
|
for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
|
||
|
|
if ( state->menus[i].count == 0 ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
PASS(errctx, akgl_ui_menu_handle_event(&state->menus[i], event, consumed));
|
||
|
|
if ( *consumed ) {
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ------------------------------------------------------------- lifecycle --- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_ui_init_akgl(akbasic_UiBackend *obj, akbasic_AkglUi *state, akgl_RenderBackend *renderer, const char *fontpath, int fontsize, int width, int height)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int count = 0;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL && fontpath != NULL),
|
||
|
|
AKERR_NULLPOINTER, "NULL argument in ui_init_akgl");
|
||
|
|
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL renderer in ui_init_akgl: the host creates it, not this");
|
||
|
|
FAIL_ZERO_RETURN(errctx, (fontsize > 0 && width > 0 && height > 0), AKBASIC_ERR_VALUE,
|
||
|
|
"A %dx%d UI at %d points is not a UI", width, height, fontsize);
|
||
|
|
PASS(errctx, akgl_error_init());
|
||
|
|
|
||
|
|
memset(state, 0, sizeof(*state));
|
||
|
|
state->renderer = renderer;
|
||
|
|
strncpy(state->fontname, UI_FONT_NAME, sizeof(state->fontname) - 1);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* The element ids, once. clay identifies an element by its string and keeps
|
||
|
|
* hover and scroll state against it between frames, so these have to be the
|
||
|
|
* same every frame -- which means they cannot be built on the stack of the
|
||
|
|
* function that declares them.
|
||
|
|
*/
|
||
|
|
for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) {
|
||
|
|
snprintf(state->labelid[i], sizeof(state->labelid[i]), "hud%d", i + 1);
|
||
|
|
}
|
||
|
|
for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
|
||
|
|
snprintf(state->menuid[i], sizeof(state->menuid[i]), "menu%d", i + 1);
|
||
|
|
state->menus[i].id = state->menuid[i];
|
||
|
|
}
|
||
|
|
(void)count;
|
||
|
|
|
||
|
|
/*
|
||
|
|
* **akgl_registry_init() does not initialize the font registry** -- its own
|
||
|
|
* header says so -- and nothing else in this repository has needed it,
|
||
|
|
* because the text sink opens its font with a bare TTF_OpenFont and holds
|
||
|
|
* the handle. akgl_ui_font_register() resolves a *registry name* per use, so
|
||
|
|
* the font has to go in there. It is idempotent.
|
||
|
|
*/
|
||
|
|
PASS(errctx, akgl_registry_init_font());
|
||
|
|
PASS(errctx, akgl_text_loadfont(state->fontname, (char *)fontpath, fontsize));
|
||
|
|
PASS(errctx, akgl_ui_init(width, height));
|
||
|
|
PASS(errctx, akgl_ui_font_register(state->fontname, &state->fontid));
|
||
|
|
state->ready = true;
|
||
|
|
|
||
|
|
obj->self = state;
|
||
|
|
obj->dialog = ui_dialog;
|
||
|
|
obj->label = ui_label;
|
||
|
|
obj->menu = ui_menu;
|
||
|
|
obj->menu_state = ui_menu_state;
|
||
|
|
obj->style = ui_style;
|
||
|
|
obj->clear = ui_clear;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
void akbasic_ui_akgl_shutdown(akbasic_UiBackend *obj)
|
||
|
|
{
|
||
|
|
akbasic_AkglUi *state = NULL;
|
||
|
|
|
||
|
|
if ( obj == NULL || obj->self == NULL ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
state = (akbasic_AkglUi *)obj->self;
|
||
|
|
if ( !state->ready ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
/*
|
||
|
|
* The subsystem first, then the font: anything still holding a clay layout
|
||
|
|
* would be talking to a disowned context after the shutdown, and the font is
|
||
|
|
* what its text commands resolve through. Both are ignorable -- this is
|
||
|
|
* called from teardown paths that are already unwinding, and a failure to
|
||
|
|
* close something down has nowhere useful to go.
|
||
|
|
*/
|
||
|
|
IGNORE(akgl_ui_shutdown());
|
||
|
|
IGNORE(akgl_text_unloadfont(state->fontname));
|
||
|
|
state->ready = false;
|
||
|
|
}
|