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}