packages feed

nano-ui-sdl-0.1.0.0: cbits/nano_ui_ttf.c

#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <SDL3_ttf/SDL_textengine.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>

bool nano_ui_ttf_init(void)
{
    return TTF_Init();
}

void nano_ui_ttf_quit(void)
{
    TTF_Quit();
}

/* Kerning is on by default; pin it so shaping survives defaults changing.
 * Direction and script stay unset: shaping detects them per run, so
 * right-to-left and complex scripts shape as themselves. Light grid-fitting
 * snaps stems to whole pixels the way terminals (alacritty/kitty) rasterize,
 * giving crisp edges instead of soft, grey antialiased outlines. NORMAL keeps
 * fractional outlines. */
static TTF_Font *pin_font_rendering(TTF_Font *font)
{
    if (font) {
        TTF_SetFontKerning(font, true);
        TTF_SetFontHinting(font, TTF_HINTING_LIGHT);
    }
    return font;
}

TTF_Font *nano_ui_ttf_open_font(const char *path, float ptsize)
{
    return pin_font_rendering(TTF_OpenFont(path, ptsize));
}

/* A font at another size over the same source as @font@: the copy shares
 * its file or memory stream, so any number of sizes read one source. */
TTF_Font *nano_ui_ttf_copy_font(TTF_Font *font, float ptsize)
{
    TTF_Font *copy = font ? TTF_CopyFont(font) : NULL;
    if (copy) {
        TTF_SetFontSize(copy, ptsize);
    }
    return pin_font_rendering(copy);
}

void nano_ui_ttf_remove_fallback(TTF_Font *font, TTF_Font *fallback)
{
    if (font && fallback) {
        TTF_RemoveFallbackFont(font, fallback);
    }
}

TTF_Font *nano_ui_ttf_open_font_memory(const void *data, size_t size, float ptsize)
{
    /* The font reads this stream after the Haskell ByteString callback ends.
     * Give the stream its own storage, released by its autoclose lifetime. */
    SDL_IOStream *stream = SDL_IOFromDynamicMem();
    if (!stream) {
        return NULL;
    }
    if (SDL_WriteIO(stream, data, size) != size ||
        SDL_SeekIO(stream, 0, SDL_IO_SEEK_SET) < 0) {
        SDL_CloseIO(stream);
        return NULL;
    }
    SDL_PropertiesID props = SDL_CreateProperties();
    if (!props) {
        SDL_CloseIO(stream);
        return NULL;
    }
    SDL_SetPointerProperty(props, TTF_PROP_FONT_CREATE_IOSTREAM_POINTER, stream);
    SDL_SetBooleanProperty(props, TTF_PROP_FONT_CREATE_IOSTREAM_AUTOCLOSE_BOOLEAN, true);
    SDL_SetFloatProperty(props, TTF_PROP_FONT_CREATE_SIZE_FLOAT, ptsize);
    TTF_Font *font = TTF_OpenFontWithProperties(props);
    SDL_DestroyProperties(props);
    return pin_font_rendering(font);
}

void nano_ui_ttf_close_font(TTF_Font *font)
{
    if (font) {
        TTF_CloseFont(font);
    }
}

float nano_ui_ttf_line_skip(TTF_Font *font)
{
    return (float)TTF_GetFontLineSkip(font);
}

float nano_ui_ttf_ascent(TTF_Font *font)
{
    return (float)TTF_GetFontAscent(font);
}

float nano_ui_ttf_space_advance(TTF_Font *font)
{
    int w = 0;
    int h = 0;
    if (TTF_GetStringSize(font, " ", 1, &w, &h) && w > 0) {
        return (float)w;
    }
    int advance = 0;
    if (TTF_GetGlyphMetrics(font, ' ', NULL, NULL, NULL, NULL, &advance) && advance > 0) {
        return (float)advance;
    }
    return 0.f;
}

