detached 3a8fe0c

fix(wayland): validate drag grabs and finish drops like wlroots

commit
3a8fe0c3f92daa999eb9ededac91e9f894153e6a
author
brice · · 2026-10-17 15:07 UTC
parent
45d51da
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +460 -56
M internal/adapters/wayland/dnd.go +96 -38
@@ -24,6 +24,8 @@ import (
2424 // did not cause: the source or the origin client died.
2525
2626 // dndAllActions is every wl_data_device_manager.dnd_action bit.
27+const actionAsk = uint32(wayland.DataDeviceManagerDndActionAsk)
28+
2729 const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk)
2830
2931 // dndState is the drag in progress.
@@ -61,9 +63,12 @@ type dndTarget struct {
6163 accepted string
6264 action uint32
6365 state int
64- // legacy: no device speaks wl_data_offer v3, so the accepted MIME type
65- // alone allows the drop and nothing finishes it.
66+ // legacy: no device speaks wl_data_offer v3: the target copies and
67+ // nothing finishes it; destroying its offers does.
6668 legacy bool
69+ // ask: the drop came with the ask action; the target picks the final
70+ // one after it.
71+ ask bool
6772 }
6873
6974 // dndOffer is a wl_data_offer of a drag.
@@ -143,7 +148,7 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or
143148 }
144149 client := r.Client()
145150 valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client &&
146- s.seat.press != 0 && serial == s.seat.press && s.seat.pressClient == client
151+ s.validGrab(origin, serial)
147152 if cs != nil && (cs.used || !valid) {
148153 // A used source is the selection, a drag source or already
149154 // cancelled: only a source nobody holds is cancelled again.
@@ -169,6 +174,10 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or
169174 iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) }
170175 }
171176 s.dnd = d
177+ // Core consumes the releases of a drag: the buttons are not held for
178+ // the next press.
179+ clear(s.seat.pointerHeld)
180+ s.seat.grabSerial = 0
172181 s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started")
173182 s.emit(ports.DataDragStarted{Icon: d.iconID})
174183 if iconSurf != nil && iconSurf.has {
@@ -179,6 +188,20 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or
179188 }
180189 }
181190
191+// validGrab reports whether serial is the press of the pointer button still
192+// held, on the surface with pointer focus (or a subsurface of its tree).
193+func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool {
194+ if serial == 0 || serial != s.seat.grabSerial || len(s.seat.pointerHeld) == 0 {
195+ return false
196+ }
197+ focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0)
198+ if focus == nil {
199+ return false
200+ }
201+ o, f := s.surfaces[origin.Resource], s.surfaces[focus.Resource]
202+ return o != nil && f != nil && o.root() == f.root()
203+}
204+
182205 // sourceBusy reports whether src is the selection, the source of the drag
183206 // or of a drop waiting for its finish.
184207 func (s *Server) sourceBusy(src *clipSource) bool {
@@ -314,12 +337,16 @@ func (s *Server) dndLeave(d *dndState) {
314337 }
315338 d.target = nil
316339 t.state = targetDone
340+ s.dndSendLeave(t)
341+ s.dndResetSource(t)
342+}
343+
344+func (s *Server) dndSendLeave(t *dndTarget) {
317345 for _, dev := range t.devices {
318346 if dev.Resource.Alive() {
319347 dev.SendLeave()
320348 }
321349 }
322- s.dndResetSource(t)
323350 }
324351
325352 // dndResetSource tells the source nothing accepts or acts any more.
@@ -338,41 +365,48 @@ func (s *Server) dndResetSource(t *dndTarget) {
338365 }
339366
340367 // dndDrop handles the release of the drag button: a target that accepted
341-// and agreed on an action gets drop; else the drag is cancelled.
368+// and agreed on an action gets drop, then leave (its offers stay usable until
369+// finish); else the drag is cancelled.
342370 func (s *Server) dndDrop(d *dndState) {
343371 t := d.target
344- switch {
345- case t == nil:
372+ if t == nil {
346373 s.dndEnd(d, true, false)
347- case d.src == nil:
374+ return
375+ }
376+ if d.src == nil {
348377 // Internal drag: the origin client decides alone.
349378 d.target = nil
350379 t.state = targetDone
351380 s.dndSendDrop(t)
381+ s.dndSendLeave(t)
352382 s.dndEnd(d, false, false)
353- case t.accepted == "" || t.action == 0 && !t.legacy:
354- s.dndEnd(d, true, false)
355- default:
356- d.target = nil
357- t.state = targetDropped
358- if t.legacy {
359- // A pre-v3 target copies and nothing finishes its transfer.
360- if t.action == 0 {
361- t.action = uint32(wayland.DataDeviceManagerDndActionCopy)
362- if d.src.alive() && d.src.res.Version() >= 3 {
363- d.src.res.SendAction(t.action)
364- }
383+ return
384+ }
385+ if t.legacy {
386+ // A pre-v3 target cannot negotiate: it copies.
387+ action := chooseAction(uint32(wayland.DataDeviceManagerDndActionCopy)&d.src.srcActions(), 0)
388+ if action != t.action {
389+ t.action = action
390+ if action != 0 && d.src.alive() && d.src.res.Version() >= 3 {
391+ d.src.res.SendAction(action)
365392 }
366- } else {
367- s.dndDrops = append(s.dndDrops, t)
368393 }
369- s.dndSendDrop(t)
370- if d.src.alive() && d.src.res.Version() >= 3 {
371- d.src.res.SendDndDropPerformed()
372- }
373- s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped")
374- s.dndEnd(d, false, false)
375394 }
395+ if t.accepted == "" || t.action == 0 {
396+ s.dndEnd(d, true, false)
397+ return
398+ }
399+ d.target = nil
400+ t.state = targetDropped
401+ t.ask = t.action == actionAsk
402+ s.dndDrops = append(s.dndDrops, t)
403+ s.dndSendDrop(t)
404+ s.dndSendLeave(t)
405+ if d.src.alive() && d.src.res.Version() >= 3 {
406+ d.src.res.SendDndDropPerformed()
407+ }
408+ s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped")
409+ s.dndEnd(d, false, false)
376410 }
377411
378412 func (s *Server) dndSendDrop(t *dndTarget) {
@@ -447,34 +481,47 @@ func (s *Server) dndCancelAll() {
447481 }
448482 }
449483
450-// dndOfferGone runs when an offer of a drag is destroyed: a dropped target
451-// whose offers are all gone without finish cancels the source.
484+// dndOfferGone runs when an offer of a drag is destroyed. With none left on
485+// a live target, nothing is accepted any more. With none left on a dropped
486+// one the transfer is over: a pre-v3 target has finished, any other
487+// cancelled it.
452488 func (s *Server) dndOfferGone(o *dndOffer) {
453489 t := o.t
454490 t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o })
455- if t.state == targetDropped && !t.legacy && len(t.offers) == 0 {
491+ if len(t.offers) != 0 {
492+ return
493+ }
494+ switch t.state {
495+ case targetActive:
496+ s.dndResetSource(t)
497+ case targetDropped:
456498 if src := t.d.src; src != nil && src.alive() {
457- src.cancel()
499+ if !t.legacy {
500+ src.cancel()
501+ } else if src.res.Version() >= 3 {
502+ src.res.SendDndFinished()
503+ }
458504 }
459505 s.dndDropDone(t)
460506 }
461507 }
462508
463509 // update recomputes the action after the target or the source changed
464-// something, and tells both when it changed.
510+// something, and tells both when it changed. After an ask drop the choice is
511+// silent until finish.
465512 func (o *dndOffer) update() {
466513 t, src := o.t, o.t.d.src
467- if t.state == targetDone || src == nil || !src.alive() {
514+ if t.state == targetDone || t.state == targetDropped && !t.ask || src == nil || !src.alive() {
468515 return
469516 }
470517 action := chooseAction(o.actions&src.srcActions(), o.preferred)
471518 if action != t.action {
472519 t.action = action
473- if src.res.Version() >= 3 {
520+ if t.state == targetActive && src.res.Version() >= 3 {
474521 src.res.SendAction(action)
475522 }
476523 }
477- if action != o.sent {
524+ if t.state == targetActive && action != o.sent {
478525 o.sent = action
479526 if o.res.Version() >= 3 {
480527 o.res.SendAction(action)
@@ -484,7 +531,7 @@ func (o *dndOffer) update() {
484531
485532 func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) {
486533 t := o.t
487- if t.state != targetActive || !t.d.src.alive() {
534+ if t.state != targetActive && !(t.state == targetDropped && t.ask) || !t.d.src.alive() {
488535 return
489536 }
490537 t.accepted = mime
@@ -513,12 +560,19 @@ func (o *dndOffer) Finish(r *wayland.DataOffer) {
513560 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop")
514561 return
515562 }
516- if t.action == 0 || t.action == uint32(wayland.DataDeviceManagerDndActionAsk) {
563+ if t.action == 0 || t.action == actionAsk {
517564 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action")
518565 return
519566 }
567+ if t.accepted == "" {
568+ r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without an accepted MIME type")
569+ return
570+ }
520571 o.server.dndDropDone(t)
521572 if src := t.d.src; src.alive() && src.res.Version() >= 3 {
573+ if t.ask {
574+ src.res.SendAction(t.action)
575+ }
522576 src.res.SendDndFinished()
523577 }
524578 }
@@ -532,6 +586,10 @@ func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) {
532586 r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action")
533587 return
534588 }
589+ if t := o.t; t.state == targetDropped && t.ask && preferred != 0 && t.d.src != nil && preferred&t.d.src.srcActions() == 0 {
590+ r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "final action not allowed by the source")
591+ return
592+ }
535593 o.actions, o.preferred = actions, preferred
536594 o.update()
537595 }
@@ -24,6 +24,8 @@ import (
24 // did not cause: the source or the origin client died.24 // did not cause: the source or the origin client died.
25 25
26 // dndAllActions is every wl_data_device_manager.dnd_action bit.26 // dndAllActions is every wl_data_device_manager.dnd_action bit.
27+const actionAsk = uint32(wayland.DataDeviceManagerDndActionAsk)
28+
27 const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk)29 const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk)
28 30
29 // dndState is the drag in progress.31 // dndState is the drag in progress.
@@ -61,9 +63,12 @@ type dndTarget struct {
61 accepted string63 accepted string
62 action uint3264 action uint32
63 state int65 state int
64- // legacy: no device speaks wl_data_offer v3, so the accepted MIME type66+ // legacy: no device speaks wl_data_offer v3: the target copies and
65- // alone allows the drop and nothing finishes it.67+ // nothing finishes it; destroying its offers does.
66 legacy bool68 legacy bool
69+ // ask: the drop came with the ask action; the target picks the final
70+ // one after it.
71+ ask bool
67 }72 }
68 73
69 // dndOffer is a wl_data_offer of a drag.74 // dndOffer is a wl_data_offer of a drag.
@@ -143,7 +148,7 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or
143 }148 }
144 client := r.Client()149 client := r.Client()
145 valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client &&150 valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client &&
146- s.seat.press != 0 && serial == s.seat.press && s.seat.pressClient == client151+ s.validGrab(origin, serial)
147 if cs != nil && (cs.used || !valid) {152 if cs != nil && (cs.used || !valid) {
148 // A used source is the selection, a drag source or already153 // A used source is the selection, a drag source or already
149 // cancelled: only a source nobody holds is cancelled again.154 // cancelled: only a source nobody holds is cancelled again.
@@ -169,6 +174,10 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or
169 iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) }174 iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) }
170 }175 }
171 s.dnd = d176 s.dnd = d
177+ // Core consumes the releases of a drag: the buttons are not held for
178+ // the next press.
179+ clear(s.seat.pointerHeld)
180+ s.seat.grabSerial = 0
172 s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started")181 s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started")
173 s.emit(ports.DataDragStarted{Icon: d.iconID})182 s.emit(ports.DataDragStarted{Icon: d.iconID})
174 if iconSurf != nil && iconSurf.has {183 if iconSurf != nil && iconSurf.has {
@@ -179,6 +188,20 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or
179 }188 }
180 }189 }
181 190
191+// validGrab reports whether serial is the press of the pointer button still
192+// held, on the surface with pointer focus (or a subsurface of its tree).
193+func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool {
194+ if serial == 0 || serial != s.seat.grabSerial || len(s.seat.pointerHeld) == 0 {
195+ return false
196+ }
197+ focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0)
198+ if focus == nil {
199+ return false
200+ }
201+ o, f := s.surfaces[origin.Resource], s.surfaces[focus.Resource]
202+ return o != nil && f != nil && o.root() == f.root()
203+}
204+
182 // sourceBusy reports whether src is the selection, the source of the drag205 // sourceBusy reports whether src is the selection, the source of the drag
183 // or of a drop waiting for its finish.206 // or of a drop waiting for its finish.
184 func (s *Server) sourceBusy(src *clipSource) bool {207 func (s *Server) sourceBusy(src *clipSource) bool {
@@ -314,12 +337,16 @@ func (s *Server) dndLeave(d *dndState) {
314 }337 }
315 d.target = nil338 d.target = nil
316 t.state = targetDone339 t.state = targetDone
340+ s.dndSendLeave(t)
341+ s.dndResetSource(t)
342+}
343+
344+func (s *Server) dndSendLeave(t *dndTarget) {
317 for _, dev := range t.devices {345 for _, dev := range t.devices {
318 if dev.Resource.Alive() {346 if dev.Resource.Alive() {
319 dev.SendLeave()347 dev.SendLeave()
320 }348 }
321 }349 }
322- s.dndResetSource(t)
323 }350 }
324 351
325 // dndResetSource tells the source nothing accepts or acts any more.352 // dndResetSource tells the source nothing accepts or acts any more.
@@ -338,41 +365,48 @@ func (s *Server) dndResetSource(t *dndTarget) {
338 }365 }
339 366
340 // dndDrop handles the release of the drag button: a target that accepted367 // dndDrop handles the release of the drag button: a target that accepted
341-// and agreed on an action gets drop; else the drag is cancelled.368+// and agreed on an action gets drop, then leave (its offers stay usable until
369+// finish); else the drag is cancelled.
342 func (s *Server) dndDrop(d *dndState) {370 func (s *Server) dndDrop(d *dndState) {
343 t := d.target371 t := d.target
344- switch {372+ if t == nil {
345- case t == nil:
346 s.dndEnd(d, true, false)373 s.dndEnd(d, true, false)
347- case d.src == nil:374+ return
375+ }
376+ if d.src == nil {
348 // Internal drag: the origin client decides alone.377 // Internal drag: the origin client decides alone.
349 d.target = nil378 d.target = nil
350 t.state = targetDone379 t.state = targetDone
351 s.dndSendDrop(t)380 s.dndSendDrop(t)
381+ s.dndSendLeave(t)
352 s.dndEnd(d, false, false)382 s.dndEnd(d, false, false)
353- case t.accepted == "" || t.action == 0 && !t.legacy:383+ return
354- s.dndEnd(d, true, false)384+ }
355- default:385+ if t.legacy {
356- d.target = nil386+ // A pre-v3 target cannot negotiate: it copies.
357- t.state = targetDropped387+ action := chooseAction(uint32(wayland.DataDeviceManagerDndActionCopy)&d.src.srcActions(), 0)
358- if t.legacy {388+ if action != t.action {
359- // A pre-v3 target copies and nothing finishes its transfer.389+ t.action = action
360- if t.action == 0 {390+ if action != 0 && d.src.alive() && d.src.res.Version() >= 3 {
361- t.action = uint32(wayland.DataDeviceManagerDndActionCopy)391+ d.src.res.SendAction(action)
362- if d.src.alive() && d.src.res.Version() >= 3 {
363- d.src.res.SendAction(t.action)
364- }
365 }392 }
366- } else {
367- s.dndDrops = append(s.dndDrops, t)
368 }393 }
369- s.dndSendDrop(t)
370- if d.src.alive() && d.src.res.Version() >= 3 {
371- d.src.res.SendDndDropPerformed()
372- }
373- s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped")
374- s.dndEnd(d, false, false)
375 }394 }
395+ if t.accepted == "" || t.action == 0 {
396+ s.dndEnd(d, true, false)
397+ return
398+ }
399+ d.target = nil
400+ t.state = targetDropped
401+ t.ask = t.action == actionAsk
402+ s.dndDrops = append(s.dndDrops, t)
403+ s.dndSendDrop(t)
404+ s.dndSendLeave(t)
405+ if d.src.alive() && d.src.res.Version() >= 3 {
406+ d.src.res.SendDndDropPerformed()
407+ }
408+ s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped")
409+ s.dndEnd(d, false, false)
376 }410 }
377 411
378 func (s *Server) dndSendDrop(t *dndTarget) {412 func (s *Server) dndSendDrop(t *dndTarget) {
@@ -447,34 +481,47 @@ func (s *Server) dndCancelAll() {
447 }481 }
448 }482 }
449 483
450-// dndOfferGone runs when an offer of a drag is destroyed: a dropped target484+// dndOfferGone runs when an offer of a drag is destroyed. With none left on
451-// whose offers are all gone without finish cancels the source.485+// a live target, nothing is accepted any more. With none left on a dropped
486+// one the transfer is over: a pre-v3 target has finished, any other
487+// cancelled it.
452 func (s *Server) dndOfferGone(o *dndOffer) {488 func (s *Server) dndOfferGone(o *dndOffer) {
453 t := o.t489 t := o.t
454 t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o })490 t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o })
455- if t.state == targetDropped && !t.legacy && len(t.offers) == 0 {491+ if len(t.offers) != 0 {
492+ return
493+ }
494+ switch t.state {
495+ case targetActive:
496+ s.dndResetSource(t)
497+ case targetDropped:
456 if src := t.d.src; src != nil && src.alive() {498 if src := t.d.src; src != nil && src.alive() {
457- src.cancel()499+ if !t.legacy {
500+ src.cancel()
501+ } else if src.res.Version() >= 3 {
502+ src.res.SendDndFinished()
503+ }
458 }504 }
459 s.dndDropDone(t)505 s.dndDropDone(t)
460 }506 }
461 }507 }
462 508
463 // update recomputes the action after the target or the source changed509 // update recomputes the action after the target or the source changed
464-// something, and tells both when it changed.510+// something, and tells both when it changed. After an ask drop the choice is
511+// silent until finish.
465 func (o *dndOffer) update() {512 func (o *dndOffer) update() {
466 t, src := o.t, o.t.d.src513 t, src := o.t, o.t.d.src
467- if t.state == targetDone || src == nil || !src.alive() {514+ if t.state == targetDone || t.state == targetDropped && !t.ask || src == nil || !src.alive() {
468 return515 return
469 }516 }
470 action := chooseAction(o.actions&src.srcActions(), o.preferred)517 action := chooseAction(o.actions&src.srcActions(), o.preferred)
471 if action != t.action {518 if action != t.action {
472 t.action = action519 t.action = action
473- if src.res.Version() >= 3 {520+ if t.state == targetActive && src.res.Version() >= 3 {
474 src.res.SendAction(action)521 src.res.SendAction(action)
475 }522 }
476 }523 }
477- if action != o.sent {524+ if t.state == targetActive && action != o.sent {
478 o.sent = action525 o.sent = action
479 if o.res.Version() >= 3 {526 if o.res.Version() >= 3 {
480 o.res.SendAction(action)527 o.res.SendAction(action)
@@ -484,7 +531,7 @@ func (o *dndOffer) update() {
484 531
485 func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) {532 func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) {
486 t := o.t533 t := o.t
487- if t.state != targetActive || !t.d.src.alive() {534+ if t.state != targetActive && !(t.state == targetDropped && t.ask) || !t.d.src.alive() {
488 return535 return
489 }536 }
490 t.accepted = mime537 t.accepted = mime
@@ -513,12 +560,19 @@ func (o *dndOffer) Finish(r *wayland.DataOffer) {
513 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop")560 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop")
514 return561 return
515 }562 }
516- if t.action == 0 || t.action == uint32(wayland.DataDeviceManagerDndActionAsk) {563+ if t.action == 0 || t.action == actionAsk {
517 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action")564 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action")
518 return565 return
519 }566 }
567+ if t.accepted == "" {
568+ r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without an accepted MIME type")
569+ return
570+ }
520 o.server.dndDropDone(t)571 o.server.dndDropDone(t)
521 if src := t.d.src; src.alive() && src.res.Version() >= 3 {572 if src := t.d.src; src.alive() && src.res.Version() >= 3 {
573+ if t.ask {
574+ src.res.SendAction(t.action)
575+ }
522 src.res.SendDndFinished()576 src.res.SendDndFinished()
523 }577 }
524 }578 }
@@ -532,6 +586,10 @@ func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) {
532 r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action")586 r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action")
533 return587 return
534 }588 }
589+ if t := o.t; t.state == targetDropped && t.ask && preferred != 0 && t.d.src != nil && preferred&t.d.src.srcActions() == 0 {
590+ r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "final action not allowed by the source")
591+ return
592+ }
535 o.actions, o.preferred = actions, preferred593 o.actions, o.preferred = actions, preferred
536 o.update()594 o.update()
537 }595 }
M internal/adapters/wayland/dnd_test.go +340 -14
@@ -3,6 +3,7 @@ package wayland
33 import (
44 "context"
55 "errors"
6+ "sync/atomic"
67 "testing"
78 "time"
89
@@ -32,6 +33,8 @@ type dndDeviceEvents struct {
3233 wlturbo.BaseProxy
3334 c *wlturbo.Display
3435 log chan dndEvent
36+ // offer is the id of the latest data_offer.
37+ offer atomic.Uint32
3538 }
3639
3740 func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {
@@ -39,6 +42,7 @@ func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {
3942 case uint16(wayland.DataDeviceEventDataOffer):
4043 o := &dndOfferEvents{log: p.log}
4144 o.SetID(e.Uint32())
45+ p.offer.Store(o.ID())
4246 registerWireProxy(p.c, o)
4347 case uint16(wayland.DataDeviceEventEnter):
4448 serial, _ := e.Uint32(), e.Uint32()
@@ -98,6 +102,7 @@ func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) {
98102
99103 // dndClient is a client with a toplevel, a pointer and a data device.
100104 type dndClient struct {
105+ keys chan uint32
101106 c *wlturbo.Display
102107 manager uint32
103108 device *dndDeviceEvents
@@ -109,17 +114,39 @@ type dndClient struct {
109114 log chan dndEvent
110115 }
111116
117+// keySerials records the serial of each key event.
118+type keySerials struct {
119+ wlturbo.BaseProxy
120+ serials chan uint32
121+}
122+
123+func (p *keySerials) Dispatch(e *wlturbo.Event) {
124+ if e.Opcode == uint16(wayland.KeyboardEventKey) {
125+ p.serials <- e.Uint32()
126+ }
127+}
128+
112129 func 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.
135+func newDndClientAt(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent, version uint32) *dndClient {
113136 t.Helper()
114137 c := protocolClient(t, s, dir)
115- d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8)}
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}
116140 seat := bindVersion(t, c, "wl_seat", 8)
117141 registerProtocol(t, c, seat)
118142 ptr := &pressSerials{serials: d.serials}
119143 ptr.SetID(c.AllocateID())
120144 registerWireProxy(c, ptr)
121145 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID())
122- d.manager = bindVersion(t, c, "wl_data_device_manager", 3)
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)
123150 d.device = &dndDeviceEvents{c: c, log: d.log}
124151 d.device.SetID(c.AllocateID())
125152 registerWireProxy(c, d.device)
@@ -132,6 +159,16 @@ func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.Clien
132159 return d
133160 }
134161
162+// lastOffer is the id of the latest offer the client got.
163+func (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+
135172 // next returns the next event the client sees.
136173 func (d *dndClient) next(t *testing.T) dndEvent {
137174 t.Helper()
@@ -211,9 +248,73 @@ func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uin
211248 }
212249 }
213250
251+// press2 returns the next pointer button serial.
252+func (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.
270+func (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.
277+func (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.
297+func (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+
214309 func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) {
215310 t.Helper()
216- requestProtocol(t, d.c, d.device.ID(), wayland.DataDeviceRequestStartDrag, src, d.surf, icon, serial)
311+ d.startDragFrom(t, src, d.surf, icon, serial)
312+}
313+
314+// startDragFrom is startDrag with an origin surface.
315+func (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)
217318 if err := d.c.Roundtrip(); err != nil {
218319 t.Fatal(err)
219320 }
@@ -352,6 +453,7 @@ func TestDataDragBetweenClients(t *testing.T) {
352453 // Finishing before the drop is a protocol error, so only after it.
353454 p.commands <- ports.DataDragDrop{}
354455 p.b.expect(t, "drop")
456+ p.b.expect(t, "leave")
355457 p.a.expect(t, "drop_performed")
356458
357459 var fds [2]int
@@ -516,6 +618,42 @@ func TestDataDragInvalidStart(t *testing.T) {
516618 p.a.expect(t, "cancelled")
517619 dndNoEvent[ports.DataDragStarted](t, p.events)
518620 })
621+ t.Run("key serial", func(t *testing.T) {
622+ p := newDndPair(t)
623+ p.a.press(t, p.commands)
624+ p.a.release(t, p.commands)
625+ serial := p.a.key(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.events)
629+ })
630+ t.Run("serial after the release", func(t *testing.T) {
631+ p := newDndPair(t)
632+ serial := p.a.press(t, p.commands)
633+ p.a.release(t, p.commands)
634+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
635+ p.a.expect(t, "cancelled")
636+ dndNoEvent[ports.DataDragStarted](t, p.events)
637+ })
638+ t.Run("other surface of the client", func(t *testing.T) {
639+ p := newDndPair(t)
640+ serial := p.a.press(t, p.commands)
641+ other := p.a.surfaceNew(t)
642+ p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial)
643+ p.a.expect(t, "cancelled")
644+ dndNoEvent[ports.DataDragStarted](t, p.events)
645+ })
646+ 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.
648+ p := newDndPair(t)
649+ first := p.a.press(t, p.commands)
650+ p.commands <- ports.PointerButtonTo{ID: p.a.window, Button: 0x111, Pressed: true, Time: 2 * time.Millisecond}
651+ second := p.a.press2(t)
652+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second)
653+ p.a.expect(t, "cancelled")
654+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first)
655+ dndNextEvent[ports.DataDragStarted](t, p.events)
656+ })
519657 t.Run("used source", func(t *testing.T) {
520658 p := newDndPair(t)
521659 src := p.start(t, 0)
@@ -615,6 +753,7 @@ func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) {
615753 p.commands <- ports.DataDragDrop{}
616754 p.b.expect(t, "offer_action")
617755 p.b.expect(t, "drop")
756+ p.b.expect(t, "leave")
618757 p.a.expect(t, "drop_performed")
619758 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
620759 p.a.expect(t, "cancelled")
@@ -749,9 +888,9 @@ func TestDataDragIconDestroyed(t *testing.T) {
749888 p.enter(t)
750889 }
751890
752-// A7: the session lock cancels a drag at once: target leave, source
753-// cancelled, icon gone.
754-func TestDataDragLockCancels(t *testing.T) {
891+// lockedPair is a pair of clients on a server that can lock the session.
892+func lockedPair(t *testing.T) (*dndPair, string) {
893+ t.Helper()
755894 gate := &sessionsecurity.Gate{}
756895 events := make(chan ports.ClientEvent, 128)
757896 commands := make(chan ports.ClientCommand, 32)
@@ -777,6 +916,24 @@ func TestDataDragLockCancels(t *testing.T) {
777916 p := &dndPair{s: s, events: events, commands: commands}
778917 p.a = newDndClient(t, s, dir, events)
779918 p.b = newDndClient(t, s, dir, events)
919+ return p, dir
920+}
921+
922+func lockSession(t *testing.T, s *Server, dir string) {
923+ t.Helper()
924+ lc := protocolClient(t, s, dir)
925+ if _, err := bindLockManager(t, lc).Lock(); err != nil {
926+ t.Fatal(err)
927+ }
928+ if err := lc.Roundtrip(); err != nil {
929+ t.Fatal(err)
930+ }
931+}
932+
933+// A7: the session lock cancels a drag at once: target leave, source
934+// cancelled, icon gone.
935+func TestDataDragLockCancels(t *testing.T) {
936+ p, dir := lockedPair(t)
780937 icon := p.a.surfaceNew(t)
781938 buf := p.a.buffer(t, 8, 6)
782939 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
@@ -786,16 +943,10 @@ func TestDataDragLockCancels(t *testing.T) {
786943 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
787944 p.enter(t)
788945
789- lc := protocolClient(t, s, dir)
790- if _, err := bindLockManager(t, lc).Lock(); err != nil {
791- t.Fatal(err)
792- }
793- if err := lc.Roundtrip(); err != nil {
794- t.Fatal(err)
795- }
946+ lockSession(t, p.s, dir)
796947 p.b.expect(t, "leave")
797948 p.a.expect(t, "cancelled")
798- if iconContent(t, s, id) {
949+ if iconContent(t, p.s, id) {
799950 t.Fatal("icon content kept")
800951 }
801952 // A new drag is refused while protected.
@@ -803,3 +954,178 @@ func TestDataDragLockCancels(t *testing.T) {
803954 p.a.startDrag(t, src2, 0, uint32(1))
804955 p.a.expect(t, "cancelled")
805956 }
957+
958+// GO-006: a dropped transfer waiting for its finish dies with the lock, for
959+// a v3 and a pre-v3 target alike.
960+func TestDataDropLockCancelsPending(t *testing.T) {
961+ for _, version := range []uint32{3, 2} {
962+ p, dir := lockedPair(t)
963+ p.b = newDndClientAt(t, p.s, dir, p.events, version)
964+ p.start(t, 0)
965+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6}
966+ p.b.until(t, "enter")
967+ offer := p.b.lastOffer(t)
968+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
969+ if version >= 3 {
970+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
971+ }
972+ p.a.until(t, "target")
973+ p.commands <- ports.DataDragDrop{}
974+ p.a.until(t, "drop_performed")
975+ lockSession(t, p.s, dir)
976+ p.a.expect(t, "cancelled")
977+ }
978+}
979+
980+// A subsurface of the focused tree is a valid origin.
981+func TestDataDragFromSubsurface(t *testing.T) {
982+ p := newDndPair(t)
983+ serial := p.a.press(t, p.commands)
984+ child := p.a.surfaceNew(t)
985+ subc := bindProtocol(t, p.a.c, "wl_subcompositor")
986+ requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf)
987+ p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial)
988+ dndNextEvent[ports.DataDragStarted](t, p.events)
989+}
990+
991+// GO-003: destroying the offers of the target withdraws the acceptance: the
992+// release then cancels, with no drop.
993+func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) {
994+ p := newDndPair(t)
995+ p.start(t, 0)
996+ offer := p.enter(t)
997+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
998+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
999+ p.a.expect(t, "target")
1000+ p.a.expect(t, "action")
1001+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
1002+ if err := p.b.c.Roundtrip(); err != nil {
1003+ t.Fatal(err)
1004+ }
1005+ if ev := p.a.expect(t, "target"); ev.mime != "" {
1006+ t.Fatalf("target %+v", ev)
1007+ }
1008+ if ev := p.a.expect(t, "action"); ev.n != 0 {
1009+ t.Fatalf("action %+v", ev)
1010+ }
1011+ p.commands <- ports.DataDragDrop{}
1012+ got := p.b.until(t, "leave")
1013+ for _, ev := range got {
1014+ if ev.name == "drop" {
1015+ t.Fatalf("drop after the offer was destroyed: %+v", got)
1016+ }
1017+ }
1018+ p.a.expect(t, "cancelled")
1019+}
1020+
1021+// GO-004: with ask, the target picks the final action after the drop; the
1022+// source hears it just before dnd_finished.
1023+func TestDataDragAsk(t *testing.T) {
1024+ p := newDndPair(t)
1025+ src := p.a.source(t, actCopy|actMove|actAsk, "text/plain")
1026+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1027+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1028+ p.b.until(t, "enter")
1029+ id := p.b.lastOffer(t)
1030+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1031+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1032+ p.a.expect(t, "target")
1033+ if ev := p.a.expect(t, "action"); ev.n != actAsk {
1034+ t.Fatalf("action %+v", ev)
1035+ }
1036+ p.commands <- ports.DataDragDrop{}
1037+ p.b.until(t, "leave")
1038+ p.a.expect(t, "drop_performed")
1039+
1040+ // Finishing while the action is ask is an error; the choice comes first.
1041+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain")
1042+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1043+ p.a.expect(t, "target")
1044+ p.b.none(t) // no offer.action after the drop
1045+ p.a.none(t) // the final action waits for the finish
1046+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish)
1047+ if ev := p.a.expect(t, "action"); ev.n != actMove {
1048+ t.Fatalf("action %+v", ev)
1049+ }
1050+ p.a.expect(t, "finished")
1051+}
1052+
1053+// A final preferred action outside the source's is an error.
1054+func TestDataDragAskFinalActionNotInSource(t *testing.T) {
1055+ p := newDndPair(t)
1056+ src := p.a.source(t, actCopy|actAsk, "text/plain")
1057+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1058+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1059+ p.b.until(t, "enter")
1060+ id := p.b.lastOffer(t)
1061+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1062+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1063+ p.a.expect(t, "target")
1064+ p.a.expect(t, "action")
1065+ p.commands <- ports.DataDragDrop{}
1066+ p.b.until(t, "leave")
1067+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove)
1068+ expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction))
1069+}
1070+
1071+// GO-005: a pre-v3 target copies: a source that does not allow copy is
1072+// cancelled, one that does gets it.
1073+func TestDataDragLegacyTarget(t *testing.T) {
1074+ setup := func(t *testing.T, actions uint32) (*dndPair, uint32) {
1075+ s, events, commands, dir := keyboardServer(t)
1076+ p := &dndPair{s: s, events: events, commands: commands}
1077+ p.a = newDndClient(t, s, dir, events)
1078+ p.b = newDndClientAt(t, s, dir, events, 2)
1079+ src := p.a.source(t, actions, "text/plain")
1080+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1081+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1082+ p.b.until(t, "enter")
1083+ id := p.b.lastOffer(t)
1084+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1085+ p.a.expect(t, "target")
1086+ return p, id
1087+ }
1088+ t.Run("move only", func(t *testing.T) {
1089+ p, _ := setup(t, actMove)
1090+ p.commands <- ports.DataDragDrop{}
1091+ got := p.b.until(t, "leave")
1092+ for _, ev := range got {
1093+ if ev.name == "drop" {
1094+ t.Fatalf("drop for a source without copy: %+v", got)
1095+ }
1096+ }
1097+ p.a.until(t, "cancelled")
1098+ })
1099+ t.Run("copy", func(t *testing.T) {
1100+ p, id := setup(t, actCopy|actMove)
1101+ p.commands <- ports.DataDragDrop{}
1102+ p.b.expect(t, "drop")
1103+ p.b.expect(t, "leave")
1104+ if ev := p.a.expect(t, "action"); ev.n != actCopy {
1105+ t.Fatalf("action %+v", ev)
1106+ }
1107+ p.a.expect(t, "drop_performed")
1108+ // GO-006: destroying the last offer finishes the transfer.
1109+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy)
1110+ p.a.expect(t, "finished")
1111+ })
1112+}
1113+
1114+// GO-007: of several offset requests before one commit, the last counts.
1115+func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) {
1116+ p := newDndPair(t)
1117+ icon := p.a.surfaceNew(t)
1118+ buf := p.a.buffer(t, 8, 6)
1119+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
1120+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
1121+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1122+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1))
1123+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7))
1124+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
1125+ if err := p.a.c.Roundtrip(); err != nil {
1126+ t.Fatal(err)
1127+ }
1128+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) {
1129+ t.Fatalf("icon %+v", got)
1130+ }
1131+}
@@ -3,6 +3,7 @@ package wayland
3 import (3 import (
4 "context"4 "context"
5 "errors"5 "errors"
6+ "sync/atomic"
6 "testing"7 "testing"
7 "time"8 "time"
8 9
@@ -32,6 +33,8 @@ type dndDeviceEvents struct {
32 wlturbo.BaseProxy33 wlturbo.BaseProxy
33 c *wlturbo.Display34 c *wlturbo.Display
34 log chan dndEvent35 log chan dndEvent
36+ // offer is the id of the latest data_offer.
37+ offer atomic.Uint32
35 }38 }
36 39
37 func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {40 func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {
@@ -39,6 +42,7 @@ func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {
39 case uint16(wayland.DataDeviceEventDataOffer):42 case uint16(wayland.DataDeviceEventDataOffer):
40 o := &dndOfferEvents{log: p.log}43 o := &dndOfferEvents{log: p.log}
41 o.SetID(e.Uint32())44 o.SetID(e.Uint32())
45+ p.offer.Store(o.ID())
42 registerWireProxy(p.c, o)46 registerWireProxy(p.c, o)
43 case uint16(wayland.DataDeviceEventEnter):47 case uint16(wayland.DataDeviceEventEnter):
44 serial, _ := e.Uint32(), e.Uint32()48 serial, _ := e.Uint32(), e.Uint32()
@@ -98,6 +102,7 @@ func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) {
98 102
99 // dndClient is a client with a toplevel, a pointer and a data device.103 // dndClient is a client with a toplevel, a pointer and a data device.
100 type dndClient struct {104 type dndClient struct {
105+ keys chan uint32
101 c *wlturbo.Display106 c *wlturbo.Display
102 manager uint32107 manager uint32
103 device *dndDeviceEvents108 device *dndDeviceEvents
@@ -109,17 +114,39 @@ type dndClient struct {
109 log chan dndEvent114 log chan dndEvent
110 }115 }
111 116
117+// keySerials records the serial of each key event.
118+type keySerials struct {
119+ wlturbo.BaseProxy
120+ serials chan uint32
121+}
122+
123+func (p *keySerials) Dispatch(e *wlturbo.Event) {
124+ if e.Opcode == uint16(wayland.KeyboardEventKey) {
125+ p.serials <- e.Uint32()
126+ }
127+}
128+
112 func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent) *dndClient {129 func 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.
135+func newDndClientAt(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent, version uint32) *dndClient {
113 t.Helper()136 t.Helper()
114 c := protocolClient(t, s, dir)137 c := protocolClient(t, s, dir)
115- d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8)}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}
116 seat := bindVersion(t, c, "wl_seat", 8)140 seat := bindVersion(t, c, "wl_seat", 8)
117 registerProtocol(t, c, seat)141 registerProtocol(t, c, seat)
118 ptr := &pressSerials{serials: d.serials}142 ptr := &pressSerials{serials: d.serials}
119 ptr.SetID(c.AllocateID())143 ptr.SetID(c.AllocateID())
120 registerWireProxy(c, ptr)144 registerWireProxy(c, ptr)
121 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID())145 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID())
122- d.manager = bindVersion(t, c, "wl_data_device_manager", 3)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)
123 d.device = &dndDeviceEvents{c: c, log: d.log}150 d.device = &dndDeviceEvents{c: c, log: d.log}
124 d.device.SetID(c.AllocateID())151 d.device.SetID(c.AllocateID())
125 registerWireProxy(c, d.device)152 registerWireProxy(c, d.device)
@@ -132,6 +159,16 @@ func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.Clien
132 return d159 return d
133 }160 }
134 161
162+// lastOffer is the id of the latest offer the client got.
163+func (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+
135 // next returns the next event the client sees.172 // next returns the next event the client sees.
136 func (d *dndClient) next(t *testing.T) dndEvent {173 func (d *dndClient) next(t *testing.T) dndEvent {
137 t.Helper()174 t.Helper()
@@ -211,9 +248,73 @@ func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uin
211 }248 }
212 }249 }
213 250
251+// press2 returns the next pointer button serial.
252+func (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.
270+func (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.
277+func (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.
297+func (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+
214 func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) {309 func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) {
215 t.Helper()310 t.Helper()
216- requestProtocol(t, d.c, d.device.ID(), wayland.DataDeviceRequestStartDrag, src, d.surf, icon, serial)311+ d.startDragFrom(t, src, d.surf, icon, serial)
312+}
313+
314+// startDragFrom is startDrag with an origin surface.
315+func (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)
217 if err := d.c.Roundtrip(); err != nil {318 if err := d.c.Roundtrip(); err != nil {
218 t.Fatal(err)319 t.Fatal(err)
219 }320 }
@@ -352,6 +453,7 @@ func TestDataDragBetweenClients(t *testing.T) {
352 // Finishing before the drop is a protocol error, so only after it.453 // Finishing before the drop is a protocol error, so only after it.
353 p.commands <- ports.DataDragDrop{}454 p.commands <- ports.DataDragDrop{}
354 p.b.expect(t, "drop")455 p.b.expect(t, "drop")
456+ p.b.expect(t, "leave")
355 p.a.expect(t, "drop_performed")457 p.a.expect(t, "drop_performed")
356 458
357 var fds [2]int459 var fds [2]int
@@ -516,6 +618,42 @@ func TestDataDragInvalidStart(t *testing.T) {
516 p.a.expect(t, "cancelled")618 p.a.expect(t, "cancelled")
517 dndNoEvent[ports.DataDragStarted](t, p.events)619 dndNoEvent[ports.DataDragStarted](t, p.events)
518 })620 })
621+ t.Run("key serial", func(t *testing.T) {
622+ p := newDndPair(t)
623+ p.a.press(t, p.commands)
624+ p.a.release(t, p.commands)
625+ serial := p.a.key(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.events)
629+ })
630+ t.Run("serial after the release", func(t *testing.T) {
631+ p := newDndPair(t)
632+ serial := p.a.press(t, p.commands)
633+ p.a.release(t, p.commands)
634+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
635+ p.a.expect(t, "cancelled")
636+ dndNoEvent[ports.DataDragStarted](t, p.events)
637+ })
638+ t.Run("other surface of the client", func(t *testing.T) {
639+ p := newDndPair(t)
640+ serial := p.a.press(t, p.commands)
641+ other := p.a.surfaceNew(t)
642+ p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial)
643+ p.a.expect(t, "cancelled")
644+ dndNoEvent[ports.DataDragStarted](t, p.events)
645+ })
646+ 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.
648+ p := newDndPair(t)
649+ first := p.a.press(t, p.commands)
650+ p.commands <- ports.PointerButtonTo{ID: p.a.window, Button: 0x111, Pressed: true, Time: 2 * time.Millisecond}
651+ second := p.a.press2(t)
652+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second)
653+ p.a.expect(t, "cancelled")
654+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first)
655+ dndNextEvent[ports.DataDragStarted](t, p.events)
656+ })
519 t.Run("used source", func(t *testing.T) {657 t.Run("used source", func(t *testing.T) {
520 p := newDndPair(t)658 p := newDndPair(t)
521 src := p.start(t, 0)659 src := p.start(t, 0)
@@ -615,6 +753,7 @@ func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) {
615 p.commands <- ports.DataDragDrop{}753 p.commands <- ports.DataDragDrop{}
616 p.b.expect(t, "offer_action")754 p.b.expect(t, "offer_action")
617 p.b.expect(t, "drop")755 p.b.expect(t, "drop")
756+ p.b.expect(t, "leave")
618 p.a.expect(t, "drop_performed")757 p.a.expect(t, "drop_performed")
619 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)758 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
620 p.a.expect(t, "cancelled")759 p.a.expect(t, "cancelled")
@@ -749,9 +888,9 @@ func TestDataDragIconDestroyed(t *testing.T) {
749 p.enter(t)888 p.enter(t)
750 }889 }
751 890
752-// A7: the session lock cancels a drag at once: target leave, source891+// lockedPair is a pair of clients on a server that can lock the session.
753-// cancelled, icon gone.892+func lockedPair(t *testing.T) (*dndPair, string) {
754-func TestDataDragLockCancels(t *testing.T) {893+ t.Helper()
755 gate := &sessionsecurity.Gate{}894 gate := &sessionsecurity.Gate{}
756 events := make(chan ports.ClientEvent, 128)895 events := make(chan ports.ClientEvent, 128)
757 commands := make(chan ports.ClientCommand, 32)896 commands := make(chan ports.ClientCommand, 32)
@@ -777,6 +916,24 @@ func TestDataDragLockCancels(t *testing.T) {
777 p := &dndPair{s: s, events: events, commands: commands}916 p := &dndPair{s: s, events: events, commands: commands}
778 p.a = newDndClient(t, s, dir, events)917 p.a = newDndClient(t, s, dir, events)
779 p.b = newDndClient(t, s, dir, events)918 p.b = newDndClient(t, s, dir, events)
919+ return p, dir
920+}
921+
922+func lockSession(t *testing.T, s *Server, dir string) {
923+ t.Helper()
924+ lc := protocolClient(t, s, dir)
925+ if _, err := bindLockManager(t, lc).Lock(); err != nil {
926+ t.Fatal(err)
927+ }
928+ if err := lc.Roundtrip(); err != nil {
929+ t.Fatal(err)
930+ }
931+}
932+
933+// A7: the session lock cancels a drag at once: target leave, source
934+// cancelled, icon gone.
935+func TestDataDragLockCancels(t *testing.T) {
936+ p, dir := lockedPair(t)
780 icon := p.a.surfaceNew(t)937 icon := p.a.surfaceNew(t)
781 buf := p.a.buffer(t, 8, 6)938 buf := p.a.buffer(t, 8, 6)
782 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))939 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
@@ -786,16 +943,10 @@ func TestDataDragLockCancels(t *testing.T) {
786 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon943 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
787 p.enter(t)944 p.enter(t)
788 945
789- lc := protocolClient(t, s, dir)946+ lockSession(t, p.s, dir)
790- if _, err := bindLockManager(t, lc).Lock(); err != nil {
791- t.Fatal(err)
792- }
793- if err := lc.Roundtrip(); err != nil {
794- t.Fatal(err)
795- }
796 p.b.expect(t, "leave")947 p.b.expect(t, "leave")
797 p.a.expect(t, "cancelled")948 p.a.expect(t, "cancelled")
798- if iconContent(t, s, id) {949+ if iconContent(t, p.s, id) {
799 t.Fatal("icon content kept")950 t.Fatal("icon content kept")
800 }951 }
801 // A new drag is refused while protected.952 // A new drag is refused while protected.
@@ -803,3 +954,178 @@ func TestDataDragLockCancels(t *testing.T) {
803 p.a.startDrag(t, src2, 0, uint32(1))954 p.a.startDrag(t, src2, 0, uint32(1))
804 p.a.expect(t, "cancelled")955 p.a.expect(t, "cancelled")
805 }956 }
957+
958+// GO-006: a dropped transfer waiting for its finish dies with the lock, for
959+// a v3 and a pre-v3 target alike.
960+func TestDataDropLockCancelsPending(t *testing.T) {
961+ for _, version := range []uint32{3, 2} {
962+ p, dir := lockedPair(t)
963+ p.b = newDndClientAt(t, p.s, dir, p.events, version)
964+ p.start(t, 0)
965+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6}
966+ p.b.until(t, "enter")
967+ offer := p.b.lastOffer(t)
968+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
969+ if version >= 3 {
970+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
971+ }
972+ p.a.until(t, "target")
973+ p.commands <- ports.DataDragDrop{}
974+ p.a.until(t, "drop_performed")
975+ lockSession(t, p.s, dir)
976+ p.a.expect(t, "cancelled")
977+ }
978+}
979+
980+// A subsurface of the focused tree is a valid origin.
981+func TestDataDragFromSubsurface(t *testing.T) {
982+ p := newDndPair(t)
983+ serial := p.a.press(t, p.commands)
984+ child := p.a.surfaceNew(t)
985+ subc := bindProtocol(t, p.a.c, "wl_subcompositor")
986+ requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf)
987+ p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial)
988+ dndNextEvent[ports.DataDragStarted](t, p.events)
989+}
990+
991+// GO-003: destroying the offers of the target withdraws the acceptance: the
992+// release then cancels, with no drop.
993+func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) {
994+ p := newDndPair(t)
995+ p.start(t, 0)
996+ offer := p.enter(t)
997+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
998+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
999+ p.a.expect(t, "target")
1000+ p.a.expect(t, "action")
1001+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
1002+ if err := p.b.c.Roundtrip(); err != nil {
1003+ t.Fatal(err)
1004+ }
1005+ if ev := p.a.expect(t, "target"); ev.mime != "" {
1006+ t.Fatalf("target %+v", ev)
1007+ }
1008+ if ev := p.a.expect(t, "action"); ev.n != 0 {
1009+ t.Fatalf("action %+v", ev)
1010+ }
1011+ p.commands <- ports.DataDragDrop{}
1012+ got := p.b.until(t, "leave")
1013+ for _, ev := range got {
1014+ if ev.name == "drop" {
1015+ t.Fatalf("drop after the offer was destroyed: %+v", got)
1016+ }
1017+ }
1018+ p.a.expect(t, "cancelled")
1019+}
1020+
1021+// GO-004: with ask, the target picks the final action after the drop; the
1022+// source hears it just before dnd_finished.
1023+func TestDataDragAsk(t *testing.T) {
1024+ p := newDndPair(t)
1025+ src := p.a.source(t, actCopy|actMove|actAsk, "text/plain")
1026+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1027+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1028+ p.b.until(t, "enter")
1029+ id := p.b.lastOffer(t)
1030+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1031+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1032+ p.a.expect(t, "target")
1033+ if ev := p.a.expect(t, "action"); ev.n != actAsk {
1034+ t.Fatalf("action %+v", ev)
1035+ }
1036+ p.commands <- ports.DataDragDrop{}
1037+ p.b.until(t, "leave")
1038+ p.a.expect(t, "drop_performed")
1039+
1040+ // Finishing while the action is ask is an error; the choice comes first.
1041+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain")
1042+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1043+ p.a.expect(t, "target")
1044+ p.b.none(t) // no offer.action after the drop
1045+ p.a.none(t) // the final action waits for the finish
1046+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish)
1047+ if ev := p.a.expect(t, "action"); ev.n != actMove {
1048+ t.Fatalf("action %+v", ev)
1049+ }
1050+ p.a.expect(t, "finished")
1051+}
1052+
1053+// A final preferred action outside the source's is an error.
1054+func TestDataDragAskFinalActionNotInSource(t *testing.T) {
1055+ p := newDndPair(t)
1056+ src := p.a.source(t, actCopy|actAsk, "text/plain")
1057+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1058+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1059+ p.b.until(t, "enter")
1060+ id := p.b.lastOffer(t)
1061+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1062+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1063+ p.a.expect(t, "target")
1064+ p.a.expect(t, "action")
1065+ p.commands <- ports.DataDragDrop{}
1066+ p.b.until(t, "leave")
1067+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove)
1068+ expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction))
1069+}
1070+
1071+// GO-005: a pre-v3 target copies: a source that does not allow copy is
1072+// cancelled, one that does gets it.
1073+func TestDataDragLegacyTarget(t *testing.T) {
1074+ setup := func(t *testing.T, actions uint32) (*dndPair, uint32) {
1075+ s, events, commands, dir := keyboardServer(t)
1076+ p := &dndPair{s: s, events: events, commands: commands}
1077+ p.a = newDndClient(t, s, dir, events)
1078+ p.b = newDndClientAt(t, s, dir, events, 2)
1079+ src := p.a.source(t, actions, "text/plain")
1080+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1081+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1082+ p.b.until(t, "enter")
1083+ id := p.b.lastOffer(t)
1084+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1085+ p.a.expect(t, "target")
1086+ return p, id
1087+ }
1088+ t.Run("move only", func(t *testing.T) {
1089+ p, _ := setup(t, actMove)
1090+ p.commands <- ports.DataDragDrop{}
1091+ got := p.b.until(t, "leave")
1092+ for _, ev := range got {
1093+ if ev.name == "drop" {
1094+ t.Fatalf("drop for a source without copy: %+v", got)
1095+ }
1096+ }
1097+ p.a.until(t, "cancelled")
1098+ })
1099+ t.Run("copy", func(t *testing.T) {
1100+ p, id := setup(t, actCopy|actMove)
1101+ p.commands <- ports.DataDragDrop{}
1102+ p.b.expect(t, "drop")
1103+ p.b.expect(t, "leave")
1104+ if ev := p.a.expect(t, "action"); ev.n != actCopy {
1105+ t.Fatalf("action %+v", ev)
1106+ }
1107+ p.a.expect(t, "drop_performed")
1108+ // GO-006: destroying the last offer finishes the transfer.
1109+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy)
1110+ p.a.expect(t, "finished")
1111+ })
1112+}
1113+
1114+// GO-007: of several offset requests before one commit, the last counts.
1115+func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) {
1116+ p := newDndPair(t)
1117+ icon := p.a.surfaceNew(t)
1118+ buf := p.a.buffer(t, 8, 6)
1119+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
1120+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
1121+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1122+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1))
1123+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7))
1124+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
1125+ if err := p.a.c.Roundtrip(); err != nil {
1126+ t.Fatal(err)
1127+ }
1128+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) {
1129+ t.Fatalf("icon %+v", got)
1130+ }
1131+}
M internal/adapters/wayland/seat.go +17 -0
@@ -43,6 +43,11 @@ type seatState struct {
4343 // pressClient: popup grabs must come from it.
4444 press uint32
4545 pressClient server.Client
46+ // pointerHeld are the pointer buttons held, and grabSerial the serial of
47+ // the press that took the first one: the only serial start_drag accepts
48+ // (wlr_seat_validate_pointer_grab_serial).
49+ pointerHeld map[uint32]bool
50+ grabSerial uint32
4651 // pressAt and focusAt date the last press and keyboard focus change,
4752 // for xdg-activation tokens.
4853 pressAt, focusAt time.Time
@@ -133,6 +138,18 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
133138 if c.Pressed {
134139 state = 1
135140 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
141+ if len(s.seat.pointerHeld) == 0 {
142+ s.seat.grabSerial = s.serial
143+ }
144+ if s.seat.pointerHeld == nil {
145+ s.seat.pointerHeld = make(map[uint32]bool)
146+ }
147+ s.seat.pointerHeld[c.Button] = true
148+ } else {
149+ delete(s.seat.pointerHeld, c.Button)
150+ if len(s.seat.pointerHeld) == 0 {
151+ s.seat.grabSerial = 0
152+ }
136153 }
137154 for _, p := range pointers {
138155 s.stampPointer(p, c.Time)
@@ -43,6 +43,11 @@ type seatState struct {
43 // pressClient: popup grabs must come from it.43 // pressClient: popup grabs must come from it.
44 press uint3244 press uint32
45 pressClient server.Client45 pressClient server.Client
46+ // pointerHeld are the pointer buttons held, and grabSerial the serial of
47+ // the press that took the first one: the only serial start_drag accepts
48+ // (wlr_seat_validate_pointer_grab_serial).
49+ pointerHeld map[uint32]bool
50+ grabSerial uint32
46 // pressAt and focusAt date the last press and keyboard focus change,51 // pressAt and focusAt date the last press and keyboard focus change,
47 // for xdg-activation tokens.52 // for xdg-activation tokens.
48 pressAt, focusAt time.Time53 pressAt, focusAt time.Time
@@ -133,6 +138,18 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
133 if c.Pressed {138 if c.Pressed {
134 state = 1139 state = 1
135 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()140 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
141+ if len(s.seat.pointerHeld) == 0 {
142+ s.seat.grabSerial = s.serial
143+ }
144+ if s.seat.pointerHeld == nil {
145+ s.seat.pointerHeld = make(map[uint32]bool)
146+ }
147+ s.seat.pointerHeld[c.Button] = true
148+ } else {
149+ delete(s.seat.pointerHeld, c.Button)
150+ if len(s.seat.pointerHeld) == 0 {
151+ s.seat.grabSerial = 0
152+ }
136 }153 }
137 for _, p := range pointers {154 for _, p := range pointers {
138 s.stampPointer(p, c.Time)155 s.stampPointer(p, c.Time)
M internal/adapters/wayland/session_lock.go +2 -0
@@ -147,6 +147,8 @@ func (s *Server) resetProtectionInput() {
147147 clear(s.seat.grabKeys)
148148 s.seat.modState = ports.ModState{}
149149 s.seat.press, s.seat.pressClient = 0, server.Client{}
150+ clear(s.seat.pointerHeld)
151+ s.seat.grabSerial = 0
150152 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
151153 s.cursorSurface = nil
152154 }
@@ -147,6 +147,8 @@ func (s *Server) resetProtectionInput() {
147 clear(s.seat.grabKeys)147 clear(s.seat.grabKeys)
148 s.seat.modState = ports.ModState{}148 s.seat.modState = ports.ModState{}
149 s.seat.press, s.seat.pressClient = 0, server.Client{}149 s.seat.press, s.seat.pressClient = 0, server.Client{}
150+ clear(s.seat.pointerHeld)
151+ s.seat.grabSerial = 0
150 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}152 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
151 s.cursorSurface = nil153 s.cursorSurface = nil
152 }154 }
M internal/adapters/wayland/surface.go +5 -4
@@ -139,8 +139,9 @@ type pendingCommit struct {
139139 // barrier and wait are fifo requests; at the commit timestamp.
140140 barrier, wait bool
141141 at time.Time
142- // dx, dy accumulate the attach offsets and wl_surface.offset requests;
143- // only a drag icon uses them.
142+ // dx, dy are the last attach offset or wl_surface.offset request since
143+ // the commit (the latest replaces the earlier); only a drag icon uses
144+ // them.
144145 dx, dy int32
145146 // damage is in surface (logical) pixels, bufDamage in buffer pixels.
146147 damage, bufDamage []ports.Rect
@@ -360,7 +361,7 @@ func (s *surface) Attach(r *wayland.Surface, b *wayland.Buffer, dx, dy int32) {
360361 s.next.buffer = b
361362 s.next.attached = true
362363 if r.Version() < 5 {
363- s.next.dx, s.next.dy = s.next.dx+dx, s.next.dy+dy
364+ s.next.dx, s.next.dy = dx, dy
364365 }
365366 }
366367 func (s *surface) Frame(r *wayland.Surface, id uint32) {
@@ -778,6 +779,6 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
778779
779780 // Offset moves the next commit's surface (v5); only a drag icon uses it.
780781 func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) {
781- s.next.dx, s.next.dy = s.next.dx+dx, s.next.dy+dy
782+ s.next.dx, s.next.dy = dx, dy
782783 }
783784 func (*surface) GetRelease(*wayland.Surface, uint32) {}
@@ -139,8 +139,9 @@ type pendingCommit struct {
139 // barrier and wait are fifo requests; at the commit timestamp.139 // barrier and wait are fifo requests; at the commit timestamp.
140 barrier, wait bool140 barrier, wait bool
141 at time.Time141 at time.Time
142- // dx, dy accumulate the attach offsets and wl_surface.offset requests;142+ // dx, dy are the last attach offset or wl_surface.offset request since
143- // only a drag icon uses them.143+ // the commit (the latest replaces the earlier); only a drag icon uses
144+ // them.
144 dx, dy int32145 dx, dy int32
145 // damage is in surface (logical) pixels, bufDamage in buffer pixels.146 // damage is in surface (logical) pixels, bufDamage in buffer pixels.
146 damage, bufDamage []ports.Rect147 damage, bufDamage []ports.Rect
@@ -360,7 +361,7 @@ func (s *surface) Attach(r *wayland.Surface, b *wayland.Buffer, dx, dy int32) {
360 s.next.buffer = b361 s.next.buffer = b
361 s.next.attached = true362 s.next.attached = true
362 if r.Version() < 5 {363 if r.Version() < 5 {
363- s.next.dx, s.next.dy = s.next.dx+dx, s.next.dy+dy364+ s.next.dx, s.next.dy = dx, dy
364 }365 }
365 }366 }
366 func (s *surface) Frame(r *wayland.Surface, id uint32) {367 func (s *surface) Frame(r *wayland.Surface, id uint32) {
@@ -778,6 +779,6 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
778 779
779 // Offset moves the next commit's surface (v5); only a drag icon uses it.780 // Offset moves the next commit's surface (v5); only a drag icon uses it.
780 func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) {781 func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) {
781- s.next.dx, s.next.dy = s.next.dx+dx, s.next.dy+dy782+ s.next.dx, s.next.dy = dx, dy
782 }783 }
783 func (*surface) GetRelease(*wayland.Surface, uint32) {}784 func (*surface) GetRelease(*wayland.Surface, uint32) {}

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