Files
8bitMyGoUI/pixel/font_test.go
T
ZacharyZhang-NY 0e21870b64 Add media components 346-365; input.SlideBase, guarded Pad
An audio player with transport, scrubber and volume; an equalizer and
a visualizer; a playlist; a cassette, cartridge and record that turn
while they play and hold where they stop; piano keys, a step sequencer
and a tracker you can type into; a waveform, a 16:9 video frame, an
image compare, a gallery, a filmstrip, a record button and an FM tuner.

input exports SlideBase, the undrawn rail its sliders share; the
gallery treats exported Bases as building blocks, not components.
pixel's micro font gains '#' for notes like C#4. pixel.Pad and the
slide rail read input only while their base is enabled, so Inert or a
fresh Disabled stops a drag or a key already sent. AGENTS records why
a drawn container keeps the DISABLED description: MyGo paints a
disabled subtree at half opacity.
2026-10-12 06:17:12 -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("@")
}