1package core_test
2
3import (
4 "math"
5 "testing"
6
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"
9
10 "github.com/bnema/nefercap/internal/core"
11 "github.com/bnema/nefercap/internal/ports"
12)
13
14func newPicker(t testing.TB, id uint32, w, h int) *core.Picker {
15 t.Helper()
16 p := &core.Picker{}
17 require.NoError(t, p.Begin(id, w, h))
18 return p
19}
20
21func TestPickerBeginValidation(t *testing.T) {
22 for name, c := range map[string]struct {
23 id uint32
24 w, h int
25 }{
26 "zero id": {0, 100, 100},
27 "zero width": {1, 0, 100},
28 "neg height": {1, 100, -1},
29 "too large": {1, ports.MaxDimension + 1, 100},
30 "too large h": {1, 100, ports.MaxDimension + 1},
31 } {
32 t.Run(name, func(t *testing.T) {
33 p := &core.Picker{}
34 assert.ErrorIs(t, p.Begin(c.id, c.w, c.h), core.ErrInvalidBounds)
35 assert.Equal(t, core.PickIdle, p.Status())
36 assert.False(t, p.MouseDown(1, 1))
37 })
38 }
39 p := newPicker(t, 3, 1920, 1080)
40 assert.Equal(t, core.PickActive, p.Status())
41 assert.Equal(t, core.PickRegion, p.Kind())
42}
43
44func TestPickerDragAcceptsOnRelease(t *testing.T) {
45 p := newPicker(t, 5, 1000, 800)
46 assert.True(t, p.MouseDown(100, 200))
47 assert.True(t, p.MouseMove(250, 300))
48 r, ok := p.Rect()
49 assert.True(t, ok)
50 assert.Equal(t, ports.Region{X: 100, Y: 200, Width: 150, Height: 100}, r)
51 assert.Equal(t, core.PickActive, p.Status(), "not accepted before release")
52 _, ok = p.Result()
53 assert.False(t, ok)
54
55 assert.True(t, p.MouseUp(250, 300))
56 res, ok := p.Result()
57 require.True(t, ok)
58 assert.Equal(t, core.PickResult{OutputID: 5, Region: ports.Region{X: 100, Y: 200, Width: 150, Height: 100}, Kind: core.PickRegion}, res)
59 assert.Equal(t, ports.Target{OutputID: 5, Region: res.Region}, res.Target())
60}
61
62func TestPickerReverseDragNormalizes(t *testing.T) {
63 for name, pts := range map[string][4]float64{
64 "up-left": {300, 300, 100, 120},
65 "up-right": {100, 300, 300, 120},
66 "down-left": {300, 120, 100, 300},
67 "down-right": {100, 120, 300, 300},
68 } {
69 t.Run(name, func(t *testing.T) {
70 p := newPicker(t, 1, 1000, 1000)
71 p.MouseDown(pts[0], pts[1])
72 p.MouseMove(pts[2], pts[3])
73 p.MouseUp(pts[2], pts[3])
74 res, ok := p.Result()
75 require.True(t, ok)
76 assert.Equal(t, ports.Region{X: 100, Y: 120, Width: 200, Height: 180}, res.Region)
77 })
78 }
79}
80
81func TestPickerClampsToOutput(t *testing.T) {
82 p := newPicker(t, 1, 500, 400)
83 p.MouseDown(-50, -20)
84 p.MouseMove(9000, 9000)
85 r, dragging := p.Rect()
86 require.True(t, dragging)
87 assert.Equal(t, ports.Region{Width: 500, Height: 400}, r, "live drag is clamped too")
88 p.MouseUp(9000, 9000)
89 res, ok := p.Result()
90 require.True(t, ok)
91 assert.Equal(t, ports.Region{Width: 500, Height: 400}, res.Region)
92 assert.Equal(t, core.PickRegion, res.Kind, "a dragged full-output region stays a region")
93
94 // Never leaves the output, wherever it starts.
95 q := newPicker(t, 1, 500, 400)
96 q.MouseDown(480, 390)
97 q.MouseMove(700, -100)
98 q.MouseUp(700, -100)
99 res, ok = q.Result()
100 require.True(t, ok)
101 assert.Equal(t, ports.Region{X: 480, Y: 0, Width: 20, Height: 390}, res.Region)
102
103 // NaN is ignored; infinities clamp to the edges.
104 n := newPicker(t, 1, 500, 400)
105 assert.False(t, n.MouseMove(math.NaN(), 5))
106 assert.False(t, n.MouseMove(5, math.NaN()))
107 i := newPicker(t, 1, 500, 400)
108 i.MouseDown(math.Inf(-1), math.Inf(-1))
109 i.MouseMove(math.Inf(1), math.Inf(1))
110 i.MouseUp(math.Inf(1), math.Inf(1))
111 res, ok = i.Result()
112 require.True(t, ok)
113 assert.Equal(t, ports.Region{Width: 500, Height: 400}, res.Region)
114}
115
116func TestPickerThresholdAndClick(t *testing.T) {
117 p := newPicker(t, 9, 1920, 1080)
118 p.MouseDown(500, 500)
119 assert.True(t, p.MouseMove(502, 502), "pointer moved")
120 assert.False(t, p.Dragging(), "under threshold")
121 assert.False(t, p.MouseMove(502, 502), "no change no redraw")
122 assert.True(t, p.MouseMove(504, 500), "exactly the threshold starts a drag")
123 assert.True(t, p.Dragging())
124
125 // A click, even with jitter below the threshold, picks the monitor.
126 c := newPicker(t, 9, 1920, 1080)
127 c.MouseDown(500, 500)
128 c.MouseMove(503, 500)
129 assert.True(t, c.MouseUp(503, 500))
130 res, ok := c.Result()
131 require.True(t, ok)
132 assert.Equal(t, core.PickResult{OutputID: 9, Region: ports.Region{Width: 1920, Height: 1080}, Kind: core.PickMonitor}, res)
133 assert.Equal(t, ports.Target{OutputID: 9}, res.Target(), "monitor is the full-output zero region")
134}
135
136func TestPickerMinimumSize(t *testing.T) {
137 p := newPicker(t, 1, 1000, 1000)
138 p.MouseDown(100, 100)
139 p.MouseMove(300, 102) // past threshold horizontally, height 2
140 assert.True(t, p.MouseUp(300, 102))
141 assert.Equal(t, core.PickActive, p.Status(), "tiny drag is discarded")
142 _, dragging := p.Rect()
143 assert.False(t, dragging)
144
145 // picking continues
146 p.MouseDown(10, 10)
147 p.MouseMove(60, 60)
148 p.MouseUp(60, 60)
149 res, ok := p.Result()
150 require.True(t, ok)
151 assert.Equal(t, ports.Region{X: 10, Y: 10, Width: 50, Height: 50}, res.Region)
152}
153
154func TestPickerEscape(t *testing.T) {
155 p := newPicker(t, 1, 100, 100)
156 p.MouseDown(10, 10)
157 p.MouseMove(50, 50)
158 assert.True(t, p.Cancel())
159 assert.Equal(t, core.PickCanceled, p.Status())
160 _, ok := p.Result()
161 assert.False(t, ok)
162 assert.False(t, p.MouseUp(50, 50))
163 assert.False(t, p.Confirm())
164 assert.False(t, p.Cancel())
165 assert.False(t, p.MouseDown(1, 1))
166}
167
168func TestPickerMonitorKeyAndConfirm(t *testing.T) {
169 p := newPicker(t, 2, 2560, 1440)
170 assert.False(t, p.Confirm(), "region kind needs a drag")
171 assert.True(t, p.SelectMonitor())
172 assert.False(t, p.SelectMonitor())
173 assert.Equal(t, core.PickMonitor, p.Kind())
174 assert.True(t, p.Confirm())
175 res, ok := p.Result()
176 require.True(t, ok)
177 assert.Equal(t, core.PickMonitor, res.Kind)
178 assert.Equal(t, ports.Region{Width: 2560, Height: 1440}, res.Region)
179 assert.Zero(t, res.WorkspaceID)
180
181 // A click also picks the monitor when m was chosen, even under the pointer.
182 q := newPicker(t, 2, 2560, 1440)
183 q.MouseMove(1000, 1000)
184 q.SelectMonitor()
185 q.MouseDown(1000, 1000)
186 q.MouseUp(1000, 1000)
187 res, ok = q.Result()
188 require.True(t, ok)
189 assert.Equal(t, core.PickMonitor, res.Kind)
190}
191
192func TestPickerWorkspaceNeedsRealIdentity(t *testing.T) {
193 rect := ports.Region{X: 0, Y: 0, Width: 1900, Height: 1000}
194
195 p := newPicker(t, 4, 1920, 1080)
196 assert.False(t, p.SelectWorkspace(), "no workspace supplied")
197 assert.False(t, p.SetWorkspace(0, rect), "zero id is not an identity")
198 assert.False(t, p.SelectWorkspace())
199 assert.False(t, p.SetWorkspace(7, ports.Region{}), "empty rect is not geometry")
200 assert.False(t, p.SetWorkspace(7, ports.Region{X: 5000, Y: 0, Width: 10, Height: 10}), "outside output")
201 assert.False(t, p.SetWorkspace(7, ports.Region{Width: -5, Height: 10}))
202 _, _, ok := p.Workspace()
203 assert.False(t, ok)
204 assert.False(t, p.SelectWorkspace())
205
206 assert.True(t, p.SetWorkspace(7, rect))
207 assert.False(t, p.SetWorkspace(7, rect), "unchanged")
208 assert.True(t, p.SelectWorkspace())
209 assert.True(t, p.Confirm())
210 res, ok := p.Result()
211 require.True(t, ok)
212 assert.Equal(t, core.PickResult{OutputID: 4, Region: rect, Kind: core.PickWorkspace, WorkspaceID: 7}, res)
213 assert.Equal(t, rect, res.Region, "geometry stays available for drawing")
214 assert.Equal(t, ports.Target{OutputID: 4, WorkspaceID: 7}, res.Target())
215}
216
217func TestPickResultTargetWorkspaceKeepsIdentity(t *testing.T) {
218 // A workspace that fills the whole output is still a workspace, not the
219 // monitor: the exact ID survives and no geometry snapshot is cached.
220 full := ports.Region{Width: 1920, Height: 1080}
221 p := newPicker(t, 4, 1920, 1080)
222 require.True(t, p.SetWorkspace(1<<40+5, full))
223 require.True(t, p.SelectWorkspace())
224 require.True(t, p.Confirm())
225 res, ok := p.Result()
226 require.True(t, ok)
227 assert.Equal(t, full, res.Region)
228 assert.Equal(t, uint64(1<<40+5), res.WorkspaceID)
229 got := res.Target()
230 assert.Equal(t, ports.Target{OutputID: 4, WorkspaceID: 1<<40 + 5}, got)
231 assert.Zero(t, got.Region)
232 assert.NotEqual(t, ports.Target{OutputID: 4}, got)
233
234 // Monitor and region results never carry a workspace ID.
235 assert.Equal(t, ports.Target{OutputID: 4}, core.PickResult{OutputID: 4, Kind: core.PickMonitor, Region: full, WorkspaceID: 9}.Target())
236 assert.Equal(t, ports.Target{OutputID: 4, Region: full}, core.PickResult{OutputID: 4, Kind: core.PickRegion, Region: full, WorkspaceID: 9}.Target())
237}
238
239func TestPickerWorkspaceClickAndClip(t *testing.T) {
240 p := newPicker(t, 4, 1000, 500)
241 p.SetWorkspace(11, ports.Region{X: -20, Y: 30, Width: 2000, Height: 2000})
242 id, r, ok := p.Workspace()
243 require.True(t, ok)
244 assert.Equal(t, uint64(11), id)
245 assert.Equal(t, ports.Region{X: 0, Y: 30, Width: 1000, Height: 470}, r, "clipped to the output")
246 p.SelectWorkspace()
247 p.MouseDown(5, 5)
248 p.MouseUp(5, 5)
249 res, ok := p.Result()
250 require.True(t, ok)
251 assert.Equal(t, core.PickWorkspace, res.Kind)
252 assert.Equal(t, uint64(11), res.WorkspaceID)
253
254 // Drag still wins as a region even with workspace chosen.
255 q := newPicker(t, 4, 1000, 500)
256 q.SetWorkspace(11, ports.Region{Width: 1000, Height: 500})
257 q.SelectWorkspace()
258 q.MouseDown(10, 10)
259 q.MouseMove(90, 90)
260 q.MouseUp(90, 90)
261 res, ok = q.Result()
262 require.True(t, ok)
263 assert.Equal(t, core.PickRegion, res.Kind)
264 assert.Zero(t, res.WorkspaceID)
265}
266
267func TestPickerWorkspaceLostFallsBack(t *testing.T) {
268 p := newPicker(t, 4, 1000, 500)
269 p.SetWorkspace(3, ports.Region{Width: 100, Height: 100})
270 p.SelectWorkspace()
271 assert.True(t, p.SetWorkspace(0, ports.Region{}))
272 assert.Equal(t, core.PickRegion, p.Kind())
273 assert.False(t, p.Confirm())
274}
275
276func TestPickerFullscreenMaxMonitor(t *testing.T) {
277 const max = ports.MaxDimension
278 p := newPicker(t, 1, max, max)
279 p.MouseDown(0, 0)
280 p.MouseMove(1e12, 1e12)
281 p.MouseUp(1e12, 1e12)
282 res, ok := p.Result()
283 require.True(t, ok)
284 assert.Equal(t, ports.Region{Width: max, Height: max}, res.Region)
285
286 q := newPicker(t, 1, max, max)
287 q.MouseDown(0, 0)
288 q.MouseUp(0, 0)
289 res, ok = q.Result()
290 require.True(t, ok)
291 assert.Equal(t, core.PickMonitor, res.Kind)
292 assert.Equal(t, ports.Region{Width: max, Height: max}, res.Region)
293}
294
295func TestPickerChangedFlags(t *testing.T) {
296 p := newPicker(t, 1, 100, 100)
297 assert.True(t, p.MouseMove(10, 10))
298 assert.False(t, p.MouseMove(10, 10), "same position")
299 assert.False(t, p.MouseUp(10, 10), "no press")
300 assert.True(t, p.MouseDown(10, 10))
301 assert.False(t, p.MouseDown(20, 20), "already pressed")
302 assert.False(t, p.SelectMonitor(), "mode cannot change mid-press")
303 assert.False(t, p.Confirm(), "no confirm mid-press")
304}
305
306func TestPickerAllocations(t *testing.T) {
307 p := newPicker(t, 1, 1920, 1080)
308 p.SetWorkspace(1, ports.Region{Width: 100, Height: 100})
309 n := testing.AllocsPerRun(10, func() {
310 p.MouseDown(10, 10)
311 for i := 0; i < 1000; i++ {
312 p.MouseMove(float64(10+i%900), float64(10+(i*7)%900))
313 _, _ = p.Rect()
314 _, _ = p.Result()
315 }
316 p.SelectMonitor()
317 p.SelectRegion()
318 p.Cancel()
319 _ = p.Begin(1, 1920, 1080)
320 p.SetWorkspace(1, ports.Region{Width: 100, Height: 100})
321 })
322 assert.Zero(t, n)
323}
324
325func BenchmarkPickerDrag(b *testing.B) {
326 p := &core.Picker{}
327 _ = p.Begin(1, 3840, 2160)
328 b.ReportAllocs()
329 b.ResetTimer()
330 for i := 0; i < b.N; i++ {
331 p.MouseDown(100, 100)
332 for j := 0; j < 100; j++ {
333 p.MouseMove(float64(100+j*30), float64(100+j*15))
334 }
335 p.MouseUp(3000, 1500)
336 _ = p.Begin(1, 3840, 2160)
337 }
338}