610 lines
20 KiB
C
610 lines
20 KiB
C
|
|
/**
|
||
|
|
* @file uidemo.c
|
||
|
|
* @brief The UI demo: a title menu, an options screen, and a HUD, three ways.
|
||
|
|
*
|
||
|
|
* The chapter on the UI subsystem quotes this program rather than restating
|
||
|
|
* it. Run it with no arguments for a window:
|
||
|
|
*
|
||
|
|
* ./examples/uidemo/uidemo
|
||
|
|
*
|
||
|
|
* The title menu answers to the arrow keys, Return, the D-pad, and the mouse.
|
||
|
|
* Start opens a stand-in play screen -- a checkerboard where a game would be
|
||
|
|
* -- with a score counter ticking in a HUD label; Space opens and closes a
|
||
|
|
* dialog panel there, and Escape backs out. Options is a screen written in
|
||
|
|
* raw CLAY() declarations, because the widgets are a convenience, not a
|
||
|
|
* boundary. `--frames N` bounds the run and `--demo` drives the whole tour
|
||
|
|
* from a script, which is what makes this runnable as a headless smoke test.
|
||
|
|
*
|
||
|
|
* What is deliberately absent: a tilemap, actors, physics. The world here is
|
||
|
|
* one draw call, so everything left is the subject -- what a UI costs and
|
||
|
|
* where it goes in a frame.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <stdbool.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <SDL3/SDL.h>
|
||
|
|
#include <SDL3_image/SDL_image.h>
|
||
|
|
#include <akerror.h>
|
||
|
|
#include <akstdlib.h>
|
||
|
|
|
||
|
|
#include <akgl/draw.h>
|
||
|
|
#include <akgl/error.h>
|
||
|
|
#include <akgl/game.h>
|
||
|
|
#include <akgl/registry.h>
|
||
|
|
#include <akgl/renderer.h>
|
||
|
|
#include <akgl/text.h>
|
||
|
|
#include <akgl/ui.h>
|
||
|
|
|
||
|
|
#include "uidemo.h"
|
||
|
|
|
||
|
|
/** @brief Milliseconds one 60 Hz frame is allowed to take, for the interactive loop. */
|
||
|
|
#define UIDEMO_FRAME_BUDGET_MS 16
|
||
|
|
|
||
|
|
/*
|
||
|
|
* The scripted tour `--demo` drives. Every route through the demo, by every
|
||
|
|
* input device it supports: the mouse clicks the title menu open (dead centre
|
||
|
|
* lands on the middle row by symmetry -- the menu is centred and Options is
|
||
|
|
* its middle entry), the keyboard walks back out, into the play screen,
|
||
|
|
* through the dialog, and finally down to Quit.
|
||
|
|
*
|
||
|
|
* Frame Event Key / position What it proves
|
||
|
|
*/
|
||
|
|
static const uidemo_ScriptStep UIDEMO_SCRIPT[] = {
|
||
|
|
{ 5, SDL_EVENT_MOUSE_MOTION, 0, 320.0f, 240.0f }, /* hover the menu */
|
||
|
|
{ 10, SDL_EVENT_MOUSE_BUTTON_DOWN, 0, 320.0f, 240.0f }, /* consumed click */
|
||
|
|
{ 11, SDL_EVENT_MOUSE_BUTTON_UP, 0, 320.0f, 240.0f }, /* -> Options screen */
|
||
|
|
{ 40, SDL_EVENT_KEY_DOWN, SDLK_ESCAPE, 0.0f, 0.0f }, /* back to the title */
|
||
|
|
{ 50, SDL_EVENT_KEY_DOWN, SDLK_UP, 0.0f, 0.0f }, /* select Start */
|
||
|
|
{ 60, SDL_EVENT_KEY_DOWN, SDLK_RETURN, 0.0f, 0.0f }, /* -> play screen */
|
||
|
|
{ 80, SDL_EVENT_KEY_DOWN, SDLK_SPACE, 0.0f, 0.0f }, /* open the dialog */
|
||
|
|
{ 120, SDL_EVENT_KEY_DOWN, SDLK_SPACE, 0.0f, 0.0f }, /* close it again */
|
||
|
|
{ 140, SDL_EVENT_KEY_DOWN, SDLK_ESCAPE, 0.0f, 0.0f }, /* back to the title */
|
||
|
|
{ 150, SDL_EVENT_KEY_DOWN, SDLK_DOWN, 0.0f, 0.0f }, /* down to Options */
|
||
|
|
{ 160, SDL_EVENT_KEY_DOWN, SDLK_DOWN, 0.0f, 0.0f }, /* down to Quit */
|
||
|
|
{ 170, SDL_EVENT_KEY_DOWN, SDLK_RETURN, 0.0f, 0.0f } /* and out */
|
||
|
|
};
|
||
|
|
|
||
|
|
#define UIDEMO_SCRIPT_STEPS (sizeof(UIDEMO_SCRIPT) / sizeof(UIDEMO_SCRIPT[0]))
|
||
|
|
|
||
|
|
static long frame_limit = 0;
|
||
|
|
/** @brief Where `--screenshot` writes, and on which frame. NULL means never. */
|
||
|
|
static char *shotpath = NULL;
|
||
|
|
static long shotframe = 0;
|
||
|
|
static bool demo = false;
|
||
|
|
static bool running = true;
|
||
|
|
static int exitstatus = 0;
|
||
|
|
static bool lowfps_warned = false;
|
||
|
|
|
||
|
|
/** @brief Which screen the frame declares. */
|
||
|
|
static uidemo_State state = UIDEMO_STATE_TITLE;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief The title menu. Caller-owned state, zero machinery: this struct and
|
||
|
|
* the akgl_ui_menu call each frame are the whole main menu.
|
||
|
|
*/
|
||
|
|
static akgl_UiMenu title_menu = {
|
||
|
|
.id = "title",
|
||
|
|
.items = { "Start", "Options", "Quit" },
|
||
|
|
.count = 3,
|
||
|
|
};
|
||
|
|
|
||
|
|
/** @brief The options screen's two settings. What the toggles flip. */
|
||
|
|
static bool opt_music = true;
|
||
|
|
static bool opt_sound = true;
|
||
|
|
|
||
|
|
/** @brief The play screen's HUD numbers. The score ticks so the label visibly updates. */
|
||
|
|
static int score = 0;
|
||
|
|
static int lives = 3;
|
||
|
|
/** @brief Whether the play screen's dialog is up. Space flips it. */
|
||
|
|
static bool dialog_open = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A left click arrived this frame, wherever it landed.
|
||
|
|
*
|
||
|
|
* The raw CLAY() options screen needs a press edge to pair with
|
||
|
|
* Clay_Hovered(), and the application is the right owner of it: it sees
|
||
|
|
* every event before the UI does. Set in route_event, cleared at the end of
|
||
|
|
* each frame. The widget helpers keep their own edge internally -- this one
|
||
|
|
* exists precisely because the options screen does not use them.
|
||
|
|
*/
|
||
|
|
static bool clicked = false;
|
||
|
|
|
||
|
|
/** @brief Replacement `akgl_game.lowfpsfunc`: say it once, not sixty times a second. */
|
||
|
|
static void lowfps_quiet(void)
|
||
|
|
{
|
||
|
|
if ( lowfps_warned == false ) {
|
||
|
|
lowfps_warned = true;
|
||
|
|
SDL_Log("Frame rate is under 30 and this demo does nothing about it");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Read `--frames N`, `--demo` and the screenshot flags off the command line.
|
||
|
|
*
|
||
|
|
* @param argc Argument count, from `main`.
|
||
|
|
* @param argv Argument vector, from `main`. Required.
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
* @throws AKERR_NULLPOINTER If @p argv is `NULL`.
|
||
|
|
* @throws AKERR_VALUE If a flag needing an argument is last, or its argument
|
||
|
|
* is not a number.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *parse_args(int argc, char *argv[])
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int i = 0;
|
||
|
|
int number = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, argv, AKERR_NULLPOINTER, "argv");
|
||
|
|
|
||
|
|
for ( i = 1; i < argc; i++ ) {
|
||
|
|
if ( strcmp(argv[i], "--demo") == 0 ) {
|
||
|
|
demo = true;
|
||
|
|
} else if ( strcmp(argv[i], "--frames") == 0 ) {
|
||
|
|
if ( (i + 1) >= argc ) {
|
||
|
|
FAIL_RETURN(errctx, AKERR_VALUE, "--frames needs a frame count");
|
||
|
|
}
|
||
|
|
i += 1;
|
||
|
|
PASS(errctx, aksl_atoi(argv[i], &number));
|
||
|
|
frame_limit = number;
|
||
|
|
} else if ( strcmp(argv[i], "--screenshot") == 0 ) {
|
||
|
|
if ( (i + 1) >= argc ) {
|
||
|
|
FAIL_RETURN(errctx, AKERR_VALUE, "--screenshot needs a path");
|
||
|
|
}
|
||
|
|
i += 1;
|
||
|
|
shotpath = argv[i];
|
||
|
|
} else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) {
|
||
|
|
if ( (i + 1) >= argc ) {
|
||
|
|
FAIL_RETURN(errctx, AKERR_VALUE, "--screenshot-frame needs a number");
|
||
|
|
}
|
||
|
|
i += 1;
|
||
|
|
PASS(errctx, aksl_atoi(argv[i], &number));
|
||
|
|
shotframe = number;
|
||
|
|
} else {
|
||
|
|
FAIL_RETURN(errctx, AKERR_VALUE,
|
||
|
|
"usage: uidemo [--frames N] [--demo]"
|
||
|
|
" [--screenshot PATH] [--screenshot-frame N]");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Bring the library and the UI subsystem up.
|
||
|
|
*
|
||
|
|
* The UI half is three calls: load a font, akgl_ui_init at the screen size,
|
||
|
|
* and register the font for a clay fontId. The id comes back 0 because it is
|
||
|
|
* the first registered, which is what the widgets' default style uses -- so
|
||
|
|
* nothing here ever mentions a fontId again.
|
||
|
|
*
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *startup(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
uint16_t fontid = 0;
|
||
|
|
|
||
|
|
PASS(errctx, aksl_strncpy(akgl_game.name, sizeof(akgl_game.name), "libakgl UI demo", sizeof(akgl_game.name) - 1));
|
||
|
|
PASS(errctx, aksl_strncpy(akgl_game.version, sizeof(akgl_game.version), "1.0.0", sizeof(akgl_game.version) - 1));
|
||
|
|
PASS(errctx, aksl_strncpy(akgl_game.uri, sizeof(akgl_game.uri), "net.aklabs.libakgl.examples.uidemo", sizeof(akgl_game.uri) - 1));
|
||
|
|
|
||
|
|
PASS(errctx, akgl_game_init());
|
||
|
|
akgl_game.lowfpsfunc = &lowfps_quiet;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_set_property("game.screenwidth", UIDEMO_SCREEN_WIDTH));
|
||
|
|
PASS(errctx, akgl_set_property("game.screenheight", UIDEMO_SCREEN_HEIGHT));
|
||
|
|
PASS(errctx, akgl_render_2d_init(akgl_renderer));
|
||
|
|
|
||
|
|
PASS(errctx, akgl_text_loadfont(UIDEMO_FONT_NAME, UIDEMO_FONT_FILE, UIDEMO_FONT_SIZE));
|
||
|
|
PASS(errctx, akgl_ui_init(UIDEMO_WIDTH, UIDEMO_HEIGHT));
|
||
|
|
PASS(errctx, akgl_ui_font_register(UIDEMO_FONT_NAME, &fontid));
|
||
|
|
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Route one event: the UI first, then whatever the current screen wants.
|
||
|
|
*
|
||
|
|
* This is the call-order contract from akgl/ui.h in the flesh. The UI gets
|
||
|
|
* first refusal; a consumed event goes no further, which is what keeps a
|
||
|
|
* click on a menu from also being a click in the game. Only then does the
|
||
|
|
* current screen read the keyboard -- and *that* routing is the whole focus
|
||
|
|
* model: the title menu hears keys because this function sends them there
|
||
|
|
* while the title screen is up, not because anything owns "focus".
|
||
|
|
*
|
||
|
|
* @param event The event to route. Required.
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *route_event(SDL_Event *event)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
bool consumed = false;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
|
||
|
|
|
||
|
|
if ( event->type == SDL_EVENT_QUIT ) {
|
||
|
|
running = false;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
// The options screen's press edge, recorded before the UI can consume
|
||
|
|
// the event -- a click on a toggle row is *always* consumed (the row is
|
||
|
|
// UI), so waiting until afterwards would record nothing.
|
||
|
|
if ( (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
|
||
|
|
&& (event->button.button == SDL_BUTTON_LEFT) ) {
|
||
|
|
clicked = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
PASS(errctx, akgl_ui_handle_event((void *)&akgl_game.state, event, &consumed));
|
||
|
|
if ( consumed ) {
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
switch ( state ) {
|
||
|
|
case UIDEMO_STATE_TITLE:
|
||
|
|
PASS(errctx, akgl_ui_menu_handle_event(&title_menu, event, &consumed));
|
||
|
|
break;
|
||
|
|
case UIDEMO_STATE_OPTIONS:
|
||
|
|
if ( (event->type == SDL_EVENT_KEY_DOWN) && (event->key.key == SDLK_ESCAPE) ) {
|
||
|
|
state = UIDEMO_STATE_TITLE;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case UIDEMO_STATE_PLAY:
|
||
|
|
if ( event->type == SDL_EVENT_KEY_DOWN ) {
|
||
|
|
if ( event->key.key == SDLK_ESCAPE ) {
|
||
|
|
state = UIDEMO_STATE_TITLE;
|
||
|
|
} else if ( event->key.key == SDLK_SPACE ) {
|
||
|
|
dialog_open = !dialog_open;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Feed the scripted events due on this frame through the real routing.
|
||
|
|
*
|
||
|
|
* Synthesized SDL_Events, not direct state changes: the demo run proves the
|
||
|
|
* event chain -- consumed and not, menu and screen -- because it travels it.
|
||
|
|
*
|
||
|
|
* @param frameno The frame about to be drawn.
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *demo_step(long frameno)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
SDL_Event event;
|
||
|
|
size_t i = 0;
|
||
|
|
|
||
|
|
for ( i = 0; i < UIDEMO_SCRIPT_STEPS; i++ ) {
|
||
|
|
if ( UIDEMO_SCRIPT[i].frame != frameno ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
PASS(errctx, aksl_memset((void *)&event, 0x00, sizeof(event)));
|
||
|
|
event.type = UIDEMO_SCRIPT[i].type;
|
||
|
|
switch ( UIDEMO_SCRIPT[i].type ) {
|
||
|
|
case SDL_EVENT_KEY_DOWN:
|
||
|
|
event.key.key = UIDEMO_SCRIPT[i].key;
|
||
|
|
break;
|
||
|
|
case SDL_EVENT_MOUSE_MOTION:
|
||
|
|
event.motion.x = UIDEMO_SCRIPT[i].x;
|
||
|
|
event.motion.y = UIDEMO_SCRIPT[i].y;
|
||
|
|
break;
|
||
|
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||
|
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||
|
|
event.button.button = SDL_BUTTON_LEFT;
|
||
|
|
event.button.x = UIDEMO_SCRIPT[i].x;
|
||
|
|
event.button.y = UIDEMO_SCRIPT[i].y;
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
PASS(errctx, route_event(&event));
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Declare the title screen: one widget call.
|
||
|
|
*
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *declare_title(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
PASS(errctx, akgl_ui_label("heading", "libakgl UI demo", AKGL_UI_ANCHOR_TOP_LEFT, NULL));
|
||
|
|
PASS(errctx, akgl_ui_menu(&title_menu));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Declare the options screen in raw CLAY(), no widgets anywhere.
|
||
|
|
*
|
||
|
|
* This screen exists to show the other way in: a centred panel, a column of
|
||
|
|
* rows, hover highlighting via Clay_Hovered() *inside* the declaration, and
|
||
|
|
* clicks paired with the application's own press edge. Everything the widgets
|
||
|
|
* do is done with these same pieces; a screen the widgets cannot express is
|
||
|
|
* written like this rather than waiting for a widget to exist.
|
||
|
|
*
|
||
|
|
* The row labels live in statics because clay borrows the pointer until
|
||
|
|
* frame_end -- a local buffer here would be dangling by the time the text is
|
||
|
|
* drawn.
|
||
|
|
*
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *declare_options(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
static char musicrow[32];
|
||
|
|
static char soundrow[32];
|
||
|
|
int count = 0;
|
||
|
|
|
||
|
|
PASS(errctx, aksl_snprintf(&count, musicrow, sizeof(musicrow), "Music: %s", opt_music ? "ON" : "OFF"));
|
||
|
|
PASS(errctx, aksl_snprintf(&count, soundrow, sizeof(soundrow), "Sound: %s", opt_sound ? "ON" : "OFF"));
|
||
|
|
|
||
|
|
CLAY({
|
||
|
|
.id = CLAY_ID("options"),
|
||
|
|
.layout = {
|
||
|
|
.padding = { 16, 16, 16, 16 },
|
||
|
|
.childGap = 8,
|
||
|
|
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||
|
|
},
|
||
|
|
.backgroundColor = { 24, 20, 37, 235 },
|
||
|
|
.border = { .color = { 240, 236, 214, 255 }, .width = { 1, 1, 1, 1, 0 } },
|
||
|
|
.floating = {
|
||
|
|
.attachPoints = {
|
||
|
|
.element = CLAY_ATTACH_POINT_CENTER_CENTER,
|
||
|
|
.parent = CLAY_ATTACH_POINT_CENTER_CENTER
|
||
|
|
},
|
||
|
|
.attachTo = CLAY_ATTACH_TO_ROOT
|
||
|
|
}
|
||
|
|
}) {
|
||
|
|
CLAY_TEXT(CLAY_STRING("OPTIONS"), CLAY_TEXT_CONFIG({
|
||
|
|
.textColor = { 240, 236, 214, 255 },
|
||
|
|
.fontId = 0
|
||
|
|
}));
|
||
|
|
|
||
|
|
CLAY({
|
||
|
|
.id = CLAY_ID("options-music"),
|
||
|
|
.layout = { .padding = { 8, 8, 4, 4 } },
|
||
|
|
.backgroundColor = Clay_Hovered()
|
||
|
|
? (Clay_Color){ 64, 58, 88, 255 }
|
||
|
|
: (Clay_Color){ 0, 0, 0, 0 }
|
||
|
|
}) {
|
||
|
|
if ( Clay_Hovered() && clicked ) {
|
||
|
|
opt_music = !opt_music;
|
||
|
|
}
|
||
|
|
CLAY_TEXT(((Clay_String){ .length = (int32_t)strlen(musicrow), .chars = musicrow }),
|
||
|
|
CLAY_TEXT_CONFIG({ .textColor = { 240, 236, 214, 255 }, .fontId = 0 }));
|
||
|
|
}
|
||
|
|
|
||
|
|
CLAY({
|
||
|
|
.id = CLAY_ID("options-sound"),
|
||
|
|
.layout = { .padding = { 8, 8, 4, 4 } },
|
||
|
|
.backgroundColor = Clay_Hovered()
|
||
|
|
? (Clay_Color){ 64, 58, 88, 255 }
|
||
|
|
: (Clay_Color){ 0, 0, 0, 0 }
|
||
|
|
}) {
|
||
|
|
if ( Clay_Hovered() && clicked ) {
|
||
|
|
opt_sound = !opt_sound;
|
||
|
|
}
|
||
|
|
CLAY_TEXT(((Clay_String){ .length = (int32_t)strlen(soundrow), .chars = soundrow }),
|
||
|
|
CLAY_TEXT_CONFIG({ .textColor = { 240, 236, 214, 255 }, .fontId = 0 }));
|
||
|
|
}
|
||
|
|
|
||
|
|
CLAY({
|
||
|
|
.id = CLAY_ID("options-back"),
|
||
|
|
.layout = { .padding = { 8, 8, 4, 4 } },
|
||
|
|
.backgroundColor = Clay_Hovered()
|
||
|
|
? (Clay_Color){ 64, 58, 88, 255 }
|
||
|
|
: (Clay_Color){ 0, 0, 0, 0 }
|
||
|
|
}) {
|
||
|
|
if ( Clay_Hovered() && clicked ) {
|
||
|
|
state = UIDEMO_STATE_TITLE;
|
||
|
|
}
|
||
|
|
CLAY_TEXT(CLAY_STRING("Back (Esc)"), CLAY_TEXT_CONFIG({
|
||
|
|
.textColor = { 240, 236, 214, 255 },
|
||
|
|
.fontId = 0
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Declare the play screen's HUD: two labels, and the dialog while it is up.
|
||
|
|
*
|
||
|
|
* The score buffer is static for the same borrowed-until-frame_end reason as
|
||
|
|
* the options rows. Note what is *not* here: no visible flag, no draw call,
|
||
|
|
* no geometry. The dialog is open because this frame declares it.
|
||
|
|
*
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *declare_play(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
static char scoretext[32];
|
||
|
|
static char livestext[32];
|
||
|
|
int count = 0;
|
||
|
|
|
||
|
|
PASS(errctx, aksl_snprintf(&count, scoretext, sizeof(scoretext), "SCORE %05d", score));
|
||
|
|
PASS(errctx, aksl_snprintf(&count, livestext, sizeof(livestext), "LIVES %d", lives));
|
||
|
|
PASS(errctx, akgl_ui_label("score", scoretext, AKGL_UI_ANCHOR_TOP_RIGHT, NULL));
|
||
|
|
PASS(errctx, akgl_ui_label("lives", livestext, AKGL_UI_ANCHOR_TOP_LEFT, NULL));
|
||
|
|
if ( dialog_open ) {
|
||
|
|
PASS(errctx, akgl_ui_dialog("dialog",
|
||
|
|
"This panel is one call. Space dismisses it; "
|
||
|
|
"compare examples/jrpg/textbox.c.",
|
||
|
|
NULL));
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Read the render target back and write it out as a PNG, for the chapter figure. */
|
||
|
|
static akerr_ErrorContext *save_screenshot(char *path)
|
||
|
|
{
|
||
|
|
SDL_Surface *shot = NULL;
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
|
||
|
|
|
||
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||
|
|
FAIL_ZERO_RETURN(errctx, shot, AKGL_ERR_SDL, "SDL_RenderReadPixels: %s", SDL_GetError());
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
FAIL_ZERO_BREAK(errctx, IMG_SavePNG(shot, path), AKGL_ERR_SDL,
|
||
|
|
"IMG_SavePNG(%s): %s", path, SDL_GetError());
|
||
|
|
} CLEANUP {
|
||
|
|
SDL_DestroySurface(shot);
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SDL_Log("Wrote %s", path);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief One frame: events, the world stand-in, the UI bracket, present.
|
||
|
|
*
|
||
|
|
* The shape to compare with the JRPG's frame(): where that program calls
|
||
|
|
* akgl_game_update between frame_start and frame_end, this one paints a
|
||
|
|
* checkerboard -- and the UI bracket sits in the same overlay slot the
|
||
|
|
* hand-rolled textbox draw did.
|
||
|
|
*
|
||
|
|
* @param frameno The frame number, for the demo script.
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *frame(long frameno)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
SDL_Event event;
|
||
|
|
|
||
|
|
while ( SDL_PollEvent(&event) ) {
|
||
|
|
PASS(errctx, route_event(&event));
|
||
|
|
}
|
||
|
|
if ( demo ) {
|
||
|
|
PASS(errctx, demo_step(frameno));
|
||
|
|
}
|
||
|
|
|
||
|
|
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
|
||
|
|
if ( state == UIDEMO_STATE_PLAY ) {
|
||
|
|
// Where a game would call akgl_game_update. The score ticking is the
|
||
|
|
// stand-in for play, so the HUD label visibly earns its redraw.
|
||
|
|
PASS(errctx, akgl_draw_background(akgl_renderer, UIDEMO_WIDTH, UIDEMO_HEIGHT));
|
||
|
|
score += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
PASS(errctx, akgl_ui_frame_begin());
|
||
|
|
switch ( state ) {
|
||
|
|
case UIDEMO_STATE_TITLE:
|
||
|
|
PASS(errctx, declare_title());
|
||
|
|
break;
|
||
|
|
case UIDEMO_STATE_OPTIONS:
|
||
|
|
PASS(errctx, declare_options());
|
||
|
|
break;
|
||
|
|
case UIDEMO_STATE_PLAY:
|
||
|
|
PASS(errctx, declare_play());
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
PASS(errctx, akgl_ui_frame_end(akgl_renderer));
|
||
|
|
|
||
|
|
if ( (shotpath != NULL) && (frameno == shotframe) ) {
|
||
|
|
PASS(errctx, save_screenshot(shotpath));
|
||
|
|
}
|
||
|
|
PASS(errctx, akgl_renderer->frame_end(akgl_renderer));
|
||
|
|
|
||
|
|
// The press edge lives exactly one frame: the declarations above have
|
||
|
|
// seen it, so the next frame starts clean.
|
||
|
|
clicked = false;
|
||
|
|
|
||
|
|
// Menu activation is read after the frame, so it catches both routes in:
|
||
|
|
// Return through akgl_ui_menu_handle_event before the declarations, and a
|
||
|
|
// click through the declaration itself.
|
||
|
|
if ( title_menu.activated ) {
|
||
|
|
title_menu.activated = false;
|
||
|
|
switch ( title_menu.selected ) {
|
||
|
|
case 0:
|
||
|
|
state = UIDEMO_STATE_PLAY;
|
||
|
|
score = 0;
|
||
|
|
dialog_open = false;
|
||
|
|
break;
|
||
|
|
case 1:
|
||
|
|
state = UIDEMO_STATE_OPTIONS;
|
||
|
|
break;
|
||
|
|
case 2:
|
||
|
|
default:
|
||
|
|
running = false;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Give back what has to be given back, in the order that works.
|
||
|
|
*
|
||
|
|
* akgl_ui_shutdown first because it is the newest thing up, then the fonts
|
||
|
|
* **before** SDL_Quit -- the registry the fonts live in is an SDL property
|
||
|
|
* set, and SDL_Quit destroys it with them inside. The pools and the clay
|
||
|
|
* arena are static storage; there is nothing to free.
|
||
|
|
*/
|
||
|
|
static void teardown(void)
|
||
|
|
{
|
||
|
|
IGNORE(akgl_ui_shutdown());
|
||
|
|
IGNORE(akgl_text_unloadallfonts());
|
||
|
|
if ( akgl_window != NULL ) {
|
||
|
|
SDL_DestroyWindow(akgl_window);
|
||
|
|
akgl_window = NULL;
|
||
|
|
}
|
||
|
|
SDL_Quit();
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(int argc, char *argv[])
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
long frameno = 0;
|
||
|
|
uint64_t started = 0;
|
||
|
|
uint64_t spent = 0;
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, parse_args(argc, argv));
|
||
|
|
CATCH(errctx, startup());
|
||
|
|
|
||
|
|
// The loop is the last thing in this ATTEMPT block on purpose: CATCH
|
||
|
|
// reports failure by `break`ing out of the loop, which falls straight
|
||
|
|
// into CLEANUP. See examples/jrpg/jrpg.c for the long form of this
|
||
|
|
// note.
|
||
|
|
while ( running ) {
|
||
|
|
started = SDL_GetTicks();
|
||
|
|
CATCH(errctx, frame(frameno));
|
||
|
|
frameno += 1;
|
||
|
|
if ( (frame_limit > 0) && (frameno >= frame_limit) ) {
|
||
|
|
running = false;
|
||
|
|
}
|
||
|
|
if ( demo == false ) {
|
||
|
|
spent = SDL_GetTicks() - started;
|
||
|
|
if ( spent < UIDEMO_FRAME_BUDGET_MS ) {
|
||
|
|
SDL_Delay((uint32_t)(UIDEMO_FRAME_BUDGET_MS - spent));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} CLEANUP {
|
||
|
|
teardown();
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} HANDLE_DEFAULT(errctx) {
|
||
|
|
LOG_ERROR_WITH_MESSAGE(errctx, "the UI demo could not finish");
|
||
|
|
exitstatus = 1;
|
||
|
|
} FINISH_NORETURN(errctx);
|
||
|
|
|
||
|
|
// Enough for the smoke run to be read rather than merely passed: the
|
||
|
|
// script quits from the title menu, so a run that finished its tour says
|
||
|
|
// so here.
|
||
|
|
printf("uidemo: %ld frames, score %d, music %s, sound %s\n",
|
||
|
|
frameno, score, opt_music ? "on" : "off", opt_sound ? "on" : "off");
|
||
|
|
return exitstatus;
|
||
|
|
}
|