Merge branch 'fix/review-dnd' into fix/release-review
6 files · +232 -40
M internal/adapters/wayland/dnd.go +27 -7
| @@ -58,9 +58,11 @@ type dndTarget struct { | |||
| 58 | 58 | surface *wayland.Surface | |
| 59 | 59 | devices []*wayland.DataDevice | |
| 60 | 60 | offers []*dndOffer | |
| 61 | - | // accepted is the MIME type the target accepted; action the action | |
| 62 | - | // chosen for the source. | |
| 61 | + | // accepted is the MIME type the target accepted, owned by acceptor. | |
| 62 | + | // action is the shared result of the latest set_actions request, | |
| 63 | + | // broadcast to every offer so drop and finish use the same result. | |
| 63 | 64 | accepted string | |
| 65 | + | acceptor *dndOffer | |
| 64 | 66 | action uint32 | |
| 65 | 67 | state int | |
| 66 | 68 | // legacy: no device speaks wl_data_offer v3: the target copies and | |
| @@ -118,6 +120,9 @@ func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) { | |||
| 118 | 120 | list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev }) | |
| 119 | 121 | if len(list) == 0 { | |
| 120 | 122 | delete(s.dataDevices, c) | |
| 123 | + | if d := s.dnd; d != nil && d.target != nil && d.target.surface.Client() == c { | |
| 124 | + | s.dndLeave(d) | |
| 125 | + | } | |
| 121 | 126 | // An internal drag has nobody left to tell. | |
| 122 | 127 | if d := s.dnd; d != nil && d.client == c && d.src == nil { | |
| 123 | 128 | s.dndEnd(d, false, true) | |
| @@ -391,7 +396,7 @@ func (s *Server) dndDrop(d *dndState) { | |||
| 391 | 396 | } | |
| 392 | 397 | } | |
| 393 | 398 | } | |
| 394 | - | if t.accepted == "" || t.action == 0 { | |
| 399 | + | if !t.legacy && t.accepted == "" || t.action == 0 { | |
| 395 | 400 | s.dndEnd(d, true, false) | |
| 396 | 401 | return | |
| 397 | 402 | } | |
| @@ -487,6 +492,16 @@ func (s *Server) dndCancelAll() { | |||
| 487 | 492 | func (s *Server) dndOfferGone(o *dndOffer) { | |
| 488 | 493 | t := o.t | |
| 489 | 494 | t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o }) | |
| 495 | + | if t.state == targetActive && t.acceptor == o { | |
| 496 | + | t.acceptor = nil | |
| 497 | + | s.dndResetSource(t) | |
| 498 | + | for _, offer := range t.offers { | |
| 499 | + | if offer.sent != 0 && offer.res.Version() >= 3 { | |
| 500 | + | offer.sent = 0 | |
| 501 | + | offer.res.SendAction(0) | |
| 502 | + | } | |
| 503 | + | } | |
| 504 | + | } | |
| 490 | 505 | if len(t.offers) != 0 { | |
| 491 | 506 | return | |
| 492 | 507 | } | |
| @@ -520,10 +535,14 @@ func (o *dndOffer) update() { | |||
| 520 | 535 | src.res.SendAction(action) | |
| 521 | 536 | } | |
| 522 | 537 | } | |
| 523 | - | if t.state == targetActive && action != o.sent { | |
| 524 | - | o.sent = action | |
| 525 | - | if o.res.Version() >= 3 { | |
| 526 | - | o.res.SendAction(action) | |
| 538 | + | if t.state == targetActive { | |
| 539 | + | for _, offer := range t.offers { | |
| 540 | + | if action != offer.sent { | |
| 541 | + | offer.sent = action | |
| 542 | + | if offer.res.Version() >= 3 { | |
| 543 | + | offer.res.SendAction(action) | |
| 544 | + | } | |
| 545 | + | } | |
| 527 | 546 | } | |
| 528 | 547 | } | |
| 529 | 548 | } | |
| @@ -534,6 +553,7 @@ func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) { | |||
| 534 | 553 | return | |
| 535 | 554 | } | |
| 536 | 555 | t.accepted = mime | |
| 556 | + | t.acceptor = o | |
| 537 | 557 | t.d.src.res.SendTarget(mime) | |
| 538 | 558 | } | |
| 539 | 559 | ||
| @@ -58,9 +58,11 @@ type dndTarget struct { | |||||
| 58 | surface *wayland.Surface | 58 | surface *wayland.Surface | ||
| 59 | devices []*wayland.DataDevice | 59 | devices []*wayland.DataDevice | ||
| 60 | offers []*dndOffer | 60 | offers []*dndOffer | ||
| 61 | - | // accepted is the MIME type the target accepted; action the action | 61 | + | // accepted is the MIME type the target accepted, owned by acceptor. |
| 62 | - | // chosen for the source. | 62 | + | // action is the shared result of the latest set_actions request, |
| 63 | + | // broadcast to every offer so drop and finish use the same result. | |||
| 63 | accepted string | 64 | accepted string | ||
| 65 | + | acceptor *dndOffer | |||
| 64 | action uint32 | 66 | action uint32 | ||
| 65 | state int | 67 | state int | ||
| 66 | // legacy: no device speaks wl_data_offer v3: the target copies and | 68 | // legacy: no device speaks wl_data_offer v3: the target copies and | ||
| @@ -118,6 +120,9 @@ func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) { | |||||
| 118 | list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev }) | 120 | list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev }) | ||
| 119 | if len(list) == 0 { | 121 | if len(list) == 0 { | ||
| 120 | delete(s.dataDevices, c) | 122 | delete(s.dataDevices, c) | ||
| 123 | + | if d := s.dnd; d != nil && d.target != nil && d.target.surface.Client() == c { | |||
| 124 | + | s.dndLeave(d) | |||
| 125 | + | } | |||
| 121 | // An internal drag has nobody left to tell. | 126 | // An internal drag has nobody left to tell. | ||
| 122 | if d := s.dnd; d != nil && d.client == c && d.src == nil { | 127 | if d := s.dnd; d != nil && d.client == c && d.src == nil { | ||
| 123 | s.dndEnd(d, false, true) | 128 | s.dndEnd(d, false, true) | ||
| @@ -391,7 +396,7 @@ func (s *Server) dndDrop(d *dndState) { | |||||
| 391 | } | 396 | } | ||
| 392 | } | 397 | } | ||
| 393 | } | 398 | } | ||
| 394 | - | if t.accepted == "" || t.action == 0 { | 399 | + | if !t.legacy && t.accepted == "" || t.action == 0 { |
| 395 | s.dndEnd(d, true, false) | 400 | s.dndEnd(d, true, false) | ||
| 396 | return | 401 | return | ||
| 397 | } | 402 | } | ||
| @@ -487,6 +492,16 @@ func (s *Server) dndCancelAll() { | |||||
| 487 | func (s *Server) dndOfferGone(o *dndOffer) { | 492 | func (s *Server) dndOfferGone(o *dndOffer) { | ||
| 488 | t := o.t | 493 | t := o.t | ||
| 489 | t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o }) | 494 | t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o }) | ||
| 495 | + | if t.state == targetActive && t.acceptor == o { | |||
| 496 | + | t.acceptor = nil | |||
| 497 | + | s.dndResetSource(t) | |||
| 498 | + | for _, offer := range t.offers { | |||
| 499 | + | if offer.sent != 0 && offer.res.Version() >= 3 { | |||
| 500 | + | offer.sent = 0 | |||
| 501 | + | offer.res.SendAction(0) | |||
| 502 | + | } | |||
| 503 | + | } | |||
| 504 | + | } | |||
| 490 | if len(t.offers) != 0 { | 505 | if len(t.offers) != 0 { | ||
| 491 | return | 506 | return | ||
| 492 | } | 507 | } | ||
| @@ -520,10 +535,14 @@ func (o *dndOffer) update() { | |||||
| 520 | src.res.SendAction(action) | 535 | src.res.SendAction(action) | ||
| 521 | } | 536 | } | ||
| 522 | } | 537 | } | ||
| 523 | - | if t.state == targetActive && action != o.sent { | 538 | + | if t.state == targetActive { |
| 524 | - | o.sent = action | 539 | + | for _, offer := range t.offers { |
| 525 | - | if o.res.Version() >= 3 { | 540 | + | if action != offer.sent { |
| 526 | - | o.res.SendAction(action) | 541 | + | offer.sent = action |
| 542 | + | if offer.res.Version() >= 3 { | |||
| 543 | + | offer.res.SendAction(action) | |||
| 544 | + | } | |||
| 545 | + | } | |||
| 527 | } | 546 | } | ||
| 528 | } | 547 | } | ||
| 529 | } | 548 | } | ||
| @@ -534,6 +553,7 @@ func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) { | |||||
| 534 | return | 553 | return | ||
| 535 | } | 554 | } | ||
| 536 | t.accepted = mime | 555 | t.accepted = mime | ||
| 556 | + | t.acceptor = o | |||
| 537 | t.d.src.res.SendTarget(mime) | 557 | t.d.src.res.SendTarget(mime) | ||
| 538 | } | 558 | } | ||
| 539 | 559 | ||||
M internal/adapters/wayland/dnd_test.go +137 -31
| @@ -198,13 +198,13 @@ func (d *dndClient) expect(t *testing.T, name string) dndEvent { | |||
| 198 | 198 | } | |
| 199 | 199 | ||
| 200 | 200 | // none checks that no event arrives. | |
| 201 | - | func (d *dndClient) none(t *testing.T) { | |
| 201 | + | func (d *dndClient) none(t *testing.T, commands chan<- ports.ClientCommand) { | |
| 202 | 202 | t.Helper() | |
| 203 | - | for range 2 { | |
| 204 | - | if err := d.c.Roundtrip(); err != nil { | |
| 205 | - | t.Fatal(err) | |
| 206 | - | } | |
| 207 | - | time.Sleep(30 * time.Millisecond) | |
| 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 | 208 | } | |
| 209 | 209 | select { | |
| 210 | 210 | case ev := <-d.log: | |
| @@ -366,17 +366,26 @@ func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientE | |||
| 366 | 366 | } | |
| 367 | 367 | ||
| 368 | 368 | // dndNoEvent checks that no client event of type T is pending. | |
| 369 | - | func dndNoEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) { | |
| 369 | + | func dndNoEvent[T ports.ClientEvent](t *testing.T, s *Server, events <-chan ports.ClientEvent) { | |
| 370 | 370 | t.Helper() | |
| 371 | - | time.Sleep(50 * time.Millisecond) | |
| 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) | |
| 372 | 378 | for { | |
| 373 | 379 | select { | |
| 374 | 380 | case ev := <-events: | |
| 381 | + | if v, ok := ev.(ports.WindowResized); ok && v.ID == marker.ID { | |
| 382 | + | return | |
| 383 | + | } | |
| 375 | 384 | if _, ok := ev.(T); ok { | |
| 376 | 385 | t.Fatalf("unexpected %T: %+v", ev, ev) | |
| 377 | 386 | } | |
| 378 | - | default: | |
| 379 | - | return | |
| 387 | + | case <-deadline: | |
| 388 | + | t.Fatal("no event barrier") | |
| 380 | 389 | } | |
| 381 | 390 | } | |
| 382 | 391 | } | |
| @@ -483,10 +492,10 @@ func TestDataDragBetweenClients(t *testing.T) { | |||
| 483 | 492 | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |
| 484 | 493 | p.a.expect(t, "finished") | |
| 485 | 494 | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |
| 486 | - | p.a.none(t) | |
| 487 | - | p.b.none(t) | |
| 495 | + | p.a.none(t, p.commands) | |
| 496 | + | p.b.none(t, p.commands) | |
| 488 | 497 | // core ended the drag: wayland does not echo it. | |
| 489 | - | dndNoEvent[ports.DataDragEnded](t, p.events) | |
| 498 | + | dndNoEvent[ports.DataDragEnded](t, p.s, p.events) | |
| 490 | 499 | } | |
| 491 | 500 | ||
| 492 | 501 | // The action follows wlroots: the preferred one if both allow it, else the | |
| @@ -534,7 +543,7 @@ func TestDataDragRefusedDrop(t *testing.T) { | |||
| 534 | 543 | p.start(t, 0) | |
| 535 | 544 | p.commands <- ports.DataDragDrop{} | |
| 536 | 545 | p.a.expect(t, "cancelled") | |
| 537 | - | p.b.none(t) | |
| 546 | + | p.b.none(t, p.commands) | |
| 538 | 547 | }) | |
| 539 | 548 | t.Run("not accepted", func(t *testing.T) { | |
| 540 | 549 | p := newDndPair(t) | |
| @@ -543,7 +552,7 @@ func TestDataDragRefusedDrop(t *testing.T) { | |||
| 543 | 552 | p.commands <- ports.DataDragDrop{} | |
| 544 | 553 | p.b.expect(t, "leave") | |
| 545 | 554 | p.a.expect(t, "cancelled") | |
| 546 | - | p.b.none(t) | |
| 555 | + | p.b.none(t, p.commands) | |
| 547 | 556 | }) | |
| 548 | 557 | t.Run("escape", func(t *testing.T) { | |
| 549 | 558 | p := newDndPair(t) | |
| @@ -552,7 +561,7 @@ func TestDataDragRefusedDrop(t *testing.T) { | |||
| 552 | 561 | p.commands <- ports.DataDragCancel{} | |
| 553 | 562 | p.b.expect(t, "leave") | |
| 554 | 563 | p.a.expect(t, "cancelled") | |
| 555 | - | dndNoEvent[ports.DataDragEnded](t, p.events) | |
| 564 | + | dndNoEvent[ports.DataDragEnded](t, p.s, p.events) | |
| 556 | 565 | }) | |
| 557 | 566 | } | |
| 558 | 567 | ||
| @@ -587,7 +596,7 @@ func TestDataDragSourceDestroyed(t *testing.T) { | |||
| 587 | 596 | dndNextEvent[ports.DataDragEnded](t, p.events) | |
| 588 | 597 | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9} | |
| 589 | 598 | p.commands <- ports.DataDragDrop{} | |
| 590 | - | p.b.none(t) | |
| 599 | + | p.b.none(t, p.commands) | |
| 591 | 600 | } | |
| 592 | 601 | ||
| 593 | 602 | // A drag whose origin client dies ends the same way. | |
| @@ -608,7 +617,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 608 | 617 | serial := p.a.press(t, p.commands) | |
| 609 | 618 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1) | |
| 610 | 619 | p.a.expect(t, "cancelled") | |
| 611 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 620 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) | |
| 612 | 621 | }) | |
| 613 | 622 | t.Run("other client's press", func(t *testing.T) { | |
| 614 | 623 | p := newDndPair(t) | |
| @@ -616,7 +625,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 616 | 625 | serial := p.b.press(t, p.commands) | |
| 617 | 626 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 618 | 627 | p.a.expect(t, "cancelled") | |
| 619 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 628 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) | |
| 620 | 629 | }) | |
| 621 | 630 | t.Run("key serial", func(t *testing.T) { | |
| 622 | 631 | p := newDndPair(t) | |
| @@ -625,7 +634,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 625 | 634 | serial := p.a.key(t, p.commands) | |
| 626 | 635 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 627 | 636 | p.a.expect(t, "cancelled") | |
| 628 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 637 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) | |
| 629 | 638 | }) | |
| 630 | 639 | t.Run("serial after the release", func(t *testing.T) { | |
| 631 | 640 | p := newDndPair(t) | |
| @@ -633,7 +642,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 633 | 642 | p.a.release(t, p.commands) | |
| 634 | 643 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 635 | 644 | p.a.expect(t, "cancelled") | |
| 636 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 645 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) | |
| 637 | 646 | }) | |
| 638 | 647 | t.Run("other surface of the client", func(t *testing.T) { | |
| 639 | 648 | p := newDndPair(t) | |
| @@ -641,7 +650,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 641 | 650 | other := p.a.surfaceNew(t) | |
| 642 | 651 | p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial) | |
| 643 | 652 | p.a.expect(t, "cancelled") | |
| 644 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 653 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) | |
| 645 | 654 | }) | |
| 646 | 655 | t.Run("press of the first button", func(t *testing.T) { | |
| 647 | 656 | // A second button does not make a new grab: only the first serial. | |
| @@ -675,7 +684,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 675 | 684 | p.a.key(t, p.commands) // the release is applied once the key is seen | |
| 676 | 685 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 677 | 686 | p.a.expect(t, "cancelled") | |
| 678 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 687 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) | |
| 679 | 688 | }) | |
| 680 | 689 | t.Run("used source", func(t *testing.T) { | |
| 681 | 690 | p := newDndPair(t) | |
| @@ -692,7 +701,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 692 | 701 | other := p.a.source(t, actCopy, "text/plain") | |
| 693 | 702 | p.a.startDrag(t, other, 0, p.a.press(t, p.commands)) | |
| 694 | 703 | p.a.expect(t, "cancelled") | |
| 695 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 704 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) | |
| 696 | 705 | }) | |
| 697 | 706 | t.Run("icon with another role", func(t *testing.T) { | |
| 698 | 707 | p := newDndPair(t) | |
| @@ -701,7 +710,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 701 | 710 | // The origin toplevel is a role already. | |
| 702 | 711 | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial) | |
| 703 | 712 | expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole)) | |
| 704 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 713 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) | |
| 705 | 714 | }) | |
| 706 | 715 | } | |
| 707 | 716 | ||
| @@ -712,7 +721,7 @@ func TestDataDragWithoutSource(t *testing.T) { | |||
| 712 | 721 | p.a.startDrag(t, 0, 0, p.a.press(t, p.commands)) | |
| 713 | 722 | dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 714 | 723 | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 715 | - | p.b.none(t) | |
| 724 | + | p.b.none(t, p.commands) | |
| 716 | 725 | p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5} | |
| 717 | 726 | if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 { | |
| 718 | 727 | t.Fatalf("enter %+v", ev) | |
| @@ -837,7 +846,7 @@ func TestDataDragIcon(t *testing.T) { | |||
| 837 | 846 | t.Fatalf("icon %+v", got) | |
| 838 | 847 | } | |
| 839 | 848 | // The icon never takes input. | |
| 840 | - | dndNoEvent[ports.InputRegionChanged](t, p.events) | |
| 849 | + | dndNoEvent[ports.InputRegionChanged](t, p.s, p.events) | |
| 841 | 850 | ||
| 842 | 851 | p.commands <- ports.DataDragCancel{} | |
| 843 | 852 | p.a.expect(t, "cancelled") | |
| @@ -1064,8 +1073,8 @@ func TestDataDragAsk(t *testing.T) { | |||
| 1064 | 1073 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain") | |
| 1065 | 1074 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | |
| 1066 | 1075 | p.a.expect(t, "target") | |
| 1067 | - | p.b.none(t) // no offer.action after the drop | |
| 1068 | - | p.a.none(t) // the final action waits for the finish | |
| 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 | |
| 1069 | 1078 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish) | |
| 1070 | 1079 | if ev := p.a.expect(t, "action"); ev.n != actMove { | |
| 1071 | 1080 | t.Fatalf("action %+v", ev) | |
| @@ -1104,8 +1113,7 @@ func TestDataDragLegacyTarget(t *testing.T) { | |||
| 1104 | 1113 | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 1105 | 1114 | p.b.until(t, "enter") | |
| 1106 | 1115 | id := p.b.lastOffer(t) | |
| 1107 | - | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 1108 | - | p.a.expect(t, "target") | |
| 1116 | + | // Legacy accept is feedback only: deliberately never send it. | |
| 1109 | 1117 | return p, id | |
| 1110 | 1118 | } | |
| 1111 | 1119 | t.Run("move only", func(t *testing.T) { | |
| @@ -1152,3 +1160,101 @@ func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) { | |||
| 1152 | 1160 | t.Fatalf("icon %+v", got) | |
| 1153 | 1161 | } | |
| 1154 | 1162 | } | |
| 1163 | + | ||
| 1164 | + | func 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 | + | ||
| 1195 | + | func 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 | + | } | |
| @@ -198,13 +198,13 @@ func (d *dndClient) expect(t *testing.T, name string) dndEvent { | |||||
| 198 | } | 198 | } | ||
| 199 | 199 | ||||
| 200 | // none checks that no event arrives. | 200 | // none checks that no event arrives. | ||
| 201 | - | func (d *dndClient) none(t *testing.T) { | 201 | + | func (d *dndClient) none(t *testing.T, commands chan<- ports.ClientCommand) { |
| 202 | t.Helper() | 202 | t.Helper() | ||
| 203 | - | for range 2 { | 203 | + | // Observing a key sent through the command queue proves earlier |
| 204 | - | if err := d.c.Roundtrip(); err != nil { | 204 | + | // compositor commands ran. The roundtrip then drains wire events. |
| 205 | - | t.Fatal(err) | 205 | + | d.key(t, commands) |
| 206 | - | } | 206 | + | if err := d.c.Roundtrip(); err != nil { |
| 207 | - | time.Sleep(30 * time.Millisecond) | 207 | + | t.Fatal(err) |
| 208 | } | 208 | } | ||
| 209 | select { | 209 | select { | ||
| 210 | case ev := <-d.log: | 210 | case ev := <-d.log: | ||
| @@ -366,17 +366,26 @@ func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientE | |||||
| 366 | } | 366 | } | ||
| 367 | 367 | ||||
| 368 | // dndNoEvent checks that no client event of type T is pending. | 368 | // dndNoEvent checks that no client event of type T is pending. | ||
| 369 | - | func dndNoEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) { | 369 | + | func dndNoEvent[T ports.ClientEvent](t *testing.T, s *Server, events <-chan ports.ClientEvent) { |
| 370 | t.Helper() | 370 | t.Helper() | ||
| 371 | - | time.Sleep(50 * time.Millisecond) | 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) | |||
| 372 | for { | 378 | for { | ||
| 373 | select { | 379 | select { | ||
| 374 | case ev := <-events: | 380 | case ev := <-events: | ||
| 381 | + | if v, ok := ev.(ports.WindowResized); ok && v.ID == marker.ID { | |||
| 382 | + | return | |||
| 383 | + | } | |||
| 375 | if _, ok := ev.(T); ok { | 384 | if _, ok := ev.(T); ok { | ||
| 376 | t.Fatalf("unexpected %T: %+v", ev, ev) | 385 | t.Fatalf("unexpected %T: %+v", ev, ev) | ||
| 377 | } | 386 | } | ||
| 378 | - | default: | 387 | + | case <-deadline: |
| 379 | - | return | 388 | + | t.Fatal("no event barrier") |
| 380 | } | 389 | } | ||
| 381 | } | 390 | } | ||
| 382 | } | 391 | } | ||
| @@ -483,10 +492,10 @@ func TestDataDragBetweenClients(t *testing.T) { | |||||
| 483 | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | 492 | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | ||
| 484 | p.a.expect(t, "finished") | 493 | p.a.expect(t, "finished") | ||
| 485 | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | 494 | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | ||
| 486 | - | p.a.none(t) | 495 | + | p.a.none(t, p.commands) |
| 487 | - | p.b.none(t) | 496 | + | p.b.none(t, p.commands) |
| 488 | // core ended the drag: wayland does not echo it. | 497 | // core ended the drag: wayland does not echo it. | ||
| 489 | - | dndNoEvent[ports.DataDragEnded](t, p.events) | 498 | + | dndNoEvent[ports.DataDragEnded](t, p.s, p.events) |
| 490 | } | 499 | } | ||
| 491 | 500 | ||||
| 492 | // The action follows wlroots: the preferred one if both allow it, else the | 501 | // The action follows wlroots: the preferred one if both allow it, else the | ||
| @@ -534,7 +543,7 @@ func TestDataDragRefusedDrop(t *testing.T) { | |||||
| 534 | p.start(t, 0) | 543 | p.start(t, 0) | ||
| 535 | p.commands <- ports.DataDragDrop{} | 544 | p.commands <- ports.DataDragDrop{} | ||
| 536 | p.a.expect(t, "cancelled") | 545 | p.a.expect(t, "cancelled") | ||
| 537 | - | p.b.none(t) | 546 | + | p.b.none(t, p.commands) |
| 538 | }) | 547 | }) | ||
| 539 | t.Run("not accepted", func(t *testing.T) { | 548 | t.Run("not accepted", func(t *testing.T) { | ||
| 540 | p := newDndPair(t) | 549 | p := newDndPair(t) | ||
| @@ -543,7 +552,7 @@ func TestDataDragRefusedDrop(t *testing.T) { | |||||
| 543 | p.commands <- ports.DataDragDrop{} | 552 | p.commands <- ports.DataDragDrop{} | ||
| 544 | p.b.expect(t, "leave") | 553 | p.b.expect(t, "leave") | ||
| 545 | p.a.expect(t, "cancelled") | 554 | p.a.expect(t, "cancelled") | ||
| 546 | - | p.b.none(t) | 555 | + | p.b.none(t, p.commands) |
| 547 | }) | 556 | }) | ||
| 548 | t.Run("escape", func(t *testing.T) { | 557 | t.Run("escape", func(t *testing.T) { | ||
| 549 | p := newDndPair(t) | 558 | p := newDndPair(t) | ||
| @@ -552,7 +561,7 @@ func TestDataDragRefusedDrop(t *testing.T) { | |||||
| 552 | p.commands <- ports.DataDragCancel{} | 561 | p.commands <- ports.DataDragCancel{} | ||
| 553 | p.b.expect(t, "leave") | 562 | p.b.expect(t, "leave") | ||
| 554 | p.a.expect(t, "cancelled") | 563 | p.a.expect(t, "cancelled") | ||
| 555 | - | dndNoEvent[ports.DataDragEnded](t, p.events) | 564 | + | dndNoEvent[ports.DataDragEnded](t, p.s, p.events) |
| 556 | }) | 565 | }) | ||
| 557 | } | 566 | } | ||
| 558 | 567 | ||||
| @@ -587,7 +596,7 @@ func TestDataDragSourceDestroyed(t *testing.T) { | |||||
| 587 | dndNextEvent[ports.DataDragEnded](t, p.events) | 596 | dndNextEvent[ports.DataDragEnded](t, p.events) | ||
| 588 | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9} | 597 | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9} | ||
| 589 | p.commands <- ports.DataDragDrop{} | 598 | p.commands <- ports.DataDragDrop{} | ||
| 590 | - | p.b.none(t) | 599 | + | p.b.none(t, p.commands) |
| 591 | } | 600 | } | ||
| 592 | 601 | ||||
| 593 | // A drag whose origin client dies ends the same way. | 602 | // A drag whose origin client dies ends the same way. | ||
| @@ -608,7 +617,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 608 | serial := p.a.press(t, p.commands) | 617 | serial := p.a.press(t, p.commands) | ||
| 609 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1) | 618 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1) | ||
| 610 | p.a.expect(t, "cancelled") | 619 | p.a.expect(t, "cancelled") | ||
| 611 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | 620 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) |
| 612 | }) | 621 | }) | ||
| 613 | t.Run("other client's press", func(t *testing.T) { | 622 | t.Run("other client's press", func(t *testing.T) { | ||
| 614 | p := newDndPair(t) | 623 | p := newDndPair(t) | ||
| @@ -616,7 +625,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 616 | serial := p.b.press(t, p.commands) | 625 | serial := p.b.press(t, p.commands) | ||
| 617 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | 626 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | ||
| 618 | p.a.expect(t, "cancelled") | 627 | p.a.expect(t, "cancelled") | ||
| 619 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | 628 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) |
| 620 | }) | 629 | }) | ||
| 621 | t.Run("key serial", func(t *testing.T) { | 630 | t.Run("key serial", func(t *testing.T) { | ||
| 622 | p := newDndPair(t) | 631 | p := newDndPair(t) | ||
| @@ -625,7 +634,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 625 | serial := p.a.key(t, p.commands) | 634 | serial := p.a.key(t, p.commands) | ||
| 626 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | 635 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | ||
| 627 | p.a.expect(t, "cancelled") | 636 | p.a.expect(t, "cancelled") | ||
| 628 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | 637 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) |
| 629 | }) | 638 | }) | ||
| 630 | t.Run("serial after the release", func(t *testing.T) { | 639 | t.Run("serial after the release", func(t *testing.T) { | ||
| 631 | p := newDndPair(t) | 640 | p := newDndPair(t) | ||
| @@ -633,7 +642,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 633 | p.a.release(t, p.commands) | 642 | p.a.release(t, p.commands) | ||
| 634 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | 643 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | ||
| 635 | p.a.expect(t, "cancelled") | 644 | p.a.expect(t, "cancelled") | ||
| 636 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | 645 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) |
| 637 | }) | 646 | }) | ||
| 638 | t.Run("other surface of the client", func(t *testing.T) { | 647 | t.Run("other surface of the client", func(t *testing.T) { | ||
| 639 | p := newDndPair(t) | 648 | p := newDndPair(t) | ||
| @@ -641,7 +650,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 641 | other := p.a.surfaceNew(t) | 650 | other := p.a.surfaceNew(t) | ||
| 642 | p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial) | 651 | p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial) | ||
| 643 | p.a.expect(t, "cancelled") | 652 | p.a.expect(t, "cancelled") | ||
| 644 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | 653 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) |
| 645 | }) | 654 | }) | ||
| 646 | t.Run("press of the first button", func(t *testing.T) { | 655 | t.Run("press of the first button", func(t *testing.T) { | ||
| 647 | // A second button does not make a new grab: only the first serial. | 656 | // A second button does not make a new grab: only the first serial. | ||
| @@ -675,7 +684,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 675 | p.a.key(t, p.commands) // the release is applied once the key is seen | 684 | p.a.key(t, p.commands) // the release is applied once the key is seen | ||
| 676 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | 685 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | ||
| 677 | p.a.expect(t, "cancelled") | 686 | p.a.expect(t, "cancelled") | ||
| 678 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | 687 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) |
| 679 | }) | 688 | }) | ||
| 680 | t.Run("used source", func(t *testing.T) { | 689 | t.Run("used source", func(t *testing.T) { | ||
| 681 | p := newDndPair(t) | 690 | p := newDndPair(t) | ||
| @@ -692,7 +701,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 692 | other := p.a.source(t, actCopy, "text/plain") | 701 | other := p.a.source(t, actCopy, "text/plain") | ||
| 693 | p.a.startDrag(t, other, 0, p.a.press(t, p.commands)) | 702 | p.a.startDrag(t, other, 0, p.a.press(t, p.commands)) | ||
| 694 | p.a.expect(t, "cancelled") | 703 | p.a.expect(t, "cancelled") | ||
| 695 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | 704 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) |
| 696 | }) | 705 | }) | ||
| 697 | t.Run("icon with another role", func(t *testing.T) { | 706 | t.Run("icon with another role", func(t *testing.T) { | ||
| 698 | p := newDndPair(t) | 707 | p := newDndPair(t) | ||
| @@ -701,7 +710,7 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 701 | // The origin toplevel is a role already. | 710 | // The origin toplevel is a role already. | ||
| 702 | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial) | 711 | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial) | ||
| 703 | expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole)) | 712 | expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole)) | ||
| 704 | - | dndNoEvent[ports.DataDragStarted](t, p.events) | 713 | + | dndNoEvent[ports.DataDragStarted](t, p.s, p.events) |
| 705 | }) | 714 | }) | ||
| 706 | } | 715 | } | ||
| 707 | 716 | ||||
| @@ -712,7 +721,7 @@ func TestDataDragWithoutSource(t *testing.T) { | |||||
| 712 | p.a.startDrag(t, 0, 0, p.a.press(t, p.commands)) | 721 | p.a.startDrag(t, 0, 0, p.a.press(t, p.commands)) | ||
| 713 | dndNextEvent[ports.DataDragStarted](t, p.events) | 722 | dndNextEvent[ports.DataDragStarted](t, p.events) | ||
| 714 | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | 723 | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | ||
| 715 | - | p.b.none(t) | 724 | + | p.b.none(t, p.commands) |
| 716 | p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5} | 725 | p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5} | ||
| 717 | if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 { | 726 | if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 { | ||
| 718 | t.Fatalf("enter %+v", ev) | 727 | t.Fatalf("enter %+v", ev) | ||
| @@ -837,7 +846,7 @@ func TestDataDragIcon(t *testing.T) { | |||||
| 837 | t.Fatalf("icon %+v", got) | 846 | t.Fatalf("icon %+v", got) | ||
| 838 | } | 847 | } | ||
| 839 | // The icon never takes input. | 848 | // The icon never takes input. | ||
| 840 | - | dndNoEvent[ports.InputRegionChanged](t, p.events) | 849 | + | dndNoEvent[ports.InputRegionChanged](t, p.s, p.events) |
| 841 | 850 | ||||
| 842 | p.commands <- ports.DataDragCancel{} | 851 | p.commands <- ports.DataDragCancel{} | ||
| 843 | p.a.expect(t, "cancelled") | 852 | p.a.expect(t, "cancelled") | ||
| @@ -1064,8 +1073,8 @@ func TestDataDragAsk(t *testing.T) { | |||||
| 1064 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain") | 1073 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain") | ||
| 1065 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | 1074 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | ||
| 1066 | p.a.expect(t, "target") | 1075 | p.a.expect(t, "target") | ||
| 1067 | - | p.b.none(t) // no offer.action after the drop | 1076 | + | p.b.none(t, p.commands) // no offer.action after the drop |
| 1068 | - | p.a.none(t) // the final action waits for the finish | 1077 | + | p.a.none(t, p.commands) // the final action waits for the finish |
| 1069 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish) | 1078 | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish) | ||
| 1070 | if ev := p.a.expect(t, "action"); ev.n != actMove { | 1079 | if ev := p.a.expect(t, "action"); ev.n != actMove { | ||
| 1071 | t.Fatalf("action %+v", ev) | 1080 | t.Fatalf("action %+v", ev) | ||
| @@ -1104,8 +1113,7 @@ func TestDataDragLegacyTarget(t *testing.T) { | |||||
| 1104 | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | 1113 | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | ||
| 1105 | p.b.until(t, "enter") | 1114 | p.b.until(t, "enter") | ||
| 1106 | id := p.b.lastOffer(t) | 1115 | id := p.b.lastOffer(t) | ||
| 1107 | - | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | 1116 | + | // Legacy accept is feedback only: deliberately never send it. |
| 1108 | - | p.a.expect(t, "target") | |||
| 1109 | return p, id | 1117 | return p, id | ||
| 1110 | } | 1118 | } | ||
| 1111 | t.Run("move only", func(t *testing.T) { | 1119 | t.Run("move only", func(t *testing.T) { | ||
| @@ -1152,3 +1160,101 @@ func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) { | |||||
| 1152 | t.Fatalf("icon %+v", got) | 1160 | t.Fatalf("icon %+v", got) | ||
| 1153 | } | 1161 | } | ||
| 1154 | } | 1162 | } | ||
| 1163 | + | ||||
| 1164 | + | func 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 | + | ||||
| 1195 | + | func 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 | + | } | |||
M internal/core/core.go +7 -2
| @@ -1153,8 +1153,13 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1153 | 1153 | if stop { | |
| 1154 | 1154 | return nil | |
| 1155 | 1155 | } | |
| 1156 | - | if publish && c.publish(ctx) != nil { | |
| 1157 | - | return nil | |
| 1156 | + | if publish { | |
| 1157 | + | if c.publish(ctx) != nil { | |
| 1158 | + | return nil | |
| 1159 | + | } | |
| 1160 | + | if c.dataDrag != nil && dataDragHitChanged(ev) && c.dataDragRetarget(ctx, c.motionTime) != nil { | |
| 1161 | + | return nil | |
| 1162 | + | } | |
| 1158 | 1163 | } | |
| 1159 | 1164 | case f, ok := <-c.ch.Frames: | |
| 1160 | 1165 | if c.securityCheckpoint(ctx) != nil { | |
| @@ -1153,8 +1153,13 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 1153 | if stop { | 1153 | if stop { | ||
| 1154 | return nil | 1154 | return nil | ||
| 1155 | } | 1155 | } | ||
| 1156 | - | if publish && c.publish(ctx) != nil { | 1156 | + | if publish { |
| 1157 | - | return nil | 1157 | + | if c.publish(ctx) != nil { |
| 1158 | + | return nil | |||
| 1159 | + | } | |||
| 1160 | + | if c.dataDrag != nil && dataDragHitChanged(ev) && c.dataDragRetarget(ctx, c.motionTime) != nil { | |||
| 1161 | + | return nil | |||
| 1162 | + | } | |||
| 1158 | } | 1163 | } | ||
| 1159 | case f, ok := <-c.ch.Frames: | 1164 | case f, ok := <-c.ch.Frames: | ||
| 1160 | if c.securityCheckpoint(ctx) != nil { | 1165 | if c.securityCheckpoint(ctx) != nil { | ||
M internal/core/datadrag.go +15 -0
| @@ -94,6 +94,21 @@ func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |||
| 94 | 94 | return nil | |
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | + | // dataDragHitChanged excludes notifications that only change metadata, | |
| 98 | + | // capture state or the drag icon. The type switch allocates nothing. | |
| 99 | + | func dataDragHitChanged(ev ports.ClientEvent) bool { | |
| 100 | + | switch ev.(type) { | |
| 101 | + | case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped, | |
| 102 | + | ports.WindowResized, ports.PopupRequest, ports.PopupMapped, | |
| 103 | + | ports.OutputPower, ports.WindowParent, ports.WindowModal, | |
| 104 | + | ports.WindowUnmapped, ports.PointerWarp, ports.WindowMoveRequest, | |
| 105 | + | ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate: | |
| 106 | + | return true | |
| 107 | + | default: | |
| 108 | + | return false | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | ||
| 97 | 112 | // dataDragRetarget re-routes a still cursor: the target is told only when | |
| 98 | 113 | // it or the point in it changed. | |
| 99 | 114 | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | |
| @@ -94,6 +94,21 @@ func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |||||
| 94 | return nil | 94 | return nil | ||
| 95 | } | 95 | } | ||
| 96 | 96 | ||||
| 97 | + | // dataDragHitChanged excludes notifications that only change metadata, | |||
| 98 | + | // capture state or the drag icon. The type switch allocates nothing. | |||
| 99 | + | func dataDragHitChanged(ev ports.ClientEvent) bool { | |||
| 100 | + | switch ev.(type) { | |||
| 101 | + | case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped, | |||
| 102 | + | ports.WindowResized, ports.PopupRequest, ports.PopupMapped, | |||
| 103 | + | ports.OutputPower, ports.WindowParent, ports.WindowModal, | |||
| 104 | + | ports.WindowUnmapped, ports.PointerWarp, ports.WindowMoveRequest, | |||
| 105 | + | ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate: | |||
| 106 | + | return true | |||
| 107 | + | default: | |||
| 108 | + | return false | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 97 | // dataDragRetarget re-routes a still cursor: the target is told only when | 112 | // dataDragRetarget re-routes a still cursor: the target is told only when | ||
| 98 | // it or the point in it changed. | 113 | // it or the point in it changed. | ||
| 99 | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | 114 | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | ||
A internal/core/datadrag_alloc_test.go +34 -0
| @@ -0,0 +1,34 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestDataDragUnchangedRetargetAllocations(t *testing.T) { | |
| 11 | + | c := publishRig(t, 1) | |
| 12 | + | c.cursorX, c.cursorY = 20, 30 | |
| 13 | + | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 14 | + | if id == 0 { | |
| 15 | + | t.Fatal("no target") | |
| 16 | + | } | |
| 17 | + | c.dataDrag = &dataDragState{target: id, at: [2]float64{x, y}} | |
| 18 | + | ctx := context.Background() | |
| 19 | + | var ev ports.ClientEvent = ports.InputRegionChanged{ID: id, All: true} | |
| 20 | + | if n := testing.AllocsPerRun(1000, func() { | |
| 21 | + | if dataDragHitChanged(ev) { | |
| 22 | + | if err := c.dataDragRetarget(ctx, 0); err != nil { | |
| 23 | + | panic(err) | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | }); n != 0 { | |
| 27 | + | t.Fatalf("unchanged retarget: %g allocations, want 0", n) | |
| 28 | + | } | |
| 29 | + | for _, ev := range []ports.ClientEvent{ports.DataDragIcon{}, ports.WindowAppID{}, ports.CaptureSessionOpen{}, ports.PointerConstrained{}} { | |
| 30 | + | if dataDragHitChanged(ev) { | |
| 31 | + | t.Fatalf("rehit for irrelevant event %T", ev) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | } | |
| @@ -0,0 +1,34 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestDataDragUnchangedRetargetAllocations(t *testing.T) { | |||
| 11 | + | c := publishRig(t, 1) | |||
| 12 | + | c.cursorX, c.cursorY = 20, 30 | |||
| 13 | + | id, x, y := c.hit(c.cursorX, c.cursorY) | |||
| 14 | + | if id == 0 { | |||
| 15 | + | t.Fatal("no target") | |||
| 16 | + | } | |||
| 17 | + | c.dataDrag = &dataDragState{target: id, at: [2]float64{x, y}} | |||
| 18 | + | ctx := context.Background() | |||
| 19 | + | var ev ports.ClientEvent = ports.InputRegionChanged{ID: id, All: true} | |||
| 20 | + | if n := testing.AllocsPerRun(1000, func() { | |||
| 21 | + | if dataDragHitChanged(ev) { | |||
| 22 | + | if err := c.dataDragRetarget(ctx, 0); err != nil { | |||
| 23 | + | panic(err) | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | }); n != 0 { | |||
| 27 | + | t.Fatalf("unchanged retarget: %g allocations, want 0", n) | |||
| 28 | + | } | |||
| 29 | + | for _, ev := range []ports.ClientEvent{ports.DataDragIcon{}, ports.WindowAppID{}, ports.CaptureSessionOpen{}, ports.PointerConstrained{}} { | |||
| 30 | + | if dataDragHitChanged(ev) { | |||
| 31 | + | t.Fatalf("rehit for irrelevant event %T", ev) | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | } | |||
M internal/core/datadrag_test.go +12 -0
| @@ -575,3 +575,15 @@ func TestDataDragIgnoresCompositorSwipes(t *testing.T) { | |||
| 575 | 575 | t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows) | |
| 576 | 576 | } | |
| 577 | 577 | } | |
| 578 | + | ||
| 579 | + | func 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 | + | } | |
| @@ -575,3 +575,15 @@ func TestDataDragIgnoresCompositorSwipes(t *testing.T) { | |||||
| 575 | t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", 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 | } | 576 | } | ||
| 577 | } | 577 | } | ||
| 578 | + | ||||
| 579 | + | func 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 | + | } | |||