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) + } +}