/** * @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 _TEXT_H_ #define _TEXT_H_ #include #include #include /** * @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 `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 * `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_bind2d(); 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 // _TEXT_H_