Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
141 lines
5.1 KiB
C
141 lines
5.1 KiB
C
/**
|
|
* @file text.c
|
|
* @brief Implements the text subsystem.
|
|
*/
|
|
|
|
#include <akerror.h>
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_ttf/SDL_ttf.h>
|
|
#include <akgl/text.h>
|
|
#include <akgl/registry.h>
|
|
#include <akgl/game.h>
|
|
|
|
akerr_ErrorContext *akgl_text_loadfont(char *name, char *filepath, int size)
|
|
{
|
|
TTF_Font *font = NULL;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
|
FAIL_ZERO_RETURN(errctx, filepath, AKERR_NULLPOINTER, "Null filepath");
|
|
font = TTF_OpenFont(filepath, size);
|
|
FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
// Loading over an existing name used to abandon the font it displaced --
|
|
// there was nothing in the library that could close one. The old font goes
|
|
// back first now, and only once the new one has opened, so a failed load
|
|
// leaves the caller with the font they already had.
|
|
if ( SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL) != NULL ) {
|
|
PASS(errctx, akgl_text_unloadfont(name));
|
|
}
|
|
FAIL_ZERO_RETURN(
|
|
errctx,
|
|
SDL_SetPointerProperty(AKGL_REGISTRY_FONT, name, (void *)font),
|
|
AKERR_KEY,
|
|
"Unable to add font %p to registry as %s : %s",
|
|
(void *)font,
|
|
name,
|
|
SDL_GetError());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_text_unloadfont(char *name)
|
|
{
|
|
TTF_Font *font = NULL;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
|
|
|
font = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL);
|
|
FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "No font named %s in the registry", name);
|
|
|
|
// Cleared before the close, so a font is never reachable through the
|
|
// registry after it has been handed back to SDL_ttf.
|
|
SDL_ClearProperty(AKGL_REGISTRY_FONT, name);
|
|
TTF_CloseFont(font);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
|
|
{
|
|
SDL_Surface *textsurf = NULL;
|
|
SDL_Texture *texture = NULL;
|
|
SDL_FRect dest;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
|
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
|
// Checked before anything is rasterized, and checked at all because a
|
|
// backend that has an SDL_Renderer but was never run through
|
|
// akgl_render_2d_bind has a NULL draw_texture -- which is exactly the state
|
|
// akgl_render_2d_init used to be the only escape from.
|
|
FAIL_ZERO_RETURN(errctx, akgl_renderer, AKERR_NULLPOINTER, "No renderer backend");
|
|
FAIL_ZERO_RETURN(errctx, akgl_renderer->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
|
FAIL_ZERO_RETURN(errctx, akgl_renderer->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
|
|
if ( wraplength > 0 ) {
|
|
textsurf = TTF_RenderText_Blended_Wrapped(
|
|
font,
|
|
text,
|
|
0,
|
|
color,
|
|
wraplength);
|
|
} else {
|
|
textsurf = TTF_RenderText_Blended(
|
|
font,
|
|
text,
|
|
0,
|
|
color);
|
|
}
|
|
FAIL_ZERO_RETURN(errctx, textsurf, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
|
texture = SDL_CreateTextureFromSurface(akgl_renderer->sdl_renderer, textsurf);
|
|
FAIL_ZERO_RETURN(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
|
dest.x = x;
|
|
dest.y = y;
|
|
SDL_GetTextureSize(texture, &dest.w, &dest.h);
|
|
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
|
|
SDL_DestroyTexture(texture);
|
|
SDL_DestroySurface(textsurf);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
|
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
|
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
|
|
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
|
|
// A zero length means "the string is null terminated", not "the empty
|
|
// string" -- an empty text measures 0 wide and one line high.
|
|
FAIL_ZERO_RETURN(
|
|
errctx,
|
|
TTF_GetStringSize(font, text, 0, w, h),
|
|
AKGL_ERR_SDL,
|
|
"%s",
|
|
SDL_GetError());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
|
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
|
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
|
|
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
|
|
// SDL_ttf takes the wrap width as an int and reads a negative one as a
|
|
// very large unsigned width, which silently disables wrapping instead of
|
|
// reporting anything. Refuse it here rather than return a wrong measurement.
|
|
FAIL_NONZERO_RETURN(
|
|
errctx,
|
|
(wraplength < 0),
|
|
AKERR_OUTOFBOUNDS,
|
|
"Wrap length %d is negative",
|
|
wraplength);
|
|
FAIL_ZERO_RETURN(
|
|
errctx,
|
|
TTF_GetStringSizeWrapped(font, text, 0, wraplength, w, h),
|
|
AKGL_ERR_SDL,
|
|
"%s",
|
|
SDL_GetError());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|