From d3977ff1fe0c2cba469ef4f66c263cc4db8018b1 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 7 Sep 2026 20:02:07 +0200 Subject: [PATCH] Say which glyphs the built-in font actually has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A rune with no glyph still ADVANCES -- deliberately, so that a column of text stays lined up -- so measuring a string says nothing about whether it can be read. Until now the only way to know was to read the table, and the table was unexported, so the answer was folklore. The folklore was wrong in both directions. go-xrkit/desk carries a hand-written rule forbidding apostrophes and dashes in its settings window, arrived at after "the glasses' own menu bar" came out with a hole in it. But the plain hyphen draws fine (5 pixels), and that same window ships "Turn this Mac's screen off" -- an apostrophe, which does not. BitmapCovers(r) and BitmapMissing(s) answer it. A consumer can now assert its own strings instead of remembering a rule, which is the difference between a rule and a guard. ⚠ For the BUILT-IN font only. A TrueType face has far wider coverage; this is the fallback, and the fallback is what a platform with no system face falls back TO. Co-Authored-By: Claude Opus 5 --- font.go | 35 +++++++++++++++++++++++++++++++++++ font_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/font.go b/font.go index 7556d6c..52be66b 100644 --- a/font.go +++ b/font.go @@ -346,3 +346,38 @@ func putPixel(p painter.Painter, px, py int, ink RGBA) { // caller that writes its own ends up wrapping to different rules than the card // beside it. func WrapText(f Font, text string, width int) []string { return wrapText(f, text, width) } + +// BitmapCovers reports whether the built-in 5x7 font has a glyph for r. +// +// ⭐ WHY A CONSUMER NEEDS TO ASK. A rune with no glyph still ADVANCES -- that is +// deliberate, it keeps a column of text lined up -- so measuring a string tells +// you nothing about whether it can be read. The only way to know is to ask the +// table, and until now the table was unexported and the answer was folklore: +// go-xrkit/desk carries a hand-written rule forbidding apostrophes in one +// window, arrived at after "the glasses' own menu bar" came out with a hole in +// it, and that rule is both incomplete (it says "no dashes" while the plain +// hyphen draws fine) and unenforced (that same window ships "Turn this Mac's +// screen off"). +// +// ⚠ IT ANSWERS FOR THE BUILT-IN FONT ONLY. A TrueType face has its own, +// far wider coverage; this is the fallback, and the fallback is what a platform +// with no system face falls back TO. +func BitmapCovers(r rune) bool { + _, ok := font5x7[r] + return ok +} + +// BitmapMissing lists the runes of s the built-in font cannot draw, in order, +// without repeats. Empty means every character in s would be legible. +func BitmapMissing(s string) []rune { + var out []rune + seen := map[rune]bool{} + for _, r := range s { + if BitmapCovers(r) || seen[r] { + continue + } + seen[r] = true + out = append(out, r) + } + return out +} diff --git a/font_test.go b/font_test.go index 11e1cb0..37311ca 100644 --- a/font_test.go +++ b/font_test.go @@ -278,3 +278,37 @@ func TestTheEllipsisHasAGlyph(t *testing.T) { t.Errorf("the ellipsis glyph lights %d pixels, want 3 dots", lit) } } + +// TestBitmapCoverageAnswersWhatMeasuringCannot. +// +// ⛔ A RUNE WITH NO GLYPH STILL ADVANCES, on purpose, so that a column of text +// stays lined up. Measuring therefore says nothing about legibility, and the +// only honest answer comes from the table. +func TestBitmapCoverageAnswersWhatMeasuringCannot(t *testing.T) { + for _, c := range []struct { + r rune + drawn bool + }{ + {'a', true}, {'.', true}, {'-', true}, {'(', true}, + {'…', true}, // added with the rune-keyed table + {'\'', false}, {'’', false}, {'—', false}, {'é', false}, + } { + if got := BitmapCovers(c.r); got != c.drawn { + t.Errorf("BitmapCovers(%q) = %v, want %v", c.r, got, c.drawn) + } + } + + // ⭐ AND THE WIDTH IS THE SAME EITHER WAY, which is the whole point. + f := NewBitmapFont(1) + if f.Measure("a") != f.Measure("’") { + t.Error("a covered and an uncovered rune must measure the same; if they " + + "differ, callers could have used width to detect coverage") + } + + if got := BitmapMissing("Turn this Mac's screen off"); len(got) != 1 || got[0] != '\'' { + t.Errorf("BitmapMissing = %q, want just the apostrophe", got) + } + if got := BitmapMissing("plain - hyphens are fine"); len(got) != 0 { + t.Errorf("BitmapMissing = %q, want none: the hyphen draws", got) + } +}