1package text
2
3import (
4 "bytes"
5 "image"
6 "math"
7 "reflect"
8 "testing"
9
10 "github.com/bnema/nefergui/internal/bidi"
11 "github.com/go-text/typesetting/di"
12 "github.com/go-text/typesetting/font"
13)
14
15func runTexts(t *testing.T, s string, l Line) []string {
16 t.Helper()
17 source := []rune(s)
18 out := make([]string, 0, len(l.Runs))
19 lastX := -1.0
20 for _, r := range l.Runs {
21 if r.Start < 0 || r.End > len(source) || r.Start >= r.End {
22 t.Fatalf("invalid range %d:%d", r.Start, r.End)
23 }
24 if r.X <= lastX {
25 t.Fatalf("x not increasing at %q: %v <= %v", string(source[r.Start:r.End]), r.X, lastX)
26 }
27 if math.Abs(r.X+r.Advance-l.Width) > 0.02 && r.X+r.Advance > l.Width+0.02 {
28 t.Fatalf("run beyond line: %+v", r)
29 }
30 lastX = r.X
31 out = append(out, string(source[r.Start:r.End]))
32 }
33 return out
34}
35func TestVisualOrder(t *testing.T) {
36 c := fixture(t)
37 e := NewEngine(c)
38 cases := []struct {
39 name, text string
40 base bidi.BaseDirection
41 direction di.Direction
42 want []string
43 }{
44 {"ltr", "abc سلام 123 عالم def", bidi.LTR, di.DirectionLTR, []string{"abc ", "عالم", " ", "123", " ", "سلام", " def"}},
45 {"rtl", "سلام abc def عالم", bidi.RTL, di.DirectionRTL, []string{"عالم", " ", "abc def", " ", "سلام"}},
46 {"numbers", "سعر 123 دولار", bidi.Auto, di.DirectionRTL, []string{"دولار", " ", "123", " ", "سعر"}},
47 }
48 for _, tc := range cases {
49 t.Run(tc.name, func(t *testing.T) {
50 l, err := e.Measure(tc.text, Request{Families: []string{"Noto Sans"}, Size: 20, Direction: tc.base}, 0)
51 if err != nil {
52 t.Fatal(err)
53 }
54 if l.Direction != tc.direction || len(l.Lines) != 1 {
55 t.Fatalf("direction %v lines %d", l.Direction, len(l.Lines))
56 }
57 got := runTexts(t, tc.text, l.Lines[0])
58 if !reflect.DeepEqual(got, tc.want) {
59 t.Fatalf("visual order %q; want %q", got, tc.want)
60 }
61 for _, r := range l.Lines[0].Runs {
62 if r.Direction != di.Direction(r.Level&1) {
63 t.Fatalf("run %q direction %v level %d", string([]rune(tc.text)[r.Start:r.End]), r.Direction, r.Level)
64 }
65 }
66 })
67 }
68}
69func TestWrapVisualPerLine(t *testing.T) {
70 c := fixture(t)
71 s := "abc سلام 123 عالم def abc سلام 123 عالم def"
72 l, err := NewEngine(c).Measure(s, Request{Families: []string{"Noto Sans"}, Size: 20, Direction: bidi.LTR}, 145)
73 if err != nil {
74 t.Fatal(err)
75 }
76 want := [][]string{{"abc ", "123", " ", "سلام", " "}, {"عالم", " def abc "}, {"عالم", " ", "123", " ", "سلام", " "}, {"def"}}
77 if len(l.Lines) != len(want) {
78 t.Fatalf("got %d lines; want %d", len(l.Lines), len(want))
79 }
80 for i, line := range l.Lines {
81 got := runTexts(t, s, line)
82 if !reflect.DeepEqual(got, want[i]) {
83 t.Errorf("line %d got %q want %q", i, got, want[i])
84 }
85 }
86}
87
88// A shadow renderer applies only the published tightly-packed uploads. An
89// eviction after an undrained dirty rect must replay the *current* whole page,
90// including zeros, not stale glyph pixels.
91func TestAtlasUploadReplay(t *testing.T) {
92 a, err := NewAtlas(16, 1)
93 if err != nil {
94 t.Fatal(err)
95 }
96 shadow := make([]byte, 16*16)
97 replay := func() {
98 for _, u := range a.Uploads() {
99 if u.Page != 0 || len(u.Bytes) != u.Rect.Dx()*u.Rect.Dy() {
100 t.Fatalf("invalid upload %+v", u)
101 }
102 w := u.Rect.Dx()
103 for y := 0; y < u.Rect.Dy(); y++ {
104 copy(shadow[(u.Rect.Min.Y+y)*16+u.Rect.Min.X:], u.Bytes[y*w:(y+1)*w])
105 }
106 }
107 if !bytes.Equal(shadow, a.pages[0].pixels) {
108 t.Fatal("shadow differs from atlas pixels")
109 }
110 }
111 m := Mask{Alpha: image.NewAlpha(image.Rect(0, 0, 5, 5))}
112 for i := range m.Alpha.Pix {
113 m.Alpha.Pix[i] = 0x75
114 }
115 if _, err := a.Insert(Key{Glyph: 1}, m); err != nil {
116 t.Fatal(err)
117 }
118 replay()
119 for i := 2; i <= 4; i++ {
120 if _, err := a.Insert(Key{Glyph: font.GID(i)}, m); err != nil {
121 t.Fatal(err)
122 }
123 } // pending partial rects
124 a.BeginFrame()
125 for i := 5; i <= 8; i++ {
126 if _, err := a.Insert(Key{Glyph: font.GID(i)}, m); err != nil {
127 t.Fatal(err)
128 }
129 } // one page eviction + clear, not yet drained
130 if len(a.dirty) != 1 {
131 t.Fatalf("pending pages %d", len(a.dirty))
132 }
133 replay()
134 a.BeginFrame()
135 m.Alpha.Pix[0] = 0xff
136 if _, err := a.Insert(Key{Glyph: 9}, m); err != nil {
137 t.Fatal(err)
138 }
139 replay()
140}