main bcc9a45

inputrects_test.go RawHistoryFolder

1//go:build linux
2
3package ui
4
5import (
6	"math"
7	"strconv"
8	"testing"
9
10	"github.com/bnema/nefergui/internal/layout"
11	"github.com/stretchr/testify/mock"
12)
13
14func TestFrameSetInputRects(t *testing.T) {
15	r, tg := newTestRenderer(t)
16	tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
17	drawOK(tg, nil).Maybe()
18	r.Resize(200, 100, 1)
19	var out Output
20	var setErr error
21	rects := []Rect{{1, 2, 3, 4}, {5, 6, 7, 8}}
22	render := func(view func(*Frame, *testModel)) bool {
23		t.Helper()
24		r.Invalidate()
25		ok, err := r.Render(&out, &testModel{}, view)
26		if err != nil {
27			t.Fatal(err)
28		}
29		return ok
30	}
31	if !render(func(f *Frame, _ *testModel) { f.Root(); setErr = f.SetInputRects(rects) }) || setErr != nil {
32		t.Fatalf("render or set: %v", setErr)
33	}
34	rects[0].Width = 99 // caller reuse must not affect the staged copy
35	if !out.InputRectsChanged || len(out.InputRects) != 2 || out.InputRects[0] != (Rect{1, 2, 3, 4}) {
36		t.Fatalf("applied %+v", out.InputRects)
37	}
38	// An unchanged region is not reported again.
39	if render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects([]Rect{{1, 2, 3, 4}, {5, 6, 7, 8}}) }) && out.InputRectsChanged {
40		t.Fatalf("restaged %+v", out.InputRects)
41	}
42	// A frame that never calls SetInputRects stages nothing new.
43	if render(func(f *Frame, _ *testModel) { f.Root() }) && out.InputRectsChanged {
44		t.Fatal("unchanged frame restaged the region")
45	}
46	// nil restores the full surface; empty non-nil is click-through.
47	if !render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects(nil) }) || !out.InputRectsChanged || out.InputRects != nil {
48		t.Fatalf("nil region: %+v", out)
49	}
50	if !render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects([]Rect{}) }) || !out.InputRectsChanged || out.InputRects == nil || len(out.InputRects) != 0 {
51		t.Fatalf("empty region: %+v", out)
52	}
53	// Invalid rects are rejected in the frame call and stage nothing.
54	for _, bad := range []Rect{{0, 0, 0, 5}, {0, 0, 5, -1}, {math.MaxInt32, 0, 5, 5}} {
55		render(func(f *Frame, _ *testModel) { f.Root(); setErr = f.SetInputRects([]Rect{bad}) })
56		if setErr == nil || r.rt.inputStaged {
57			t.Fatalf("accepted %+v", bad)
58		}
59	}
60	// Steady-state staging does not allocate once buffers exist.
61	rt := newRuntime()
62	rt.Build(func(f *Frame) {
63		f.Root()
64		_ = f.SetInputRects(rects)
65		if n := testing.AllocsPerRun(50, func() { _ = f.SetInputRects(rects) }); n != 0 {
66			t.Fatalf("SetInputRects allocs %v", n)
67		}
68	})
69}
70
71func TestNodeRectAndFrameSize(t *testing.T) {
72	r := newRuntime()
73	r.width, r.height = 400, 300
74	var w, h float64
75	r.Build(func(f *Frame) {
76		w, h = f.Size()
77		stack := f.Root().Stack()
78		stack.Box(Key("a")).Rect(10, 20, 30, 40)
79		stack.Box(Key("b")).Rect(50.5, 60.25, 70, 80)
80	})
81	if w != 400 || h != 300 {
82		t.Fatalf("size %v x %v", w, h)
83	}
84	kids := r.output.Tree.Children[0].Children
85	if len(kids) != 2 {
86		t.Fatalf("children %d", len(kids))
87	}
88	if kids[0].Border != (layout.Rect{X: 10, Y: 20, W: 30, H: 40}) || kids[1].Border != (layout.Rect{X: 50.5, Y: 60.25, W: 70, H: 80}) {
89		t.Fatalf("rects %+v %+v", kids[0].Border, kids[1].Border)
90	}
91	// Hit testing follows the rect geometry.
92	if id := layout.Hit(r.output.Tree, 12, 22); id != kids[0].ID {
93		t.Fatalf("hit %q want %q", id, kids[0].ID)
94	}
95}
96
97func TestNodeRectOutsideStackIsIgnored(t *testing.T) {
98	r := newRuntime()
99	r.width, r.height = 200, 100
100	r.Build(func(f *Frame) {
101		col := f.Root().Column()
102		col.Box(Key("a"), Inline("height:10px")).Rect(50, 50, 5, 5)
103		col.Box(Key("b"), Inline("height:10px"))
104	})
105	kids := r.output.Tree.Children[0].Children
106	if kids[0].Border.X != 0 || kids[0].Border.H != 10 || kids[1].Border.Y != 10 {
107		t.Fatalf("Rect changed normal flow: %+v %+v", kids[0].Border, kids[1].Border)
108	}
109}
110
111// The absolute geometry path adds no allocations over the same tree without it.
112func TestNodeRectAddsNoAllocations(t *testing.T) {
113	build := func(withRect bool) func() {
114		r := newRuntime()
115		r.width, r.height = 640, 480
116		view := func(f *Frame) {
117			st := f.Root().Stack()
118			for i := 0; i < 32; i++ {
119				b := st.Box(Key(strconv.Itoa(i)), Inline("background:#fff"))
120				if withRect {
121					b.Rect(float64(i), float64(i), 20, 20)
122				}
123			}
124		}
125		r.Build(view)
126		return func() { r.Redraw(); r.Build(view) }
127	}
128	plain := testing.AllocsPerRun(20, build(false))
129	rect := testing.AllocsPerRun(20, build(true))
130	if rect > plain {
131		t.Fatalf("Rect added allocations: %v vs %v", rect, plain)
132	}
133}

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