Files
libakgl/include/akgl/text.h
Andrew Kesterson 9924d74dcc Namespace every exported symbol, and bump to 0.5.0
Closes internal-consistency items 1 through 6, 12 and 13. Every include guard
is _AKGL_<FILE>_H_, every in-project header include is angled, and every
exported function, type and global carries the akgl_ prefix. This is an ABI
break; the soname goes to libakgl.so.0.5. TODO.md carries the full rename
table.

The renames were driven by renaming each declaration and letting the compiler
find the uses, not by pattern substitution: renderer, physics and camera are
also parameter and struct-member names, and a sed would have rewritten
map->physics and every akgl_RenderBackend *renderer parameter without a word.

Item 4 turned out not to be cosmetic. The library exported a global called
renderer and tests/character.c defined an SDL_Renderer *renderer of its own;
the executable's definition preempted the library's, akgl_sprite_load_json
read a SDL_Renderer * through an akgl_RenderBackend *, and every texture load
in that suite failed. The suite reported success anyway, because libakerror's
unhandled-error handler ends in exit(errctx->status), exit keeps only the low
byte, and AKGL_ERR_SDL is exactly 256. So character had been green while
running one of its four tests, and every suite in the tree was unable to fail
on the most common status in a library built on SDL.

Both are fixed. tests/testutil.h gains TEST_TRAP_UNHANDLED_ERRORS(), which
collapses any status a byte cannot carry onto 1, and every suite installs it.
character binds a real backend with akgl_render_2d_bind. Its fourth test then
runs for the first time and fails on a defect it has asserted all along, so
akgl_heap_release_character now walks state_sprites with
AKGL_ITERATOR_OP_RELEASE and destroys the property set before zeroing the slot
-- TODO.md Defects item 21 and half of Carried over item 1.

AKGL_TIME_ONESEC_MS said "one second in milliseconds" and held 1000000, so
akgl_game_state_lock waited roughly sixteen minutes rather than one second. It
is AKGL_TIME_ONEMS_NS now, the budget is its own named constant, and
tests/game.c holds the mutex from a second thread to assert the wait -- the
contended path had no coverage at all.

Headers are self-contained and it is enforced: AKGL_PUBLIC_HEADERS drives both
install() and a generated translation unit per header, so a header that ships
is a header that is checked. Writing that found registry.h, which used
SDL_PropertiesID in eight declarations and included no SDL header.

23/23 suites pass, memcheck is clean, reindent --check is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:35 -04:00

159 lines
8.6 KiB
C

