v0.1.0 47aaa0f

visual_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close