/** * @file ui.c * @brief Unit tests for the UI subsystem: lifecycle, fonts, measurement and rendering. * * 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 #include #include #include #include #include #include #include #include #include #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. * * Runs before anything initializes the subsystem, because the "not * initialized" refusal is half of what it asserts. */ akerr_ErrorContext *test_ui_validation(void) { PREPARE_ERROR(e); ATTEMPT { TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_ui_init(0, 240), "akgl_ui_init accepted a zero width"); TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_ui_init(320, -1), "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); SUCCEED_RETURN(e); } /** * @brief Init, double-init refusal, idempotent shutdown, re-init. * * The double init has to be refused rather than absorbed: a second * Clay_Initialize over a live context would silently discard every element id * and scroll position the first one held, which is the kind of reset a game * only notices as "the menu forgot where it was". */ akerr_ErrorContext *test_ui_lifecycle(void) { PREPARE_ERROR(e); ATTEMPT { TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "first akgl_ui_init failed"); TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_init(320, 240), "a second akgl_ui_init was not refused"); TEST_EXPECT_OK(e, akgl_ui_resize(640, 480), "resize on a live subsystem failed"); TEST_EXPECT_OK(e, akgl_ui_shutdown(), "akgl_ui_shutdown failed"); TEST_EXPECT_OK(e, akgl_ui_shutdown(), "a second akgl_ui_shutdown was not a no-op"); TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "re-init after shutdown failed"); TEST_EXPECT_OK(e, akgl_ui_shutdown(), "shutdown after re-init failed"); } CLEANUP { IGNORE(akgl_ui_shutdown()); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } /** * @brief A too-small arena must refuse at init, loudly, and recover. * * The narrowed limit stands in for a consumer who raised AKGL_UI_MAX_ELEMENTS * without raising AKGL_UI_ARENA_BYTES: the refusal at startup, with both * numbers in the message, is the whole safety story -- past init, clay trusts * the arena completely. Same shape as the collision arena's exhaustion test, * for the same reason: rebuilding with a smaller ceiling is not something CI * can do. */ akerr_ErrorContext *test_ui_arena_exhaustion(void) { PREPARE_ERROR(e); ATTEMPT { akgl_ui_arena_limit(64); TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_init(320, 240), "akgl_ui_init accepted an arena clay cannot fit in"); akgl_ui_arena_limit(0); TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "akgl_ui_init failed after the arena limit was restored"); TEST_EXPECT_OK(e, akgl_ui_shutdown(), "shutdown after the exhaustion test failed"); } CLEANUP { akgl_ui_arena_limit(0); IGNORE(akgl_ui_shutdown()); } PROCESS(e) { } FINISH(e, true); 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 Build a left-button mouse event at a position, down or up. */ static void make_button_event(SDL_Event *event, uint32_t type, float x, float y) { SDL_memset(event, 0x00, sizeof(SDL_Event)); event->type = type; event->button.button = SDL_BUTTON_LEFT; event->button.x = x; event->button.y = y; } /** * @brief The consumed contract: clicks on the UI are the UI's, everything else passes. * * The hit test runs against the layout the previous frame retained, so the * test lays one panel out, then throws events at it: a click inside is * consumed, a click outside is not, motion never is, and -- the case most * likely to surprise -- a click before any layout exists is not consumed, * because there is nothing to be over. */ akerr_ErrorContext *test_ui_handle_event(void) { PREPARE_ERROR(e); SDL_Event event; bool consumed = true; int token = 1; ATTEMPT { // Uninitialized: everything passes through untouched. make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f); TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "an event before init was refused"); TEST_ASSERT(e, consumed == false, "an event before init was consumed"); TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE), "init for the event test failed"); // Initialized but nothing laid out yet: a click lands on no tree. make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f); TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "a click before any layout was refused"); TEST_ASSERT(e, consumed == false, "a click was consumed before any layout existed"); make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 10.0f, 10.0f); TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "the release before any layout was refused"); // Lay out one 20x20 panel at (8,8) for the hit tests that follow. CATCH(e, clear_target()); TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the hit-test 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 hit-test frame failed"); // A press on the panel is the UI's; its release is too. make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f); TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "the press on the panel was refused"); TEST_ASSERT(e, consumed == true, "a press on a UI panel was not consumed"); make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 10.0f, 10.0f); TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "the release on the panel was refused"); TEST_ASSERT(e, consumed == true, "a release on a UI panel was not consumed"); // A press beside the panel belongs to the game. make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 50.0f, 50.0f); TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "the press beside the panel was refused"); TEST_ASSERT(e, consumed == false, "a press beside the panel was consumed"); make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 50.0f, 50.0f); TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "the release beside the panel was refused"); // A right-button press is not the UI's business at all. make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f); event.button.button = SDL_BUTTON_RIGHT; TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "a right-button press was refused"); TEST_ASSERT(e, consumed == false, "a right-button press was consumed"); // Motion is never consumed, wherever it lands. SDL_memset(&event, 0x00, sizeof(SDL_Event)); event.type = SDL_EVENT_MOUSE_MOTION; event.motion.x = 10.0f; event.motion.y = 10.0f; TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "motion over the panel was refused"); TEST_ASSERT(e, consumed == false, "motion over a panel was consumed"); // The wheel goes to the UI only while the pointer is over it. The // motion event above left the pointer on the panel. SDL_memset(&event, 0x00, sizeof(SDL_Event)); event.type = SDL_EVENT_MOUSE_WHEEL; event.wheel.y = 1.0f; TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "the wheel over the panel was refused"); TEST_ASSERT(e, consumed == true, "the wheel over a panel was not consumed"); SDL_memset(&event, 0x00, sizeof(SDL_Event)); event.type = SDL_EVENT_MOUSE_MOTION; event.motion.x = 50.0f; event.motion.y = 50.0f; TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "motion off the panel was refused"); SDL_memset(&event, 0x00, sizeof(SDL_Event)); event.type = SDL_EVENT_MOUSE_WHEEL; event.wheel.y = 1.0f; TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "the wheel beside the panel was refused"); TEST_ASSERT(e, consumed == false, "the wheel beside the panel was consumed"); // A resize reaches the layout engine and still passes through. SDL_memset(&event, 0x00, sizeof(SDL_Event)); event.type = SDL_EVENT_WINDOW_RESIZED; event.window.data1 = TEST_TARGET_SIZE; event.window.data2 = TEST_TARGET_SIZE; TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "a resize event was refused"); TEST_ASSERT(e, consumed == false, "a resize event was consumed"); // A key event is not the UI's; keyboard focus is the application's. SDL_memset(&event, 0x00, sizeof(SDL_Event)); event.type = SDL_EVENT_KEY_DOWN; event.key.key = SDLK_RETURN; TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed), "a key event was refused"); TEST_ASSERT(e, consumed == false, "a key event was consumed"); make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(NULL, &event, &consumed), "a NULL appstate was accepted"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(&token, NULL, &consumed), "a NULL event was accepted"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(&token, &event, NULL), "a NULL consumed destination was accepted"); } CLEANUP { 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_handle_event()); CATCH(errctx, test_ui_layout_error_surfaces()); } CLEANUP { IGNORE(akgl_text_unloadallfonts()); TTF_Quit(); SDL_Quit(); } PROCESS(errctx) { } FINISH_NORETURN(errctx); }