// 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 #include #include 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; } const char *ui_font_name(UIFont *f) { if (!f || !f->face) return NULL; const char *postscript = FT_Get_Postscript_Name(f->face); if (postscript && postscript[0]) return postscript; if (f->face->family_name && f->face->family_name[0]) return f->face->family_name; return NULL; } 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; } static int 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, int centered) { 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 = centered ? (cell_w - (int)bitmap->width) / 2 : glyph->bitmap_left + (cell_w - advance) / 2; const int gy = centered ? (cell_h - (int)bitmap->rows) / 2 : 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; } 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) { return raster(f, px, cp, out, stride, cell_w, cell_h, ascent, 0); } int ui_font_raster_centered(UIFont *f, float px, int32_t cp, uint8_t *out, int32_t stride, int32_t width, int32_t height) { return raster(f, px, cp, out, stride, width, height, 0, 1); }