Skip to content

refactor(ui): one text stack — delete the private x/image rasteriser - #96

Merged
tannevaled merged 1 commit into
mainfrom
feat/one-text-stack
Aug 8, 2026
Merged

refactor(ui): one text stack — delete the private x/image rasteriser#96
tannevaled merged 1 commit into
mainfrom
feat/one-text-stack

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Two stacks, one of them wrong

Widgets drew through a go-widgets toolkit font, which shapes the run with go-opentype/shape. Everything else — 38 call sites of chrome (newsgroup names, network-log URLs, "‹ Back", status lines) — went through a private textFace over x/image/font, with its own duplicate fallback chain, its own per-rune routing and its own run splitting.

That second stack applied no GSUB at all. It measured and drew Arabic in unjoined isolated forms, left Indic clusters unreordered, and could not compose an emoji sequence.

Measured before touching anything:

text x/image shaped
Accounts, Total solar eclipses…, 中文标题, Ελληνικά 62, 320, 60, 65 identical
مرحبا بالعالم 104 72
नमस्ते दुनिया 73 69

A 31% error on Arabic is not rounding — it is the width of text rendered wrong.

What lands

textFace is now a thin adapter over the same cached toolkit font ttFont hands to widgets, keeping the top-left-corner positioning its callers expect. A string measured by a layout and drawn by a widget can no longer disagree, because there is only one measurer left. 266 lines deleted, 104 added, no call site touched.

Gone with it: the duplicate fallback chain (faces/faceFor/primaryCovers/splitRuns/fallbackSrcs), and stripFormat — the shaper now hides default-ignorable code points itself (shape v0.4.0), which is both the right place and more complete than stripping ever was.

SetSystemFont keeps working. A host typeface (macOS SFNS) is one variable font whose only reachable master is Regular, so bold is over-struck via the toolkit's new NewSyntheticBoldFont — applied to the whole chain, so a CJK or Arabic run in a heading is bold too rather than staying at body weight beside bold Latin.

Measured impact, not assumed

  • Text widths are unchanged — title 342, meta 262, side 67, tab 34, badge 31, identical before and after — so every wrap point and truncation is unchanged.
  • Line heights drop by 1px (title 18→17, side 16→15), because the metric now comes from the rasteriser that actually draws rather than from x/image's rounding. Cards go 128→126px, banners 32→31. Everything derived stays self-consistent.
  • All 45 rendered snapshots keep identical structure — same wrap points, same element positions — differing only in that 1px and in anti-aliasing.

textFace.ascent is deleted rather than ported: no production code read it, and the baseline is the font's own business now.

Verification

  • go test -race over the gated packages → 100.0% of statements, race-clean.
  • Rendered Arabic, Hebrew, Devanagari, CJK, Greek and a ZWJ emoji sequence through the surviving stack: Arabic joins cursively and runs right-to-left, Devanagari conjoins, the astronaut composes.
  • Before/after snapshot comparison across all 45 PNGs, plus a direct geometry diff of every metric and card height.

Companion releases

  • go-opentype/shape v0.4.0 — hide default-ignorables
  • go-widgets/toolkit v0.128.0NewSyntheticBoldFont, honour hidden glyphs
  • go-widgets/toolkit v0.129.0 — a fallback chain reports Ascent/FontData/SizePx

🤖 Generated with Claude Code

The reader carried two text stacks. Widgets drew through a go-widgets toolkit
font, which shapes the run with go-opentype/shape. Everything else — 38 call
sites of chrome: newsgroup names, network-log URLs, "‹ Back", status lines —
went through a private textFace over x/image/font, with its own duplicate
fallback chain, its own per-rune routing, and its own run splitting.

That second stack applied NO GSUB. It measured and drew Arabic in unjoined
isolated forms, left Indic clusters unreordered, and could not compose an emoji
sequence. Measured before the change: "مرحبا بالعالم" 104px through x/image
against 72px through the shaper — a 31% error that is not rounding, it is the
width of text rendered wrong.

textFace is now a thin adapter over the same cached toolkit font ttFont hands to
widgets, keeping the top-left-corner positioning its callers expect. A string
measured by a layout and drawn by a widget can no longer disagree, because
there is only one measurer left. 266 lines deleted, 104 added.

Gone with it: the duplicate fallback chain (faces/faceFor/primaryCovers/
splitRuns/fallbackSrcs), and stripFormat — the shaper now hides
default-ignorable code points itself (go-opentype/shape v0.4.0), which is both
the right place and more complete than stripping ever was.

SetSystemFont keeps working. A host typeface (macOS SFNS) is one variable font
whose only reachable master is Regular, so bold is over-struck via the toolkit's
new NewSyntheticBoldFont — applied to the WHOLE chain, so a CJK or Arabic run in
a heading is bold too rather than staying at body weight beside bold Latin.

MEASURED IMPACT, not assumed:
- Text WIDTHS are unchanged (title 342, meta 262, side 67, tab 34, badge 31 —
  identical before and after), so every wrap point and truncation is unchanged.
- Line HEIGHTS drop by 1px (title 18→17, side 16→15), because the metric now
  comes from the rasteriser that actually draws rather than from x/image's
  rounding. Cards go 128→126px, banners 32→31. Everything derived stays
  self-consistent; the 45 rendered snapshots keep identical structure, same
  wrap points and same element positions, differing only in that 1px and in
  anti-aliasing.

textFace.ascent is deleted rather than ported: no production code read it, and
the baseline is the font's own business now.

100% coverage held; race-clean. Verified by rendering Arabic, Hebrew, Devanagari,
CJK, Greek and a ZWJ emoji sequence through the surviving stack: Arabic joins
cursively and runs right-to-left, Devanagari conjoins, the astronaut composes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit b16e8f5 into main Aug 8, 2026
11 checks passed
@tannevaled
tannevaled deleted the feat/one-text-stack branch August 8, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant