1package core_test
2
3import (
4 "context"
5 "slices"
6 "sync/atomic"
7 "testing"
8 "time"
9
10 "github.com/bnema/neferwl/internal/adapters/config"
11 "github.com/bnema/neferwl/internal/core"
12 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
13 "github.com/bnema/neferwl/internal/ports"
14)
15
16type dragRig struct {
17 client chan ports.ClientEvent
18 input chan ports.InputEvent
19 output chan ports.OutputEvent
20 commands chan ports.ClientCommand
21 scenes chan []ports.Scene
22 last []ports.Scene
23 epoch *atomic.Value
24}
25
26// startDrag runs core with Super as Cmd, no border, 10 px gaps, and
27// the given outputs. edit may change the config.
28func startDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig {
29 t.Helper()
30 cfg := config.Defaults()
31 cfg.Border.Width = 0
32 cfg.Layout.Gaps = 10
33 cfg.Terminal.AutoOpen = "off"
34 if edit != nil {
35 edit(&cfg)
36 }
37 epoch := &atomic.Value{}
38 epoch.Store(ports.SecurityState{})
39 gate := portsmocks.NewMockSessionSecurity(t)
40 gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }).Maybe()
41 // Unbuffered: a send returns once core took the event, so it handled
42 // every event sent before (settle relies on it).
43 r := &dragRig{
44 client: make(chan ports.ClientEvent), input: make(chan ports.InputEvent),
45 output: make(chan ports.OutputEvent), commands: make(chan ports.ClientCommand, 4096),
46 scenes: make(chan []ports.Scene, 1), epoch: epoch,
47 }
48 c, err := core.New(cfg, core.Channels{Security: gate, Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes})
49 if err != nil {
50 t.Fatal(err)
51 }
52 ctx, cancel := context.WithCancel(context.Background())
53 done := make(chan error, 1)
54 go func() { done <- c.Run(ctx) }()
55 t.Cleanup(func() { cancel(); <-done })
56 for _, o := range outs {
57 r.output <- ports.OutputAdded{Info: o}
58 r.settle(t)
59 }
60 return r
61}
62
63// in sends a raw input event; the gate is on, so it is wrapped.
64func (r *dragRig) in(ev ports.InputEvent) {
65 send(r.input, ports.InputEvent(ports.SecurityInput{State: r.epoch.Load().(ports.SecurityState), Event: ev}))
66}
67
68// send fails the test binary fast instead of hanging when core is stuck.
69func send[T any](ch chan T, v T) {
70 select {
71 case ch <- v:
72 case <-time.After(5 * time.Second):
73 panic("core stuck")
74 }
75}
76
77// barrier is a window no test maps: a resize of it changes nothing but
78// makes core publish.
79const barrier ports.WindowID = 1 << 30
80
81// settle returns the scene set once core handled every event sent so
82// far. The second barrier is taken only after the first one published.
83func (r *dragRig) settle(t *testing.T) []ports.Scene {
84 t.Helper()
85 send(r.client, ports.ClientEvent(ports.WindowResized{ID: barrier}))
86 send(r.client, ports.ClientEvent(ports.WindowResized{ID: barrier}))
87 select {
88 case s := <-r.scenes:
89 r.last = s
90 case <-time.After(5 * time.Second):
91 t.Fatal("no scene")
92 }
93 return r.last
94}
95
96// drain returns the commands sent since the last drain.
97func (r *dragRig) drain() []ports.ClientCommand {
98 var out []ports.ClientCommand
99 for {
100 select {
101 case v := <-r.commands:
102 if s, ok := v.(ports.SecurityCommand); ok {
103 v = s.Command
104 }
105 out = append(out, v)
106 default:
107 return out
108 }
109 }
110}
111
112func pointerSent(cmds []ports.ClientCommand) []ports.ClientCommand {
113 var out []ports.ClientCommand
114 for _, v := range cmds {
115 switch v.(type) {
116 case ports.PointerButtonTo, ports.PointerMotionTo, ports.ForwardKey:
117 out = append(out, v)
118 }
119 }
120 return out
121}
122
123func dragRect(t *testing.T, set []ports.Scene, output string, id ports.WindowID) ports.Rect {
124 t.Helper()
125 w, ok := windowIn(set, output, id)
126 if !ok {
127 t.Fatalf("window %d not on %s", id, output)
128 }
129 return w.Rect
130}
131
132func dragScene(set []ports.Scene, output string) ports.Scene {
133 for _, s := range set {
134 if s.Output == output {
135 return s
136 }
137 }
138 return ports.Scene{}
139}
140
141// columns lists the tiles left to right by x, then top to bottom.
142func columnsOf(set []ports.Scene, output string) [][]ports.WindowID {
143 s := dragScene(set, output)
144 var wins []ports.SceneWindow
145 for _, w := range s.Windows {
146 if !w.Hidden && !w.Floating && !w.Popup {
147 wins = append(wins, w)
148 }
149 }
150 slices.SortFunc(wins, func(a, b ports.SceneWindow) int {
151 if a.Rect.X != b.Rect.X {
152 return a.Rect.X - b.Rect.X
153 }
154 return a.Rect.Y - b.Rect.Y
155 })
156 var out [][]ports.WindowID
157 lastX := -1 << 30
158 for _, w := range wins {
159 if w.Rect.X != lastX {
160 out = append(out, nil)
161 lastX = w.Rect.X
162 }
163 out[len(out)-1] = append(out[len(out)-1], w.ID)
164 }
165 return out
166}
167
168var dragOut = ports.OutputInfo{Name: "A", Width: 1000, Height: 600}
169
170func (r *dragRig) moveTo(x, y float64, msec uint32) {
171 r.in(ports.PointerMotion{X: x, Y: y, TimeMsec: msec})
172}
173
174func (r *dragRig) press(t *testing.T, button uint32, mods ports.Mods) {
175 t.Helper()
176 if mods != 0 {
177 r.in(ports.KeyEvent{Keysym: "Super_L", Mods: mods, Pressed: true})
178 }
179 r.in(ports.PointerButton{Button: button, Pressed: true})
180}
181
182func TestCmdDragMovesFloatWithoutClientEvents(t *testing.T) {
183 r := startDragRig(t, nil, dragOut)
184 r.client <- ports.WindowMapped{ID: 1}
185 r.client <- ports.WindowMapped{ID: 2, Floating: true, Width: 200, Height: 100}
186 set := r.settle(t)
187 f := dragRect(t, set, "A", 2)
188 r.moveTo(float64(f.X+10), float64(f.Y+10), 1)
189 r.settle(t)
190 r.drain()
191 r.press(t, 0x110, ports.ModSuper)
192 r.moveTo(float64(f.X+60), float64(f.Y+40), 2)
193 set = r.settle(t)
194 got := dragRect(t, set, "A", 2)
195 if got.X != f.X+50 || got.Y != f.Y+30 || got.W != f.W {
196 t.Fatalf("moved to %+v from %+v", got, f)
197 }
198 if w, _ := windowIn(set, "A", 2); !w.Floating {
199 t.Fatal("not floating")
200 }
201 r.in(ports.PointerButton{Button: 0x110})
202 r.in(ports.KeyEvent{Keysym: "Super_L"})
203 r.settle(t)
204 cmds := r.drain()
205 for _, v := range pointerSent(cmds) {
206 if _, ok := v.(ports.PointerButtonTo); ok {
207 t.Fatalf("client saw %#v", v)
208 }
209 if _, ok := v.(ports.PointerMotionTo); ok {
210 t.Fatalf("client saw %#v", v)
211 }
212 }
213 // It stays there: a free float now.
214 r.moveTo(float64(f.X+61), float64(f.Y+41), 3)
215 if got2 := dragRect(t, r.settle(t), "A", 2); got2 != got {
216 t.Fatalf("moved after release: %+v", got2)
217 }
218 // A plain press goes to the client.
219 r.drain()
220 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
221 r.settle(t)
222 found := false
223 for _, v := range r.drain() {
224 if b, ok := v.(ports.PointerButtonTo); ok && b.ID == 2 && b.Pressed {
225 found = true
226 }
227 }
228 if !found {
229 t.Fatal("plain press not forwarded")
230 }
231}
232
233func TestDragEscapeRestoresFloat(t *testing.T) {
234 r := startDragRig(t, nil, dragOut)
235 r.client <- ports.WindowMapped{ID: 2, Floating: true, Width: 200, Height: 100}
236 f := dragRect(t, r.settle(t), "A", 2)
237 r.moveTo(float64(f.X+10), float64(f.Y+10), 1)
238 r.settle(t)
239 r.press(t, 0x111, ports.ModSuper)
240 r.moveTo(float64(f.X+110), float64(f.Y+60), 2)
241 if got := dragRect(t, r.settle(t), "A", 2); got.W != f.W+100 || got.H != f.H+50 || got.X != f.X {
242 t.Fatalf("resize: %+v from %+v", got, f)
243 }
244 r.drain()
245 r.in(ports.KeyEvent{Keysym: "Escape", Mods: ports.ModSuper, Pressed: true})
246 if got := dragRect(t, r.settle(t), "A", 2); got != f {
247 t.Fatalf("escape: %+v, want %+v", got, f)
248 }
249 r.in(ports.KeyEvent{Keysym: "Escape", Mods: ports.ModSuper})
250 r.in(ports.PointerButton{Button: 0x111})
251 r.settle(t)
252 for _, v := range pointerSent(r.drain()) {
253 t.Fatalf("client saw %#v", v)
254 }
255}
256
257func TestDragTileZonesAndHints(t *testing.T) {
258 r := startDragRig(t, func(c *ports.Config) { c.Layout.MaxColumns = 3 }, dragOut)
259 for id := ports.WindowID(1); id <= 3; id++ {
260 r.client <- ports.WindowMapped{ID: id}
261 r.settle(t)
262 }
263 set := r.settle(t)
264 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) {
265 t.Fatal(got)
266 }
267 a, c := dragRect(t, set, "A", 1), dragRect(t, set, "A", 3)
268 // Drag 1 onto the bottom band of 3: it stacks below 3.
269 r.moveTo(float64(a.X+a.W/2), float64(a.Y+a.H/2), 1)
270 r.settle(t)
271 r.press(t, 0x110, ports.ModSuper)
272 r.moveTo(float64(c.X+c.W/2), float64(c.Y+c.H-5), 2)
273 set = r.settle(t)
274 hints := dragScene(set, "A").DropHints
275 if len(hints) != 1 || hints[0].Y+hints[0].H != c.Y+c.H || hints[0].W != c.W {
276 t.Fatalf("hints %+v", hints)
277 }
278 // Tiles do not move until the drop.
279 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) {
280 t.Fatal(got)
281 }
282 r.in(ports.PointerButton{Button: 0x110})
283 set = r.settle(t)
284 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{2}, {3, 1}}, slices.Equal) {
285 t.Fatal(got)
286 }
287 if len(dragScene(set, "A").DropHints) != 0 {
288 t.Fatal("hints left")
289 }
290 // Centre of 2 with stacked 1 dragged from its right: 1 becomes a
291 // column in 2's place, 2 moves toward where 1 came from.
292 b := dragRect(t, set, "A", 2)
293 one := dragRect(t, set, "A", 1)
294 r.moveTo(float64(one.X+one.W/2), float64(one.Y+one.H/2), 3)
295 r.settle(t)
296 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
297 r.moveTo(float64(b.X+b.W/2), float64(b.Y+b.H/2), 4)
298 if hints := dragScene(r.settle(t), "A").DropHints; len(hints) != 4 {
299 t.Fatalf("swap outline %+v", hints)
300 }
301 r.in(ports.PointerButton{Button: 0x110})
302 set = r.settle(t)
303 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) {
304 t.Fatal(got)
305 }
306 // Lone 3 on the centre of 2: the columns swap.
307 b, c = dragRect(t, set, "A", 2), dragRect(t, set, "A", 3)
308 r.moveTo(float64(c.X+c.W/2), float64(c.Y+c.H/2), 5)
309 r.settle(t)
310 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
311 r.moveTo(float64(b.X+b.W/2), float64(b.Y+b.H/2), 6)
312 r.settle(t)
313 r.in(ports.PointerButton{Button: 0x110})
314 set = r.settle(t)
315 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {3}, {2}}, slices.Equal) {
316 t.Fatal(got)
317 }
318 // Escape mid tile drag: nothing moves, no hints.
319 one = dragRect(t, set, "A", 1)
320 b = dragRect(t, set, "A", 2)
321 r.moveTo(float64(one.X+one.W/2), float64(one.Y+one.H/2), 7)
322 r.settle(t)
323 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
324 r.moveTo(float64(b.X+b.W/2), float64(b.Y+5), 8)
325 if len(dragScene(r.settle(t), "A").DropHints) == 0 {
326 t.Fatal("no hint")
327 }
328 r.in(ports.KeyEvent{Keysym: "Escape", Mods: ports.ModSuper, Pressed: true})
329 set = r.settle(t)
330 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {3}, {2}}, slices.Equal) || len(dragScene(set, "A").DropHints) != 0 {
331 t.Fatal(got, dragScene(set, "A").DropHints)
332 }
333}
334
335func TestDragTileGapInsert(t *testing.T) {
336 r := startDragRig(t, func(c *ports.Config) { c.Layout.MaxColumns = 3 }, dragOut)
337 for id := ports.WindowID(1); id <= 3; id++ {
338 r.client <- ports.WindowMapped{ID: id}
339 r.settle(t)
340 }
341 r.client <- ports.WindowMapped{ID: 4}
342 r.settle(t)
343 // Stack 4 under 3 with the keyboard, then put 4 between 1 and 2.
344 r.in(ports.KeyEvent{Keysym: "bracketleft", Mods: ports.ModSuper, Pressed: true})
345 r.in(ports.KeyEvent{Keysym: "bracketleft", Mods: ports.ModSuper})
346 r.in(ports.KeyEvent{Keysym: "Left", Mods: ports.ModSuper, Pressed: true})
347 r.in(ports.KeyEvent{Keysym: "Left", Mods: ports.ModSuper})
348 r.in(ports.KeyEvent{Keysym: "Left", Mods: ports.ModSuper, Pressed: true})
349 r.in(ports.KeyEvent{Keysym: "Left", Mods: ports.ModSuper})
350 set := r.settle(t)
351 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3, 4}}, slices.Equal) {
352 t.Fatal(got)
353 }
354 one, two, four := dragRect(t, set, "A", 1), dragRect(t, set, "A", 2), dragRect(t, set, "A", 4)
355 gapX := (one.X + one.W + two.X) / 2
356 r.moveTo(float64(four.X+four.W/2), float64(four.Y+four.H/2), 1)
357 r.settle(t)
358 r.press(t, 0x110, ports.ModSuper)
359 r.moveTo(float64(gapX), float64(one.Y+one.H/2), 2)
360 hints := dragScene(r.settle(t), "A").DropHints
361 if len(hints) != 1 || hints[0].H != one.H || hints[0].X+hints[0].W/2 != gapX {
362 t.Fatalf("gap hint %+v", hints)
363 }
364 r.in(ports.PointerButton{Button: 0x110})
365 if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got[:2], [][]ports.WindowID{{1}, {4}}, slices.Equal) {
366 t.Fatal(got)
367 }
368}
369
370func TestDragFixedNoGapZone(t *testing.T) {
371 r := startDragRig(t, func(c *ports.Config) { c.Layout.MaxColumns = 3; c.Layout.Overflow = "fixed" }, dragOut)
372 for id := ports.WindowID(1); id <= 2; id++ {
373 r.client <- ports.WindowMapped{ID: id}
374 r.settle(t)
375 }
376 set := r.settle(t)
377 one, two := dragRect(t, set, "A", 1), dragRect(t, set, "A", 2)
378 r.moveTo(float64(one.X+one.W/2), float64(one.Y+one.H/2), 1)
379 r.settle(t)
380 r.press(t, 0x110, ports.ModSuper)
381 r.moveTo(float64((one.X+one.W+two.X)/2), float64(one.Y+one.H/2), 2)
382 if hints := dragScene(r.settle(t), "A").DropHints; len(hints) != 0 {
383 t.Fatalf("fixed gap hint %+v", hints)
384 }
385 // Top band of 2 stacks 1 above it.
386 r.moveTo(float64(two.X+two.W/2), float64(two.Y+3), 3)
387 r.settle(t)
388 r.in(ports.PointerButton{Button: 0x110})
389 if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1, 2}}, slices.Equal) {
390 t.Fatal(got)
391 }
392}
393
394func TestDragAcrossOutputs(t *testing.T) {
395 b := ports.OutputInfo{Name: "B", Width: 800, Height: 600}
396 r := startDragRig(t, nil, dragOut, b)
397 r.client <- ports.WindowMapped{ID: 1}
398 r.client <- ports.WindowMapped{ID: 2}
399 r.settle(t)
400 // Put 3 on B.
401 r.moveTo(1500, 300, 1)
402 r.settle(t)
403 r.client <- ports.WindowMapped{ID: 3}
404 set := r.settle(t)
405 if _, ok := windowIn(set, "B", 3); !ok {
406 t.Fatal("3 not on B")
407 }
408 one := dragRect(t, set, "A", 1)
409 r.moveTo(float64(one.X+one.W/2), float64(one.Y+one.H/2), 2)
410 r.settle(t)
411 r.press(t, 0x110, ports.ModSuper)
412 r.moveTo(1400, 300, 3)
413 set = r.settle(t)
414 if len(dragScene(set, "B").DropHints) == 0 || len(dragScene(set, "A").DropHints) != 0 {
415 t.Fatalf("hints A %+v B %+v", dragScene(set, "A").DropHints, dragScene(set, "B").DropHints)
416 }
417 r.in(ports.PointerButton{Button: 0x110})
418 set = r.settle(t)
419 if _, ok := windowIn(set, "B", 1); !ok {
420 t.Fatal("1 not on B")
421 }
422 if w, _ := windowIn(set, "B", 1); !w.Focused {
423 t.Fatal("1 not focused")
424 }
425}
426
427func TestClientMoveRequest(t *testing.T) {
428 r := startDragRig(t, nil, dragOut)
429 r.client <- ports.WindowMapped{ID: 2, Floating: true, Width: 200, Height: 100}
430 f := dragRect(t, r.settle(t), "A", 2)
431 // Without a held button: ignored.
432 r.client <- ports.WindowMoveRequest{ID: 2}
433 r.moveTo(float64(f.X+10), float64(f.Y+10), 1)
434 r.settle(t)
435 r.moveTo(float64(f.X+20), float64(f.Y+10), 2)
436 if got := dragRect(t, r.settle(t), "A", 2); got != f {
437 t.Fatalf("moved without a button: %+v", got)
438 }
439 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
440 r.settle(t)
441 r.client <- ports.WindowMoveRequest{ID: 2}
442 r.settle(t)
443 r.drain()
444 r.moveTo(float64(f.X+60), float64(f.Y+10), 3)
445 if got := dragRect(t, r.settle(t), "A", 2); got.X != f.X+40 {
446 t.Fatalf("move request: %+v from %+v", got, f)
447 }
448 r.in(ports.PointerButton{Button: 0x110})
449 r.settle(t)
450 for _, v := range pointerSent(r.drain()) {
451 t.Fatalf("client saw %#v", v)
452 }
453}
454
455func TestLockClearsDrag(t *testing.T) {
456 r := startDragRig(t, nil, dragOut)
457 r.client <- ports.WindowMapped{ID: 1}
458 r.client <- ports.WindowMapped{ID: 2}
459 set := r.settle(t)
460 one, two := dragRect(t, set, "A", 1), dragRect(t, set, "A", 2)
461 r.moveTo(float64(one.X+one.W/2), float64(one.Y+one.H/2), 1)
462 r.settle(t)
463 r.press(t, 0x110, ports.ModSuper)
464 r.moveTo(float64(two.X+two.W/2), float64(two.Y+5), 2)
465 if len(dragScene(r.settle(t), "A").DropHints) == 0 {
466 t.Fatal("no hint")
467 }
468 locked := ports.SecurityState{Generation: 1, Protected: true}
469 r.epoch.Store(locked)
470 r.client <- ports.SessionLockChanged{State: locked}
471 set = r.settle(t)
472 if len(dragScene(set, "A").DropHints) != 0 {
473 t.Fatal("hints while locked")
474 }
475 unlocked := ports.SecurityState{Generation: 2}
476 r.epoch.Store(unlocked)
477 r.client <- ports.SessionLockChanged{State: unlocked}
478 r.settle(t)
479 r.in(ports.PointerButton{Button: 0x110})
480 set = r.settle(t)
481 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}}, slices.Equal) {
482 t.Fatal(got)
483 }
484}
485
486func TestDragEdgeScroll(t *testing.T) {
487 r := startDragRig(t, nil, dragOut)
488 for id := ports.WindowID(1); id <= 4; id++ {
489 r.client <- ports.WindowMapped{ID: id}
490 r.settle(t)
491 }
492 // Back to the first column: 3 and 4 are off screen to the right.
493 for range 3 {
494 r.in(ports.KeyEvent{Keysym: "Left", Mods: ports.ModSuper, Pressed: true})
495 r.in(ports.KeyEvent{Keysym: "Left", Mods: ports.ModSuper})
496 }
497 set := r.settle(t)
498 one := dragRect(t, set, "A", 1)
499 if one.X != 10 {
500 t.Fatalf("%+v", one)
501 }
502 r.moveTo(float64(one.X+one.W/2), float64(one.Y+one.H/2), 1000)
503 r.settle(t)
504 r.press(t, 0x110, ports.ModSuper)
505 r.moveTo(995, 300, 1001)
506 first := dragRect(t, r.settle(t), "A", 1)
507 if first.X >= one.X {
508 t.Fatalf("no scroll: %+v", first)
509 }
510 // Under 250 ms: no second step.
511 r.moveTo(996, 300, 1100)
512 if got := dragRect(t, r.settle(t), "A", 1); got != first {
513 t.Fatalf("scrolled too soon: %+v", got)
514 }
515 r.moveTo(997, 300, 1300)
516 second := dragRect(t, r.settle(t), "A", 1)
517 r.moveTo(998, 300, 1600)
518 r.moveTo(999, 300, 1900)
519 end := dragRect(t, r.settle(t), "A", 1)
520 r.moveTo(998, 300, 2200)
521 if got := dragRect(t, r.settle(t), "A", 1); second.X >= first.X || got != end {
522 t.Fatalf("first %+v second %+v end %+v got %+v", first, second, end, got)
523 }
524 r.in(ports.KeyEvent{Keysym: "Escape", Mods: ports.ModSuper, Pressed: true})
525 r.settle(t)
526}
527
528func TestDragFullFixedStacksInOutlinedColumn(t *testing.T) {
529 r := startDragRig(t, func(c *ports.Config) { c.Layout.MaxColumns = 3; c.Layout.Overflow = "fixed" }, dragOut)
530 for id := ports.WindowID(1); id <= 3; id++ {
531 r.client <- ports.WindowMapped{ID: id}
532 r.settle(t)
533 }
534 set := r.settle(t)
535 one, two := dragRect(t, set, "A", 1), dragRect(t, set, "A", 2)
536 // 4 joins 1's column from the top band of 1.
537 r.client <- ports.WindowMapped{ID: 4}
538 set = r.settle(t)
539 four := dragRect(t, set, "A", 4)
540 r.moveTo(float64(four.X+four.W/2), float64(four.Y+four.H/2), 1)
541 r.settle(t)
542 r.press(t, 0x110, ports.ModSuper)
543 r.moveTo(float64(one.X+one.W/2), float64(one.Y+3), 2)
544 r.settle(t)
545 r.in(ports.PointerButton{Button: 0x110})
546 set = r.settle(t)
547 if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{4, 1}, {2}, {3}}, slices.Equal) {
548 t.Fatal(got)
549 }
550 one, two = dragRect(t, set, "A", 1), dragRect(t, set, "A", 2)
551 r.moveTo(float64(one.X+one.W/2), float64(one.Y+one.H/2), 3)
552 r.settle(t)
553 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
554 r.moveTo(float64(two.X+two.W/2), float64(two.Y+two.H/2), 4)
555 r.settle(t)
556 r.in(ports.PointerButton{Button: 0x110})
557 if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got, [][]ports.WindowID{{4}, {2, 1}, {3}}, slices.Equal) {
558 t.Fatal(got)
559 }
560}
561
562func TestPressAfterCancelGrabs(t *testing.T) {
563 r := startDragRig(t, nil, dragOut)
564 r.client <- ports.WindowMapped{ID: 1}
565 r.client <- ports.WindowMapped{ID: 2, Floating: true, Width: 200, Height: 100}
566 f := dragRect(t, r.settle(t), "A", 2)
567 r.moveTo(float64(f.X+10), float64(f.Y+10), 1)
568 r.settle(t)
569 r.press(t, 0x110, ports.ModSuper)
570 r.in(ports.KeyEvent{Keysym: "Escape", Mods: ports.ModSuper, Pressed: true})
571 r.in(ports.KeyEvent{Keysym: "Escape", Mods: ports.ModSuper})
572 r.in(ports.KeyEvent{Keysym: "Super_L"})
573 r.settle(t)
574 r.drain()
575 // Left is still held, swallowed. A right click on 2 goes to 2, and so
576 // does its release after the pointer left it.
577 r.in(ports.PointerButton{Button: 0x111, Pressed: true})
578 r.moveTo(5, 5, 2)
579 r.in(ports.PointerButton{Button: 0x111})
580 r.settle(t)
581 var got []ports.PointerButtonTo
582 for _, v := range r.drain() {
583 if b, ok := v.(ports.PointerButtonTo); ok {
584 got = append(got, b)
585 }
586 }
587 if len(got) != 2 || got[0].ID != 2 || got[1].ID != 2 || got[1].Pressed {
588 t.Fatalf("%+v", got)
589 }
590}
591
592func TestClientResizeWithoutEdgesIgnored(t *testing.T) {
593 r := startDragRig(t, nil, dragOut)
594 r.client <- ports.WindowMapped{ID: 2, Floating: true, Width: 200, Height: 100}
595 f := dragRect(t, r.settle(t), "A", 2)
596 r.moveTo(float64(f.X+10), float64(f.Y+10), 1)
597 r.settle(t)
598 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
599 r.client <- ports.WindowMoveRequest{ID: 2, Resize: true}
600 r.moveTo(float64(f.X+60), float64(f.Y+60), 2)
601 if got := dragRect(t, r.settle(t), "A", 2); got != f {
602 t.Fatalf("%+v from %+v", got, f)
603 }
604}
605
606func TestClientDragUsesLastPress(t *testing.T) {
607 r := startDragRig(t, nil, dragOut)
608 r.client <- ports.WindowMapped{ID: 2, Floating: true, Width: 200, Height: 100}
609 f := dragRect(t, r.settle(t), "A", 2)
610 r.moveTo(float64(f.X+10), float64(f.Y+10), 1)
611 r.settle(t)
612 r.in(ports.PointerButton{Button: 0x111, Pressed: true})
613 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
614 r.client <- ports.WindowMoveRequest{ID: 2}
615 r.moveTo(float64(f.X+60), float64(f.Y+10), 2)
616 r.settle(t)
617 // Releasing left ends the drag: the window stays put afterwards.
618 r.in(ports.PointerButton{Button: 0x110})
619 r.moveTo(float64(f.X+90), float64(f.Y+10), 3)
620 if got := dragRect(t, r.settle(t), "A", 2); got.X != f.X+50 {
621 t.Fatalf("%+v from %+v", got, f)
622 }
623}
624
625func TestEdgeScrollPublishesWithoutTarget(t *testing.T) {
626 r := startDragRig(t, nil, dragOut)
627 for id := ports.WindowID(1); id <= 4; id++ {
628 r.client <- ports.WindowMapped{ID: id}
629 r.settle(t)
630 }
631 for range 3 {
632 r.in(ports.KeyEvent{Keysym: "Left", Mods: ports.ModSuper, Pressed: true})
633 r.in(ports.KeyEvent{Keysym: "Left", Mods: ports.ModSuper})
634 }
635 // Drag 2 into the top gap, then along it to the right edge: no drop
636 // target before or after, but the view scrolls.
637 two := dragRect(t, r.settle(t), "A", 2)
638 edge := float64(dragOut.Width - 30)
639 if float64(two.X+two.W) <= edge {
640 t.Fatalf("2 does not reach the edge: %+v", two)
641 }
642 r.moveTo(float64(two.X+two.W/2), float64(two.Y+two.H/2), 1000)
643 r.settle(t)
644 r.press(t, 0x110, ports.ModSuper)
645 r.moveTo(float64(two.X+two.W/2), 5, 1001)
646 if hints := dragScene(r.settle(t), "A").DropHints; len(hints) != 0 {
647 t.Fatalf("hint in the gap %+v", hints)
648 }
649 r.moveTo(edge, 5, 1300)
650 // An axis event publishes nothing mid-drag: once core took it, the
651 // motion's scene, if any, is in the channel.
652 r.in(ports.PointerAxis{})
653 select {
654 case set := <-r.scenes:
655 if got := dragRect(t, set, "A", 2); got.X >= two.X {
656 t.Fatalf("not scrolled: %+v", got)
657 }
658 default:
659 t.Fatal("edge scroll not published")
660 }
661}
662
663func TestModsFollowKeys(t *testing.T) {
664 r := startDragRig(t, nil, dragOut)
665 r.client <- ports.WindowMapped{ID: 1}
666 r.client <- ports.WindowMapped{ID: 2, Floating: true, Width: 200, Height: 100}
667 f := dragRect(t, r.settle(t), "A", 2)
668 r.moveTo(float64(f.X+10), float64(f.Y+10), 1)
669 r.settle(t)
670 // Super pressed then released: a click is a plain click again.
671 r.in(ports.KeyEvent{Keysym: "Super_L", Mods: ports.ModSuper, Pressed: true})
672 r.in(ports.KeyEvent{Keysym: "Super_L"})
673 r.settle(t)
674 r.drain()
675 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
676 r.settle(t)
677 found := false
678 for _, v := range r.drain() {
679 if b, ok := v.(ports.PointerButtonTo); ok && b.ID == 2 {
680 found = true
681 }
682 }
683 if !found {
684 t.Fatal("click after cmd release not forwarded")
685 }
686}