summaryrefslogtreecommitdiff
path: root/src/gui/font.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/font.c')
-rw-r--r--src/gui/font.c283
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];
+ }
}