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
This commit is contained in:
2026-08-02 11:06:27 -04:00
parent 064e6569e8
commit 3b3bfcbe10
3 changed files with 1122 additions and 30 deletions

514
src/ui.c
View File

@@ -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) ) {