// FreeType and HarfBuzz 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. // HarfBuzz only picks glyphs (liga, calt); the grid, not the font's advances, // places them. #include "font.h" #include #include #include #include #include #include #include struct UIFont { // FreeType allocates through ui_malloc and friends: the library holds a // pointer to this for its lifetime, which the UIFont outlives. struct FT_MemoryRec_ memory; FT_Library library; FT_Face face; FT_F26Dot6 size; // The charmap's answer for ASCII, which is most of every screen: a word the // renderer has not seen before looks up each of its cells. uint32_t ascii[128]; // HarfBuzz state, made by the first ui_font_substitutes question (most // fallback faces are never asked). hb_font_t *hb; hb_buffer_t *buffer; FT_F26Dot6 hb_size; // Every glyph a lookup of a ligature feature could replace. Nothing else // can change a glyph, so a cell outside it is its nominal glyph without // shaping: every cell, in a font without ligatures. hb_set_t *substitutes; // ui_font_shape's per-cell scratch, grown to the longest run seen uint32_t *own; uint8_t *state; int32_t scratch; }; 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; } static void *ft_alloc(FT_Memory memory, long size) { (void)memory; return ui_malloc((size_t)size); } static void ft_free(FT_Memory memory, void *block) { (void)memory; ui_free(block); } static void *ft_realloc(FT_Memory memory, long cur_size, long new_size, void *block) { (void)memory; (void)cur_size; return ui_realloc(block, (size_t)new_size); } UIFont *ui_font_new(const uint8_t *data, int32_t len) { if (!data || len <= 0) return NULL; UIFont *f = (UIFont *)ui_calloc(1, sizeof(UIFont)); if (!f) return NULL; // FT_Init_FreeType is exactly FT_New_Library over FT_New_Memory's malloc // manager, then the default modules and properties: the same, over ours. f->memory = (struct FT_MemoryRec_){NULL, ft_alloc, ft_free, ft_realloc}; if (FT_New_Library(&f->memory, &f->library) != 0) { ui_free(f); return NULL; } FT_Add_Default_Modules(f->library); FT_Set_Default_Properties(f->library); if (FT_New_Memory_Face(f->library, data, (FT_Long)len, 0, &f->face) != 0) { FT_Done_Library(f->library); ui_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); for (uint32_t cp = 0; cp < 128; cp++) f->ascii[cp] = FT_Get_Char_Index(f->face, cp); return f; } // HarfBuzz reads this same face (and its charmap) through FreeType, so shaping // and rasterizing agree on glyph ids. None of these calls returns NULL: a // failure is an inert empty object, and shaping then yields nominal glyphs. static void start_harfbuzz(UIFont *f) { f->hb = hb_ft_font_create_referenced(f->face); f->buffer = hb_buffer_create(); f->substitutes = hb_set_create(); static const hb_tag_t features[] = { HB_TAG('l', 'i', 'g', 'a'), HB_TAG('c', 'a', 'l', 't'), HB_TAG('c', 'l', 'i', 'g'), HB_TAG('r', 'l', 'i', 'g'), HB_TAG('r', 'c', 'l', 't'), HB_TAG_NONE}; hb_face_t *face = hb_font_get_face(f->hb); hb_set_t *lookups = hb_set_create(); hb_ot_layout_collect_lookups(face, HB_OT_TAG_GSUB, NULL, NULL, features, lookups); hb_codepoint_t lookup = HB_SET_VALUE_INVALID; while (hb_set_next(lookups, &lookup)) hb_ot_layout_lookup_collect_glyphs(face, HB_OT_TAG_GSUB, lookup, NULL, f->substitutes, NULL, NULL); hb_set_destroy(lookups); } 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; } uint32_t ui_font_glyph(UIFont *f, int32_t cp) { if (!f || cp < 0) return 0; if (cp < 128) return f->ascii[cp]; return FT_Get_Char_Index(f->face, (FT_ULong)(uint32_t)cp); } void ui_font_free(UIFont *f) { if (!f) return; // The HarfBuzz font holds a reference on the face: release it first. (All // three accept NULL, a face that never shaped.) hb_set_destroy(f->substitutes); hb_buffer_destroy(f->buffer); hb_font_destroy(f->hb); if (f->face) FT_Done_Face(f->face); if (f->library) FT_Done_Library(f->library); ui_free(f->own); ui_free(f->state); ui_free(f); } void ui_font_prime(UIFont *f) { if (!f) return; hb_font_destroy(hb_ft_font_create_referenced(f->face)); hb_buffer_t *buffer = hb_buffer_create(); hb_buffer_add_utf8(buffer, "a", 1, 0, 1); hb_buffer_guess_segment_properties(buffer); hb_buffer_destroy(buffer); } 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, uint32_t glyph_index, int bold, uint8_t *out, int32_t stride, int32_t width, int32_t height, int32_t cell_x, int32_t cell_w, int32_t ascent, int centered) { if (!f || !out || stride < width || width <= 0 || height <= 0) return 0; for (int y = 0; y < height; y++) memset(out + (size_t)y * (size_t)stride, 0, (size_t)width); if (!set_size(f, px)) return 0; if (FT_Load_Glyph(f->face, glyph_index, load_flags()) != 0) return 0; if (bold) { // Synthetic bold thickens the hinted outline before it is rendered, so // the wider stems are rasterized as one shape and keep hinted edges; the // advance is untouched, as the cell grid requires. Strength is Ghostty's // heuristic: 1/32 of the line height in 26.6, rounded up (one pixel at a // 32 px line). A bitmap strike has no outline: widen it by one whole pixel, // the classic overstrike, which keeps its pixels sharp. FT_GlyphSlot slot = f->face->glyph; if (slot->format == FT_GLYPH_FORMAT_OUTLINE) { const FT_Pos height = f->face->size->metrics.height; FT_Outline_Embolden(&slot->outline, (height + 31) / 32); } else if (slot->format == FT_GLYPH_FORMAT_BITMAP && FT_GlyphSlot_Own_Bitmap(slot) == 0) { FT_Bitmap_Embolden(f->library, &slot->bitmap, 64, 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 ? (width - (int)bitmap->width) / 2 : cell_x + glyph->bitmap_left + (cell_w - advance) / 2; const int gy = centered ? (height - (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 >= height) 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 >= width) 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, uint32_t glyph, int bold, uint8_t *out, int32_t stride, int32_t width, int32_t height, int32_t cell_x, int32_t cell_w, int32_t ascent) { return raster(f, px, glyph, bold, out, stride, width, height, cell_x, cell_w, ascent, 0); } int ui_font_raster_centered(UIFont *f, float px, uint32_t glyph, int bold, uint8_t *out, int32_t stride, int32_t width, int32_t height) { return raster(f, px, glyph, bold, out, stride, width, height, 0, width, 0, 1); } // The horizontal ink of `glyph` placed as ui_font_raster places it in a cell // `cell` wide (26.6), relative to the cell's left edge. 0 when it has no ink. static int ink(UIFont *f, uint32_t glyph, FT_Pos cell, FT_Pos *left, FT_Pos *right) { if (FT_Load_Glyph(f->face, glyph, load_flags()) != 0) return 0; const FT_GlyphSlot slot = f->face->glyph; if (slot->metrics.width <= 0 || slot->metrics.height <= 0) return 0; const FT_Pos origin = (FT_Pos)(((int)(cell >> 6) - ceil_26_6(slot->advance.x)) / 2) * 64; *left = origin + slot->metrics.horiBearingX; *right = *left + slot->metrics.width; return 1; } // ui_font_shape's per-cell marks: no glyph of its own, keep the nominal glyph, // drawn as a slice of another cell's glyph. #define NONE UINT32_MAX #define PLAIN 1 #define CLAIMED 2 int ui_font_substitutes(UIFont *f, uint32_t glyph) { if (!f) return 0; if (!f->hb) start_harfbuzz(f); return hb_set_has(f->substitutes, glyph); } // The GSUB features shaping leaves on are the ligature ones; the others // HarfBuzz would apply (glyph composition, local forms, fractions around // U+2044, ...) are off, so a cell is either its nominal glyph, as the renderer // always drew it, or part of a ligature. Positioning is untouched: the grid // ignores advances and keeps nominal glyphs where marks would move. static const hb_feature_t shaping_features[] = { {HB_TAG('c', 'c', 'm', 'p'), 0, 0, (unsigned int)-1}, {HB_TAG('l', 'o', 'c', 'l'), 0, 0, (unsigned int)-1}, {HB_TAG('r', 'v', 'r', 'n'), 0, 0, (unsigned int)-1}, {HB_TAG('l', 't', 'r', 'a'), 0, 0, (unsigned int)-1}, {HB_TAG('l', 't', 'r', 'm'), 0, 0, (unsigned int)-1}, {HB_TAG('r', 'a', 'n', 'd'), 0, 0, (unsigned int)-1}, {HB_TAG('f', 'r', 'a', 'c'), 0, 0, (unsigned int)-1}, {HB_TAG('n', 'u', 'm', 'r'), 0, 0, (unsigned int)-1}, {HB_TAG('d', 'n', 'o', 'm'), 0, 0, (unsigned int)-1}, }; void ui_font_shape(UIFont *f, float px, int32_t cell_w, const uint32_t *cps, int32_t n, UIShapedCell *out) { if (!f || !cps || !out || n <= 0) return; for (int32_t i = 0; i < n; i++) out[i] = (UIShapedCell){ui_font_glyph(f, (int32_t)cps[i]), 0, 1, 0}; if (!f->hb) start_harfbuzz(f); int substitutable = 0; for (int32_t i = 0; i < n && !substitutable; i++) substitutable = hb_set_has(f->substitutes, out[i].glyph); if (!substitutable || cell_w <= 0 || !set_size(f, px)) return; if (n > f->scratch) { uint32_t *own = (uint32_t *)ui_realloc(f->own, (size_t)n * sizeof *own); if (!own) return; f->own = own; uint8_t *state = (uint8_t *)ui_realloc(f->state, (size_t)n); if (!state) return; f->state = state; f->scratch = n; } // hb-ft reads the FreeType size when told; positions are only consulted // for being nonzero, but a zero scale would hide exactly that. if (f->hb_size != f->size) { hb_ft_font_changed(f->hb); f->hb_size = f->size; } hb_buffer_clear_contents(f->buffer); // One codepoint per cell, so a cluster value IS a cell index. hb_buffer_add_codepoints(f->buffer, cps, n, 0, n); hb_buffer_set_direction(f->buffer, HB_DIRECTION_LTR); hb_buffer_guess_segment_properties(f->buffer); hb_shape(f->hb, f->buffer, shaping_features, sizeof shaping_features / sizeof shaping_features[0]); unsigned int count = 0; const hb_glyph_info_t *info = hb_buffer_get_glyph_infos(f->buffer, &count); const hb_glyph_position_t *pos = hb_buffer_get_glyph_positions(f->buffer, &count); // Each cell's own glyph is the one HarfBuzz left in its cluster. A cell a // ligature merged into an earlier cluster has NONE. A cluster the grid // cannot place (several glyphs, a positioned mark) is PLAIN, and so are the // cells merged into it: they keep their nominal glyphs. uint32_t *own = f->own; uint8_t *state = f->state; for (int32_t i = 0; i < n; i++) { own[i] = NONE; state[i] = 0; } for (unsigned int k = 0; k < count; k++) { const uint32_t cluster = info[k].cluster; if (cluster >= (uint32_t)n) continue; if (own[cluster] != NONE || pos[k].x_offset != 0 || pos[k].y_offset != 0) state[cluster] = PLAIN; else own[cluster] = info[k].codepoint; } for (int32_t i = 1; i < n; i++) if (own[i] == NONE && (state[i - 1] & PLAIN)) state[i] = PLAIN; // A substituted glyph (or one whose cluster swallowed cells) may reach over // its neighbours: it claims those that draw nothing of their own, nearest // first, and each draws its slice of the one strip. Ink past a cell edge by // half a pixel or less is noise. const FT_Pos cell = (FT_Pos)cell_w * 64; for (int32_t i = 0; i < n; i++) { if ((state[i] & (PLAIN | CLAIMED)) || own[i] == NONE) continue; int32_t merged = 0; while (i + 1 + merged < n && own[i + 1 + merged] == NONE && !(state[i + 1 + merged] & PLAIN)) merged++; if (own[i] == out[i].glyph && merged == 0) continue; FT_Pos left = 0, right = 0; if (!ink(f, own[i], cell, &left, &right)) continue; // a spacer: claimed, never claiming int32_t want_left = left < -32 ? (int32_t)((-left - 32 + cell - 1) / cell) : 0; int32_t want_right = right > cell + 32 ? (int32_t)((right - cell - 32 + cell - 1) / cell) : 0; if (want_right < merged) want_right = merged; int32_t lead = 0, trail = 0; while (lead < want_left && 1 + lead + trail < UI_MAX_SPAN) { const int32_t j = i - lead - 1; FT_Pos l, r; if (j < 0 || (state[j] & (PLAIN | CLAIMED)) || (own[j] != NONE && (own[j] == out[j].glyph || ink(f, own[j], cell, &l, &r)))) break; lead++; } while (trail < want_right && 1 + lead + trail < UI_MAX_SPAN) { const int32_t j = i + trail + 1; FT_Pos l, r; if (j >= n || (state[j] & (PLAIN | CLAIMED)) || (own[j] != NONE && (own[j] == out[j].glyph || ink(f, own[j], cell, &l, &r)))) break; trail++; } for (int32_t j = i - lead; j <= i + trail; j++) { out[j] = (UIShapedCell){own[i], (uint8_t)lead, (uint8_t)(1 + lead + trail), (uint8_t)(j - (i - lead))}; state[j] |= CLAIMED; } } // The rest draw their own glyph; a merged cell nothing claimed draws blank. const uint32_t space = f->ascii[' ']; for (int32_t i = 0; i < n; i++) { if (state[i] & (PLAIN | CLAIMED)) continue; out[i].glyph = own[i] == NONE ? space : own[i]; } }