1package core_test
2
3import (
4 "slices"
5 "testing"
6 "time"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11const dndIcon ports.WindowID = 900
12
13// dndRig is a drag rig with tiles 1 and 2 side by side and the pointer
14// on 1 with its button held, as a client that is about to start_drag.
15type dndRig struct {
16 *dragRig
17 one, two ports.Rect
18}
19
20func startDnd(t *testing.T, outs ...ports.OutputInfo) *dndRig {
21 t.Helper()
22 if len(outs) == 0 {
23 outs = []ports.OutputInfo{dragOut}
24 }
25 r := startDragRig(t, nil, outs...)
26 r.client <- ports.WindowMapped{ID: 1}
27 r.client <- ports.WindowMapped{ID: 2}
28 set := r.settle(t)
29 d := &dndRig{dragRig: r, one: dragRect(t, set, outs[0].Name, 1), two: dragRect(t, set, outs[0].Name, 2)}
30 r.moveTo(float64(d.one.X+20), float64(d.one.Y+30), 1)
31 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
32 r.settle(t)
33 r.drain()
34 return d
35}
36
37// dataDragCommands keeps what the data drag sends and the pointer events
38// no client may see during it.
39func dataDragCommands(cmds []ports.ClientCommand) []ports.ClientCommand {
40 var out []ports.ClientCommand
41 for _, v := range cmds {
42 switch v.(type) {
43 case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel,
44 ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo,
45 ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
46 out = append(out, v)
47 }
48 }
49 return out
50}
51
52func (d *dndRig) start(t *testing.T, icon ports.WindowID) []ports.ClientCommand {
53 t.Helper()
54 d.client <- ports.DataDragStarted{Button: 0x110, Icon: icon}
55 d.settle(t)
56 return dataDragCommands(d.drain())
57}
58
59func wantCommands(t *testing.T, got []ports.ClientCommand, want ...ports.ClientCommand) {
60 t.Helper()
61 if !slices.Equal(got, want) {
62 t.Fatalf("commands %#v, want %#v", got, want)
63 }
64}
65
66func TestDataDragRoutesToSurfaceUnderPointer(t *testing.T) {
67 d := startDnd(t)
68 // The origin loses the pointer and is the first target.
69 wantCommands(t, d.start(t, 0),
70 ports.PointerFocus{},
71 ports.DataDragFocus{ID: 1, X: 20, Y: 30, Time: time.Millisecond})
72 // Motion over the same surface.
73 d.moveTo(float64(d.one.X+25), float64(d.one.Y+35), 2)
74 d.settle(t)
75 wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 1, X: 25, Y: 35, Time: 2 * time.Millisecond})
76 // Over the other window: it is the new target.
77 d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
78 d.settle(t)
79 wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond})
80 d.moveTo(float64(d.two.X+7), float64(d.two.Y+8), 4)
81 d.settle(t)
82 wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 2, X: 7, Y: 8, Time: 4 * time.Millisecond})
83 // Over nothing: no target.
84 d.moveTo(float64(d.two.X+d.two.W+5), float64(d.two.Y+8), 5)
85 d.settle(t)
86 wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{Time: 5 * time.Millisecond})
87 d.moveTo(float64(d.two.X+d.two.W+6), float64(d.two.Y+8), 6)
88 d.settle(t)
89 wantCommands(t, dataDragCommands(d.drain()))
90}
91
92func TestDataDragDropsOnRelease(t *testing.T) {
93 d := startDnd(t)
94 d.start(t, 0)
95 d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
96 d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond})
97 d.settle(t)
98 // The release is the drop; the pointer then points at the window under
99 // the cursor, not at the origin that grabbed it.
100 wantCommands(t, dataDragCommands(d.drain()),
101 ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond},
102 ports.DataDragDrop{},
103 ports.PointerFocus{ID: 2, X: 5, Y: 6})
104 // The drag is over: motion is plain pointer motion again.
105 d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5)
106 d.settle(t)
107 wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond})
108 // The next press is an ordinary click.
109 d.in(ports.PointerButton{Button: 0x110, Pressed: true})
110 d.settle(t)
111 wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true, Grab: true})
112}
113
114func TestDataDragSendsNothingToClients(t *testing.T) {
115 d := startDnd(t)
116 d.start(t, 0)
117 d.in(ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}})
118 d.in(ports.PinchBegin{Fingers: 2, Time: time.Second})
119 d.in(ports.PinchUpdate{Scale: 2, Time: time.Second})
120 d.in(ports.PinchEnd{Time: time.Second})
121 d.in(ports.HoldBegin{Fingers: 2, Time: time.Second})
122 d.in(ports.HoldEnd{Time: time.Second})
123 d.in(ports.SwipeBegin{Fingers: 2, Time: time.Second})
124 d.in(ports.SwipeEnd{Time: time.Second})
125 // Other buttons neither press nor release anywhere.
126 d.in(ports.PointerButton{Button: 0x111, Pressed: true})
127 d.in(ports.PointerButton{Button: 0x111})
128 d.settle(t)
129 for _, v := range dataDragCommands(d.drain()) {
130 switch v.(type) {
131 case ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.PointerMotionTo, ports.DataDragDrop, ports.DataDragCancel:
132 t.Fatalf("sent %#v", v)
133 }
134 }
135 // Still dragging: the drag button's release drops.
136 d.in(ports.PointerButton{Button: 0x110})
137 d.settle(t)
138 if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
139 t.Fatalf("commands %#v", got)
140 }
141}
142
143func TestDataDragKeyboardKeepsForwarding(t *testing.T) {
144 d := startDnd(t)
145 d.start(t, 0)
146 d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42, Pressed: true, Mods: ports.ModShift})
147 d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42})
148 d.settle(t)
149 n := 0
150 for _, v := range d.drain() {
151 if _, ok := v.(ports.ForwardKey); ok {
152 n++
153 }
154 }
155 if n != 2 {
156 t.Fatalf("%d keys forwarded, want 2", n)
157 }
158}
159
160func TestDataDragEscapeCancels(t *testing.T) {
161 d := startDnd(t)
162 d.start(t, 0)
163 d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
164 d.settle(t)
165 d.drain()
166 d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Pressed: true})
167 d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1})
168 d.in(ports.PointerButton{Button: 0x110})
169 d.settle(t)
170 cmds := d.drain()
171 wantCommands(t, dataDragCommands(cmds),
172 ports.DataDragCancel{},
173 ports.PointerFocus{ID: 2, X: 5, Y: 6})
174 for _, v := range cmds {
175 if k, ok := v.(ports.ForwardKey); ok {
176 t.Fatalf("client saw %#v", k)
177 }
178 }
179 // The press that started the drag is over.
180 d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5)
181 d.settle(t)
182 wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond})
183}
184
185func TestDataDragEndedByWayland(t *testing.T) {
186 d := startDnd(t)
187 d.start(t, dndIcon)
188 d.client <- ports.DataDragIcon{ID: dndIcon, X: 1, Y: 2, W: 16, H: 16}
189 d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
190 d.settle(t)
191 d.drain()
192 // The source died: no command, the icon goes and the pointer is
193 // pointed at the window under the cursor.
194 d.client <- ports.DataDragEnded{}
195 set := d.settle(t)
196 if _, ok := windowIn(set, "A", dndIcon); ok {
197 t.Fatal("icon still drawn")
198 }
199 wantCommands(t, dataDragCommands(d.drain()),
200 ports.PointerFocus{ID: 2, X: 5, Y: 6})
201 // The held button's release reaches nobody.
202 d.in(ports.PointerButton{Button: 0x110})
203 d.settle(t)
204 wantCommands(t, dataDragCommands(d.drain()))
205 // An end with no drag is ignored.
206 d.client <- ports.DataDragEnded{}
207 d.settle(t)
208 wantCommands(t, dataDragCommands(d.drain()))
209}
210
211func TestDataDragRefused(t *testing.T) {
212 t.Run("no press", func(t *testing.T) {
213 d := startDnd(t)
214 d.in(ports.PointerButton{Button: 0x110})
215 d.settle(t)
216 d.drain()
217 wantCommands(t, d.start(t, 0), ports.DataDragCancel{})
218 // No drag started: motion is plain.
219 d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 2)
220 d.settle(t)
221 wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 2 * time.Millisecond})
222 })
223 t.Run("window drag", func(t *testing.T) {
224 r := startDragRig(t, nil, dragOut)
225 r.client <- ports.WindowMapped{ID: 1}
226 r.client <- ports.WindowMapped{ID: 2}
227 set := r.settle(t)
228 one := dragRect(t, set, "A", 1)
229 r.moveTo(float64(one.X+20), float64(one.Y+30), 1)
230 r.settle(t)
231 r.press(t, 0x110, ports.ModSuper)
232 r.settle(t)
233 r.drain()
234 r.client <- ports.DataDragStarted{Button: 0x110}
235 r.settle(t)
236 wantCommands(t, r.drain(), ports.DataDragCancel{})
237 // The window drag goes on.
238 r.moveTo(float64(one.X+200), float64(one.Y+30), 2)
239 r.in(ports.PointerButton{Button: 0x110})
240 r.settle(t)
241 wantCommands(t, dataDragCommands(r.drain()), ports.PointerFocus{ID: 1, X: 200, Y: 30})
242 })
243 t.Run("twice", func(t *testing.T) {
244 d := startDnd(t)
245 d.start(t, 0)
246 wantCommands(t, d.start(t, 0), ports.DataDragCancel{})
247 })
248 t.Run("locked", func(t *testing.T) {
249 d := startDnd(t)
250 locked := ports.SecurityState{Generation: 1, Protected: true}
251 d.epoch.Store(locked)
252 d.client <- ports.SessionLockChanged{State: locked}
253 d.settle(t)
254 d.drain()
255 d.client <- ports.DataDragStarted{Button: 0x110}
256 d.settle(t)
257 var got []ports.ClientCommand
258 for _, v := range d.drain() {
259 switch v.(type) {
260 case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
261 got = append(got, v)
262 }
263 }
264 wantCommands(t, got, ports.DataDragCancel{})
265 })
266}
267
268func TestDataDragCommandsAreSecurityWrapped(t *testing.T) {
269 d := startDnd(t)
270 d.client <- ports.DataDragStarted{Button: 0x110}
271 d.settle(t)
272 n := 0
273 for {
274 select {
275 case v := <-d.commands:
276 s, ok := v.(ports.SecurityCommand)
277 if !ok {
278 t.Fatalf("unwrapped %#v", v)
279 }
280 if _, ok := s.Command.(ports.DataDragFocus); ok {
281 n++
282 }
283 continue
284 default:
285 }
286 break
287 }
288 if n != 1 {
289 t.Fatalf("%d DataDragFocus", n)
290 }
291}
292
293func TestDataDragLockCancelsIt(t *testing.T) {
294 d := startDnd(t)
295 d.start(t, dndIcon)
296 d.client <- ports.DataDragIcon{ID: dndIcon, W: 16, H: 16}
297 d.settle(t)
298 locked := ports.SecurityState{Generation: 1, Protected: true}
299 d.epoch.Store(locked)
300 d.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 1000, Height: 600}}}
301 set := d.settle(t)
302 if _, ok := windowIn(set, "A", dndIcon); ok {
303 t.Fatal("icon drawn while locked")
304 }
305 d.drain()
306 d.in(ports.PointerMotion{X: 100, Y: 100, Time: time.Second})
307 d.settle(t)
308 for _, v := range d.drain() {
309 switch v.(type) {
310 case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop:
311 t.Fatalf("data drag alive while locked: %#v", v)
312 }
313 }
314 // After the unlock, the old drag is gone: plain pointer events.
315 unlocked := ports.SecurityState{Generation: 2}
316 d.epoch.Store(unlocked)
317 d.client <- ports.SessionLockChanged{State: unlocked}
318 d.settle(t)
319 d.drain()
320 d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 9)
321 d.settle(t)
322 got := dataDragCommands(d.drain())
323 wantCommands(t, got, ports.PointerFocus{ID: 1, X: 21, Y: 30}, ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 9 * time.Millisecond})
324}
325
326func TestDataDragTargetUnmapped(t *testing.T) {
327 d := startDnd(t)
328 d.start(t, 0)
329 d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
330 d.settle(t)
331 d.drain()
332 d.client <- ports.WindowUnmapped{ID: 2}
333 d.settle(t)
334 cmds := dataDragCommands(d.drain())
335 if len(cmds) == 0 || cmds[0] != (ports.DataDragFocus{Time: 3 * time.Millisecond}) {
336 t.Fatalf("commands %#v", cmds)
337 }
338}
339
340func TestDataDragIconIsLastWindowAtCursor(t *testing.T) {
341 d := startDnd(t)
342 d.start(t, dndIcon)
343 // Not drawn until wayland measured it.
344 if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
345 t.Fatal("icon drawn before its size")
346 }
347 cx, cy := d.one.X+20, d.one.Y+30
348 d.client <- ports.DataDragIcon{ID: dndIcon, X: -4, Y: -6, W: 32, H: 24}
349 set := d.settle(t)
350 s := dragScene(set, "A")
351 last := s.Windows[len(s.Windows)-1]
352 want := ports.SceneWindow{ID: dndIcon, Rect: ports.Rect{X: cx - 4, Y: cy - 6, W: 32, H: 24}, Popup: true, OverLayers: true}
353 if last != want {
354 t.Fatalf("last window %+v, want %+v", last, want)
355 }
356 // It follows the cursor; the scene changes.
357 d.moveTo(float64(cx+100), float64(cy+50), 2)
358 moved := dragScene(d.settle(t), "A")
359 if moved.SameAs(s) || moved.Seq == s.Seq {
360 t.Fatal("scene did not change with the icon")
361 }
362 if got := moved.Windows[len(moved.Windows)-1]; got.Rect.X != cx+96 || got.Rect.Y != cy+44 || got.ID != dndIcon {
363 t.Fatalf("moved icon %+v", got)
364 }
365 // Under the icon, the surface below is still the target.
366 d.drain()
367 d.moveTo(float64(cx+101), float64(cy+50), 3)
368 d.settle(t)
369 for _, v := range d.drain() {
370 if f, ok := v.(ports.DataDragFocus); ok && f.ID == dndIcon {
371 t.Fatalf("icon was targeted: %#v", f)
372 }
373 }
374 // A commit that changes the offset moves it; a zero size hides it.
375 d.client <- ports.DataDragIcon{ID: dndIcon, X: 0, Y: 0, W: 32, H: 24}
376 if got := dragScene(d.settle(t), "A"); got.Windows[len(got.Windows)-1].Rect.X != cx+101 {
377 t.Fatalf("offset ignored: %+v", got.Windows[len(got.Windows)-1])
378 }
379 d.client <- ports.DataDragIcon{ID: dndIcon}
380 if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
381 t.Fatal("hidden icon drawn")
382 }
383 // Geometry of another surface is not the icon's.
384 d.client <- ports.DataDragIcon{ID: dndIcon + 1, W: 10, H: 10}
385 if _, ok := windowIn(d.settle(t), "A", dndIcon+1); ok {
386 t.Fatal("foreign icon drawn")
387 }
388 // The drop removes it.
389 d.client <- ports.DataDragIcon{ID: dndIcon, W: 32, H: 24}
390 d.settle(t)
391 d.in(ports.PointerButton{Button: 0x110})
392 set = d.settle(t)
393 if _, ok := windowIn(set, "A", dndIcon); ok {
394 t.Fatal("icon drawn after the drop")
395 }
396}
397
398func TestDataDragIconOnScreenUnderCursor(t *testing.T) {
399 other := ports.OutputInfo{Name: "B", Width: 800, Height: 600}
400 d := startDnd(t, dragOut, other)
401 d.start(t, dndIcon)
402 d.client <- ports.DataDragIcon{ID: dndIcon, X: 2, Y: 3, W: 10, H: 10}
403 d.moveTo(1000+100, 50, 2)
404 set := d.settle(t)
405 if _, ok := windowIn(set, "A", dndIcon); ok {
406 t.Fatal("icon on the other screen")
407 }
408 w, ok := windowIn(set, "B", dndIcon)
409 if !ok {
410 t.Fatal("no icon on B")
411 }
412 if w.Rect != (ports.Rect{X: 102, Y: 53, W: 10, H: 10}) {
413 t.Fatalf("icon %+v", w.Rect)
414 }
415}
416
417func TestDataDragNoIconIgnoresIconEvents(t *testing.T) {
418 d := startDnd(t)
419 d.start(t, 0)
420 d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10}
421 if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
422 t.Fatal("icon drawn for a drag without one")
423 }
424 // Nor with no drag at all.
425 d.in(ports.PointerButton{Button: 0x110})
426 d.settle(t)
427 d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10}
428 if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
429 t.Fatal("icon drawn without a drag")
430 }
431}
432
433func TestDataDragFocusScreenFollowsPointer(t *testing.T) {
434 r := startDragRig(t, nil, dragOut, ports.OutputInfo{Name: "B", Width: 1000, Height: 600})
435 r.client <- ports.WindowMapped{ID: 1}
436 r.settle(t)
437 r.moveTo(1500, 300, 1)
438 r.settle(t)
439 r.client <- ports.WindowMapped{ID: 3}
440 set := r.settle(t)
441 one := dragRect(t, set, "A", 1)
442 r.moveTo(float64(one.X+20), float64(one.Y+30), 2)
443 set = r.settle(t)
444 if w, _ := windowIn(set, "A", 1); !w.Focused {
445 t.Fatal("1 not focused")
446 }
447 r.in(ports.PointerButton{Button: 0x110, Pressed: true})
448 r.settle(t)
449 r.client <- ports.DataDragStarted{Button: 0x110}
450 r.moveTo(1500, 300, 3)
451 set = r.settle(t)
452 if w, _ := windowIn(set, "B", 3); !w.Focused {
453 t.Fatal("the screen under the drag did not take the focus")
454 }
455 three := dragRect(t, set, "B", 3)
456 var focus []ports.DataDragFocus
457 for _, v := range r.drain() {
458 if f, ok := v.(ports.DataDragFocus); ok {
459 focus = append(focus, f)
460 }
461 }
462 if n := len(focus); n != 2 || focus[1].ID != 3 || focus[1].X != float64(1500-1000-three.X) {
463 t.Fatalf("focus %+v", focus)
464 }
465}
466
467func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) {
468 d := startDnd(t)
469 d.start(t, 0)
470 d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
471 d.settle(t)
472 d.drain()
473 // cmd+2 shows an empty workspace under the still cursor: the target
474 // leaves, and the release drops on nothing.
475 d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true})
476 d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper})
477 d.settle(t)
478 got := dataDragCommands(d.drain())
479 if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) {
480 t.Fatalf("no leave after the switch: %#v", got)
481 }
482 d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond})
483 d.settle(t)
484 got = dataDragCommands(d.drain())
485 if len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
486 t.Fatalf("commands %#v", got)
487 }
488 for _, v := range got {
489 if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 {
490 t.Fatalf("hidden window targeted: %#v", got)
491 }
492 }
493}
494
495func TestDataDragTargetMovedUnderStillCursor(t *testing.T) {
496 d := startDnd(t)
497 d.start(t, 0)
498 // Over 2, focused by a click-free keyboard move, then cmd+f maximizes
499 // its column: still the target under the cursor, at another point.
500 d.moveTo(float64(d.two.X+d.two.W-20), float64(d.two.Y+30), 2)
501 d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper, Pressed: true})
502 d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper})
503 d.settle(t)
504 d.drain()
505 d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true})
506 d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper})
507 set := d.settle(t)
508 wide := dragRect(t, set, dragOut.Name, 2)
509 if wide.X == d.two.X {
510 t.Fatalf("the column did not move: %v", wide)
511 }
512 x, y := float64(d.two.X+d.two.W-20-wide.X), float64(d.two.Y+30-wide.Y)
513 got := dataDragCommands(d.drain())
514 if !slices.Contains(got, ports.ClientCommand(ports.DataDragMotion{ID: 2, X: x, Y: y, Time: 2 * time.Millisecond})) {
515 t.Fatalf("no motion to %v,%v: %#v", x, y, got)
516 }
517}
518
519func TestDataDragDropsOnItsOwnButton(t *testing.T) {
520 d := startDnd(t)
521 // Right pressed after left: the drag is left's, the grab's.
522 d.in(ports.PointerButton{Button: 0x111, Pressed: true})
523 d.settle(t)
524 d.drain()
525 d.start(t, 0)
526 d.in(ports.PointerButton{Button: 0x111})
527 d.settle(t)
528 if got := dataDragCommands(d.drain()); slices.Contains(got, ports.ClientCommand(ports.DataDragDrop{})) {
529 t.Fatalf("right's release dropped: %#v", got)
530 }
531 d.in(ports.PointerButton{Button: 0x110})
532 d.settle(t)
533 if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
534 t.Fatalf("left's release did not drop: %#v", got)
535 }
536}
537
538func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) {
539 d := startDnd(t)
540 d.start(t, 0)
541 d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true})
542 d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true})
543 d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper})
544 d.settle(t)
545 d.drain()
546 d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true})
547 d.settle(t)
548 got := dataDragCommands(d.drain())
549 if len(got) == 0 || got[0] != (ports.DataDragCancel{}) {
550 t.Fatalf("Escape did not cancel the drag: %#v", got)
551 }
552 d.in(ports.PointerButton{Button: 0x110})
553 d.settle(t)
554 if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) {
555 t.Fatal("dropped after Escape")
556 }
557}
558
559func TestDataDragIgnoresCompositorSwipes(t *testing.T) {
560 d := startDnd(t)
561 d.start(t, 0)
562 before := d.settle(t)
563 // Flicks that page columns and workspaces, as navigation would.
564 for _, fingers := range []int{3, 4} {
565 at := time.Second
566 d.in(ports.SwipeBegin{Fingers: fingers, Time: at})
567 for range 10 {
568 at += 8 * time.Millisecond
569 d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at})
570 }
571 d.in(ports.SwipeEnd{Time: at})
572 }
573 after := d.settle(t)
574 if !slices.Equal(before[0].Windows, after[0].Windows) {
575 t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows)
576 }
577}
578
579func TestDataDragRehitsAfterInputRegionChange(t *testing.T) {
580 d := startDnd(t)
581 d.start(t, 0)
582 // The scene is settled and the cursor never moves after drag start.
583 d.client <- ports.InputRegionChanged{ID: 1}
584 d.settle(t)
585 wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{Time: time.Millisecond})
586 d.client <- ports.InputRegionChanged{ID: 1, All: true}
587 d.settle(t)
588 wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{ID: 1, X: 20, Y: 30, Time: time.Millisecond})
589}