diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-24 10:12:10 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | e5f764bd201df6554272427fc8cedffece18066f (patch) | |
| tree | e90e186ac4f1d7c642232f4d97c5a2dcb547b3b3 /src/gui/font.h | |
| parent | d32452950262b4ec2b50b0e0af66f7870c8e5d5d (diff) | |
| download | pardes-e5f764bd201df6554272427fc8cedffece18066f.tar.gz pardes-e5f764bd201df6554272427fc8cedffece18066f.zip | |
Shape text with HarfBuzz, so a font's programming ligatures draw across their cells
The SDL shell rasterized every cell by its own codepoint, so a font's `->`,
`!=` or `<=` ligature never appeared. Text is now shaped with HarfBuzz 11.0.0,
built from source like FreeType: a lazy build.zig.zon dependency (the tarball
Ghostty pins) compiled only for the gui shell, its FreeType integration
linked against the freetype package, so the binary carries no system
HarfBuzz or FreeType and a tty build never compiles it.
Shaping turns on only the font's ligature features (liga, calt, clig, rlig,
rclt); composition, local forms and fractions stay off, so a cell draws either
its nominal glyph, as it always did, or part of a ligature. A glyph no lookup
of those features covers cannot change, so its cell is resolved alone, and
that resolution is cached by codepoint: in a font without ligatures (Adwaita
Mono) no cell is ever shaped, and the pipeline draws pixel for pixel as
before.
A cell whose glyph a ligature could replace is shaped with its word: the
cells between spaces that share a decoration and a role. A word ends either
side of the cursor, block or bar, so the cell being edited shows its own
character (Ghostty's rule), and inside f|i, f|l and s|t, whose typographic
ligatures do not belong on a grid (Ghostty again). Colours never end a word:
a ligature spans a syntax colour change or a selection edge. A codepoint the
primary font lacks shapes in the fallback face that has it. Taglines are not
shaped: they are drawn at two pitches, and a strip would not line up in one.
HarfBuzz only chooses glyphs; the grid places them. A substituted glyph whose
ink reaches over neighbouring cells that draw nothing of their own (the
spacer glyphs of Fira Code-style fonts such as Maple Mono, or the cells a
merged ligature cluster swallowed) claims them: it is rasterized once as a
strip that many cells wide, and each cell samples its own slice, so
selection, the cursor and per-cell colours work unchanged. Anything the grid
cannot place (several glyphs in one cell, a positioned mark) keeps its
nominal glyphs.
A frame must not pay for this. A shaped word's atlas slots are cached by its
text (checked against the stored text, not just its hash), so a frame costs
one lookup per word and a compare for the rest of the word being walked.
The atlas is keyed by (face, glyph, role, decoration, lead, span), packed into
64 bits so a lookup hashes one word; ASCII glyph ids are a table, and a
face's HarfBuzz state is made when it is first asked about a glyph. Measured
with the latency trace, whose F line now also carries the submit time (CPU =
submit - present entry), in 10 interleaved rounds against main with an A/A
pair, at 160x50 cells: with Adwaita Mono every scenario (idle, scrolling,
typing, terminal output) is as fast or 1-3% faster, and the first frame and
time to first paint are unchanged. With Maple Mono, ligatures cost the first
frame about 0.8 ms (HarfBuzz setup and shaping the screen) and scrolling new
text about 1-2%.
With it the codepoint raster path is gone: grips look up their glyph and
stay centered, since a grip is not text. ui_font_scale_for_height, an
identity kept as a "size token", is removed (px is the size everywhere),
ui_font_has_glyph becomes ui_font_glyph, and the space slot is simply the
first, cleared cell of the atlas rather than a rasterized space. CellStyle.ul
gets an explicit u3 tag so a decoration fits in the packed keys.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to 'src/gui/font.h')
| -rw-r--r-- | src/gui/font.h | 66 |
1 files changed, 48 insertions, 18 deletions
diff --git a/src/gui/font.h b/src/gui/font.h index 4410bb7c..b38f822a 100644 --- a/src/gui/font.h +++ b/src/gui/font.h @@ -1,6 +1,7 @@ -// Tiny C shim over FreeType, so gui.zig only imports the stable renderer API. -// The implementation owns FreeType's library and face objects and keeps the -// caller's font bytes borrowed for the face lifetime. +// Tiny C shim over FreeType and HarfBuzz, so gui.zig only imports the stable +// renderer API. The implementation owns FreeType's library and face objects +// and HarfBuzz's font and buffer, and keeps the caller's font bytes borrowed +// for the face lifetime. #ifndef UI_FONT_H #define UI_FONT_H #include <stdint.h> @@ -20,27 +21,56 @@ UIFont *ui_font_new(const uint8_t *data, int32_t len); // font has no PostScript table. Borrowed for the UIFont lifetime. const char *ui_font_name(UIFont *f); -// Whether the selected Unicode charmap maps `cp` to a real glyph. Glyph zero is -// the face's .notdef box and deliberately does not count for fallback choice. -int ui_font_has_glyph(UIFont *f, int32_t cp); +// The glyph the selected Unicode charmap maps `cp` to. Glyph zero is the +// face's .notdef box: the codepoint is missing, which is what fallback choice +// asks. +uint32_t ui_font_glyph(UIFont *f, int32_t cp); -// The size token is a pixel height. Metrics and rasterization both configure -// FreeType at that size, so hinted advances, baseline and bitmaps stay in step. -float ui_font_scale_for_height(UIFont *f, float px); +// `px` is the pixel height. Metrics and rasterization both configure FreeType +// at that size, so hinted advances, baseline and bitmaps stay in step. void ui_font_cell_metrics(UIFont *f, float px, int32_t *cell_w, int32_t *cell_h, int32_t *ascent); -// Rasterize `cp` into `out` as a hinted grayscale cell mask (`stride` bytes per -// row). The origin is the baseline `ascent` pixels from the top. Pixels outside -// the glyph are zero. Missing codepoints render the face's .notdef glyph. -// `bold` emboldens the outline before rendering (synthetic bold, same advance). -// Returns 1 when the glyph produced coverage, 0 for blank/error. -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); +// What one grid cell draws: `slice` of the glyph `glyph` rasterized across +// `span` cells with its own cell `lead` cells from the left. A plain cell is +// its own glyph, lead 0, span 1, slice 0. +typedef struct { + uint32_t glyph; + uint8_t lead; + uint8_t span; + uint8_t slice; +} UIShapedCell; + +// The longest glyph strip a ligature may claim, in cells. +#define UI_MAX_SPAN 8 + +// Whether a lookup of the font's ligature features (liga, calt, clig, rlig, +// rclt) could replace `glyph`. When it cannot, shaping leaves the glyph alone +// and its cell needs no shaping at all. +int ui_font_substitutes(UIFont *f, uint32_t glyph); + +// One cell per codepoint of `cps`, all in this face, shaped by HarfBuzz with +// the font's ligature features (a run with no glyph ui_font_substitutes +// reports comes back nominal without it). A substituted glyph whose ink +// reaches over neighbouring cells that draw nothing of their own (a +// ligature's spacers, or the cells a merged cluster swallowed) claims them as +// a strip. Otherwise, and for anything the grid cannot place (several glyphs +// in one cell, positioned marks), each cell is its nominal glyph. +void ui_font_shape(UIFont *f, float px, int32_t cell_w, const uint32_t *cps, + int32_t n, UIShapedCell *out); + +// Rasterize `glyph` into `out` as a hinted grayscale mask of `width` x +// `height` (`stride` bytes per row). The glyph's advance is centered in the +// `cell_w` pixels that start at `cell_x`, and its baseline is `ascent` pixels +// from the top; ink outside the mask is clipped. `bold` emboldens the outline +// before rendering (synthetic bold, same advance). Returns 1 when the glyph +// produced coverage, 0 for blank/error. +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); // Center bitmap ink bounds, independent of advance and baseline bearings. -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); |
