diff options
Diffstat (limited to 'src/gui/font.c')
| -rw-r--r-- | src/gui/font.c | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/src/gui/font.c b/src/gui/font.c new file mode 100644 index 00000000..e94480b5 --- /dev/null +++ b/src/gui/font.c @@ -0,0 +1,175 @@ +// FreeType wrapper for gui.zig. The policy intentionally follows Ghostty's +// FreeType backend: native hinting enabled, the auto-hinter available as a +// fallback, and the light target for shape-preserving grayscale text. +#include "font.h" +#include <freetype-zig.h> + +#include <stdlib.h> +#include <string.h> + +struct UIFont { + FT_Library library; + FT_Face face; + FT_F26Dot6 size; +}; + +static FT_F26Dot6 size_26_6(float px) { + if (px < 1.0f) + px = 1.0f; + return (FT_F26Dot6)(px * 64.0f + 0.5f); +} + +static int set_size(UIFont *f, float px) { + const FT_F26Dot6 size = size_26_6(px); + if (f->size == size) + return 1; + if (FT_Set_Char_Size(f->face, 0, size, 72, 72) != 0) + return 0; + f->size = size; + return 1; +} + +static int ceil_26_6(FT_Pos value) { return (int)((value + 63) >> 6); } + +static FT_Int32 load_flags(void) { + // Hinting is on because FT_LOAD_NO_HINTING is absent. Likewise, leaving + // FT_LOAD_NO_AUTOHINT absent lets FreeType fall back to its auto-hinter + // when the face has no native instructions. This is Ghostty's default. + return FT_LOAD_DEFAULT | FT_LOAD_TARGET_LIGHT; +} + +UIFont *ui_font_new(const uint8_t *data, int32_t len) { + if (!data || len <= 0) + return NULL; + + UIFont *f = (UIFont *)calloc(1, sizeof(UIFont)); + if (!f) + return NULL; + if (FT_Init_FreeType(&f->library) != 0) { + free(f); + return NULL; + } + if (FT_New_Memory_Face(f->library, data, (FT_Long)len, 0, &f->face) != 0) { + FT_Done_FreeType(f->library); + free(f); + return NULL; + } + // Some symbol fonts do not expose a Unicode charmap. FreeType already + // selects a usable default for those, so failure here is non-fatal. + (void)FT_Select_Charmap(f->face, FT_ENCODING_UNICODE); + return f; +} + +int ui_font_has_glyph(UIFont *f, int32_t cp) { + if (!f || cp < 0) + return 0; + return FT_Get_Char_Index(f->face, (FT_ULong)(uint32_t)cp) != 0; +} + +void ui_font_free(UIFont *f) { + if (!f) + return; + if (f->face) + FT_Done_Face(f->face); + if (f->library) + FT_Done_FreeType(f->library); + free(f); +} + +float ui_font_scale_for_height(UIFont *f, float px) { + (void)f; + // Kept as a size token so the Zig renderer can continue to distinguish + // measurement size from high-DPI raster size. + return px; +} + +void ui_font_cell_metrics(UIFont *f, float px, int32_t *cell_w, int32_t *cell_h, + int32_t *ascent) { + if (!f || !cell_w || !cell_h || !ascent || !set_size(f, px)) { + if (cell_w) + *cell_w = 1; + if (cell_h) + *cell_h = 1; + if (ascent) + *ascent = 1; + return; + } + + const FT_Size_Metrics metrics = f->face->size->metrics; + const int asc = ceil_26_6(metrics.ascender); + const int desc = ceil_26_6(-metrics.descender); + int height = ceil_26_6(metrics.height); + if (height < asc + desc) + height = asc + desc; + + int width = ceil_26_6(metrics.max_advance); + const FT_UInt m = FT_Get_Char_Index(f->face, 'M'); + if (FT_Load_Glyph(f->face, m, load_flags()) == 0) { + const int advance = ceil_26_6(f->face->glyph->advance.x); + if (advance > 0) + width = advance; + } + + *cell_w = width > 0 ? width : 1; + *cell_h = height > 0 ? height : 1; + *ascent = asc > 0 ? asc : 1; +} + +int ui_font_raster(UIFont *f, float px, int32_t cp, uint8_t *out, + int32_t stride, int32_t cell_w, int32_t cell_h, + int32_t ascent) { + if (!f || !out || stride < cell_w || cell_w <= 0 || cell_h <= 0) + return 0; + for (int y = 0; y < cell_h; y++) + memset(out + (size_t)y * (size_t)stride, 0, (size_t)cell_w); + if (!set_size(f, px)) + return 0; + + const FT_UInt glyph_index = + FT_Get_Char_Index(f->face, (FT_ULong)(uint32_t)cp); + if (FT_Load_Glyph(f->face, glyph_index, load_flags()) != 0) + return 0; + if (FT_Render_Glyph(f->face->glyph, FT_RENDER_MODE_NORMAL) != 0) + return 0; + + const FT_GlyphSlot glyph = f->face->glyph; + const FT_Bitmap *bitmap = &glyph->bitmap; + if (!bitmap->buffer || bitmap->width == 0 || bitmap->rows == 0) + return 0; + + const int advance = ceil_26_6(glyph->advance.x); + // Fallbacks may be proportional. Center their advance in the primary face's + // fixed cell, exactly as Ghostty centers a narrower fallback face. + const int gx = glyph->bitmap_left + (cell_w - advance) / 2; + const int gy = ascent - glyph->bitmap_top; + const int pitch = bitmap->pitch; + const int row_bytes = pitch < 0 ? -pitch : pitch; + + for (unsigned int y = 0; y < bitmap->rows; y++) { + const int dy = gy + (int)y; + if (dy < 0 || dy >= cell_h) + continue; + const unsigned int source_y = pitch < 0 ? bitmap->rows - 1 - y : y; + const uint8_t *src = bitmap->buffer + (size_t)source_y * (size_t)row_bytes; + + for (unsigned int x = 0; x < bitmap->width; x++) { + const int dx = gx + (int)x; + if (dx < 0 || dx >= cell_w) + continue; + + uint8_t coverage; + if (bitmap->pixel_mode == FT_PIXEL_MODE_MONO) { + coverage = (src[x >> 3] & (0x80u >> (x & 7))) ? 255 : 0; + } else if (bitmap->pixel_mode == FT_PIXEL_MODE_GRAY) { + coverage = src[x]; + if (bitmap->num_grays > 1 && bitmap->num_grays != 256) + coverage = (uint8_t)((unsigned int)coverage * 255u / + (bitmap->num_grays - 1u)); + } else { + continue; + } + out[(size_t)dy * (size_t)stride + (size_t)dx] = coverage; + } + } + return 1; +} |
