Files
8bitMyGoUI/pixel/font_test.go
T
ZacharyZhang-NY c745b082a9 Add account components 386-405; Reduce setting, PinInput Cmd+V
Twenty account components: SettingsList, OptionCycler, KeyBindings,
ControllerMap, ProfileCard, CharacterCreator, LoginForm, PasswordGrid,
SessionList, LanguagePicker, PalettePicker, VolumeMixer,
DifficultySelect, AccessibilityOptions, AccountSwitcher, PlayerSelect,
TwoFactor, About, PatchNotes and FeedbackForm.

The components never check credentials, and no secret reaches the log:
LoginForm reports Submitted and the caller counts Failures; each new
failure clears the password, refocuses it, shakes the form and names no
field. TwoFactor builds on input.PinInput and drains an epoch-aligned
period bar. PalettePicker draws each tile in its own palette on a
pixel.Pad grid.

pixel.Settings.Reduce stills motion like the system preference, the
micro font gains '?', display.Face draws avatar faces anywhere, and
input.PinInput pastes on Cmd+V as native hosts send it.
2026-10-12 08:21:39 -04:00

221 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pixel
import (
"encoding/binary"
"encoding/hex"
"math/rand/v2"
"slices"
"strings"
"testing"
)
func TestFontHeader(t *testing.T) {
if font12.n != 24870 || font12.ascent != 13 || font12.descent != 3 {
t.Fatalf("font: %d glyphs, ascent %d, descent %d", font12.n, font12.ascent, font12.descent)
}
if LineHeight != font12.ascent+font12.descent {
t.Fatalf("LineHeight %d, want %d", LineHeight, font12.ascent+font12.descent)
}
prev := rune(-1)
for i := range font12.n {
r := rune(binary.LittleEndian.Uint32(font12.index[8*i:]))
if r <= prev {
t.Fatalf("index entry %d: %U after %U", i, r, prev)
}
prev = r
}
}
func TestBadFontPanics(t *testing.T) {
for name, b := range map[string][]byte{
"magic": []byte("XXXX\x0d\x03\x00\x00\x00\x00"),
"truncated": []byte("8BF1\x0d\x03\x05\x00\x00\x00"),
} {
func() {
defer func() {
if recover() == nil {
t.Errorf("%s: loadFont did not panic", name)
}
}()
loadFont(b)
}()
}
}
// Rows copied from ark-pixel-12px-proportional-zh_hans.bdf v2026.09.25.
func TestGlyphsMatchBDF(t *testing.T) {
cases := []struct {
r rune
adv, w, h, x, y, top int
hexRows string
}{
{'A', 8, 7, 9, 0, 0, 4, "10 10 28 28 44 7C 44 82 82"},
{'g', 6, 5, 8, 0, -2, 7, "78 88 88 88 88 78 08 70"},
{'中', 12, 11, 11, 0, -1, 3, "0400 0400 FFE0 8420 8420 8420 FFE0 0400 0400 0400 0400"},
{',', 12, 2, 4, 2, -1, 10, "C0 C0 40 80"},
{' ', 6, 0, 0, 0, 0, 13, ""},
}
for _, c := range cases {
g, ok := font12.find(c.r)
if !ok {
t.Fatalf("%q missing", c.r)
}
if g.adv != c.adv || g.w != c.w || g.h != c.h || g.x != c.x || g.y != c.y {
t.Fatalf("%q metrics adv %d box %d×%d at %d,%d", c.r, g.adv, g.w, g.h, g.x, g.y)
}
var want []byte
for _, row := range strings.Fields(c.hexRows) {
b, _ := hex.DecodeString(row)
want = append(want, b...)
}
if !slices.Equal(g.rows, want) {
t.Fatalf("%q rows % X, want % X", c.r, g.rows, want)
}
if got := g.top(); got != c.top {
t.Fatalf("%q top %d, want %d", c.r, got, c.top)
}
}
a := glyphOf('A')
if !a.on(3, 0) || a.on(0, 0) || !a.on(0, 8) || !a.on(6, 8) {
t.Fatal("'A' bits read wrong")
}
}
func TestMissingRuneDrawsTofu(t *testing.T) {
const r = '\U0001F600'
if _, ok := font12.find(r); ok {
t.Fatalf("%U is in the font; pick a missing rune", r)
}
if g := glyphOf(r); g.adv != tofu.adv || !slices.Equal(g.rows, tofu.rows) {
t.Fatalf("%U = adv %d, want tofu", r, g.adv)
}
}
func TestMeasure(t *testing.T) {
if got := Measure("Ag中"); got != 8+6+12 {
t.Fatalf("Measure = %d, want 26", got)
}
if Measure("") != 0 {
t.Fatal("Measure of empty text is not 0")
}
}
func TestLines(t *testing.T) {
cases := []struct {
s string
width int
want []string
}{
{"HELLO WORLD", max(Measure("HELLO"), Measure("WORLD")) + 2, []string{"HELLO", "WORLD"}},
{"像素风格组件库", 36, []string{"像素风", "格组件", "库"}},
{"你好,世界", 24, []string{"你", "好,", "世界"}},
{"「像素」", 24, []string{"「像", "素」"}},
{"A\nB", 100, []string{"A", "B"}},
{"a long line", 0, []string{"a long line"}},
{"x y", Measure("x"), []string{"x", "y"}},
{"a ", 1, []string{"a"}},
{"ab cd ", Measure("ab"), []string{"ab", "cd"}},
{"你 ,好", 24, []string{"你 ,", "好"}},
{"「 像素", 18, []string{"「 像", "素"}},
{"[你好]世界", 36, []string{"[你", "好]世", "界"}},
{"「 像素", 18, []string{"「 像", "素"}},
{"「(像素)」", 18, []string{"「(像", "素)」"}},
{"「(像素)」", 24, []string{"「(像", "素)」"}},
{"AB「(C", Measure("AB") + 4, []string{"AB", "「(C"}},
}
for _, c := range cases {
if got := Lines(c.s, c.width); !slices.Equal(got, c.want) {
t.Errorf("Lines(%q, %d) = %q, want %q", c.s, c.width, got, c.want)
}
}
}
func TestLinesFitAndKeepText(t *testing.T) {
texts := []string{
"The quick brown fox jumps over the lazy dog near the riverbank.",
"像素风格的组件库,在限制中创造丰富。每个点都经过认真摆放!",
"Mixed 中英文 text with ABCDEFGHIJKLMNOPQRSTUVWXYZ and 標點,結束。",
"Supercalifragilisticexpialidocious",
"「像素」风格 (测试) 文本 ,结束 。再见",
"[测试]{内容}〔括号〕《书名》“引号”‘单引’",
"「(像素)」 「 空格」 『《嵌套》』",
}
for _, s := range texts {
for width := 6; width <= 120; width += 7 {
lines := Lines(s, width)
for _, l := range lines {
core := strings.TrimLeftFunc(strings.TrimRightFunc(l, hanging(closer)), hanging(opener))
if Measure(core) > width && len([]rune(core)) > 1 {
t.Fatalf("Lines(%q, %d): %q is %d wide", s, width, l, Measure(l))
}
if r := []rune(l); closer(r[0]) {
t.Fatalf("Lines(%q, %d): %q starts with a closer", s, width, l)
}
if r := []rune(l); opener(r[len(r)-1]) {
t.Fatalf("Lines(%q, %d): %q ends with an opener", s, width, l)
}
}
if got := strings.ReplaceAll(strings.Join(lines, ""), " ", ""); got != strings.ReplaceAll(s, " ", "") {
t.Fatalf("Lines(%q, %d) lost text: %q", s, width, got)
}
}
}
}
func TestLinesRandom(t *testing.T) {
alphabet := []rune("ab 像素,。「」()[]《》“”!?.WM")
rng := rand.New(rand.NewPCG(8, 16))
for n := range 3000 {
rs := make([]rune, 1+rng.IntN(24))
for i := range rs {
rs[i] = alphabet[rng.IntN(len(alphabet))]
}
s, width := string(rs), 6+rng.IntN(60)
lines := Lines(s, width)
for j, l := range lines {
r := []rune(strings.TrimRight(l, " "))
if len(r) == 0 {
continue
}
core := strings.TrimLeftFunc(strings.TrimRightFunc(l, hanging(closer)), hanging(opener))
if j > 0 && closer(r[0]) || j < len(lines)-1 && opener(r[len(r)-1]) || Measure(core) > width && len([]rune(core)) > 1 {
t.Fatalf("case %d: Lines(%q, %d) = %q", n, s, width, lines)
}
}
if got := strings.ReplaceAll(strings.Join(lines, ""), " ", ""); got != strings.ReplaceAll(s, " ", "") {
t.Fatalf("case %d: Lines(%q, %d) lost text: %q", n, s, width, lines)
}
}
}
func hanging(punct func(rune) bool) func(rune) bool {
return func(r rune) bool { return r == ' ' || punct(r) }
}
func TestMicroFont(t *testing.T) {
set := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-#.:/%!? "
seen := map[[MicroHeight]uint8]rune{}
for _, r := range set {
g := microGlyph(r)
if prev, ok := seen[g]; ok && r != ' ' {
t.Errorf("micro %q and %q are the same glyph", prev, r)
}
seen[g] = r
}
if microGlyph('a') != microGlyph('A') {
t.Fatal("micro font does not upper-case")
}
if got := microGlyph('1'); got != [MicroHeight]uint8{2, 6, 2, 2, 7} {
t.Fatalf("micro '1' = %v", got)
}
if MicroWidth("HP 99") != 19 || MicroWidth("") != 0 {
t.Fatalf("MicroWidth = %d, %d", MicroWidth("HP 99"), MicroWidth(""))
}
defer func() {
if recover() == nil {
t.Fatal("micro '@' did not panic")
}
}()
MicroWidth("@")
}