bool nano_ui_ttf_glyph_metrics(
    TTF_Font *font,
    Uint32 codepoint,
    int *out_minx,
    int *out_maxx,
    int *out_miny,
    int *out_maxy,
    int *out_advance)
{
    if (!font) {
        return false;
    }
    int minx = 0, maxx = 0, miny = 0, maxy = 0, advance = 0;
    if (!TTF_GetGlyphMetrics(font, codepoint, &minx, &maxx, &miny, &maxy, &advance)) {
        return false;
    }
    if (out_minx)   *out_minx   = minx;
    if (out_maxx)   *out_maxx   = maxx;
    if (out_miny)   *out_miny   = miny;
    if (out_maxy)   *out_maxy   = maxy;
    if (out_advance) *out_advance = advance;
    return true;
}

static Uint8 glyph_channel_max(Uint8 r, Uint8 g, Uint8 b)
{
    Uint8 m = r;
    if (g > m) {
        m = g;
    }
    if (b > m) {
        m = b;
    }
    return m;
}

static void force_white_rgb(SDL_Surface *surf)
{
    if (!surf || !surf->pixels || surf->format != SDL_PIXELFORMAT_RGBA32) {
        return;
    }
    Uint8 *base = (Uint8 *)surf->pixels;
    int pitch = surf->pitch;
    for (int y = 0; y < surf->h; y++) {
        Uint8 *row = base + y * pitch;
        for (int x = 0; x < surf->w; x++) {
            Uint8 *px = row + x * 4;
            px[0] = 255;
            px[1] = 255;
            px[2] = 255;
        }
    }
}

static void invert_glyph_alpha(SDL_Surface *surf)
{
    if (!surf || !surf->pixels || surf->format != SDL_PIXELFORMAT_RGBA32) {
        return;
    }
    Uint8 *base = (Uint8 *)surf->pixels;
    int pitch = surf->pitch;
    for (int y = 0; y < surf->h; y++) {
        Uint8 *row = base + y * pitch;
        for (int x = 0; x < surf->w; x++) {
            row[x * 4 + 3] = (Uint8)(255 - row[x * 4 + 3]);
        }
    }
}

static SDL_Surface *glyph_image_to_rgba(SDL_Surface *raw, TTF_ImageType image_type)
{
    if (!raw) {
        return NULL;
    }

    SDL_Surface *out = SDL_ConvertSurface(raw, SDL_PIXELFORMAT_RGBA32);
    if (!out) {
        return NULL;
    }

    if (image_type == TTF_IMAGE_ALPHA || image_type == TTF_IMAGE_SDF) {
        /* Spec: color channels are white, alpha is coverage. Transparent
         * white (a=0, rgb=255) must stay transparent. Do not use luma. */
        force_white_rgb(out);
        return out;
    }

    Uint8 *base = (Uint8 *)out->pixels;
    int pitch = out->pitch;
    Uint32 opaque_sum = 0;
    Uint32 count = 0;
    for (int y = 0; y < out->h; y++) {
        Uint8 *row = base + y * pitch;
        for (int x = 0; x < out->w; x++) {
            Uint8 *px = row + x * 4;
            Uint8 luma = glyph_channel_max(px[0], px[1], px[2]);
            Uint8 a = px[3];
            Uint8 cov = a > luma ? a : luma;
            px[0] = 255;
            px[1] = 255;
            px[2] = 255;
            px[3] = cov;
            opaque_sum += cov;
            count += 1;
        }
    }
    if (count > 0 && opaque_sum > (255u * count) / 2u) {
        invert_glyph_alpha(out);
    }
    return out;
}

bool nano_ui_ttf_render_glyph_surface(
    TTF_Font *font,
    Uint32 codepoint,
    SDL_Surface **out_surface)
{
    if (!font || !out_surface) {
        return false;
    }

    TTF_ImageType image_type = TTF_IMAGE_INVALID;
    SDL_Surface *raw = TTF_GetGlyphImage(font, codepoint, &image_type);
    if (!raw) {
        SDL_Color white = {255, 255, 255, 255};
        raw = TTF_RenderGlyph_Blended(font, codepoint, white);
        image_type = TTF_IMAGE_ALPHA;
    }
    if (!raw) {
        return false;
    }

    SDL_Surface *converted = glyph_image_to_rgba(raw, image_type);
    SDL_DestroySurface(raw);
    if (!converted) {
        return false;
    }

    *out_surface = converted;
    return true;
}

