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:
475
tests/ui.c
475
tests/ui.c
@@ -1,21 +1,82 @@
|
||||
/**
|
||||
* @file ui.c
|
||||
* @brief Unit tests for the UI subsystem: lifecycle, arena sizing and validation.
|
||||
* @brief Unit tests for the UI subsystem: lifecycle, fonts, measurement and rendering.
|
||||
*
|
||||
* Everything here runs without a window, a renderer, or SDL_Init at all --
|
||||
* akgl_ui_init takes its layout dimensions as parameters precisely so that a
|
||||
* test can bring the subsystem up headless. Only akgl_error_init() is needed
|
||||
* first, so that AKGL_ERR_UI carries its name into any trace these tests
|
||||
* produce.
|
||||
* The lifecycle and arena tests run without a renderer -- akgl_ui_init takes
|
||||
* its layout dimensions as parameters precisely so that a headless program
|
||||
* can bring the subsystem up. The layout tests draw real clay layouts into a
|
||||
* small software renderer under the dummy video driver and read the target
|
||||
* back, the same arrangement tests/draw.c uses, so their assertions are about
|
||||
* pixels rather than about clay having been called.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <akerror.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 "testutil.h"
|
||||
|
||||
/** @brief Width and height of the offscreen target the layout tests draw into. */
|
||||
#define TEST_TARGET_SIZE 64
|
||||
|
||||
/** @brief The font every text test measures and draws with. */
|
||||
#define TEST_FONT_PATH "assets/akgl_test_mono.ttf"
|
||||
/** @brief Registry name the test font is loaded under. */
|
||||
#define TEST_FONT_NAME "uifont"
|
||||
/** @brief Point size the test font is loaded at. */
|
||||
#define TEST_FONT_SIZE 16
|
||||
|
||||
/** @brief Opaque black, what each layout test clears the target to. */
|
||||
static const SDL_Color testblack = { 0x00, 0x00, 0x00, 0xff };
|
||||
/** @brief The color most layout tests fill with. */
|
||||
static const SDL_Color testred = { 0xff, 0x00, 0x00, 0xff };
|
||||
/** @brief A second color, for borders against fills. */
|
||||
static const SDL_Color testgreen = { 0x00, 0xff, 0x00, 0xff };
|
||||
|
||||
/** @brief Clear the whole target to opaque black. */
|
||||
static akerr_ErrorContext *clear_target(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_RenderClear(akgl_renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Report whether one pixel of @p shot carries @p color. Alpha is not compared. */
|
||||
static bool pixel_is(SDL_Surface *shot, int x, int y, SDL_Color color)
|
||||
{
|
||||
uint8_t r = 0;
|
||||
uint8_t g = 0;
|
||||
uint8_t b = 0;
|
||||
uint8_t a = 0;
|
||||
|
||||
if ( shot == NULL ) {
|
||||
return false;
|
||||
}
|
||||
if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
|
||||
return false;
|
||||
}
|
||||
return ((r == color.r) && (g == color.g) && (b == color.b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Init must refuse bad dimensions and an uninitialized resize.
|
||||
*
|
||||
@@ -33,6 +94,8 @@ akerr_ErrorContext *test_ui_validation(void)
|
||||
"akgl_ui_init accepted a negative height");
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_resize(320, 240),
|
||||
"akgl_ui_resize worked on an uninitialized subsystem");
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_begin(),
|
||||
"akgl_ui_frame_begin worked on an uninitialized subsystem");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
@@ -97,16 +160,416 @@ akerr_ErrorContext *test_ui_arena_exhaustion(void)
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The fontId table: issue, dedupe, refuse the unknown and the overfull.
|
||||
*
|
||||
* The table stores names and resolves them per use, so the interesting cases
|
||||
* are all at registration: an id for a loaded font, the same id again for the
|
||||
* same name, a refusal for a name nothing loaded, and a refusal -- naming the
|
||||
* ceiling -- when the table is full.
|
||||
*/
|
||||
akerr_ErrorContext *test_ui_font_register(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
uint16_t fontid = 99;
|
||||
uint16_t again = 99;
|
||||
uint16_t extra = 99;
|
||||
char slotname[8];
|
||||
bool loaded = true;
|
||||
bool registered = true;
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
|
||||
"font registration worked on an uninitialized subsystem");
|
||||
TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "init for the font table test failed");
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
|
||||
"registering a loaded font failed");
|
||||
TEST_ASSERT(e, fontid == 0, "the first fontId issued was %u, not 0", fontid);
|
||||
TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &again),
|
||||
"re-registering the same name failed");
|
||||
TEST_ASSERT(e, again == fontid,
|
||||
"re-registering the same name issued a second id (%u after %u)", again, fontid);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_font_register("nothing-loaded-this", &extra),
|
||||
"a name absent from the font registry was registered");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_font_register(NULL, &extra),
|
||||
"a NULL name was accepted");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_font_register(TEST_FONT_NAME, NULL),
|
||||
"a NULL id destination was accepted");
|
||||
|
||||
// Fill the table: the same face under fresh names costs a slot each,
|
||||
// because the size-baked handle is what a slot maps to.
|
||||
for ( i = 1; i < AKGL_UI_MAX_FONTS; i++ ) {
|
||||
IGNORE(aksl_snprintf(&count, slotname, sizeof(slotname), "f%d", i));
|
||||
TEST_ASSERT_FLAG(loaded, akgl_text_loadfont(slotname, TEST_FONT_PATH, TEST_FONT_SIZE) == NULL);
|
||||
TEST_ASSERT_FLAG(registered, akgl_ui_font_register(slotname, &extra) == NULL);
|
||||
}
|
||||
TEST_ASSERT(e, loaded, "loading the fill fonts failed");
|
||||
TEST_ASSERT(e, registered, "registering the fill fonts failed");
|
||||
TEST_EXPECT_OK(e, akgl_text_loadfont("overflow", TEST_FONT_PATH, TEST_FONT_SIZE),
|
||||
"loading the overflow font failed");
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_font_register("overflow", &extra),
|
||||
"a full fontId table issued another id");
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_text_unloadfont("overflow"));
|
||||
for ( i = 1; i < AKGL_UI_MAX_FONTS; i++ ) {
|
||||
IGNORE(aksl_snprintf(&count, slotname, sizeof(slotname), "f%d", i));
|
||||
IGNORE(akgl_text_unloadfont(slotname));
|
||||
}
|
||||
IGNORE(akgl_ui_shutdown());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The measure bridge must agree with akgl_text_measure, slice or not.
|
||||
*
|
||||
* clay hands the callback slices carved out of larger strings, so the case
|
||||
* that matters is a slice whose stated length stops short of the bytes behind
|
||||
* it -- five bytes of "HelloWorld" must measure exactly as "Hello" does. A
|
||||
* fontId nothing issued reports zero-by-zero, the only failure channel the
|
||||
* callback signature allows.
|
||||
*/
|
||||
akerr_ErrorContext *test_ui_measure_text(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
Clay_TextElementConfig config;
|
||||
Clay_StringSlice slice;
|
||||
Clay_Dimensions sliced;
|
||||
Clay_Dimensions whole;
|
||||
TTF_Font *font = NULL;
|
||||
uint16_t fontid = 0;
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "init for the measure test failed");
|
||||
TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
|
||||
"registering the measure test font failed");
|
||||
font = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, TEST_FONT_NAME, NULL);
|
||||
FAIL_ZERO_BREAK(e, font, AKGL_ERR_BEHAVIOR, "the test font left the registry");
|
||||
|
||||
SDL_memset(&config, 0x00, sizeof(config));
|
||||
config.fontId = fontid;
|
||||
|
||||
slice.chars = "HelloWorld";
|
||||
slice.baseChars = slice.chars;
|
||||
slice.length = 5;
|
||||
sliced = akgl_ui_measure_text(slice, &config, NULL);
|
||||
TEST_EXPECT_OK(e, akgl_text_measure(font, "Hello", &w, &h),
|
||||
"measuring the whole string through the text subsystem failed");
|
||||
TEST_ASSERT_FEQ(e, sliced.width, (float)w,
|
||||
"a 5-byte slice of HelloWorld measured %f wide; Hello measures %d",
|
||||
(double)sliced.width, w);
|
||||
TEST_ASSERT_FEQ(e, sliced.height, (float)h,
|
||||
"a 5-byte slice of HelloWorld measured %f high; Hello measures %d",
|
||||
(double)sliced.height, h);
|
||||
|
||||
slice.length = 10;
|
||||
whole = akgl_ui_measure_text(slice, &config, NULL);
|
||||
TEST_ASSERT(e, whole.width > sliced.width,
|
||||
"HelloWorld did not measure wider than its Hello prefix");
|
||||
|
||||
config.fontId = (uint16_t)(AKGL_UI_MAX_FONTS + 1);
|
||||
sliced = akgl_ui_measure_text(slice, &config, NULL);
|
||||
TEST_ASSERT_FEQ(e, sliced.width, 0.0f,
|
||||
"an unregistered fontId measured %f wide instead of 0",
|
||||
(double)sliced.width);
|
||||
TEST_ASSERT_FEQ(e, sliced.height, 0.0f,
|
||||
"an unregistered fontId measured %f high instead of 0",
|
||||
(double)sliced.height);
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_ui_shutdown());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The frame bracket refuses to nest, to close unopened, and recovers from failure.
|
||||
*/
|
||||
akerr_ErrorContext *test_ui_frame_bracket(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
|
||||
"init for the frame bracket test failed");
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_end(akgl_renderer),
|
||||
"frame_end closed a frame nothing opened");
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening a frame failed");
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_begin(),
|
||||
"a second frame_begin was not refused");
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer),
|
||||
"closing an empty frame failed");
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_end(akgl_renderer),
|
||||
"frame_end closed the same frame twice");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_frame_end(NULL),
|
||||
"frame_end accepted a NULL backend");
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_ui_shutdown());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief One real layout, executed, read back: fill, border and clip land where clay put them.
|
||||
*
|
||||
* An outer container with 8 pixels of padding and a fixed 20x20 child pins
|
||||
* the child's box to (8,8)-(28,28) without depending on any layout behaviour
|
||||
* subtler than "padding is padding".
|
||||
*/
|
||||
akerr_ErrorContext *test_ui_layout_pixels(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Surface *shot = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
|
||||
"init for the layout test failed");
|
||||
|
||||
// A filled rectangle.
|
||||
CATCH(e, clear_target());
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the fill frame failed");
|
||||
CLAY({ .layout = { .padding = { 8, 0, 8, 0 } } }) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {
|
||||
.width = CLAY_SIZING_FIXED(20),
|
||||
.height = CLAY_SIZING_FIXED(20)
|
||||
}
|
||||
},
|
||||
.backgroundColor = { 255, 0, 0, 255 }
|
||||
}) {}
|
||||
}
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the fill frame failed");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(e, pixel_is(shot, 10, 10, testred),
|
||||
"the fill did not land inside the child's box");
|
||||
TEST_ASSERT(e, pixel_is(shot, 27, 27, testred),
|
||||
"the fill stopped short of the child's far corner");
|
||||
TEST_ASSERT(e, pixel_is(shot, 7, 7, testblack),
|
||||
"the fill leaked outside the child's box");
|
||||
TEST_ASSERT(e, pixel_is(shot, 40, 40, testblack),
|
||||
"the fill reached pixels no element occupies");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// A border: green edges two pixels wide, nothing in the middle.
|
||||
CATCH(e, clear_target());
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the border frame failed");
|
||||
CLAY({ .layout = { .padding = { 8, 0, 8, 0 } } }) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {
|
||||
.width = CLAY_SIZING_FIXED(20),
|
||||
.height = CLAY_SIZING_FIXED(20)
|
||||
}
|
||||
},
|
||||
.border = {
|
||||
.color = { 0, 255, 0, 255 },
|
||||
.width = { 2, 2, 2, 2, 0 }
|
||||
}
|
||||
}) {}
|
||||
}
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the border frame failed");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(e, pixel_is(shot, 18, 9, testgreen), "the top border edge is missing");
|
||||
TEST_ASSERT(e, pixel_is(shot, 18, 26, testgreen), "the bottom border edge is missing");
|
||||
TEST_ASSERT(e, pixel_is(shot, 9, 18, testgreen), "the left border edge is missing");
|
||||
TEST_ASSERT(e, pixel_is(shot, 26, 18, testgreen), "the right border edge is missing");
|
||||
TEST_ASSERT(e, pixel_is(shot, 18, 18, testblack), "the border filled its middle");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// A clipped child: a 30x30 fill inside a 10x10 clipping container
|
||||
// must stop at the container's edge.
|
||||
CATCH(e, clear_target());
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the clip frame failed");
|
||||
CLAY({ .layout = { .padding = { 8, 0, 8, 0 } } }) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {
|
||||
.width = CLAY_SIZING_FIXED(10),
|
||||
.height = CLAY_SIZING_FIXED(10)
|
||||
}
|
||||
},
|
||||
.clip = { .horizontal = true, .vertical = true }
|
||||
}) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {
|
||||
.width = CLAY_SIZING_FIXED(30),
|
||||
.height = CLAY_SIZING_FIXED(30)
|
||||
}
|
||||
},
|
||||
.backgroundColor = { 255, 0, 0, 255 }
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the clip frame failed");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(e, pixel_is(shot, 10, 10, testred),
|
||||
"the clipped child did not draw inside the clip");
|
||||
TEST_ASSERT(e, pixel_is(shot, 25, 25, testblack),
|
||||
"the child escaped its clipping container");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
IGNORE(akgl_ui_shutdown());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Text laid out by clay lands on the target, and in its colour.
|
||||
*
|
||||
* Which pixels a rasterized glyph occupies is SDL_ttf's business, so the
|
||||
* assertion is that *some* pixel in the text's box carries the text colour --
|
||||
* enough to prove the measure bridge, the scratch copy and the draw all met.
|
||||
*/
|
||||
akerr_ErrorContext *test_ui_layout_text(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Surface *shot = NULL;
|
||||
uint16_t fontid = 0;
|
||||
bool found = false;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
|
||||
"init for the text layout test failed");
|
||||
TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
|
||||
"registering the layout test font failed");
|
||||
|
||||
CATCH(e, clear_target());
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the text frame failed");
|
||||
CLAY({ .layout = { .padding = { 4, 0, 4, 0 } } }) {
|
||||
CLAY_TEXT(CLAY_STRING("Hi"), CLAY_TEXT_CONFIG({
|
||||
.fontId = fontid,
|
||||
.textColor = { 255, 0, 0, 255 }
|
||||
}));
|
||||
}
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the text frame failed");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
for ( y = 0; y < (TEST_TARGET_SIZE / 2); y++ ) {
|
||||
for ( x = 0; x < (TEST_TARGET_SIZE / 2); x++ ) {
|
||||
if ( pixel_is(shot, x, y, testred) ) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_ASSERT(e, found, "no pixel of the laid-out text carries the text colour");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
IGNORE(akgl_ui_shutdown());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A layout error inside clay must surface from frame_end, not vanish.
|
||||
*
|
||||
* The measure callback and clay's own error handler both report into a stash
|
||||
* with no other way out. Text declared against a fontId nothing issued is the
|
||||
* easiest such error to provoke, and the frame after the failed one must be
|
||||
* clean -- one bad frame, not a wedged subsystem.
|
||||
*/
|
||||
akerr_ErrorContext *test_ui_layout_error_surfaces(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
|
||||
"init for the layout error test failed");
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the doomed frame failed");
|
||||
CLAY_TEXT(CLAY_STRING("doomed"), CLAY_TEXT_CONFIG({
|
||||
.fontId = 7,
|
||||
.textColor = { 255, 255, 255, 255 }
|
||||
}));
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_end(akgl_renderer),
|
||||
"a layout with an unresolvable font closed cleanly");
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "the frame after a failed one was refused");
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer),
|
||||
"closing the recovery frame failed");
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_ui_shutdown());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
|
||||
// The headless tests run first, before SDL is up, because running
|
||||
// without a renderer is part of what they assert.
|
||||
CATCH(errctx, test_ui_validation());
|
||||
CATCH(errctx, test_ui_lifecycle());
|
||||
CATCH(errctx, test_ui_arena_exhaustion());
|
||||
|
||||
akgl_renderer = &akgl_default_renderer;
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_VIDEO),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
TTF_Init(),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize the font engine: %s",
|
||||
SDL_GetError());
|
||||
CATCH(errctx, akgl_registry_init_font());
|
||||
CATCH(errctx, akgl_text_loadfont(TEST_FONT_NAME, TEST_FONT_PATH, TEST_FONT_SIZE));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_CreateWindowAndRenderer(
|
||||
"net/aklabs/libakgl/test_ui",
|
||||
TEST_TARGET_SIZE,
|
||||
TEST_TARGET_SIZE,
|
||||
0,
|
||||
&akgl_window,
|
||||
&akgl_renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
CATCH(errctx, akgl_render_2d_bind(akgl_renderer));
|
||||
|
||||
CATCH(errctx, test_ui_font_register());
|
||||
CATCH(errctx, test_ui_measure_text());
|
||||
CATCH(errctx, test_ui_frame_bracket());
|
||||
CATCH(errctx, test_ui_layout_pixels());
|
||||
CATCH(errctx, test_ui_layout_text());
|
||||
CATCH(errctx, test_ui_layout_error_surfaces());
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_text_unloadallfonts());
|
||||
TTF_Quit();
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user