221 lines
6.8 KiB
Go
221 lines
6.8 KiB
Go
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("#")
|
||
}
|