diff options
Diffstat (limited to 'src/gui/font.c')
| -rw-r--r-- | src/gui/font.c | 283 |
1 files changed, 251 insertions, 32 deletions
diff --git a/src/gui/font.c b/src/gui/font.c index 5ea595f9..5cb98e1a 100644 --- a/src/gui/font.c +++ b/src/gui/font.c @@ -1,9 +1,14 @@ -// 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. +// 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 <freetype-zig.h> #include <freetype/ftbitmap.h> +#include <hb-ft.h> +#include <hb-ot.h> +#include <hb.h> #include <stdlib.h> #include <string.h> @@ -12,6 +17,22 @@ struct UIFont { 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) { @@ -58,9 +79,33 @@ UIFont *ui_font_new(const uint8_t *data, int32_t len) { // 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; @@ -72,29 +117,31 @@ const char *ui_font_name(UIFont *f) { return NULL; } -int ui_font_has_glyph(UIFont *f, int32_t cp) { +uint32_t ui_font_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; + 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_FreeType(f->library); + free(f->own); + free(f->state); 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)) { @@ -127,18 +174,17 @@ void ui_font_cell_metrics(UIFont *f, float px, int32_t *cell_w, int32_t *cell_h, *ascent = asc > 0 ? asc : 1; } -static int raster(UIFont *f, float px, int32_t cp, int bold, 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) +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 < cell_h; y++) - memset(out + (size_t)y * (size_t)stride, 0, (size_t)cell_w); + 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; - 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 (bold) { @@ -168,23 +214,24 @@ static int raster(UIFont *f, float px, int32_t cp, int bold, uint8_t *out, 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 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 >= cell_h) + 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 >= cell_w) + if (dx < 0 || dx >= width) continue; uint8_t coverage; @@ -204,14 +251,186 @@ static int raster(UIFont *f, float px, int32_t cp, int bold, uint8_t *out, return 1; } -int ui_font_raster(UIFont *f, float px, int32_t cp, int bold, uint8_t *out, - int32_t stride, int32_t cell_w, int32_t cell_h, - int32_t ascent) { - return raster(f, px, cp, bold, out, stride, cell_w, cell_h, ascent, 0); +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, int32_t cp, int bold, +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, cp, bold, out, stride, width, height, 0, 1); + 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 *)realloc(f->own, (size_t)n * sizeof *own); + if (!own) + return; + f->own = own; + uint8_t *state = (uint8_t *)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]; + } } |
