dnd_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"errors"
6	"sync/atomic"
7	"testing"
8	"time"
9
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/neferwl/internal/adapters/logging"
12	"github.com/bnema/neferwl/internal/adapters/sessionsecurity"
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/bnema/wlturbo"
15	"golang.org/x/sys/unix"
16)
17
18const (
19	actCopy = uint32(wayland.DataDeviceManagerDndActionCopy)
20	actMove = uint32(wayland.DataDeviceManagerDndActionMove)
21	actAsk  = uint32(wayland.DataDeviceManagerDndActionAsk)
22)
23
24// dndEvent is one data device, offer or source event a test client saw.
25type dndEvent struct {
26	name string
27	mime string
28	x, y float64
29	n    uint32
30}
31
32type dndDeviceEvents struct {
33	wlturbo.BaseProxy
34	c   *wlturbo.Display
35	log chan dndEvent
36	// offer is the id of the latest data_offer.
37	offer atomic.Uint32
38}
39
40func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {
41	switch e.Opcode {
42	case uint16(wayland.DataDeviceEventDataOffer):
43		o := &dndOfferEvents{log: p.log}
44		o.SetID(e.Uint32())
45		p.offer.Store(o.ID())
46		registerWireProxy(p.c, o)
47	case uint16(wayland.DataDeviceEventEnter):
48		serial, _ := e.Uint32(), e.Uint32()
49		x, y := e.Fixed().Float64(), e.Fixed().Float64()
50		p.log <- dndEvent{name: "enter", x: x, y: y, n: e.Uint32() + serial*0}
51	case uint16(wayland.DataDeviceEventLeave):
52		p.log <- dndEvent{name: "leave"}
53	case uint16(wayland.DataDeviceEventMotion):
54		_ = e.Uint32()
55		p.log <- dndEvent{name: "motion", x: e.Fixed().Float64(), y: e.Fixed().Float64()}
56	case uint16(wayland.DataDeviceEventDrop):
57		p.log <- dndEvent{name: "drop"}
58	}
59}
60
61type dndOfferEvents struct {
62	wlturbo.BaseProxy
63	log chan dndEvent
64}
65
66func (p *dndOfferEvents) Dispatch(e *wlturbo.Event) {
67	switch e.Opcode {
68	case uint16(wayland.DataOfferEventOffer):
69		p.log <- dndEvent{name: "offer", mime: e.String()}
70	case uint16(wayland.DataOfferEventSourceActions):
71		p.log <- dndEvent{name: "source_actions", n: e.Uint32()}
72	case uint16(wayland.DataOfferEventAction):
73		p.log <- dndEvent{name: "offer_action", n: e.Uint32()}
74	}
75}
76
77type dndSourceEvents struct {
78	wlturbo.BaseProxy
79	log chan dndEvent
80}
81
82func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) {
83	switch e.Opcode {
84	case uint16(wayland.DataSourceEventTarget):
85		p.log <- dndEvent{name: "target", mime: e.String()}
86	case uint16(wayland.DataSourceEventSend):
87		mime := e.String()
88		fd := int(e.Fd())
89		_, _ = unix.Write(fd, []byte("dnd data"))
90		_ = unix.Close(fd)
91		p.log <- dndEvent{name: "send", mime: mime}
92	case uint16(wayland.DataSourceEventCancelled):
93		p.log <- dndEvent{name: "cancelled"}
94	case uint16(wayland.DataSourceEventDndDropPerformed):
95		p.log <- dndEvent{name: "drop_performed"}
96	case uint16(wayland.DataSourceEventDndFinished):
97		p.log <- dndEvent{name: "finished"}
98	case uint16(wayland.DataSourceEventAction):
99		p.log <- dndEvent{name: "action", n: e.Uint32()}
100	}
101}
102
103// dndClient is a client with a toplevel, a pointer and a data device.
104type dndClient struct {
105	keys    chan uint32
106	c       *wlturbo.Display
107	manager uint32
108	device  *dndDeviceEvents
109	serials chan uint32
110	comp    uint32
111	shm     uint32
112	surf    uint32
113	window  ports.WindowID
114	log     chan dndEvent
115}
116
117// keySerials records the serial of each key event.
118type keySerials struct {
119	wlturbo.BaseProxy
120	serials chan uint32
121}
122
123func (p *keySerials) Dispatch(e *wlturbo.Event) {
124	if e.Opcode == uint16(wayland.KeyboardEventKey) {
125		p.serials <- e.Uint32()
126	}
127}
128
129func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent) *dndClient {
130	t.Helper()
131	return newDndClientAt(t, s, dir, events, 3)
132}
133
134// newDndClientAt is newDndClient with wl_data_device_manager at version.
135func newDndClientAt(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent, version uint32) *dndClient {
136	t.Helper()
137	c := protocolClient(t, s, dir)
138	d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8), keys: make(chan uint32, 8)}
139	kb := &keySerials{serials: d.keys}
140	seat := bindVersion(t, c, "wl_seat", 8)
141	registerProtocol(t, c, seat)
142	ptr := &pressSerials{serials: d.serials}
143	ptr.SetID(c.AllocateID())
144	registerWireProxy(c, ptr)
145	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID())
146	kb.SetID(c.AllocateID())
147	registerWireProxy(c, kb)
148	requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, kb.ID())
149	d.manager = bindVersion(t, c, "wl_data_device_manager", version)
150	d.device = &dndDeviceEvents{c: c, log: d.log}
151	d.device.SetID(c.AllocateID())
152	registerWireProxy(c, d.device)
153	requestProtocol(t, c, d.manager, wayland.DataDeviceManagerRequestGetDataDevice, d.device.ID(), seat)
154	d.comp = bindVersion(t, c, "wl_compositor", 5)
155	d.shm = bindProtocol(t, c, "wl_shm")
156	registerProtocol(t, c, d.shm)
157	w, surf, _ := surfaceMapperAt(t, c, events, 5)()
158	d.window, d.surf = w.ID, surf
159	return d
160}
161
162// lastOffer is the id of the latest offer the client got.
163func (d *dndClient) lastOffer(t *testing.T) uint32 {
164	t.Helper()
165	id := d.device.offer.Load()
166	if id == 0 {
167		t.Fatal("no offer")
168	}
169	return id
170}
171
172// next returns the next event the client sees.
173func (d *dndClient) next(t *testing.T) dndEvent {
174	t.Helper()
175	deadline := time.After(2 * time.Second)
176	for {
177		if err := d.c.Roundtrip(); err != nil {
178			t.Fatal(err)
179		}
180		select {
181		case ev := <-d.log:
182			return ev
183		case <-deadline:
184			t.Fatal("no event")
185		case <-time.After(5 * time.Millisecond):
186		}
187	}
188}
189
190// expect waits for the next event and checks its name.
191func (d *dndClient) expect(t *testing.T, name string) dndEvent {
192	t.Helper()
193	ev := d.next(t)
194	if ev.name != name {
195		t.Fatalf("event %+v, want %s", ev, name)
196	}
197	return ev
198}
199
200// none checks that no event arrives.
201func (d *dndClient) none(t *testing.T, commands chan<- ports.ClientCommand) {
202	t.Helper()
203	// Observing a key sent through the command queue proves earlier
204	// compositor commands ran. The roundtrip then drains wire events.
205	d.key(t, commands)
206	if err := d.c.Roundtrip(); err != nil {
207		t.Fatal(err)
208	}
209	select {
210	case ev := <-d.log:
211		t.Fatalf("unexpected event %+v", ev)
212	default:
213	}
214}
215
216func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 {
217	t.Helper()
218	src := &dndSourceEvents{log: d.log}
219	src.SetID(d.c.AllocateID())
220	registerWireProxy(d.c, src)
221	requestProtocol(t, d.c, d.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID())
222	for _, m := range mimes {
223		requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestOffer, m)
224	}
225	if actions != 0 {
226		requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestSetActions, actions)
227	}
228	return src.ID()
229}
230
231// press makes the client's window press a button and returns the serial.
232func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 {
233	t.Helper()
234	commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1}
235	commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Grab: true, Time: time.Millisecond}
236	deadline := time.After(2 * time.Second)
237	for {
238		if err := d.c.Roundtrip(); err != nil {
239			t.Fatal(err)
240		}
241		select {
242		case serial := <-d.serials:
243			return serial
244		case <-deadline:
245			t.Fatal("no button")
246		case <-time.After(5 * time.Millisecond):
247		}
248	}
249}
250
251// press2 returns the next pointer button serial.
252func (d *dndClient) press2(t *testing.T) uint32 {
253	t.Helper()
254	deadline := time.After(2 * time.Second)
255	for {
256		if err := d.c.Roundtrip(); err != nil {
257			t.Fatal(err)
258		}
259		select {
260		case serial := <-d.serials:
261			return serial
262		case <-deadline:
263			t.Fatal("no button")
264		case <-time.After(5 * time.Millisecond):
265		}
266	}
267}
268
269// release releases the button press took and waits for the client to see it.
270func (d *dndClient) release(t *testing.T, commands chan<- ports.ClientCommand) {
271	t.Helper()
272	commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Time: 2 * time.Millisecond}
273	d.press2(t)
274}
275
276// key presses a key on the client's focused window and returns the serial.
277func (d *dndClient) key(t *testing.T, commands chan<- ports.ClientCommand) uint32 {
278	t.Helper()
279	commands <- ports.FocusWindow{ID: d.window}
280	commands <- ports.ForwardKey{ID: d.window, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 3 * time.Millisecond}}
281	deadline := time.After(2 * time.Second)
282	for {
283		if err := d.c.Roundtrip(); err != nil {
284			t.Fatal(err)
285		}
286		select {
287		case serial := <-d.keys:
288			return serial
289		case <-deadline:
290			t.Fatal("no key")
291		case <-time.After(5 * time.Millisecond):
292		}
293	}
294}
295
296// events returns what the client saw up to and including name.
297func (d *dndClient) until(t *testing.T, name string) []dndEvent {
298	t.Helper()
299	var got []dndEvent
300	for {
301		ev := d.next(t)
302		got = append(got, ev)
303		if ev.name == name {
304			return got
305		}
306	}
307}
308
309func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) {
310	t.Helper()
311	d.startDragFrom(t, src, d.surf, icon, serial)
312}
313
314// startDragFrom is startDrag with an origin surface.
315func (d *dndClient) startDragFrom(t *testing.T, src, origin, icon, serial uint32) {
316	t.Helper()
317	requestProtocol(t, d.c, d.device.ID(), wayland.DataDeviceRequestStartDrag, src, origin, icon, serial)
318	if err := d.c.Roundtrip(); err != nil {
319		t.Fatal(err)
320	}
321}
322
323// buffer makes a w×h ARGB shm buffer.
324func (d *dndClient) buffer(t *testing.T, w, h int) uint32 {
325	t.Helper()
326	fd, err := unix.MemfdCreate("dnd-icon", 0)
327	if err != nil {
328		t.Fatal(err)
329	}
330	defer unix.Close(fd)
331	if err := unix.Ftruncate(fd, int64(w*h*4)); err != nil {
332		t.Fatal(err)
333	}
334	pool, buf := d.c.AllocateID(), d.c.AllocateID()
335	registerProtocol(t, d.c, pool)
336	registerProtocol(t, d.c, buf)
337	if err := wireRequest(d.c, d.shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil {
338		t.Fatal(err)
339	}
340	requestProtocol(t, d.c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
341	return buf
342}
343
344func (d *dndClient) surfaceNew(t *testing.T) uint32 {
345	t.Helper()
346	id := d.c.AllocateID()
347	requestProtocol(t, d.c, d.comp, wayland.CompositorRequestCreateSurface, id)
348	registerProtocol(t, d.c, id)
349	return id
350}
351
352// dndNextEvent returns the next client event of type T, skipping others.
353func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T {
354	t.Helper()
355	deadline := time.After(2 * time.Second)
356	for {
357		select {
358		case ev := <-events:
359			if v, ok := ev.(T); ok {
360				return v
361			}
362		case <-deadline:
363			t.Fatalf("no %T event", *new(T))
364		}
365	}
366}
367
368// dndNoEvent checks that no client event of type T is pending.
369func dndNoEvent[T ports.ClientEvent](t *testing.T, s *Server, events <-chan ports.ClientEvent) {
370	t.Helper()
371	// Callers have observed completion of the triggering request/command.
372	// Put a marker behind its emitted events, including the forwarding queue.
373	marker := ports.WindowResized{ID: ^ports.WindowID(0)}
374	if !s.display.Do(func() { s.emit(marker) }) {
375		t.Fatal("display stopped")
376	}
377	deadline := time.After(2 * time.Second)
378	for {
379		select {
380		case ev := <-events:
381			if v, ok := ev.(ports.WindowResized); ok && v.ID == marker.ID {
382				return
383			}
384			if _, ok := ev.(T); ok {
385				t.Fatalf("unexpected %T: %+v", ev, ev)
386			}
387		case <-deadline:
388			t.Fatal("no event barrier")
389		}
390	}
391}
392
393// dndPair is two clients with a drag started from a on a source.
394type dndPair struct {
395	s        *Server
396	events   chan ports.ClientEvent
397	commands chan ports.ClientCommand
398	a, b     *dndClient
399}
400
401func newDndPair(t *testing.T) *dndPair {
402	t.Helper()
403	s, events, commands, dir := keyboardServer(t)
404	p := &dndPair{s: s, events: events, commands: commands}
405	p.a = newDndClient(t, s, dir, events)
406	p.b = newDndClient(t, s, dir, events)
407	return p
408}
409
410// start begins a drag of a source offering text/plain, copy and move.
411func (p *dndPair) start(t *testing.T, icon uint32) uint32 {
412	t.Helper()
413	src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
414	p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
415	return src
416}
417
418// enter makes b the drag target and returns the offer id.
419func (p *dndPair) enter(t *testing.T) uint32 {
420	t.Helper()
421	p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond}
422	for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} {
423		if ev := p.b.next(t); ev != want {
424			t.Fatalf("event %+v, want %+v", ev, want)
425		}
426	}
427	ev := p.b.expect(t, "enter")
428	if ev.x != 5 || ev.y != 6 || ev.n == 0 {
429		t.Fatalf("enter %+v", ev)
430	}
431	return ev.n
432}
433
434// A1: a drag from a to b delivers enter, motion and drop with the offer;
435// the action is negotiated; the data reaches b; a is told every step.
436func TestDataDragBetweenClients(t *testing.T) {
437	p := newDndPair(t)
438	p.start(t, 0)
439	if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 {
440		t.Fatalf("started %+v", v)
441	}
442	offer := p.enter(t)
443
444	p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond}
445	if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 {
446		t.Fatalf("motion %+v", ev)
447	}
448
449	// b accepts and prefers move: both sides learn the action.
450	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
451	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
452	if ev := p.a.expect(t, "target"); ev.mime != "text/plain" {
453		t.Fatalf("target %+v", ev)
454	}
455	if ev := p.a.expect(t, "action"); ev.n != actMove {
456		t.Fatalf("action %+v", ev)
457	}
458	if ev := p.b.expect(t, "offer_action"); ev.n != actMove {
459		t.Fatalf("offer action %+v", ev)
460	}
461
462	// Finishing before the drop is a protocol error, so only after it.
463	p.commands <- ports.DataDragDrop{}
464	p.b.expect(t, "drop")
465	p.b.expect(t, "leave")
466	p.a.expect(t, "drop_performed")
467
468	var fds [2]int
469	if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
470		t.Fatal(err)
471	}
472	defer unix.Close(fds[0])
473	if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil {
474		t.Fatal(err)
475	}
476	_ = unix.Close(fds[1])
477	if err := p.b.c.Roundtrip(); err != nil {
478		t.Fatal(err)
479	}
480	if ev := p.a.expect(t, "send"); ev.mime != "text/plain" {
481		t.Fatalf("send %+v", ev)
482	}
483	buf := make([]byte, 16)
484	n, err := unix.Read(fds[0], buf)
485	for errors.Is(err, unix.EINTR) {
486		n, err = unix.Read(fds[0], buf)
487	}
488	if err != nil || string(buf[:n]) != "dnd data" {
489		t.Fatalf("received %q, %v", buf[:max(n, 0)], err)
490	}
491
492	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
493	p.a.expect(t, "finished")
494	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
495	p.a.none(t, p.commands)
496	p.b.none(t, p.commands)
497	// core ended the drag: wayland does not echo it.
498	dndNoEvent[ports.DataDragEnded](t, p.s, p.events)
499}
500
501// The action follows wlroots: the preferred one if both allow it, else the
502// first of copy, move, ask they share, else none.
503func TestChooseAction(t *testing.T) {
504	for _, tc := range []struct{ avail, preferred, want uint32 }{
505		{actCopy | actMove, actMove, actMove},
506		{actCopy | actMove, actAsk, actCopy},
507		{actMove | actAsk, 0, actMove},
508		{actAsk, 0, actAsk},
509		{0, actCopy, 0},
510	} {
511		if got := chooseAction(tc.avail, tc.preferred); got != tc.want {
512			t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want)
513		}
514	}
515}
516
517// A target and a source sharing no action leave the drop refused.
518func TestDataDragNoCommonAction(t *testing.T) {
519	p := newDndPair(t)
520	src := p.a.source(t, actCopy, "text/plain")
521	p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
522	p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
523	p.b.expect(t, "offer")
524	p.b.expect(t, "source_actions")
525	offer := p.b.expect(t, "enter").n
526	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
527	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove)
528	p.a.expect(t, "target")
529	p.commands <- ports.DataDragDrop{}
530	p.b.expect(t, "leave")
531	// The refused target no longer accepts.
532	if ev := p.a.expect(t, "target"); ev.mime != "" {
533		t.Fatalf("target %+v", ev)
534	}
535	p.a.expect(t, "cancelled")
536}
537
538// A2: a release over nothing, or over a target that accepted nothing,
539// cancels the source; the target gets leave and no drop.
540func TestDataDragRefusedDrop(t *testing.T) {
541	t.Run("nothing", func(t *testing.T) {
542		p := newDndPair(t)
543		p.start(t, 0)
544		p.commands <- ports.DataDragDrop{}
545		p.a.expect(t, "cancelled")
546		p.b.none(t, p.commands)
547	})
548	t.Run("not accepted", func(t *testing.T) {
549		p := newDndPair(t)
550		p.start(t, 0)
551		p.enter(t)
552		p.commands <- ports.DataDragDrop{}
553		p.b.expect(t, "leave")
554		p.a.expect(t, "cancelled")
555		p.b.none(t, p.commands)
556	})
557	t.Run("escape", func(t *testing.T) {
558		p := newDndPair(t)
559		p.start(t, 0)
560		p.enter(t)
561		p.commands <- ports.DataDragCancel{}
562		p.b.expect(t, "leave")
563		p.a.expect(t, "cancelled")
564		dndNoEvent[ports.DataDragEnded](t, p.s, p.events)
565	})
566}
567
568// Moving to another surface leaves the first and enters the second; a
569// surface of nobody (ID 0) leaves.
570func TestDataDragLeavesOnFocusChange(t *testing.T) {
571	p := newDndPair(t)
572	p.start(t, 0)
573	p.enter(t)
574	p.commands <- ports.DataDragFocus{}
575	p.b.expect(t, "leave")
576	p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3}
577	for range 3 {
578		p.b.next(t)
579	}
580	if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 {
581		t.Fatalf("enter %+v", ev)
582	}
583}
584
585// A4: the source dying mid-drag ends it: the target gets leave and core a
586// DataDragEnded; later commands change nothing.
587func TestDataDragSourceDestroyed(t *testing.T) {
588	p := newDndPair(t)
589	src := p.start(t, 0)
590	p.enter(t)
591	requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy)
592	if err := p.a.c.Roundtrip(); err != nil {
593		t.Fatal(err)
594	}
595	p.b.expect(t, "leave")
596	dndNextEvent[ports.DataDragEnded](t, p.events)
597	p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9}
598	p.commands <- ports.DataDragDrop{}
599	p.b.none(t, p.commands)
600}
601
602// A drag whose origin client dies ends the same way.
603func TestDataDragOriginDies(t *testing.T) {
604	p := newDndPair(t)
605	p.start(t, 0)
606	p.enter(t)
607	_ = p.a.c.Close()
608	p.b.expect(t, "leave")
609	dndNextEvent[ports.DataDragEnded](t, p.events)
610}
611
612// A6: a start_drag without the current press of the origin client is
613// refused: the source is cancelled and core hears nothing.
614func TestDataDragInvalidStart(t *testing.T) {
615	t.Run("stale serial", func(t *testing.T) {
616		p := newDndPair(t)
617		serial := p.a.press(t, p.commands)
618		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1)
619		p.a.expect(t, "cancelled")
620		dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
621	})
622	t.Run("other client's press", func(t *testing.T) {
623		p := newDndPair(t)
624		p.a.press(t, p.commands)
625		serial := p.b.press(t, p.commands)
626		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
627		p.a.expect(t, "cancelled")
628		dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
629	})
630	t.Run("key serial", func(t *testing.T) {
631		p := newDndPair(t)
632		p.a.press(t, p.commands)
633		p.a.release(t, p.commands)
634		serial := p.a.key(t, p.commands)
635		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
636		p.a.expect(t, "cancelled")
637		dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
638	})
639	t.Run("serial after the release", func(t *testing.T) {
640		p := newDndPair(t)
641		serial := p.a.press(t, p.commands)
642		p.a.release(t, p.commands)
643		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
644		p.a.expect(t, "cancelled")
645		dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
646	})
647	t.Run("other surface of the client", func(t *testing.T) {
648		p := newDndPair(t)
649		serial := p.a.press(t, p.commands)
650		other := p.a.surfaceNew(t)
651		p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial)
652		p.a.expect(t, "cancelled")
653		dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
654	})
655	t.Run("press of the first button", func(t *testing.T) {
656		// A second button does not make a new grab: only the first serial.
657		p := newDndPair(t)
658		first := p.a.press(t, p.commands)
659		p.commands <- ports.PointerButtonTo{ID: p.a.window, Button: 0x111, Pressed: true, Time: 2 * time.Millisecond}
660		second := p.a.press2(t)
661		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second)
662		p.a.expect(t, "cancelled")
663		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first)
664		// Core is told the grab's button: its release drops.
665		if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 {
666			t.Fatalf("drag button %#x, want the grab's", ev.Button)
667		}
668	})
669	t.Run("grab release consumed by core", func(t *testing.T) {
670		// A window drag consumes the release: the next grab press
671		// still makes the serial a drag can name.
672		p := newDndPair(t)
673		stale := p.a.press(t, p.commands)
674		serial := p.a.press(t, p.commands)
675		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale)
676		p.a.expect(t, "cancelled")
677		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
678		dndNextEvent[ports.DataDragStarted](t, p.events)
679	})
680	t.Run("grab release of a gone window", func(t *testing.T) {
681		p := newDndPair(t)
682		serial := p.a.press(t, p.commands)
683		p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond}
684		p.a.key(t, p.commands) // the release is applied once the key is seen
685		p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
686		p.a.expect(t, "cancelled")
687		dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
688	})
689	t.Run("used source", func(t *testing.T) {
690		p := newDndPair(t)
691		src := p.start(t, 0)
692		p.commands <- ports.DataDragCancel{}
693		p.a.expect(t, "cancelled")
694		p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
695		p.a.expect(t, "cancelled")
696	})
697	t.Run("drag in progress", func(t *testing.T) {
698		p := newDndPair(t)
699		p.start(t, 0)
700		dndNextEvent[ports.DataDragStarted](t, p.events)
701		other := p.a.source(t, actCopy, "text/plain")
702		p.a.startDrag(t, other, 0, p.a.press(t, p.commands))
703		p.a.expect(t, "cancelled")
704		dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
705	})
706	t.Run("icon with another role", func(t *testing.T) {
707		p := newDndPair(t)
708		serial := p.a.press(t, p.commands)
709		src := p.a.source(t, actCopy, "text/plain")
710		// The origin toplevel is a role already.
711		requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial)
712		expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole))
713		dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
714	})
715}
716
717// A nil source is a drag internal to the origin client: only its own
718// surfaces see it, without offer.
719func TestDataDragWithoutSource(t *testing.T) {
720	p := newDndPair(t)
721	p.a.startDrag(t, 0, 0, p.a.press(t, p.commands))
722	dndNextEvent[ports.DataDragStarted](t, p.events)
723	p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
724	p.b.none(t, p.commands)
725	p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5}
726	if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 {
727		t.Fatalf("enter %+v", ev)
728	}
729	p.commands <- ports.DataDragDrop{}
730	p.a.expect(t, "drop")
731}
732
733// Offer and source requests are validated.
734func TestDataDragProtocolErrors(t *testing.T) {
735	t.Run("finish before drop", func(t *testing.T) {
736		p := newDndPair(t)
737		p.start(t, 0)
738		offer := p.enter(t)
739		requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
740		expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish))
741	})
742	t.Run("offer action mask", func(t *testing.T) {
743		p := newDndPair(t)
744		p.start(t, 0)
745		offer := p.enter(t)
746		requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0))
747		expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask))
748	})
749	t.Run("offer preferred action", func(t *testing.T) {
750		p := newDndPair(t)
751		p.start(t, 0)
752		offer := p.enter(t)
753		requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove)
754		expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction))
755	})
756	t.Run("source action mask", func(t *testing.T) {
757		p := newDndPair(t)
758		src := p.a.source(t, 0, "text/plain")
759		requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16))
760		expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask))
761	})
762	t.Run("selection with actions", func(t *testing.T) {
763		p := newDndPair(t)
764		src := p.a.source(t, actCopy, "text/plain")
765		requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0))
766		expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
767	})
768	t.Run("actions after the drag started", func(t *testing.T) {
769		p := newDndPair(t)
770		src := p.start(t, 0)
771		requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy)
772		expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
773	})
774}
775
776// A destroyed offer before finish cancels the source.
777func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) {
778	p := newDndPair(t)
779	p.start(t, 0)
780	offer := p.enter(t)
781	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
782	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
783	p.a.expect(t, "target")
784	p.a.expect(t, "action")
785	p.commands <- ports.DataDragDrop{}
786	p.b.expect(t, "offer_action")
787	p.b.expect(t, "drop")
788	p.b.expect(t, "leave")
789	p.a.expect(t, "drop_performed")
790	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
791	p.a.expect(t, "cancelled")
792}
793
794// iconContent reports whether the server still holds content for id.
795func iconContent(t *testing.T, s *Server, id ports.WindowID) bool {
796	t.Helper()
797	var ok bool
798	if !s.display.Do(func() { _, ok = s.latest[id] }) {
799		t.Fatal("display stopped")
800	}
801	return ok
802}
803
804// The icon is a surface with its own window ID: its size and the
805// offsets of its commits reach core, its content is published, and both go
806// when the drag ends.
807func TestDataDragIcon(t *testing.T) {
808	p := newDndPair(t)
809	icon := p.a.surfaceNew(t)
810	buf := p.a.buffer(t, 8, 6)
811	// An offset on an ordinary surface is not used.
812	requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5))
813	requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit)
814
815	src := p.a.source(t, actCopy, "text/plain")
816	p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
817	started := dndNextEvent[ports.DataDragStarted](t, p.events)
818	if started.Icon == 0 {
819		t.Fatal("no icon ID")
820	}
821	id := started.Icon
822	if iconContent(t, p.s, id) {
823		t.Fatal("content before the first commit")
824	}
825
826	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
827	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4))
828	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
829	if err := p.a.c.Roundtrip(); err != nil {
830		t.Fatal(err)
831	}
832	if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) {
833		t.Fatalf("icon %+v", got)
834	}
835	if !iconContent(t, p.s, id) {
836		t.Fatal("no icon content")
837	}
838
839	// Offsets add up from commit to commit.
840	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2))
841	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
842	if err := p.a.c.Roundtrip(); err != nil {
843		t.Fatal(err)
844	}
845	if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) {
846		t.Fatalf("icon %+v", got)
847	}
848	// The icon never takes input.
849	dndNoEvent[ports.InputRegionChanged](t, p.s, p.events)
850
851	p.commands <- ports.DataDragCancel{}
852	p.a.expect(t, "cancelled")
853	if iconContent(t, p.s, id) {
854		t.Fatal("icon content kept after the drag")
855	}
856}
857
858// An icon committed before start_drag shows at once.
859func TestDataDragIconCommittedEarly(t *testing.T) {
860	p := newDndPair(t)
861	icon := p.a.surfaceNew(t)
862	buf := p.a.buffer(t, 4, 4)
863	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
864	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
865	p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
866	id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
867	if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) {
868		t.Fatalf("icon %+v", got)
869	}
870	if !iconContent(t, p.s, id) {
871		t.Fatal("no icon content")
872	}
873}
874
875// Before wl_surface v5 the icon offset rides on attach and adds up.
876func TestDataDragIconAttachOffset(t *testing.T) {
877	p := newDndPair(t)
878	comp4 := bindVersion(t, p.a.c, "wl_compositor", 4)
879	icon := p.a.c.AllocateID()
880	requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon)
881	registerProtocol(t, p.a.c, icon)
882	buf := p.a.buffer(t, 4, 4)
883	p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
884	id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
885	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1))
886	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
887	if err := p.a.c.Roundtrip(); err != nil {
888		t.Fatal(err)
889	}
890	if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) {
891		t.Fatalf("icon %+v", got)
892	}
893}
894
895// Destroying the icon mid-drag hides it; the drag goes on.
896func TestDataDragIconDestroyed(t *testing.T) {
897	p := newDndPair(t)
898	icon := p.a.surfaceNew(t)
899	buf := p.a.buffer(t, 8, 6)
900	p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands))
901	id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
902	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
903	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
904	if err := p.a.c.Roundtrip(); err != nil {
905		t.Fatal(err)
906	}
907	if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 {
908		t.Fatalf("icon %+v", got)
909	}
910	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy)
911	if err := p.a.c.Roundtrip(); err != nil {
912		t.Fatal(err)
913	}
914	if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) {
915		t.Fatalf("icon %+v", got)
916	}
917	if iconContent(t, p.s, id) {
918		t.Fatal("icon content kept")
919	}
920	p.enter(t)
921}
922
923// lockedPair is a pair of clients on a server that can lock the session.
924func lockedPair(t *testing.T) (*dndPair, string) {
925	t.Helper()
926	gate := &sessionsecurity.Gate{}
927	events := make(chan ports.ClientEvent, 128)
928	commands := make(chan ports.ClientCommand, 32)
929	dir := t.TempDir()
930	s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Security: gate, Keymap: keymapText(t)}, Channels{Events: events, Commands: commands, SecurityChanges: make(chan ports.SecurityState, 16), SecurityEvents: make(chan ports.SecurityBackendEvent, 16)}, logging.For(context.Background(), "wayland"))
931	if err != nil {
932		t.Fatal(err)
933	}
934	ctx, cancel := context.WithCancel(context.Background())
935	done := make(chan error, 1)
936	go func() { done <- s.Run(ctx) }()
937	t.Cleanup(func() {
938		cancel()
939		select {
940		case err := <-done:
941			if err != nil {
942				t.Error(err)
943			}
944		case <-time.After(3 * time.Second):
945			t.Error("server did not stop")
946		}
947	})
948	p := &dndPair{s: s, events: events, commands: commands}
949	p.a = newDndClient(t, s, dir, events)
950	p.b = newDndClient(t, s, dir, events)
951	return p, dir
952}
953
954func lockSession(t *testing.T, s *Server, dir string) {
955	t.Helper()
956	lc := protocolClient(t, s, dir)
957	if _, err := bindLockManager(t, lc).Lock(); err != nil {
958		t.Fatal(err)
959	}
960	if err := lc.Roundtrip(); err != nil {
961		t.Fatal(err)
962	}
963}
964
965// A7: the session lock cancels a drag at once: target leave, source
966// cancelled, icon gone.
967func TestDataDragLockCancels(t *testing.T) {
968	p, dir := lockedPair(t)
969	icon := p.a.surfaceNew(t)
970	buf := p.a.buffer(t, 8, 6)
971	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
972	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
973	src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
974	p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
975	id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
976	p.enter(t)
977
978	lockSession(t, p.s, dir)
979	p.b.expect(t, "leave")
980	p.a.expect(t, "cancelled")
981	if iconContent(t, p.s, id) {
982		t.Fatal("icon content kept")
983	}
984	// A new drag is refused while protected.
985	src2 := p.a.source(t, actCopy, "text/plain")
986	p.a.startDrag(t, src2, 0, uint32(1))
987	p.a.expect(t, "cancelled")
988}
989
990// GO-006: a dropped transfer waiting for its finish dies with the lock, for
991// a v3 and a pre-v3 target alike.
992func TestDataDropLockCancelsPending(t *testing.T) {
993	for _, version := range []uint32{3, 2} {
994		p, dir := lockedPair(t)
995		p.b = newDndClientAt(t, p.s, dir, p.events, version)
996		p.start(t, 0)
997		p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6}
998		p.b.until(t, "enter")
999		offer := p.b.lastOffer(t)
1000		requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1001		if version >= 3 {
1002			requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1003		}
1004		p.a.until(t, "target")
1005		p.commands <- ports.DataDragDrop{}
1006		p.a.until(t, "drop_performed")
1007		lockSession(t, p.s, dir)
1008		p.a.expect(t, "cancelled")
1009	}
1010}
1011
1012// A subsurface of the focused tree is a valid origin.
1013func TestDataDragFromSubsurface(t *testing.T) {
1014	p := newDndPair(t)
1015	serial := p.a.press(t, p.commands)
1016	child := p.a.surfaceNew(t)
1017	subc := bindProtocol(t, p.a.c, "wl_subcompositor")
1018	requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf)
1019	p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial)
1020	dndNextEvent[ports.DataDragStarted](t, p.events)
1021}
1022
1023// GO-003: destroying the offers of the target withdraws the acceptance: the
1024// release then cancels, with no drop.
1025func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) {
1026	p := newDndPair(t)
1027	p.start(t, 0)
1028	offer := p.enter(t)
1029	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1030	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1031	p.a.expect(t, "target")
1032	p.a.expect(t, "action")
1033	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
1034	if err := p.b.c.Roundtrip(); err != nil {
1035		t.Fatal(err)
1036	}
1037	if ev := p.a.expect(t, "target"); ev.mime != "" {
1038		t.Fatalf("target %+v", ev)
1039	}
1040	if ev := p.a.expect(t, "action"); ev.n != 0 {
1041		t.Fatalf("action %+v", ev)
1042	}
1043	p.commands <- ports.DataDragDrop{}
1044	got := p.b.until(t, "leave")
1045	for _, ev := range got {
1046		if ev.name == "drop" {
1047			t.Fatalf("drop after the offer was destroyed: %+v", got)
1048		}
1049	}
1050	p.a.expect(t, "cancelled")
1051}
1052
1053// GO-004: with ask, the target picks the final action after the drop; the
1054// source hears it just before dnd_finished.
1055func TestDataDragAsk(t *testing.T) {
1056	p := newDndPair(t)
1057	src := p.a.source(t, actCopy|actMove|actAsk, "text/plain")
1058	p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1059	p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1060	p.b.until(t, "enter")
1061	id := p.b.lastOffer(t)
1062	requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1063	requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1064	p.a.expect(t, "target")
1065	if ev := p.a.expect(t, "action"); ev.n != actAsk {
1066		t.Fatalf("action %+v", ev)
1067	}
1068	p.commands <- ports.DataDragDrop{}
1069	p.b.until(t, "leave")
1070	p.a.expect(t, "drop_performed")
1071
1072	// Finishing while the action is ask is an error; the choice comes first.
1073	requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain")
1074	requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1075	p.a.expect(t, "target")
1076	p.b.none(t, p.commands) // no offer.action after the drop
1077	p.a.none(t, p.commands) // the final action waits for the finish
1078	requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish)
1079	if ev := p.a.expect(t, "action"); ev.n != actMove {
1080		t.Fatalf("action %+v", ev)
1081	}
1082	p.a.expect(t, "finished")
1083}
1084
1085// A final preferred action outside the source's is an error.
1086func TestDataDragAskFinalActionNotInSource(t *testing.T) {
1087	p := newDndPair(t)
1088	src := p.a.source(t, actCopy|actAsk, "text/plain")
1089	p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1090	p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1091	p.b.until(t, "enter")
1092	id := p.b.lastOffer(t)
1093	requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1094	requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1095	p.a.expect(t, "target")
1096	p.a.expect(t, "action")
1097	p.commands <- ports.DataDragDrop{}
1098	p.b.until(t, "leave")
1099	requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove)
1100	expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction))
1101}
1102
1103// GO-005: a pre-v3 target copies: a source that does not allow copy is
1104// cancelled, one that does gets it.
1105func TestDataDragLegacyTarget(t *testing.T) {
1106	setup := func(t *testing.T, actions uint32) (*dndPair, uint32) {
1107		s, events, commands, dir := keyboardServer(t)
1108		p := &dndPair{s: s, events: events, commands: commands}
1109		p.a = newDndClient(t, s, dir, events)
1110		p.b = newDndClientAt(t, s, dir, events, 2)
1111		src := p.a.source(t, actions, "text/plain")
1112		p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1113		p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1114		p.b.until(t, "enter")
1115		id := p.b.lastOffer(t)
1116		// Legacy accept is feedback only: deliberately never send it.
1117		return p, id
1118	}
1119	t.Run("move only", func(t *testing.T) {
1120		p, _ := setup(t, actMove)
1121		p.commands <- ports.DataDragDrop{}
1122		got := p.b.until(t, "leave")
1123		for _, ev := range got {
1124			if ev.name == "drop" {
1125				t.Fatalf("drop for a source without copy: %+v", got)
1126			}
1127		}
1128		p.a.until(t, "cancelled")
1129	})
1130	t.Run("copy", func(t *testing.T) {
1131		p, id := setup(t, actCopy|actMove)
1132		p.commands <- ports.DataDragDrop{}
1133		p.b.expect(t, "drop")
1134		p.b.expect(t, "leave")
1135		if ev := p.a.expect(t, "action"); ev.n != actCopy {
1136			t.Fatalf("action %+v", ev)
1137		}
1138		p.a.expect(t, "drop_performed")
1139		// GO-006: destroying the last offer finishes the transfer.
1140		requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy)
1141		p.a.expect(t, "finished")
1142	})
1143}
1144
1145// GO-007: of several offset requests before one commit, the last counts.
1146func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) {
1147	p := newDndPair(t)
1148	icon := p.a.surfaceNew(t)
1149	buf := p.a.buffer(t, 8, 6)
1150	p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
1151	id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
1152	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1153	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1))
1154	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7))
1155	requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
1156	if err := p.a.c.Roundtrip(); err != nil {
1157		t.Fatal(err)
1158	}
1159	if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) {
1160		t.Fatalf("icon %+v", got)
1161	}
1162}
1163
1164func TestDataDragLastTargetDeviceReleased(t *testing.T) {
1165	p := newDndPair(t)
1166	p.start(t, 0)
1167	offer := p.enter(t)
1168	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1169	requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1170	p.a.expect(t, "target")
1171	p.a.expect(t, "action")
1172	p.b.expect(t, "offer_action")
1173	requestProtocol(t, p.b.c, p.b.device.ID(), wayland.DataDeviceRequestRelease)
1174	if err := p.b.c.Roundtrip(); err != nil {
1175		t.Fatal(err)
1176	}
1177	p.commands <- ports.DataDragDrop{}
1178	if ev := p.a.expect(t, "target"); ev.mime != "" {
1179		t.Fatalf("target %+v", ev)
1180	}
1181	if ev := p.a.expect(t, "action"); ev.n != 0 {
1182		t.Fatalf("action %+v", ev)
1183	}
1184	p.a.expect(t, "cancelled")
1185	// The offer was deliberately kept alive; it must not keep a pending drop.
1186	if !p.s.display.Do(func() {
1187		if len(p.s.dndDrops) != 0 {
1188			t.Error("pending drop without a device")
1189		}
1190	}) {
1191		t.Fatal("display stopped")
1192	}
1193}
1194
1195func TestDataDragMultipleOffersShareAction(t *testing.T) {
1196	for _, name := range []string{"finish", "acceptor destroyed"} {
1197		t.Run(name, func(t *testing.T) {
1198			p := newDndPair(t)
1199			seat := bindVersion(t, p.b.c, "wl_seat", 8)
1200			second := &dndDeviceEvents{c: p.b.c, log: make(chan dndEvent, 64)}
1201			second.SetID(p.b.c.AllocateID())
1202			registerWireProxy(p.b.c, second)
1203			requestProtocol(t, p.b.c, p.b.manager, wayland.DataDeviceManagerRequestGetDataDevice, second.ID(), seat)
1204			if err := p.b.c.Roundtrip(); err != nil {
1205				t.Fatal(err)
1206			}
1207			p.start(t, 0)
1208			first := p.enter(t)
1209			other := &dndClient{c: p.b.c, device: second, log: second.log}
1210			other.until(t, "enter")
1211			two := other.lastOffer(t)
1212			requestProtocol(t, p.b.c, first, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1213			requestProtocol(t, p.b.c, first, wayland.DataOfferRequestSetActions, actCopy|actMove, actCopy)
1214			p.a.expect(t, "target")
1215			p.a.expect(t, "action")
1216			for _, d := range []*dndClient{p.b, other} {
1217				if ev := d.expect(t, "offer_action"); ev.n != actCopy {
1218					t.Fatalf("copy: %+v", ev)
1219				}
1220			}
1221			requestProtocol(t, p.b.c, two, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1222			if ev := p.a.expect(t, "action"); ev.n != actMove {
1223				t.Fatalf("source action %+v", ev)
1224			}
1225			for _, d := range []*dndClient{p.b, other} {
1226				if ev := d.expect(t, "offer_action"); ev.n != actMove {
1227					t.Fatalf("move: %+v", ev)
1228				}
1229			}
1230			if name == "finish" {
1231				p.commands <- ports.DataDragDrop{}
1232				for _, d := range []*dndClient{p.b, other} {
1233					d.expect(t, "drop")
1234					d.expect(t, "leave")
1235				}
1236				p.a.expect(t, "drop_performed")
1237				// Either offer finishes the same negotiated move transfer.
1238				requestProtocol(t, p.b.c, two, wayland.DataOfferRequestFinish)
1239				p.a.expect(t, "finished")
1240				return
1241			}
1242			requestProtocol(t, p.b.c, first, wayland.DataOfferRequestDestroy)
1243			if err := p.b.c.Roundtrip(); err != nil {
1244				t.Fatal(err)
1245			}
1246			if ev := p.a.expect(t, "target"); ev.mime != "" {
1247				t.Fatalf("stale acceptance %+v", ev)
1248			}
1249			if ev := p.a.expect(t, "action"); ev.n != 0 {
1250				t.Fatalf("stale action %+v", ev)
1251			}
1252			if ev := other.expect(t, "offer_action"); ev.n != 0 {
1253				t.Fatalf("remaining offer %+v", ev)
1254			}
1255			p.commands <- ports.DataDragDrop{}
1256			p.a.expect(t, "cancelled")
1257			other.expect(t, "leave")
1258		})
1259	}
1260}

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 23

Tags 10

↑↓ moveenter openesc close