Render clay layouts: fonts, text measurement, executor and the frame bracket
The UI subsystem now draws. akgl_ui_frame_begin/frame_end bracket one frame's CLAY() declarations: begin clears the error stash and starts the clay layout, end computes it and walks the render commands through a backend -- RECTANGLE as (rounded) fills, BORDER as radius-shortened edge fills plus corner arcs, TEXT through akgl_text_rendertextat one wrapped line per command, IMAGE as an akgl_Sprite's first frame stretched to the bounding box, and the SCISSOR pair through akgl_draw_set_clip, cleared again on any exit so a failed frame cannot leave the world clipped. akgl_ui_font_register maps registry font names onto clay's uint16_t fontIds. The table keeps the *name* and resolves it per use -- fonts are not reference counted, and a cached TTF_Font* would dangle where a name reports "gone" honestly. Clay_TextElementConfig.fontSize is deliberately ignored: libakgl bakes the size into the handle at load, so one face at two sizes is two ids. The measure bridge passes clay's non-NUL-terminated slices straight to SDL_ttf's explicit-length TTF_GetStringSize -- no copy, no scratch. Its signature has no error channel, so failures report zero-by-zero and stash a message that frame_end raises, the same route clay's own void error handler uses; layout errors take precedence over drawing and one bad frame leaves the next one clean. Tests drive real CLAY() layouts through the software renderer and read pixels back: fill placement, border edges with an empty middle, a child clipped by its container, text landing in its colour, slice-vs-whole measurement agreement, fontId table dedupe/refusal/exhaustion, and the bracket's begin/begin, end-without-begin and failure-recovery contracts. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
514
src/ui.c
514
src/ui.c
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* @file ui.c
|
||||
* @brief Implements the UI subsystem: clay's arena, lifecycle and error stash.
|
||||
* @brief Implements the UI subsystem: clay's arena, fonts, rendering and lifecycle.
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -10,8 +11,14 @@
|
||||
#include <akstdlib.h>
|
||||
#include <clay.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/text.h>
|
||||
#include <akgl/ui.h>
|
||||
|
||||
/**
|
||||
@@ -29,44 +36,105 @@ static uint8_t ui_arena_bytes[AKGL_UI_ARENA_BYTES];
|
||||
static size_t ui_arena_usable = AKGL_UI_ARENA_BYTES;
|
||||
/** @brief The context Clay_Initialize handed back, or NULL before init / after shutdown. */
|
||||
static Clay_Context *ui_context;
|
||||
/** @brief Whether a frame is open -- between frame_begin and frame_end. */
|
||||
static bool ui_in_frame;
|
||||
|
||||
/**
|
||||
* @brief The fontId table: slot N holds the registry name behind clay fontId N.
|
||||
*
|
||||
* Names, not `TTF_Font *` handles -- fonts are not reference counted, and a
|
||||
* name resolved per use through #AKGL_REGISTRY_FONT reports "gone" honestly
|
||||
* where a cached handle would dangle.
|
||||
*/
|
||||
static char ui_fonts[AKGL_UI_MAX_FONTS][AKGL_UI_FONT_NAME_LENGTH];
|
||||
/** @brief Slots of #ui_fonts in use. Ids 0 to this minus one are live. */
|
||||
static int ui_font_count;
|
||||
|
||||
/** @brief Scratch one TEXT render command's line is copied through, so SDL_ttf gets a C string. */
|
||||
static char ui_text_scratch[AKGL_UI_MAX_TEXT_BYTES];
|
||||
|
||||
/**
|
||||
* @brief First error clay reported since the stash was last cleared.
|
||||
*
|
||||
* clay's error handler is a void callback with no way to refuse or return, so
|
||||
* a layout error cannot surface at the call that caused it -- it fires in the
|
||||
* middle of Clay_EndLayout with libakgl nowhere on the stack. The handler
|
||||
* stashes the first message here and akgl_ui_frame_end raises it, which is
|
||||
* the earliest point the error protocol can carry it. Later errors in the
|
||||
* same frame are counted but not kept: the first one is almost always the
|
||||
* cause and the rest its consequences.
|
||||
* clay's error handler is a void callback with no way to refuse or return,
|
||||
* and the measure callback's signature is no better -- a layout error cannot
|
||||
* surface at the call that caused it, because libakgl is nowhere on the stack
|
||||
* when it happens. Both stash the first message here and akgl_ui_frame_end
|
||||
* raises it, which is the earliest point the error protocol can carry it.
|
||||
* Later errors in the same frame are counted but not kept: the first one is
|
||||
* almost always the cause and the rest its consequences.
|
||||
*/
|
||||
static char ui_clay_error_text[UI_ERROR_TEXT_LENGTH];
|
||||
/** @brief How many errors clay has reported since the stash was cleared. */
|
||||
/** @brief How many errors have been stashed since the stash was cleared. */
|
||||
static uint32_t ui_clay_error_count;
|
||||
|
||||
/**
|
||||
* @brief Log a layout-time failure now and stash the first one for frame_end.
|
||||
*
|
||||
* The shared back half of the clay error handler and the measure callback --
|
||||
* the two places an error can happen with no way to return it.
|
||||
*
|
||||
* @param format printf format for the message, followed by its arguments.
|
||||
*/
|
||||
static void ui_stash_error(const char *format, ...)
|
||||
{
|
||||
va_list args;
|
||||
int count = 0;
|
||||
|
||||
ui_clay_error_count += 1;
|
||||
if ( ui_clay_error_count > 1 ) {
|
||||
// Still worth logging: the operator watching the log sees the whole
|
||||
// cascade, even though frame_end will raise only the first.
|
||||
va_start(args, format);
|
||||
SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_ERROR, format, args);
|
||||
va_end(args);
|
||||
return;
|
||||
}
|
||||
va_start(args, format);
|
||||
IGNORE(aksl_vsnprintf(&count, ui_clay_error_text, sizeof(ui_clay_error_text), format, args));
|
||||
va_end(args);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", ui_clay_error_text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The error handler given to Clay_Initialize.
|
||||
*
|
||||
* Logs every report as it happens -- an operator watching the log should not
|
||||
* have to wait for frame_end -- and stashes the first for the error protocol.
|
||||
* Clay_String is a length and a pointer, not a C string, so the copy is
|
||||
* bounded by hand before aksl_strncpy sees it.
|
||||
* Clay_String is a length and a pointer, not a C string, so the text goes
|
||||
* through the stash with an explicit precision.
|
||||
*/
|
||||
static void ui_clay_error(Clay_ErrorData error)
|
||||
{
|
||||
size_t length = 0;
|
||||
ui_stash_error("clay: %.*s", (int)error.errorText.length, error.errorText.chars);
|
||||
}
|
||||
|
||||
SDL_Log("clay: %.*s", (int)error.errorText.length, error.errorText.chars);
|
||||
ui_clay_error_count += 1;
|
||||
if ( ui_clay_error_count > 1 ) {
|
||||
return;
|
||||
}
|
||||
length = (size_t)error.errorText.length;
|
||||
if ( length > (sizeof(ui_clay_error_text) - 1) ) {
|
||||
length = sizeof(ui_clay_error_text) - 1;
|
||||
}
|
||||
IGNORE(aksl_strncpy(ui_clay_error_text, sizeof(ui_clay_error_text), error.errorText.chars, length));
|
||||
/**
|
||||
* @brief Resolve a clay fontId to the live font handle behind it.
|
||||
*
|
||||
* @param fontid The id a text element carried.
|
||||
* @param dest Receives the font. Assumed non-`NULL`; internal callers only.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_UI If the id was never registered, or the name behind it
|
||||
* has left the registry since.
|
||||
*/
|
||||
static akerr_ErrorContext *ui_font_for_id(uint16_t fontid, TTF_Font **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((int)fontid >= ui_font_count),
|
||||
AKGL_ERR_UI,
|
||||
"fontId %u was never registered; akgl_ui_font_register has issued %d",
|
||||
fontid,
|
||||
ui_font_count);
|
||||
*dest = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, ui_fonts[fontid], NULL);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
*dest,
|
||||
AKGL_ERR_UI,
|
||||
"The font named %s behind fontId %u is no longer in the registry",
|
||||
ui_fonts[fontid],
|
||||
fontid);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_init(int width, int height)
|
||||
@@ -114,6 +182,9 @@ akerr_ErrorContext *akgl_ui_init(int width, int height)
|
||||
// context would otherwise surface as a crash inside the first CLAY()
|
||||
// block, far from the cause.
|
||||
FAIL_ZERO_RETURN(errctx, ui_context, AKGL_ERR_UI, "Clay_Initialize refused: %s", ui_clay_error_text);
|
||||
// Needs the context Clay_Initialize just made current, so it cannot move
|
||||
// earlier. Without a measure function every text element is an error.
|
||||
Clay_SetMeasureTextFunction(&akgl_ui_measure_text, NULL);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -124,6 +195,8 @@ akerr_ErrorContext *akgl_ui_shutdown(void)
|
||||
// nothing to hand back, so shutdown is forgetting. The next init lays a
|
||||
// fresh context over the same bytes.
|
||||
ui_context = NULL;
|
||||
ui_in_frame = false;
|
||||
ui_font_count = 0;
|
||||
ui_clay_error_text[0] = '\0';
|
||||
ui_clay_error_count = 0;
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -143,6 +216,399 @@ akerr_ErrorContext *akgl_ui_resize(int width, int height)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_font_register(char *name, uint16_t *fontid)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
||||
FAIL_ZERO_RETURN(errctx, fontid, AKERR_NULLPOINTER, "Null fontid destination");
|
||||
FAIL_ZERO_RETURN(errctx, ui_context, AKGL_ERR_UI, "The UI subsystem is not initialized");
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL),
|
||||
AKGL_ERR_UI,
|
||||
"No font named %s in the registry; akgl_text_loadfont first",
|
||||
name);
|
||||
for ( i = 0; i < ui_font_count; i++ ) {
|
||||
if ( SDL_strcmp(ui_fonts[i], name) == 0 ) {
|
||||
*fontid = (uint16_t)i;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
}
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(ui_font_count >= AKGL_UI_MAX_FONTS),
|
||||
AKGL_ERR_UI,
|
||||
"All %d fontId slots are taken. Raise AKGL_UI_MAX_FONTS.",
|
||||
AKGL_UI_MAX_FONTS);
|
||||
PASS(errctx, aksl_strcpy(ui_fonts[ui_font_count], AKGL_UI_FONT_NAME_LENGTH, name));
|
||||
*fontid = (uint16_t)ui_font_count;
|
||||
ui_font_count += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
Clay_Dimensions akgl_ui_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData)
|
||||
{
|
||||
Clay_Dimensions dimensions = { 0.0f, 0.0f };
|
||||
TTF_Font *font = NULL;
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
|
||||
(void)userData;
|
||||
// No error protocol in here: clay's callback signature returns dimensions
|
||||
// by value, so failure is zero-by-zero plus a stashed message for
|
||||
// frame_end. The checks mirror ui_font_for_id without borrowing it,
|
||||
// because a context prepared here would have nowhere to go.
|
||||
if ( config == NULL ) {
|
||||
ui_stash_error("Text measurement was asked for with no element configuration");
|
||||
return dimensions;
|
||||
}
|
||||
if ( (int)config->fontId >= ui_font_count ) {
|
||||
ui_stash_error(
|
||||
"fontId %u was never registered; akgl_ui_font_register has issued %d",
|
||||
config->fontId,
|
||||
ui_font_count);
|
||||
return dimensions;
|
||||
}
|
||||
font = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, ui_fonts[config->fontId], NULL);
|
||||
if ( font == NULL ) {
|
||||
ui_stash_error(
|
||||
"The font named %s behind fontId %u is no longer in the registry",
|
||||
ui_fonts[config->fontId],
|
||||
config->fontId);
|
||||
return dimensions;
|
||||
}
|
||||
if ( text.length == 0 ) {
|
||||
return dimensions;
|
||||
}
|
||||
// The slice is not NUL-terminated and does not need to be: SDL_ttf takes
|
||||
// an explicit byte length, so the measurement happens in place with no
|
||||
// copy -- this callback runs for every word clay has not cached.
|
||||
if ( TTF_GetStringSize(font, text.chars, (size_t)text.length, &w, &h) == false ) {
|
||||
ui_stash_error("Measuring %d bytes of text failed: %s", text.length, SDL_GetError());
|
||||
return dimensions;
|
||||
}
|
||||
dimensions.width = (float)w;
|
||||
dimensions.height = (float)h;
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
/** @brief Clamp one clay colour channel (a float, conventionally 0-255) to a byte. */
|
||||
static uint8_t ui_color_channel(float value)
|
||||
{
|
||||
if ( value <= 0.0f ) {
|
||||
return 0;
|
||||
}
|
||||
if ( value >= 255.0f ) {
|
||||
return 255;
|
||||
}
|
||||
return (uint8_t)(value + 0.5f);
|
||||
}
|
||||
|
||||
/** @brief Convert a clay colour to the SDL_Color every draw primitive takes. */
|
||||
static SDL_Color ui_color_from_clay(Clay_Color color)
|
||||
{
|
||||
SDL_Color out;
|
||||
|
||||
out.r = ui_color_channel(color.r);
|
||||
out.g = ui_color_channel(color.g);
|
||||
out.b = ui_color_channel(color.b);
|
||||
out.a = ui_color_channel(color.a);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw one BORDER command: four edge fills and up to four corner arcs.
|
||||
*
|
||||
* The edges are shortened by the corner radius and the corners are stroked as
|
||||
* quarter arcs of the adjacent widths' larger value -- the approximation is
|
||||
* only visible when two adjacent sides have different widths *and* a radius,
|
||||
* which nothing in this library produces.
|
||||
*
|
||||
* @param self The backend. Assumed checked by the caller.
|
||||
* @param command The BORDER render command.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Whatever the fills and arcs underneath raise.
|
||||
*/
|
||||
static akerr_ErrorContext *ui_execute_border(akgl_RenderBackend *self, Clay_RenderCommand *command)
|
||||
{
|
||||
Clay_BorderRenderData *border = &command->renderData.border;
|
||||
Clay_BoundingBox box = command->boundingBox;
|
||||
SDL_Color color;
|
||||
SDL_FRect edge;
|
||||
float32_t radius = border->cornerRadius.topLeft;
|
||||
float32_t thickness = 0.0f;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
color = ui_color_from_clay(border->color);
|
||||
if ( border->width.left > 0 ) {
|
||||
edge.x = box.x;
|
||||
edge.y = box.y + radius;
|
||||
edge.w = (float32_t)border->width.left;
|
||||
edge.h = box.height - (radius * 2.0f);
|
||||
PASS(errctx, akgl_draw_filled_rect(self, &edge, color));
|
||||
}
|
||||
if ( border->width.right > 0 ) {
|
||||
edge.x = box.x + box.width - (float32_t)border->width.right;
|
||||
edge.y = box.y + radius;
|
||||
edge.w = (float32_t)border->width.right;
|
||||
edge.h = box.height - (radius * 2.0f);
|
||||
PASS(errctx, akgl_draw_filled_rect(self, &edge, color));
|
||||
}
|
||||
if ( border->width.top > 0 ) {
|
||||
edge.x = box.x + radius;
|
||||
edge.y = box.y;
|
||||
edge.w = box.width - (radius * 2.0f);
|
||||
edge.h = (float32_t)border->width.top;
|
||||
PASS(errctx, akgl_draw_filled_rect(self, &edge, color));
|
||||
}
|
||||
if ( border->width.bottom > 0 ) {
|
||||
edge.x = box.x + radius;
|
||||
edge.y = box.y + box.height - (float32_t)border->width.bottom;
|
||||
edge.w = box.width - (radius * 2.0f);
|
||||
edge.h = (float32_t)border->width.bottom;
|
||||
PASS(errctx, akgl_draw_filled_rect(self, &edge, color));
|
||||
}
|
||||
if ( radius > 0.0f ) {
|
||||
thickness = (float32_t)((border->width.top > border->width.left) ? border->width.top : border->width.left);
|
||||
if ( thickness > 0.0f ) {
|
||||
PASS(errctx, akgl_draw_arc(self, box.x + radius, box.y + radius, radius, 180.0f, 270.0f, thickness, color));
|
||||
}
|
||||
thickness = (float32_t)((border->width.top > border->width.right) ? border->width.top : border->width.right);
|
||||
if ( thickness > 0.0f ) {
|
||||
PASS(errctx, akgl_draw_arc(self, box.x + box.width - radius, box.y + radius, radius, 270.0f, 360.0f, thickness, color));
|
||||
}
|
||||
thickness = (float32_t)((border->width.bottom > border->width.right) ? border->width.bottom : border->width.right);
|
||||
if ( thickness > 0.0f ) {
|
||||
PASS(errctx, akgl_draw_arc(self, box.x + box.width - radius, box.y + box.height - radius, radius, 0.0f, 90.0f, thickness, color));
|
||||
}
|
||||
thickness = (float32_t)((border->width.bottom > border->width.left) ? border->width.bottom : border->width.left);
|
||||
if ( thickness > 0.0f ) {
|
||||
PASS(errctx, akgl_draw_arc(self, box.x + radius, box.y + box.height - radius, radius, 90.0f, 180.0f, thickness, color));
|
||||
}
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw one TEXT command through akgl_text_rendertextat().
|
||||
*
|
||||
* clay wraps text itself -- each command is one line -- so the line is drawn
|
||||
* unwrapped at the command's position. The slice is copied through the
|
||||
* bounded scratch because akgl_text_rendertextat takes a C string.
|
||||
*
|
||||
* @param self The backend. Assumed checked by the caller.
|
||||
* @param command The TEXT render command.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_UI If the line exceeds #AKGL_UI_MAX_TEXT_BYTES or the
|
||||
* fontId does not resolve.
|
||||
* @throws AKERR_* Whatever the copy or the draw underneath raises.
|
||||
*/
|
||||
static akerr_ErrorContext *ui_execute_text(akgl_RenderBackend *self, Clay_RenderCommand *command)
|
||||
{
|
||||
Clay_TextRenderData *text = &command->renderData.text;
|
||||
TTF_Font *font = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
if ( text->stringContents.length == 0 ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(text->stringContents.length >= (int32_t)sizeof(ui_text_scratch)),
|
||||
AKGL_ERR_UI,
|
||||
"A text run of %d bytes exceeds AKGL_UI_MAX_TEXT_BYTES (%d)",
|
||||
text->stringContents.length,
|
||||
(int)sizeof(ui_text_scratch));
|
||||
PASS(errctx, ui_font_for_id(text->fontId, &font));
|
||||
PASS(errctx, aksl_strncpy(
|
||||
ui_text_scratch,
|
||||
sizeof(ui_text_scratch),
|
||||
text->stringContents.chars,
|
||||
(size_t)text->stringContents.length));
|
||||
PASS(errctx, akgl_text_rendertextat(
|
||||
font,
|
||||
ui_text_scratch,
|
||||
ui_color_from_clay(text->textColor),
|
||||
0,
|
||||
(int)command->boundingBox.x,
|
||||
(int)command->boundingBox.y));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw one IMAGE command: a sprite's first frame stretched to the box.
|
||||
*
|
||||
* @param self The backend. Assumed checked by the caller, `draw_texture`
|
||||
* included.
|
||||
* @param command The IMAGE render command, whose `imageData` is an
|
||||
* `akgl_Sprite *` by the contract in akgl/ui.h.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_UI If the command carries no sprite, or the sprite has no
|
||||
* sheet behind it.
|
||||
* @throws AKERR_* Whatever the frame lookup or the draw underneath raises.
|
||||
*/
|
||||
static akerr_ErrorContext *ui_execute_image(akgl_RenderBackend *self, Clay_RenderCommand *command)
|
||||
{
|
||||
akgl_Sprite *sprite = (akgl_Sprite *)command->renderData.image.imageData;
|
||||
SDL_FRect src;
|
||||
SDL_FRect dest;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
sprite,
|
||||
AKGL_ERR_UI,
|
||||
"An IMAGE element carries no sprite; .image.imageData must be an akgl_Sprite pointer");
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
sprite->sheet,
|
||||
AKGL_ERR_UI,
|
||||
"The sprite named %s behind an IMAGE element has no sheet",
|
||||
sprite->name);
|
||||
PASS(errctx, akgl_spritesheet_coords_for_frame(sprite, &src, sprite->frameids[0]));
|
||||
dest.x = command->boundingBox.x;
|
||||
dest.y = command->boundingBox.y;
|
||||
dest.w = command->boundingBox.width;
|
||||
dest.h = command->boundingBox.height;
|
||||
PASS(errctx, self->draw_texture(self, sprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_execute_commands(akgl_RenderBackend *self, Clay_RenderCommandArray *commands)
|
||||
{
|
||||
Clay_RenderCommand *command = NULL;
|
||||
SDL_FRect fill;
|
||||
SDL_Rect clip;
|
||||
bool clipped = false;
|
||||
static bool custom_skip_logged = false;
|
||||
int32_t i = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, self->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
|
||||
FAIL_ZERO_RETURN(errctx, commands, AKERR_NULLPOINTER, "NULL command array");
|
||||
|
||||
ATTEMPT {
|
||||
// The loop is the whole ATTEMPT body on purpose: CATCH reports
|
||||
// failure by break, which must leave the block, and here leaving the
|
||||
// loop is leaving the block.
|
||||
for ( i = 0; i < commands->length; i++ ) {
|
||||
command = Clay_RenderCommandArray_Get(commands, i);
|
||||
switch ( command->commandType ) {
|
||||
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE:
|
||||
fill.x = command->boundingBox.x;
|
||||
fill.y = command->boundingBox.y;
|
||||
fill.w = command->boundingBox.width;
|
||||
fill.h = command->boundingBox.height;
|
||||
CATCH(errctx, akgl_draw_filled_rounded_rect(
|
||||
self,
|
||||
&fill,
|
||||
command->renderData.rectangle.cornerRadius.topLeft,
|
||||
ui_color_from_clay(command->renderData.rectangle.backgroundColor)));
|
||||
break;
|
||||
case CLAY_RENDER_COMMAND_TYPE_BORDER:
|
||||
CATCH(errctx, ui_execute_border(self, command));
|
||||
break;
|
||||
case CLAY_RENDER_COMMAND_TYPE_TEXT:
|
||||
CATCH(errctx, ui_execute_text(self, command));
|
||||
break;
|
||||
case CLAY_RENDER_COMMAND_TYPE_IMAGE:
|
||||
CATCH(errctx, ui_execute_image(self, command));
|
||||
break;
|
||||
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START:
|
||||
clip.x = (int)command->boundingBox.x;
|
||||
clip.y = (int)command->boundingBox.y;
|
||||
clip.w = (int)command->boundingBox.width;
|
||||
clip.h = (int)command->boundingBox.height;
|
||||
CATCH(errctx, akgl_draw_set_clip(self, &clip));
|
||||
clipped = true;
|
||||
break;
|
||||
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END:
|
||||
CATCH(errctx, akgl_draw_set_clip(self, NULL));
|
||||
clipped = false;
|
||||
break;
|
||||
case CLAY_RENDER_COMMAND_TYPE_CUSTOM:
|
||||
// Skipped in this version, once in the log rather than once
|
||||
// per frame -- sixty repeats a second is how a log gets
|
||||
// ignored.
|
||||
if ( custom_skip_logged == false ) {
|
||||
SDL_Log("akgl_ui: CUSTOM render commands are not handled and were skipped");
|
||||
custom_skip_logged = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// A CATCH inside a case reports failure with `break`, and that
|
||||
// break binds to the *switch* -- so a failed command falls out
|
||||
// here with errctx carrying the failure, and this is what leaves
|
||||
// the loop. On success errctx is still NULL; check the pointer,
|
||||
// not a status through it.
|
||||
if ( errctx != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} CLEANUP {
|
||||
// A failed frame must not leave the next frame's world clipped.
|
||||
if ( clipped == true ) {
|
||||
IGNORE(akgl_draw_set_clip(self, NULL));
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_frame_begin(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, ui_context, AKGL_ERR_UI, "The UI subsystem is not initialized");
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
ui_in_frame,
|
||||
AKGL_ERR_UI,
|
||||
"A UI frame is already open; a frame_begin/frame_begin sequence means a frame_end went missing");
|
||||
ui_clay_error_text[0] = '\0';
|
||||
ui_clay_error_count = 0;
|
||||
Clay_BeginLayout();
|
||||
ui_in_frame = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_frame_end(akgl_RenderBackend *self)
|
||||
{
|
||||
Clay_RenderCommandArray commands;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, self->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
|
||||
FAIL_ZERO_RETURN(errctx, ui_context, AKGL_ERR_UI, "The UI subsystem is not initialized");
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(ui_in_frame == false),
|
||||
AKGL_ERR_UI,
|
||||
"No UI frame is open; akgl_ui_frame_begin starts one");
|
||||
// Closed before anything can fail, so one bad frame is one bad frame and
|
||||
// the next frame_begin is legal rather than "already open".
|
||||
ui_in_frame = false;
|
||||
commands = Clay_EndLayout();
|
||||
// Layout errors take precedence over drawing: a layout that failed is not
|
||||
// a layout, and drawing its partial commands would only bury the cause
|
||||
// under whatever the draw then reported.
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(ui_clay_error_count > 0),
|
||||
AKGL_ERR_UI,
|
||||
"clay reported %u layout error(s); the first: %s",
|
||||
ui_clay_error_count,
|
||||
ui_clay_error_text);
|
||||
PASS(errctx, akgl_ui_execute_commands(self, &commands));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
void akgl_ui_arena_limit(size_t limit)
|
||||
{
|
||||
if ( (limit == 0) || (limit > AKGL_UI_ARENA_BYTES) ) {
|
||||
|
||||
Reference in New Issue
Block a user