Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions font.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
34 changes: 34 additions & 0 deletions font_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}