v0.7.1 e0abc49

natural_test.go RawHistoryFolder

1package layout
2
3import (
4	"errors"
5	"testing"
6
7	"github.com/bnema/nefergui/internal/css"
8	"github.com/bnema/nefergui/internal/text"
9)
10
11func naturalEngine(t *testing.T) *text.Engine {
12	t.Helper()
13	catalog, err := text.Load(text.DirectorySource("../../testdata/fonts"))
14	if err != nil {
15		t.Fatal(err)
16	}
17	return text.NewEngine(catalog)
18}
19
20func textNode(id, content string) *Node {
21	n := node(id, Text, css.Style{Display: css.KeywordBlock, FontSize: px(16)})
22	n.Content = content
23	return n
24}
25
26func TestNaturalColumnOfTexts(t *testing.T) {
27	engine := naturalEngine(t)
28	short, long := textNode("a", "ab"), textNode("b", "a much longer line")
29	column := node("c", Column, css.Style{Display: css.KeywordFlex, FlexDirection: css.KeywordColumn, Padding: css.LengthSides{Top: px(4), Right: px(6), Bottom: px(4), Left: px(6)}}, short, long)
30	opts := Options{Width: 1000, Height: 1 << 20, TextEngine: engine}
31	got, err := Natural(column, opts)
32	if err != nil {
33		t.Fatal(err)
34	}
35	ws, err := Natural(short, opts)
36	if err != nil {
37		t.Fatal(err)
38	}
39	wl, err := Natural(long, opts)
40	if err != nil {
41		t.Fatal(err)
42	}
43	if !(wl.W > ws.W) || ws.W <= 0 {
44		t.Fatalf("text widths short=%g long=%g", ws.W, wl.W)
45	}
46	if got.W != wl.W+12 || got.H != ws.H+wl.H+8 {
47		t.Fatalf("column = %+v, want %g x %g", got, wl.W+12, ws.H+wl.H+8)
48	}
49}
50
51func TestNaturalRowSumsWithGap(t *testing.T) {
52	engine := naturalEngine(t)
53	a, b := textNode("a", "alpha"), textNode("b", "beta")
54	opts := Options{Width: 1000, Height: 1 << 20, TextEngine: engine}
55	wa, err := Natural(a, opts)
56	if err != nil {
57		t.Fatal(err)
58	}
59	wb, err := Natural(b, opts)
60	if err != nil {
61		t.Fatal(err)
62	}
63	row := node("r", Row, css.Style{Display: css.KeywordFlex, ColumnGap: px(10)}, a, b)
64	got, err := Natural(row, opts)
65	if err != nil {
66		t.Fatal(err)
67	}
68	if got.W != wa.W+wb.W+10 || got.H != max(wa.H, wb.H) {
69		t.Fatalf("row = %+v, want W %g H %g", got, wa.W+wb.W+10, max(wa.H, wb.H))
70	}
71}
72
73func TestNaturalExplicitWidthWins(t *testing.T) {
74	engine := naturalEngine(t)
75	child := textNode("a", "alpha beta gamma delta epsilon zeta")
76	box := node("box", Box, css.Style{Display: css.KeywordBlock, Width: px(90), Padding: css.LengthSides{Left: px(5), Right: px(5)}}, child)
77	got, err := Natural(box, Options{Width: 1000, Height: 1 << 20, TextEngine: engine})
78	if err != nil {
79		t.Fatal(err)
80	}
81	if got.W != 100 {
82		t.Fatalf("width = %g, want 100 (90 content + 10 padding)", got.W)
83	}
84	if got.H <= 16 {
85		t.Fatalf("content did not wrap inside the explicit width: height %g", got.H)
86	}
87	bb := node("bb", Box, css.Style{Display: css.KeywordBlock, BoxSizing: css.KeywordBorderBox, Width: px(90), Height: px(30), Padding: css.LengthSides{Left: px(5), Right: px(5)}})
88	got, err = Natural(bb, Options{Width: 1000, Height: 1000})
89	if err != nil || got.W != 90 || got.H != 30 {
90		t.Fatalf("border-box = %+v err=%v", got, err)
91	}
92}
93
94func TestNaturalTextWrapsAtOptionsWidth(t *testing.T) {
95	engine := naturalEngine(t)
96	long := textNode("a", "alpha beta gamma delta epsilon zeta eta theta iota kappa")
97	wide, err := Natural(long, Options{Width: 2000, Height: 1 << 20, TextEngine: engine})
98	if err != nil {
99		t.Fatal(err)
100	}
101	narrow, err := Natural(long, Options{Width: 100, Height: 1 << 20, TextEngine: engine})
102	if err != nil {
103		t.Fatal(err)
104	}
105	// A wrapped line may measure slightly over the limit (103.75 for 100 here);
106	// callers clamp to the limit, as Renderer.Measure does.
107	if narrow.W >= wide.W || narrow.H <= wide.H {
108		t.Fatalf("narrow=%+v wide=%+v", narrow, wide)
109	}
110}
111
112func TestNaturalNilAndHidden(t *testing.T) {
113	if got, err := Natural(nil, Options{}); err != nil || got != (Size{}) {
114		t.Fatalf("nil: %+v %v", got, err)
115	}
116	hidden := node("h", Box, css.Style{Display: css.KeywordNone})
117	if got, err := Natural(hidden, Options{Width: 100}); err != nil || got != (Size{}) {
118		t.Fatalf("hidden: %+v %v", got, err)
119	}
120}
121
122func TestNaturalRootConstraints(t *testing.T) {
123	engine := naturalEngine(t)
124	pct := func(v float64) css.Length { return css.Length{Value: v, Unit: "%"} }
125	solid := css.KeywordSides{Top: css.KeywordSolid, Right: css.KeywordSolid, Bottom: css.KeywordSolid, Left: css.KeywordSolid}
126	// want derives the expected size from the size of the unconstrained text.
127	tests := []struct {
128		name  string
129		style css.Style
130		want  func(base Size) Size
131	}{
132		{"min-width raises", css.Style{MinWidth: px(300)}, func(b Size) Size { return Size{300, b.H} }},
133		{"max-width caps", css.Style{MaxWidth: px(10)}, func(b Size) Size { return Size{10, b.H} }},
134		{"min-height raises", css.Style{MinHeight: px(80)}, func(b Size) Size { return Size{b.W, 80} }},
135		{"max-height caps", css.Style{MaxHeight: px(5)}, func(b Size) Size { return Size{b.W, 5} }},
136		{"percent height is auto", css.Style{Height: pct(100)}, func(b Size) Size { return b }},
137		{"percent min-height is auto", css.Style{MinHeight: pct(50)}, func(b Size) Size { return b }},
138		{"percent max-height is auto", css.Style{MaxHeight: pct(50)}, func(b Size) Size { return b }},
139		{"padding and border", css.Style{
140			Padding:     css.LengthSides{Top: px(3), Right: px(5), Bottom: px(3), Left: px(5)},
141			BorderWidth: css.LengthSides{Top: px(2), Right: px(2), Bottom: px(2), Left: px(2)},
142			BorderStyle: solid,
143		}, func(b Size) Size { return Size{b.W + 14, b.H + 10} }},
144		{"border without style is ignored", css.Style{BorderWidth: css.LengthSides{Left: px(9), Right: px(9)}}, func(b Size) Size { return b }},
145		{"margin is not part of the size", css.Style{Margin: css.LengthSides{Top: px(7), Right: px(7), Bottom: px(7), Left: px(7)}}, func(b Size) Size { return b }},
146	}
147	opts := Options{Width: 1000, Height: 1 << 20, TextEngine: engine}
148	base, err := Natural(textNode("t", "ab"), opts)
149	if err != nil {
150		t.Fatal(err)
151	}
152	if base.W <= 10 || base.H <= 5 {
153		t.Fatalf("setup: base %+v too small for the max-width/max-height cases", base)
154	}
155	for _, tc := range tests {
156		t.Run(tc.name, func(t *testing.T) {
157			style := tc.style
158			style.Display, style.FontSize = css.KeywordBlock, px(16)
159			n := textNode("t", "ab")
160			n.Style = &style
161			got, err := Natural(n, opts)
162			if err != nil {
163				t.Fatal(err)
164			}
165			if want := tc.want(base); got != want {
166				t.Fatalf("got %+v, want %+v (base %+v)", got, want, base)
167			}
168		})
169	}
170}
171
172func TestNaturalNoWrapTextRootIgnoresWidth(t *testing.T) {
173	engine := naturalEngine(t)
174	long := textNode("a", "alpha beta gamma delta epsilon zeta eta theta iota kappa")
175	wrapped, err := Natural(long, Options{Width: 100, Height: 1 << 20, TextEngine: engine})
176	if err != nil {
177		t.Fatal(err)
178	}
179	long.NoWrap = true
180	flat, err := Natural(long, Options{Width: 100, Height: 1 << 20, TextEngine: engine})
181	if err != nil {
182		t.Fatal(err)
183	}
184	if flat.W <= 100 || flat.H >= wrapped.H {
185		t.Fatalf("NoWrap=%+v wrapped=%+v: want one line wider than the limit", flat, wrapped)
186	}
187}
188
189func TestNaturalError(t *testing.T) {
190	rect := textNode("r", "x")
191	rect.HasRect = true
192	stack := node("s", Stack, css.Style{Display: css.KeywordFlex}, rect)
193	if _, err := Natural(stack, Options{Width: 100, Height: 100}); err == nil {
194		t.Fatal("flex Stack with a Rect child accepted")
195	}
196	deep := node("leaf", Box, css.Style{Display: css.KeywordBlock})
197	for i := 0; i < 300; i++ {
198		deep = node("n", Box, css.Style{Display: css.KeywordBlock}, deep)
199	}
200	if _, err := Natural(deep, Options{Width: 100, Height: 100}); !errors.Is(err, ErrDepth) {
201		t.Fatalf("deep tree: %v, want ErrDepth", err)
202	}
203}

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