int nano_ui_ttf_get_kerning(TTF_Font *font, Uint32 prev_cp, Uint32 cp)
{
    if (!font) {
        return 0;
    }
    int k = 0;
    TTF_GetGlyphKerning(font, prev_cp, cp, &k);
    return k;
}

/* ------------------------------------------------------------------------ */
/* Shaping                                                                  */
/* ------------------------------------------------------------------------ */

/* One shaped line, copied out of SDL_ttf's text layout: per glyph
 * (text offset, glyph index, dst x y w h, src x y w h) and its font; per
 * cluster (byte offset, byte length, x, width, flags). Coordinates are
 * pixels from the top left of the line. */
typedef struct NanoUIShaped {
    int w;
    int h;
    int num_glyphs;
    int *glyphs;
    TTF_Font **glyph_fonts;
    int num_clusters;
    int *clusters;
    /* Bytes of the caller's text; anything after is the sentinel. */
    int text_len;
} NanoUIShaped;

static bool SDLCALL nano_ui_capture_text(void *userdata, TTF_Text *text)
{
    NanoUIShaped *out = (NanoUIShaped *)userdata;
    TTF_TextData *d = text->internal;
    int copies = 0;
    for (int i = 0; i < d->num_ops; i++) {
        if (d->ops[i].cmd == TTF_DRAW_COMMAND_COPY) {
            copies++;
        }
    }
    out->w = d->w;
    out->h = d->h;
    out->glyphs = (int *)SDL_calloc(copies > 0 ? copies : 1, 10 * sizeof(int));
    out->glyph_fonts = (TTF_Font **)SDL_calloc(copies > 0 ? copies : 1, sizeof(TTF_Font *));
    out->clusters = (int *)SDL_calloc(d->num_clusters > 0 ? d->num_clusters : 1, 5 * sizeof(int));
    if (!out->glyphs || !out->glyph_fonts || !out->clusters) {
        return false;
    }
    /* The sentinel ends a left-to-right line and starts a right-to-left
     * one, which then shifts back by its advance. */
    int shift = 0;
    for (int i = 0; i < d->num_clusters; i++) {
        TTF_SubString *c = &d->clusters[i];
        if (c->offset >= out->text_len && c->length > 0) {
            int advance = 0;
            TTF_GetGlyphMetrics(d->font, '|', NULL, NULL, NULL, NULL, &advance);
            out->w = d->w - advance;
            if ((c->flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
                shift = advance;
            }
        }
    }
    int g = 0;
    for (int i = 0; i < d->num_ops; i++) {
        TTF_DrawOperation *op = &d->ops[i];
        if (op->cmd != TTF_DRAW_COMMAND_COPY || op->copy.text_offset >= out->text_len) {
            continue;
        }
        int *slot = out->glyphs + g * 10;
        slot[0] = op->copy.text_offset;
        slot[1] = (int)op->copy.glyph_index;
        slot[2] = op->copy.dst.x - shift;
        /* SDL_ttf places a fallback font's glyphs from that font's own
         * ascent; align them to the line's font's baseline instead. */
        slot[3] = op->copy.dst.y - (TTF_GetFontAscent(op->copy.glyph_font) - TTF_GetFontAscent(d->font));
        slot[4] = op->copy.dst.w;
        slot[5] = op->copy.dst.h;
        slot[6] = op->copy.src.x;
        slot[7] = op->copy.src.y;
        slot[8] = op->copy.src.w;
        slot[9] = op->copy.src.h;
        out->glyph_fonts[g] = op->copy.glyph_font;
        g++;
    }
    out->num_glyphs = g;
    int k = 0;
    for (int i = 0; i < d->num_clusters; i++) {
        TTF_SubString *c = &d->clusters[i];
        if (c->offset >= out->text_len) {
            continue;
        }
        int *slot = out->clusters + k * 5;
        slot[0] = c->offset;
        slot[1] = c->length;
        slot[2] = c->rect.x - shift;
        slot[3] = c->rect.w;
        slot[4] = (int)c->flags;
        k++;
    }
    out->num_clusters = k;
    d->engine_text = out;
    return true;
}

static void SDLCALL nano_ui_release_text(void *userdata, TTF_Text *text)
{
    (void)userdata;
    (void)text;
}

/* Shape one line with the font and its fallbacks, in a direction (0 lets
 * SDL_ttf pick, TTF_DIRECTION_LTR or TTF_DIRECTION_RTL). The caller frees
 * the result with nano_ui_ttf_shaped_free.
 *
 * The line is shaped with a '|' after it, whose glyph and cluster are then
 * dropped. SDL_ttf 3.2 cuts a fallback span that ends the text one character
 * past its last cluster, losing a vowel sign merged into that cluster, and
 * trims the width of trailing spaces. */
bool nano_ui_ttf_shape(TTF_Font *font, const char *text, size_t len, int direction, NanoUIShaped *out)
{
    SDL_zerop(out);
    if (!font || !text || len == 0) {
        return font != NULL;
    }
    TTF_TextEngine engine;
    SDL_INIT_INTERFACE(&engine);
    engine.userdata = out;
    engine.CreateText = nano_ui_capture_text;
    engine.DestroyText = nano_ui_release_text;
    out->text_len = (int)len;
    bool sentinel = TTF_FontHasGlyph(font, '|');
    char *padded = SDL_malloc(len + 1);
    if (!padded) {
        return false;
    }
    SDL_memcpy(padded, text, len);
    padded[len] = '|';
    TTF_Text *t = TTF_CreateText(&engine, font, padded, sentinel ? len + 1 : len);
    SDL_free(padded);
    if (!t) {
        return false;
    }
    if (direction != 0) {
        TTF_SetTextDirection(t, (TTF_Direction)direction);
    }
    /* Laying the text out hands it to the engine, which copies it. */
    int w = 0, h = 0;
    bool ok = TTF_GetTextSize(t, &w, &h);
    if (ok && out->glyphs == NULL) {
        ok = TTF_UpdateText(t) && out->glyphs != NULL;
    }
    TTF_DestroyText(t);
    return ok;
}

void nano_ui_ttf_shaped_free(NanoUIShaped *shaped)
{
    if (shaped) {
        SDL_free(shaped->glyphs);
        SDL_free(shaped->glyph_fonts);
        SDL_free(shaped->clusters);
        SDL_zerop(shaped);
    }
}

size_t nano_ui_ttf_shaped_size(void)
{
    return sizeof(NanoUIShaped);
}

/* Fields of a shaped line: 0 width, 1 height, 2 glyph count, 3 cluster count. */
int nano_ui_ttf_shaped_int(const NanoUIShaped *shaped, int field)
{
    switch (field) {
    case 0: return shaped->w;
    case 1: return shaped->h;
    case 2: return shaped->num_glyphs;
    case 3: return shaped->num_clusters;
    default: return 0;
    }
}

/* 0 glyphs, 1 glyph fonts, 2 clusters. */
void *nano_ui_ttf_shaped_ptr(const NanoUIShaped *shaped, int field)
{
    switch (field) {
    case 0: return shaped->glyphs;
    case 1: return (void *)shaped->glyph_fonts;
    case 2: return shaped->clusters;
    default: return NULL;
    }
}

bool nano_ui_ttf_render_glyph_index_surface(TTF_Font *font, Uint32 glyph_index, SDL_Surface **out_surface)
{
    if (!font || !out_surface) {
        return false;
    }
    TTF_ImageType image_type = TTF_IMAGE_INVALID;
    SDL_Surface *raw = TTF_GetGlyphImageForIndex(font, glyph_index, &image_type);
    if (!raw) {
        return false;
    }
    SDL_Surface *converted = glyph_image_to_rgba(raw, image_type);
    SDL_DestroySurface(raw);
    if (!converted) {
        return false;
    }
    *out_surface = converted;
    return true;
}

bool nano_ui_ttf_has_glyph(TTF_Font *font, Uint32 ch)
{
    return font && TTF_FontHasGlyph(font, ch);
}

bool nano_ui_ttf_add_fallback(TTF_Font *font, TTF_Font *fallback)
{
    return font && fallback && TTF_AddFallbackFont(font, fallback);
}