Files
libakgl/src/text.c
Tachikoma 76d39f09d4 Give every exported function a declaration, and check that it stays that way
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>
2026-08-01 07:33:35 -04:00

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);
}