/**
* @file text.h
* @brief Loading fonts and drawing or measuring strings with them.
*
* Fonts are `TTF_Font *` handles kept in the #AKGL_REGISTRY_FONT property
* registry under a caller-chosen name; there is no akgl font type wrapping them.
* SDL_ttf must be initialized (akgl_game_init does it) before any of this.
*
* The two measure functions do not touch the renderer, so they are usable
* before -- or entirely without -- a window. Drawing is immediate mode: each
* akgl_text_rendertextat() call rasterizes, uploads, blits, and throws the
* texture away, which is fine for a HUD line and wrong for a large body of
* static text redrawn every frame.
*/
#ifndef _AKGL_TEXT_H_
#define _AKGL_TEXT_H_
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
/**
* @brief Open a TrueType font at one size and publish it in the font registry.
*
* A size is baked into the handle, so the same file at two sizes is two calls
* under two names. Nothing releases these: the handles live until the process
* ends.
*
* @param name Registry key to publish the font under. Required. An existing
* entry with the same name is replaced, and the font it
* displaced is closed through akgl_text_unloadfont -- but only
* after the new one has opened, so a failed load leaves the
* caller with the font they already had.
* @param filepath Path to a `.ttf`/`.otf` file. Required. Used verbatim -- not
* resolved against `SDL_GetBasePath()`.
* @param size Point size to rasterize at. Passed straight to SDL_ttf, which
* rejects anything that is not positive.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p name or @p filepath is `NULL`.
* @throws AKGL_ERR_SDL If the font cannot be opened -- missing, unreadable, not
* a font, or a @p size SDL_ttf refuses. The message carries
* `SDL_GetError()`.
* @throws AKERR_KEY If the font cannot be written into #AKGL_REGISTRY_FONT --
* in practice, because akgl_registry_init has not run.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size);
/**
* @brief Close a loaded font and take it out of the font registry.
*
* The other half of akgl_text_loadfont, and for a while the half that did not
* exist: a font could be opened and published but never handed back, so a game
* that changed fonts between scenes had no way to reclaim the one it had
* finished with. A `TTF_Font` is about ten kilobytes once FreeType's own
* structures are counted.
*
* The registry entry is cleared before the font is closed, so a font is never
* reachable through #AKGL_REGISTRY_FONT after it has gone back to SDL_ttf.
*
* @param name Registry key the font was published under. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p name is `NULL`.
* @throws AKERR_KEY If no font is registered under @p name -- including the
* case where it was already unloaded, which makes a double unload an
* error rather than a double close.
*
* @warning Anything still holding the `TTF_Font *` -- a caller that fetched it
* from the registry earlier, a pending akgl_text_rendertextat -- is
* left with a dangling pointer. Fonts are not reference counted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name);
/**
* @brief Rasterize a string and blit it at a screen position, in one call.
*
* Renders blended (anti-aliased, alpha-blended) through SDL_ttf, uploads the
* result to a texture, draws it through the global `akgl_renderer`, and destroys both
* the texture and the surface before returning. The text is drawn at its natural
* size -- @p x and @p y are the top-left corner, not a centre.
*
* Coordinates are screen coordinates, not world ones: this does not go through
* the camera, so a HUD stays put while the world scrolls under it.
*
* @param font Font to render with, from akgl_text_loadfont. Required.
* @param text UTF-8 text. Required. May contain newlines, which break
* lines on either path.
* @param color Text colour, including alpha.
* @param wraplength Wrap width in pixels. Greater than 0 wraps on word
* boundaries at that width; 0 or less draws a single line and
* breaks only on newlines in @p text.
* @param x Left edge of the text, in screen pixels.
* @param y Top edge of the text, in screen pixels.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p font or @p text is `NULL`; if the global
* `akgl_renderer`, its `sdl_renderer`, or its `draw_texture` is `NULL` --
* that last one is the state a backend is in between being allocated
* and being run through akgl_render_2d_bind(); if SDL_ttf cannot
* rasterize the string; or if the surface cannot be uploaded as a
* texture. The last two carry `SDL_GetError()` and are a reused status
* rather than a pointer problem.
* @throws AKERR_* Whatever the backend's `draw_texture` raises.
*
* @note On a failure after rasterizing -- the texture upload, or the draw -- the
* surface and texture are not destroyed, because the error returns before
* the cleanup. Repeated failures leak.
* @note The empty string is **refused**, not drawn as nothing: SDL_ttf reports
* "Text has zero width" and this passes that on as `AKERR_NULLPOINTER`.
* akgl_text_measure() accepts it, so the two disagree. A caller drawing a
* line of text that may be empty has to check for it. TODO.md, "Known and
* still open".
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y);
/**
* @brief Report the size, in pixels, that @p text would occupy on one line.
*
* Nothing is drawn and no renderer is required. A caller building a character
* grid measures one cell with this -- the advance width of a single glyph in a
* monospaced font -- and derives the rest of the grid from it.
*
* @param font Font to measure with, from akgl_text_loadfont. Required.
* @param text UTF-8 text to measure. Required. The empty string is legal and
* measures 0 wide by one line high.
* @param w Receives the width in pixels. Required.
* @param h Receives the height in pixels -- one line, whatever @p text
* contains, since this form does not wrap. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);
/**
* @brief Report the size, in pixels, that @p text would occupy when wrapped.
*
* The companion to akgl_text_measure() for the wrapping case, matching the
* @p wraplength argument akgl_text_rendertextat() already takes: a string
* longer than @p wraplength reports the height of every line it breaks onto.
* A @p wraplength of zero wraps only on newlines in @p text.
*
* @param font Font to measure with, from akgl_text_loadfont. Required.
* @param text UTF-8 text to measure. Required.
* @param wraplength Wrap width in pixels. 0 wraps on newlines only. Negative is
* refused rather than passed through: SDL_ttf reads a negative
* width as a very large unsigned one and silently stops
* wrapping, which would return a measurement that is wrong
* rather than an error.
* @param w Receives the width in pixels: the longest line, not
* @p wraplength. Required.
* @param h Receives the height in pixels, covering every line the text
* wraps onto. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p wraplength is negative.
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h);
#endif // _AKGL_TEXT_H_