detached 71aa990

Merge pull request 'feat: drag and drop between Wayland clients' (#61) from feat/data-dnd into main

commit
71aa990cd0f89bb11b6b74f9f4c17f48839c7abc
author
bnema · · 2026-10-17 20:38 UTC
merge of
458251f + e05cb15 · diff against the first parent
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
20 files · +2759 -31
M docs/desktop.md +1 -1
@@ -38,7 +38,7 @@ bind.cmd+v = spawn cliphist-pick
3838 cliphist list | fuzzel --dmenu | cliphist decode | wl-copy
3939 ```
4040
41-Drag and drop is not supported yet.
41+Drag and drop works between clients: the surface under the pointer gets the offer, the drop and the chosen action (copy, move or ask, from the modifiers the clients read), and the drag icon follows the cursor. Escape, a refused drop, a dying source or the session lock cancel the drag.
4242
4343 ## Input methods
4444
@@ -38,7 +38,7 @@ bind.cmd+v = spawn cliphist-pick
38 cliphist list | fuzzel --dmenu | cliphist decode | wl-copy38 cliphist list | fuzzel --dmenu | cliphist decode | wl-copy
39 ```39 ```
40 40
41-Drag and drop is not supported yet.41+Drag and drop works between clients: the surface under the pointer gets the offer, the drop and the chosen action (copy, move or ask, from the modifiers the clients read), and the drag icon follows the cursor. Escape, a refused drop, a dying source or the session lock cancel the drag.
42 42
43 ## Input methods43 ## Input methods
44 44
M internal/adapters/wayland/clipboard.go +33 -16
@@ -13,7 +13,9 @@ import (
1313 // keyboard focus may set them and sees them; clipboard managers such as
1414 // cliphist and wl-clipboard use ext_data_control_v1, which sees and sets
1515 // both without focus. Like wlroots, any client may bind it. Pasting hands the reader's pipe to the source client:
16-// the data never goes through neferwl. Drag and drop is not implemented.
16+// the data never goes through neferwl. Drag and drop (wl_data_device
17+// start_drag, dnd.go) is a drag core runs and wayland speaks to the source
18+// and the target; the primary selection and ext_data_control have none.
1719
1820 // protected is a defensive DISPLAY admission check, not lock ownership.
1921 func (s *Server) protected() bool {
@@ -35,6 +37,11 @@ type clipSource struct {
3537 // used: the source was set, rejected or dragged; it cannot be set
3638 // again and takes no more MIME types.
3739 used bool
40+ // res is the wl_data_source (nil for the other protocols); actions are
41+ // its set_actions, for drag and drop only (dnd.go).
42+ res *wayland.DataSource
43+ actions uint32
44+ actionsSet bool
3845 }
3946
4047 // addMime records a MIME type offered before the source is used.
@@ -175,6 +182,7 @@ func (s *Server) addClipDevice(d *clipDevice) {
175182
176183 // sourceGone clears the selections held by a destroyed source.
177184 func (s *Server) sourceGone(src *clipSource) {
185+ s.dndSourceGone(src)
178186 for kind := range s.selections {
179187 if s.selections[kind] == src {
180188 s.selections[kind] = nil
@@ -236,6 +244,7 @@ func (m dataManager) CreateDataSource(r *wayland.DataDeviceManager, id uint32) {
236244 if err != nil {
237245 return
238246 }
247+ src.res = res
239248 src.send = func(mime string, fd int) { res.SendSend(mime, fd) }
240249 src.cancel = res.SendCancelled
241250 src.alive = res.Resource.Alive
@@ -251,6 +260,7 @@ func (m dataManager) GetDataDevice(r *wayland.DataDeviceManager, id uint32, _ *w
251260 if err != nil {
252261 return
253262 }
263+ s.addDataDevice(r.Client(), res)
254264 s.addClipDevice(&clipDevice{client: r.Client(), kinds: [2]bool{selClipboard: true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) {
255265 if src == nil {
256266 res.SendSelection(nil)
@@ -273,24 +283,26 @@ type dataSource struct{ src *clipSource }
273283
274284 func (d dataSource) Offer(_ *wayland.DataSource, mime string) { d.src.addMime(mime) }
275285 func (dataSource) Destroy(*wayland.DataSource) {}
276-func (dataSource) SetActions(*wayland.DataSource, uint32) {}
286+
287+// SetActions records the actions the source allows for a drag: once only,
288+// before the drag.
289+func (d dataSource) SetActions(r *wayland.DataSource, actions uint32) {
290+ switch {
291+ case actions&^dndAllActions != 0:
292+ r.PostError(uint32(wayland.DataSourceErrorInvalidActionMask), "invalid action mask")
293+ case d.src.actionsSet || d.src.used:
294+ r.PostError(uint32(wayland.DataSourceErrorInvalidSource), "actions set twice or on a used source")
295+ default:
296+ d.src.actions, d.src.actionsSet = actions, true
297+ }
298+}
277299
278300 type dataDevice struct{ server *Server }
279301
280-// StartDrag is not implemented: the drag is cancelled.
281-func (d dataDevice) StartDrag(_ *wayland.DataDevice, src *wayland.DataSource, _, _ *wayland.Surface, _ uint32) {
282- if src == nil {
283- return
284- }
285- cs := d.server.dataSources[src.Resource]
286- if cs == nil || cs.used {
287- // A used source is the selection or already cancelled.
288- return
289- }
290- cs.used = true
291- if src.Resource.Alive() {
292- src.SendCancelled()
293- }
302+// StartDrag starts a drag answering the client's current press (dnd.go);
303+// anything else cancels the source.
304+func (d dataDevice) StartDrag(r *wayland.DataDevice, src *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) {
305+ d.server.startDrag(r, src, origin, icon, serial)
294306 }
295307 func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, _ uint32) {
296308 s := d.server
@@ -300,6 +312,11 @@ func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource,
300312 if src == nil {
301313 return
302314 }
315+ if src.actionsSet {
316+ // Actions make a source drag-and-drop only.
317+ res.PostError(uint32(wayland.DataSourceErrorInvalidSource), "source with dnd actions used as selection")
318+ return
319+ }
303320 }
304321 s.setFocused(r.Client(), selClipboard, src)
305322 }
@@ -13,7 +13,9 @@ import (
13 // keyboard focus may set them and sees them; clipboard managers such as13 // keyboard focus may set them and sees them; clipboard managers such as
14 // cliphist and wl-clipboard use ext_data_control_v1, which sees and sets14 // cliphist and wl-clipboard use ext_data_control_v1, which sees and sets
15 // both without focus. Like wlroots, any client may bind it. Pasting hands the reader's pipe to the source client:15 // both without focus. Like wlroots, any client may bind it. Pasting hands the reader's pipe to the source client:
16-// the data never goes through neferwl. Drag and drop is not implemented.16+// the data never goes through neferwl. Drag and drop (wl_data_device
17+// start_drag, dnd.go) is a drag core runs and wayland speaks to the source
18+// and the target; the primary selection and ext_data_control have none.
17 19
18 // protected is a defensive DISPLAY admission check, not lock ownership.20 // protected is a defensive DISPLAY admission check, not lock ownership.
19 func (s *Server) protected() bool {21 func (s *Server) protected() bool {
@@ -35,6 +37,11 @@ type clipSource struct {
35 // used: the source was set, rejected or dragged; it cannot be set37 // used: the source was set, rejected or dragged; it cannot be set
36 // again and takes no more MIME types.38 // again and takes no more MIME types.
37 used bool39 used bool
40+ // res is the wl_data_source (nil for the other protocols); actions are
41+ // its set_actions, for drag and drop only (dnd.go).
42+ res *wayland.DataSource
43+ actions uint32
44+ actionsSet bool
38 }45 }
39 46
40 // addMime records a MIME type offered before the source is used.47 // addMime records a MIME type offered before the source is used.
@@ -175,6 +182,7 @@ func (s *Server) addClipDevice(d *clipDevice) {
175 182
176 // sourceGone clears the selections held by a destroyed source.183 // sourceGone clears the selections held by a destroyed source.
177 func (s *Server) sourceGone(src *clipSource) {184 func (s *Server) sourceGone(src *clipSource) {
185+ s.dndSourceGone(src)
178 for kind := range s.selections {186 for kind := range s.selections {
179 if s.selections[kind] == src {187 if s.selections[kind] == src {
180 s.selections[kind] = nil188 s.selections[kind] = nil
@@ -236,6 +244,7 @@ func (m dataManager) CreateDataSource(r *wayland.DataDeviceManager, id uint32) {
236 if err != nil {244 if err != nil {
237 return245 return
238 }246 }
247+ src.res = res
239 src.send = func(mime string, fd int) { res.SendSend(mime, fd) }248 src.send = func(mime string, fd int) { res.SendSend(mime, fd) }
240 src.cancel = res.SendCancelled249 src.cancel = res.SendCancelled
241 src.alive = res.Resource.Alive250 src.alive = res.Resource.Alive
@@ -251,6 +260,7 @@ func (m dataManager) GetDataDevice(r *wayland.DataDeviceManager, id uint32, _ *w
251 if err != nil {260 if err != nil {
252 return261 return
253 }262 }
263+ s.addDataDevice(r.Client(), res)
254 s.addClipDevice(&clipDevice{client: r.Client(), kinds: [2]bool{selClipboard: true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) {264 s.addClipDevice(&clipDevice{client: r.Client(), kinds: [2]bool{selClipboard: true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) {
255 if src == nil {265 if src == nil {
256 res.SendSelection(nil)266 res.SendSelection(nil)
@@ -273,24 +283,26 @@ type dataSource struct{ src *clipSource }
273 283
274 func (d dataSource) Offer(_ *wayland.DataSource, mime string) { d.src.addMime(mime) }284 func (d dataSource) Offer(_ *wayland.DataSource, mime string) { d.src.addMime(mime) }
275 func (dataSource) Destroy(*wayland.DataSource) {}285 func (dataSource) Destroy(*wayland.DataSource) {}
276-func (dataSource) SetActions(*wayland.DataSource, uint32) {}286+
287+// SetActions records the actions the source allows for a drag: once only,
288+// before the drag.
289+func (d dataSource) SetActions(r *wayland.DataSource, actions uint32) {
290+ switch {
291+ case actions&^dndAllActions != 0:
292+ r.PostError(uint32(wayland.DataSourceErrorInvalidActionMask), "invalid action mask")
293+ case d.src.actionsSet || d.src.used:
294+ r.PostError(uint32(wayland.DataSourceErrorInvalidSource), "actions set twice or on a used source")
295+ default:
296+ d.src.actions, d.src.actionsSet = actions, true
297+ }
298+}
277 299
278 type dataDevice struct{ server *Server }300 type dataDevice struct{ server *Server }
279 301
280-// StartDrag is not implemented: the drag is cancelled.302+// StartDrag starts a drag answering the client's current press (dnd.go);
281-func (d dataDevice) StartDrag(_ *wayland.DataDevice, src *wayland.DataSource, _, _ *wayland.Surface, _ uint32) {303+// anything else cancels the source.
282- if src == nil {304+func (d dataDevice) StartDrag(r *wayland.DataDevice, src *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) {
283- return305+ d.server.startDrag(r, src, origin, icon, serial)
284- }
285- cs := d.server.dataSources[src.Resource]
286- if cs == nil || cs.used {
287- // A used source is the selection or already cancelled.
288- return
289- }
290- cs.used = true
291- if src.Resource.Alive() {
292- src.SendCancelled()
293- }
294 }306 }
295 func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, _ uint32) {307 func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, _ uint32) {
296 s := d.server308 s := d.server
@@ -300,6 +312,11 @@ func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource,
300 if src == nil {312 if src == nil {
301 return313 return
302 }314 }
315+ if src.actionsSet {
316+ // Actions make a source drag-and-drop only.
317+ res.PostError(uint32(wayland.DataSourceErrorInvalidSource), "source with dnd actions used as selection")
318+ return
319+ }
303 }320 }
304 s.setFocused(r.Client(), selClipboard, src)321 s.setFocused(r.Client(), selClipboard, src)
305 }322 }
A internal/adapters/wayland/dnd.go +594 -0
@@ -0,0 +1,594 @@
1+package wayland
2+
3+import (
4+ "slices"
5+
6+ "github.com/bnema/go-wayland-bindings/server/wayland"
7+ "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/server"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// Drag and drop (wl_data_device.start_drag). A client starts a drag with the
13+// serial of its current press; wayland validates it and tells core
14+// (ports.DataDragStarted). Core owns the pointer from then on: it names the
15+// surface under the cursor (DataDragFocus, DataDragMotion), the release
16+// (DataDragDrop) and the Escape key (DataDragCancel). Wayland speaks the
17+// data device protocol to the source and the target: enter, motion, leave
18+// and drop with an offer, and the action negotiation the wlroots way. The
19+// icon is a client surface with its own window ID; core draws it at the
20+// cursor from the geometry sent with DataDragIcon.
21+//
22+// Core ends a drag with DataDragDrop or DataDragCancel and is not told
23+// (the lock resets both sides). DataDragEnded tells core about an end it
24+// did not cause: the source or the origin client died.
25+
26+// dndAllActions is every wl_data_device_manager.dnd_action bit.
27+const actionAsk = uint32(wayland.DataDeviceManagerDndActionAsk)
28+
29+const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk)
30+
31+// dndState is the drag in progress.
32+type dndState struct {
33+ // src is the source; nil for a drag internal to the origin client.
34+ src *clipSource
35+ client server.Client
36+ // icon is the icon surface (nil: none or destroyed); iconID is the
37+ // window ID core knows it by (0: none).
38+ icon *surface
39+ iconID ports.WindowID
40+ // iconX, iconY is the icon's top left corner relative to the cursor:
41+ // the offsets of its commits add up. iconSent is the last geometry sent.
42+ iconX, iconY int
43+ iconSent ports.DataDragIcon
44+ target *dndTarget
45+}
46+
47+const (
48+ targetActive = iota
49+ targetDropped
50+ targetDone
51+)
52+
53+// dndTarget is the surface a drag entered. Its offers (one per data device
54+// of the client) share the negotiation. After the drop it lives on until
55+// the target finishes or destroys its offers.
56+type dndTarget struct {
57+ d *dndState
58+ surface *wayland.Surface
59+ devices []*wayland.DataDevice
60+ offers []*dndOffer
61+ // accepted is the MIME type the target accepted; action the action
62+ // chosen for the source.
63+ accepted string
64+ action uint32
65+ state int
66+ // legacy: no device speaks wl_data_offer v3: the target copies and
67+ // nothing finishes it; destroying its offers does.
68+ legacy bool
69+ // ask: the drop came with the ask action; the target picks the final
70+ // one after it.
71+ ask bool
72+}
73+
74+// dndOffer is a wl_data_offer of a drag.
75+type dndOffer struct {
76+ server *Server
77+ t *dndTarget
78+ res *wayland.DataOffer
79+ // actions and preferred are the target's set_actions; sent is the last
80+ // action told to this offer.
81+ actions, preferred, sent uint32
82+}
83+
84+// srcActions is what the source allows; a source that never said (a
85+// pre-v3 client) only copies.
86+func (src *clipSource) srcActions() uint32 {
87+ if src.actionsSet {
88+ return src.actions
89+ }
90+ return uint32(wayland.DataDeviceManagerDndActionCopy)
91+}
92+
93+// chooseAction picks the action like wlroots: the preferred one if both
94+// allow it, else the first of copy, move, ask.
95+func chooseAction(avail, preferred uint32) uint32 {
96+ if avail == 0 {
97+ return 0
98+ }
99+ if preferred&avail != 0 {
100+ return preferred
101+ }
102+ for _, a := range [...]uint32{uint32(wayland.DataDeviceManagerDndActionCopy), uint32(wayland.DataDeviceManagerDndActionMove), uint32(wayland.DataDeviceManagerDndActionAsk)} {
103+ if avail&a != 0 {
104+ return a
105+ }
106+ }
107+ return 0
108+}
109+
110+// addDataDevice tracks a client's data devices, the drag targets.
111+func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) {
112+ if s.dataDevices == nil {
113+ s.dataDevices = map[server.Client][]*wayland.DataDevice{}
114+ }
115+ s.dataDevices[c] = append(s.dataDevices[c], dev)
116+ dev.OnDestroy = func() {
117+ // Copy on write: a drag may be iterating the old list.
118+ list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev })
119+ if len(list) == 0 {
120+ delete(s.dataDevices, c)
121+ // An internal drag has nobody left to tell.
122+ if d := s.dnd; d != nil && d.client == c && d.src == nil {
123+ s.dndEnd(d, false, true)
124+ }
125+ } else {
126+ s.dataDevices[c] = list
127+ }
128+ }
129+}
130+
131+// startDrag handles wl_data_device.start_drag.
132+func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) {
133+ var cs *clipSource
134+ if srcRes != nil {
135+ if cs = s.dataSources[srcRes.Resource]; cs == nil {
136+ return
137+ }
138+ }
139+ var iconSurf *surface
140+ if icon != nil {
141+ if iconSurf = s.surfaces[icon.Resource]; iconSurf == nil {
142+ return
143+ }
144+ if iconSurf.kind != roleNone && iconSurf.kind != roleDragIcon {
145+ r.PostError(uint32(wayland.DataDeviceErrorRole), "icon surface already has another role")
146+ return
147+ }
148+ }
149+ client := r.Client()
150+ valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client &&
151+ s.validGrab(origin, serial)
152+ if cs != nil && (cs.used || !valid) {
153+ // A used source is the selection, a drag source or already
154+ // cancelled: only a source nobody holds is cancelled again.
155+ if !s.sourceBusy(cs) {
156+ cs.used = true
157+ if cs.alive() {
158+ cs.cancel()
159+ }
160+ }
161+ return
162+ }
163+ if !valid {
164+ return
165+ }
166+ d := &dndState{src: cs, client: client, icon: iconSurf}
167+ if cs != nil {
168+ cs.used = true
169+ }
170+ if iconSurf != nil {
171+ d.iconID = s.nextWindow
172+ s.nextWindow++
173+ iconSurf.kind = roleDragIcon
174+ iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) }
175+ }
176+ s.dnd = d
177+ // Core consumes the releases of a drag: the grab ends here.
178+ button := s.seat.grabButton
179+ s.seat.grabSerial, s.seat.grabButton = 0, 0
180+ s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started")
181+ s.emit(ports.DataDragStarted{Button: button, Icon: d.iconID})
182+ if iconSurf != nil && iconSurf.has {
183+ // The icon was committed before the drag: show what it holds.
184+ iconSurf.committed = damage{full: true}
185+ iconSurf.redraw()
186+ s.dndIconSend(d)
187+ }
188+}
189+
190+// validGrab reports whether serial is the press of the implicit grab still
191+// held, on the surface with pointer focus (or a subsurface of its tree).
192+func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool {
193+ if serial == 0 || serial != s.seat.grabSerial {
194+ return false
195+ }
196+ focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0)
197+ if focus == nil {
198+ return false
199+ }
200+ o, f := s.surfaces[origin.Resource], s.surfaces[focus.Resource]
201+ return o != nil && f != nil && o.root() == f.root()
202+}
203+
204+// sourceBusy reports whether src is the selection, the source of the drag
205+// or of a drop waiting for its finish.
206+func (s *Server) sourceBusy(src *clipSource) bool {
207+ if s.selections[selClipboard] == src || s.dnd != nil && s.dnd.src == src {
208+ return true
209+ }
210+ return slices.ContainsFunc(s.dndDrops, func(t *dndTarget) bool { return t.d.src == src })
211+}
212+
213+// dndIconRole runs on each commit of the icon surface, and on its
214+// destruction.
215+func (s *Server) dndIconRole(d *dndState, icon *surface) {
216+ if !icon.destroyed || d.icon != icon {
217+ return
218+ }
219+ // The drag goes on without icon.
220+ id := d.iconID
221+ d.icon, d.iconID = nil, 0
222+ s.emit(ports.DataDragIcon{ID: id})
223+ s.emitContent(ports.SurfaceContent{ID: id}, damage{full: true})
224+}
225+
226+// dndIconCommit follows a commit of the active icon: its offsets add up
227+// and its geometry goes to core.
228+func (s *Server) dndIconCommit(icon *surface, dx, dy int32) {
229+ d := s.dnd
230+ if d == nil || d.icon != icon {
231+ return
232+ }
233+ d.iconX += int(dx)
234+ d.iconY += int(dy)
235+ s.dndIconSend(d)
236+}
237+
238+func (s *Server) dndIconSend(d *dndState) {
239+ ic := ports.DataDragIcon{ID: d.iconID, X: d.iconX, Y: d.iconY}
240+ if d.icon != nil && d.icon.has {
241+ ic.W, ic.H = d.icon.content.LogicalW, d.icon.content.LogicalH
242+ }
243+ if ic != d.iconSent {
244+ d.iconSent = ic
245+ s.emit(ic)
246+ }
247+}
248+
249+// applyDrag applies the commands core sends during a drag.
250+func (s *Server) applyDrag(cmd ports.ClientCommand) {
251+ d := s.dnd
252+ if d == nil {
253+ return
254+ }
255+ switch c := cmd.(type) {
256+ case ports.DataDragCancel:
257+ s.dndEnd(d, true, false)
258+ case ports.DataDragDrop:
259+ s.dndDrop(d)
260+ case ports.DataDragFocus:
261+ s.dndFocus(d, c)
262+ case ports.DataDragMotion:
263+ if t := d.target; t != nil && !s.protected() {
264+ surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y)
265+ if surf == nil || surf.Resource != t.surface.Resource {
266+ return
267+ }
268+ for _, dev := range t.devices {
269+ if dev.Resource.Alive() {
270+ dev.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y))
271+ }
272+ }
273+ }
274+ }
275+}
276+
277+// dndFocus moves the drag to the surface core names: the old target gets
278+// leave, the new one enter with an offer.
279+func (s *Server) dndFocus(d *dndState, c ports.DataDragFocus) {
280+ s.dndLeave(d)
281+ if c.ID == 0 || s.protected() {
282+ return
283+ }
284+ surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y)
285+ if surf == nil {
286+ return
287+ }
288+ client := surf.Client()
289+ // Without a source the drag is internal to the origin client.
290+ if d.src == nil && client != d.client {
291+ return
292+ }
293+ devices := s.dataDevices[client]
294+ if len(devices) == 0 {
295+ return
296+ }
297+ t := &dndTarget{d: d, surface: surf, devices: devices, legacy: true}
298+ d.target = t
299+ for _, dev := range devices {
300+ if !dev.Resource.Alive() {
301+ continue
302+ }
303+ if dev.Version() >= 3 {
304+ t.legacy = false
305+ }
306+ var offer *wayland.DataOffer
307+ if d.src != nil {
308+ o := &dndOffer{server: s, t: t}
309+ res, err := wayland.NewDataOffer(dev.Client(), dev.Version(), 0, o)
310+ if err != nil {
311+ continue
312+ }
313+ o.res = res
314+ res.OnDestroy = func() { s.dndOfferGone(o) }
315+ t.offers = append(t.offers, o)
316+ dev.SendDataOffer(res)
317+ for _, mime := range d.src.mimes {
318+ res.SendOffer(mime)
319+ }
320+ if dev.Version() >= 3 {
321+ res.SendSourceActions(d.src.srcActions())
322+ }
323+ offer = res
324+ }
325+ s.serial++
326+ dev.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y), offer)
327+ }
328+}
329+
330+// dndLeave sends leave to the target of the drag and forgets it: its
331+// offers become inert.
332+func (s *Server) dndLeave(d *dndState) {
333+ t := d.target
334+ if t == nil {
335+ return
336+ }
337+ d.target = nil
338+ t.state = targetDone
339+ s.dndSendLeave(t)
340+ s.dndResetSource(t)
341+}
342+
343+func (s *Server) dndSendLeave(t *dndTarget) {
344+ for _, dev := range t.devices {
345+ if dev.Resource.Alive() {
346+ dev.SendLeave()
347+ }
348+ }
349+}
350+
351+// dndResetSource tells the source nothing accepts or acts any more.
352+func (s *Server) dndResetSource(t *dndTarget) {
353+ src := t.d.src
354+ if src == nil || !src.alive() {
355+ return
356+ }
357+ if t.accepted != "" {
358+ src.res.SendTarget("")
359+ }
360+ if t.action != 0 && src.res.Version() >= 3 {
361+ src.res.SendAction(0)
362+ }
363+ t.accepted, t.action = "", 0
364+}
365+
366+// dndDrop handles the release of the drag button: a target that accepted
367+// and agreed on an action gets drop, then leave (its offers stay usable until
368+// finish); else the drag is cancelled.
369+func (s *Server) dndDrop(d *dndState) {
370+ t := d.target
371+ if t == nil {
372+ s.dndEnd(d, true, false)
373+ return
374+ }
375+ if d.src == nil {
376+ // Internal drag: the origin client decides alone.
377+ d.target = nil
378+ t.state = targetDone
379+ s.dndSendDrop(t)
380+ s.dndSendLeave(t)
381+ s.dndEnd(d, false, false)
382+ return
383+ }
384+ if t.legacy {
385+ // A pre-v3 target cannot negotiate: it copies.
386+ action := chooseAction(uint32(wayland.DataDeviceManagerDndActionCopy)&d.src.srcActions(), 0)
387+ if action != t.action {
388+ t.action = action
389+ if action != 0 && d.src.alive() && d.src.res.Version() >= 3 {
390+ d.src.res.SendAction(action)
391+ }
392+ }
393+ }
394+ if t.accepted == "" || t.action == 0 {
395+ s.dndEnd(d, true, false)
396+ return
397+ }
398+ d.target = nil
399+ t.state = targetDropped
400+ t.ask = t.action == actionAsk
401+ s.dndDrops = append(s.dndDrops, t)
402+ s.dndSendDrop(t)
403+ s.dndSendLeave(t)
404+ if d.src.alive() && d.src.res.Version() >= 3 {
405+ d.src.res.SendDndDropPerformed()
406+ }
407+ s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped")
408+ s.dndEnd(d, false, false)
409+}
410+
411+func (s *Server) dndSendDrop(t *dndTarget) {
412+ for _, dev := range t.devices {
413+ if dev.Resource.Alive() {
414+ dev.SendDrop()
415+ }
416+ }
417+}
418+
419+// dndEnd ends the drag d: the target gets leave, the source cancelled if
420+// cancel (it is not once dropped), the icon goes. ended tells core about an
421+// end it did not cause.
422+func (s *Server) dndEnd(d *dndState, cancel, ended bool) {
423+ s.dndLeave(d)
424+ if cancel && d.src != nil && d.src.alive() {
425+ d.src.cancel()
426+ }
427+ if s.dnd == d {
428+ s.dnd = nil
429+ }
430+ if d.icon != nil {
431+ d.icon.role = nil
432+ d.icon = nil
433+ }
434+ if d.iconID != 0 {
435+ s.emitContent(ports.SurfaceContent{ID: d.iconID}, damage{full: true})
436+ d.iconID = 0
437+ }
438+ if ended {
439+ s.log.Info().Msg("data drag ended")
440+ s.emit(ports.DataDragEnded{})
441+ }
442+}
443+
444+// dndSourceGone ends the drags of a destroyed source.
445+func (s *Server) dndSourceGone(src *clipSource) {
446+ if d := s.dnd; d != nil && d.src == src {
447+ s.dndEnd(d, false, true)
448+ }
449+ for _, t := range slices.Clone(s.dndDrops) {
450+ if t.d.src == src {
451+ s.dndDropDone(t)
452+ }
453+ }
454+}
455+
456+// dndDropDone forgets a dropped target.
457+func (s *Server) dndDropDone(t *dndTarget) {
458+ t.state = targetDone
459+ s.dndDrops = slices.DeleteFunc(s.dndDrops, func(x *dndTarget) bool { return x == t })
460+}
461+
462+// dndSurfaceGone clears the drag target when its surface is destroyed.
463+func (s *Server) dndSurfaceGone(surf *surface) {
464+ if d := s.dnd; d != nil && d.target != nil && surf.wl != nil && d.target.surface.Resource == surf.wl.Resource {
465+ s.dndLeave(d)
466+ }
467+}
468+
469+// dndCancelAll cancels the drag and the drops waiting for their finish: the
470+// session is protected.
471+func (s *Server) dndCancelAll() {
472+ if d := s.dnd; d != nil {
473+ s.dndEnd(d, true, false)
474+ }
475+ for _, t := range slices.Clone(s.dndDrops) {
476+ if src := t.d.src; src != nil && src.alive() {
477+ src.cancel()
478+ }
479+ s.dndDropDone(t)
480+ }
481+}
482+
483+// dndOfferGone runs when an offer of a drag is destroyed. With none left on
484+// a live target, nothing is accepted any more. With none left on a dropped
485+// one the transfer is over: a pre-v3 target has finished, any other
486+// cancelled it.
487+func (s *Server) dndOfferGone(o *dndOffer) {
488+ t := o.t
489+ t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o })
490+ if len(t.offers) != 0 {
491+ return
492+ }
493+ switch t.state {
494+ case targetActive:
495+ s.dndResetSource(t)
496+ case targetDropped:
497+ if src := t.d.src; src != nil && src.alive() {
498+ if !t.legacy {
499+ src.cancel()
500+ } else if src.res.Version() >= 3 {
501+ src.res.SendDndFinished()
502+ }
503+ }
504+ s.dndDropDone(t)
505+ }
506+}
507+
508+// update recomputes the action after the target or the source changed
509+// something, and tells both when it changed. After an ask drop the choice is
510+// silent until finish.
511+func (o *dndOffer) update() {
512+ t, src := o.t, o.t.d.src
513+ if t.state == targetDone || t.state == targetDropped && !t.ask || src == nil || !src.alive() {
514+ return
515+ }
516+ action := chooseAction(o.actions&src.srcActions(), o.preferred)
517+ if action != t.action {
518+ t.action = action
519+ if t.state == targetActive && src.res.Version() >= 3 {
520+ src.res.SendAction(action)
521+ }
522+ }
523+ if t.state == targetActive && action != o.sent {
524+ o.sent = action
525+ if o.res.Version() >= 3 {
526+ o.res.SendAction(action)
527+ }
528+ }
529+}
530+
531+func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) {
532+ t := o.t
533+ if t.state != targetActive && !(t.state == targetDropped && t.ask) || !t.d.src.alive() {
534+ return
535+ }
536+ t.accepted = mime
537+ t.d.src.res.SendTarget(mime)
538+}
539+
540+func (o *dndOffer) Receive(_ *wayland.DataOffer, mime string, fd int) {
541+ defer unix.Close(fd)
542+ src := o.t.d.src
543+ if o.server.protected() || o.t.state == targetDone || !src.alive() {
544+ return
545+ }
546+ if slices.Contains(src.mimes, mime) {
547+ src.send(mime, fd)
548+ }
549+}
550+
551+func (*dndOffer) Destroy(*wayland.DataOffer) {}
552+
553+func (o *dndOffer) Finish(r *wayland.DataOffer) {
554+ t := o.t
555+ switch t.state {
556+ case targetDone:
557+ return
558+ case targetActive:
559+ r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop")
560+ return
561+ }
562+ if t.action == 0 || t.action == actionAsk {
563+ r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action")
564+ return
565+ }
566+ if t.accepted == "" {
567+ r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without an accepted MIME type")
568+ return
569+ }
570+ o.server.dndDropDone(t)
571+ if src := t.d.src; src.alive() && src.res.Version() >= 3 {
572+ if t.ask {
573+ src.res.SendAction(t.action)
574+ }
575+ src.res.SendDndFinished()
576+ }
577+}
578+
579+func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) {
580+ if actions&^dndAllActions != 0 {
581+ r.PostError(uint32(wayland.DataOfferErrorInvalidActionMask), "invalid action mask")
582+ return
583+ }
584+ if preferred != 0 && (preferred&(preferred-1) != 0 || preferred&actions == 0) {
585+ r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action")
586+ return
587+ }
588+ if t := o.t; t.state == targetDropped && t.ask && preferred != 0 && t.d.src != nil && preferred&t.d.src.srcActions() == 0 {
589+ r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "final action not allowed by the source")
590+ return
591+ }
592+ o.actions, o.preferred = actions, preferred
593+ o.update()
594+}
@@ -0,0 +1,594 @@
1+package wayland
2+
3+import (
4+ "slices"
5+
6+ "github.com/bnema/go-wayland-bindings/server/wayland"
7+ "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/server"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// Drag and drop (wl_data_device.start_drag). A client starts a drag with the
13+// serial of its current press; wayland validates it and tells core
14+// (ports.DataDragStarted). Core owns the pointer from then on: it names the
15+// surface under the cursor (DataDragFocus, DataDragMotion), the release
16+// (DataDragDrop) and the Escape key (DataDragCancel). Wayland speaks the
17+// data device protocol to the source and the target: enter, motion, leave
18+// and drop with an offer, and the action negotiation the wlroots way. The
19+// icon is a client surface with its own window ID; core draws it at the
20+// cursor from the geometry sent with DataDragIcon.
21+//
22+// Core ends a drag with DataDragDrop or DataDragCancel and is not told
23+// (the lock resets both sides). DataDragEnded tells core about an end it
24+// did not cause: the source or the origin client died.
25+
26+// dndAllActions is every wl_data_device_manager.dnd_action bit.
27+const actionAsk = uint32(wayland.DataDeviceManagerDndActionAsk)
28+
29+const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk)
30+
31+// dndState is the drag in progress.
32+type dndState struct {
33+ // src is the source; nil for a drag internal to the origin client.
34+ src *clipSource
35+ client server.Client
36+ // icon is the icon surface (nil: none or destroyed); iconID is the
37+ // window ID core knows it by (0: none).
38+ icon *surface
39+ iconID ports.WindowID
40+ // iconX, iconY is the icon's top left corner relative to the cursor:
41+ // the offsets of its commits add up. iconSent is the last geometry sent.
42+ iconX, iconY int
43+ iconSent ports.DataDragIcon
44+ target *dndTarget
45+}
46+
47+const (
48+ targetActive = iota
49+ targetDropped
50+ targetDone
51+)
52+
53+// dndTarget is the surface a drag entered. Its offers (one per data device
54+// of the client) share the negotiation. After the drop it lives on until
55+// the target finishes or destroys its offers.
56+type dndTarget struct {
57+ d *dndState
58+ surface *wayland.Surface
59+ devices []*wayland.DataDevice
60+ offers []*dndOffer
61+ // accepted is the MIME type the target accepted; action the action
62+ // chosen for the source.
63+ accepted string
64+ action uint32
65+ state int
66+ // legacy: no device speaks wl_data_offer v3: the target copies and
67+ // nothing finishes it; destroying its offers does.
68+ legacy bool
69+ // ask: the drop came with the ask action; the target picks the final
70+ // one after it.
71+ ask bool
72+}
73+
74+// dndOffer is a wl_data_offer of a drag.
75+type dndOffer struct {
76+ server *Server
77+ t *dndTarget
78+ res *wayland.DataOffer
79+ // actions and preferred are the target's set_actions; sent is the last
80+ // action told to this offer.
81+ actions, preferred, sent uint32
82+}
83+
84+// srcActions is what the source allows; a source that never said (a
85+// pre-v3 client) only copies.
86+func (src *clipSource) srcActions() uint32 {
87+ if src.actionsSet {
88+ return src.actions
89+ }
90+ return uint32(wayland.DataDeviceManagerDndActionCopy)
91+}
92+
93+// chooseAction picks the action like wlroots: the preferred one if both
94+// allow it, else the first of copy, move, ask.
95+func chooseAction(avail, preferred uint32) uint32 {
96+ if avail == 0 {
97+ return 0
98+ }
99+ if preferred&avail != 0 {
100+ return preferred
101+ }
102+ for _, a := range [...]uint32{uint32(wayland.DataDeviceManagerDndActionCopy), uint32(wayland.DataDeviceManagerDndActionMove), uint32(wayland.DataDeviceManagerDndActionAsk)} {
103+ if avail&a != 0 {
104+ return a
105+ }
106+ }
107+ return 0
108+}
109+
110+// addDataDevice tracks a client's data devices, the drag targets.
111+func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) {
112+ if s.dataDevices == nil {
113+ s.dataDevices = map[server.Client][]*wayland.DataDevice{}
114+ }
115+ s.dataDevices[c] = append(s.dataDevices[c], dev)
116+ dev.OnDestroy = func() {
117+ // Copy on write: a drag may be iterating the old list.
118+ list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev })
119+ if len(list) == 0 {
120+ delete(s.dataDevices, c)
121+ // An internal drag has nobody left to tell.
122+ if d := s.dnd; d != nil && d.client == c && d.src == nil {
123+ s.dndEnd(d, false, true)
124+ }
125+ } else {
126+ s.dataDevices[c] = list
127+ }
128+ }
129+}
130+
131+// startDrag handles wl_data_device.start_drag.
132+func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) {
133+ var cs *clipSource
134+ if srcRes != nil {
135+ if cs = s.dataSources[srcRes.Resource]; cs == nil {
136+ return
137+ }
138+ }
139+ var iconSurf *surface
140+ if icon != nil {
141+ if iconSurf = s.surfaces[icon.Resource]; iconSurf == nil {
142+ return
143+ }
144+ if iconSurf.kind != roleNone && iconSurf.kind != roleDragIcon {
145+ r.PostError(uint32(wayland.DataDeviceErrorRole), "icon surface already has another role")
146+ return
147+ }
148+ }
149+ client := r.Client()
150+ valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client &&
151+ s.validGrab(origin, serial)
152+ if cs != nil && (cs.used || !valid) {
153+ // A used source is the selection, a drag source or already
154+ // cancelled: only a source nobody holds is cancelled again.
155+ if !s.sourceBusy(cs) {
156+ cs.used = true
157+ if cs.alive() {
158+ cs.cancel()
159+ }
160+ }
161+ return
162+ }
163+ if !valid {
164+ return
165+ }
166+ d := &dndState{src: cs, client: client, icon: iconSurf}
167+ if cs != nil {
168+ cs.used = true
169+ }
170+ if iconSurf != nil {
171+ d.iconID = s.nextWindow
172+ s.nextWindow++
173+ iconSurf.kind = roleDragIcon
174+ iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) }
175+ }
176+ s.dnd = d
177+ // Core consumes the releases of a drag: the grab ends here.
178+ button := s.seat.grabButton
179+ s.seat.grabSerial, s.seat.grabButton = 0, 0
180+ s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started")
181+ s.emit(ports.DataDragStarted{Button: button, Icon: d.iconID})
182+ if iconSurf != nil && iconSurf.has {
183+ // The icon was committed before the drag: show what it holds.
184+ iconSurf.committed = damage{full: true}
185+ iconSurf.redraw()
186+ s.dndIconSend(d)
187+ }
188+}
189+
190+// validGrab reports whether serial is the press of the implicit grab still
191+// held, on the surface with pointer focus (or a subsurface of its tree).
192+func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool {
193+ if serial == 0 || serial != s.seat.grabSerial {
194+ return false
195+ }
196+ focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0)
197+ if focus == nil {
198+ return false
199+ }
200+ o, f := s.surfaces[origin.Resource], s.surfaces[focus.Resource]
201+ return o != nil && f != nil && o.root() == f.root()
202+}
203+
204+// sourceBusy reports whether src is the selection, the source of the drag
205+// or of a drop waiting for its finish.
206+func (s *Server) sourceBusy(src *clipSource) bool {
207+ if s.selections[selClipboard] == src || s.dnd != nil && s.dnd.src == src {
208+ return true
209+ }
210+ return slices.ContainsFunc(s.dndDrops, func(t *dndTarget) bool { return t.d.src == src })
211+}
212+
213+// dndIconRole runs on each commit of the icon surface, and on its
214+// destruction.
215+func (s *Server) dndIconRole(d *dndState, icon *surface) {
216+ if !icon.destroyed || d.icon != icon {
217+ return
218+ }
219+ // The drag goes on without icon.
220+ id := d.iconID
221+ d.icon, d.iconID = nil, 0
222+ s.emit(ports.DataDragIcon{ID: id})
223+ s.emitContent(ports.SurfaceContent{ID: id}, damage{full: true})
224+}
225+
226+// dndIconCommit follows a commit of the active icon: its offsets add up
227+// and its geometry goes to core.
228+func (s *Server) dndIconCommit(icon *surface, dx, dy int32) {
229+ d := s.dnd
230+ if d == nil || d.icon != icon {
231+ return
232+ }
233+ d.iconX += int(dx)
234+ d.iconY += int(dy)
235+ s.dndIconSend(d)
236+}
237+
238+func (s *Server) dndIconSend(d *dndState) {
239+ ic := ports.DataDragIcon{ID: d.iconID, X: d.iconX, Y: d.iconY}
240+ if d.icon != nil && d.icon.has {
241+ ic.W, ic.H = d.icon.content.LogicalW, d.icon.content.LogicalH
242+ }
243+ if ic != d.iconSent {
244+ d.iconSent = ic
245+ s.emit(ic)
246+ }
247+}
248+
249+// applyDrag applies the commands core sends during a drag.
250+func (s *Server) applyDrag(cmd ports.ClientCommand) {
251+ d := s.dnd
252+ if d == nil {
253+ return
254+ }
255+ switch c := cmd.(type) {
256+ case ports.DataDragCancel:
257+ s.dndEnd(d, true, false)
258+ case ports.DataDragDrop:
259+ s.dndDrop(d)
260+ case ports.DataDragFocus:
261+ s.dndFocus(d, c)
262+ case ports.DataDragMotion:
263+ if t := d.target; t != nil && !s.protected() {
264+ surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y)
265+ if surf == nil || surf.Resource != t.surface.Resource {
266+ return
267+ }
268+ for _, dev := range t.devices {
269+ if dev.Resource.Alive() {
270+ dev.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y))
271+ }
272+ }
273+ }
274+ }
275+}
276+
277+// dndFocus moves the drag to the surface core names: the old target gets
278+// leave, the new one enter with an offer.
279+func (s *Server) dndFocus(d *dndState, c ports.DataDragFocus) {
280+ s.dndLeave(d)
281+ if c.ID == 0 || s.protected() {
282+ return
283+ }
284+ surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y)
285+ if surf == nil {
286+ return
287+ }
288+ client := surf.Client()
289+ // Without a source the drag is internal to the origin client.
290+ if d.src == nil && client != d.client {
291+ return
292+ }
293+ devices := s.dataDevices[client]
294+ if len(devices) == 0 {
295+ return
296+ }
297+ t := &dndTarget{d: d, surface: surf, devices: devices, legacy: true}
298+ d.target = t
299+ for _, dev := range devices {
300+ if !dev.Resource.Alive() {
301+ continue
302+ }
303+ if dev.Version() >= 3 {
304+ t.legacy = false
305+ }
306+ var offer *wayland.DataOffer
307+ if d.src != nil {
308+ o := &dndOffer{server: s, t: t}
309+ res, err := wayland.NewDataOffer(dev.Client(), dev.Version(), 0, o)
310+ if err != nil {
311+ continue
312+ }
313+ o.res = res
314+ res.OnDestroy = func() { s.dndOfferGone(o) }
315+ t.offers = append(t.offers, o)
316+ dev.SendDataOffer(res)
317+ for _, mime := range d.src.mimes {
318+ res.SendOffer(mime)
319+ }
320+ if dev.Version() >= 3 {
321+ res.SendSourceActions(d.src.srcActions())
322+ }
323+ offer = res
324+ }
325+ s.serial++
326+ dev.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y), offer)
327+ }
328+}
329+
330+// dndLeave sends leave to the target of the drag and forgets it: its
331+// offers become inert.
332+func (s *Server) dndLeave(d *dndState) {
333+ t := d.target
334+ if t == nil {
335+ return
336+ }
337+ d.target = nil
338+ t.state = targetDone
339+ s.dndSendLeave(t)
340+ s.dndResetSource(t)
341+}
342+
343+func (s *Server) dndSendLeave(t *dndTarget) {
344+ for _, dev := range t.devices {
345+ if dev.Resource.Alive() {
346+ dev.SendLeave()
347+ }
348+ }
349+}
350+
351+// dndResetSource tells the source nothing accepts or acts any more.
352+func (s *Server) dndResetSource(t *dndTarget) {
353+ src := t.d.src
354+ if src == nil || !src.alive() {
355+ return
356+ }
357+ if t.accepted != "" {
358+ src.res.SendTarget("")
359+ }
360+ if t.action != 0 && src.res.Version() >= 3 {
361+ src.res.SendAction(0)
362+ }
363+ t.accepted, t.action = "", 0
364+}
365+
366+// dndDrop handles the release of the drag button: a target that accepted
367+// and agreed on an action gets drop, then leave (its offers stay usable until
368+// finish); else the drag is cancelled.
369+func (s *Server) dndDrop(d *dndState) {
370+ t := d.target
371+ if t == nil {
372+ s.dndEnd(d, true, false)
373+ return
374+ }
375+ if d.src == nil {
376+ // Internal drag: the origin client decides alone.
377+ d.target = nil
378+ t.state = targetDone
379+ s.dndSendDrop(t)
380+ s.dndSendLeave(t)
381+ s.dndEnd(d, false, false)
382+ return
383+ }
384+ if t.legacy {
385+ // A pre-v3 target cannot negotiate: it copies.
386+ action := chooseAction(uint32(wayland.DataDeviceManagerDndActionCopy)&d.src.srcActions(), 0)
387+ if action != t.action {
388+ t.action = action
389+ if action != 0 && d.src.alive() && d.src.res.Version() >= 3 {
390+ d.src.res.SendAction(action)
391+ }
392+ }
393+ }
394+ if t.accepted == "" || t.action == 0 {
395+ s.dndEnd(d, true, false)
396+ return
397+ }
398+ d.target = nil
399+ t.state = targetDropped
400+ t.ask = t.action == actionAsk
401+ s.dndDrops = append(s.dndDrops, t)
402+ s.dndSendDrop(t)
403+ s.dndSendLeave(t)
404+ if d.src.alive() && d.src.res.Version() >= 3 {
405+ d.src.res.SendDndDropPerformed()
406+ }
407+ s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped")
408+ s.dndEnd(d, false, false)
409+}
410+
411+func (s *Server) dndSendDrop(t *dndTarget) {
412+ for _, dev := range t.devices {
413+ if dev.Resource.Alive() {
414+ dev.SendDrop()
415+ }
416+ }
417+}
418+
419+// dndEnd ends the drag d: the target gets leave, the source cancelled if
420+// cancel (it is not once dropped), the icon goes. ended tells core about an
421+// end it did not cause.
422+func (s *Server) dndEnd(d *dndState, cancel, ended bool) {
423+ s.dndLeave(d)
424+ if cancel && d.src != nil && d.src.alive() {
425+ d.src.cancel()
426+ }
427+ if s.dnd == d {
428+ s.dnd = nil
429+ }
430+ if d.icon != nil {
431+ d.icon.role = nil
432+ d.icon = nil
433+ }
434+ if d.iconID != 0 {
435+ s.emitContent(ports.SurfaceContent{ID: d.iconID}, damage{full: true})
436+ d.iconID = 0
437+ }
438+ if ended {
439+ s.log.Info().Msg("data drag ended")
440+ s.emit(ports.DataDragEnded{})
441+ }
442+}
443+
444+// dndSourceGone ends the drags of a destroyed source.
445+func (s *Server) dndSourceGone(src *clipSource) {
446+ if d := s.dnd; d != nil && d.src == src {
447+ s.dndEnd(d, false, true)
448+ }
449+ for _, t := range slices.Clone(s.dndDrops) {
450+ if t.d.src == src {
451+ s.dndDropDone(t)
452+ }
453+ }
454+}
455+
456+// dndDropDone forgets a dropped target.
457+func (s *Server) dndDropDone(t *dndTarget) {
458+ t.state = targetDone
459+ s.dndDrops = slices.DeleteFunc(s.dndDrops, func(x *dndTarget) bool { return x == t })
460+}
461+
462+// dndSurfaceGone clears the drag target when its surface is destroyed.
463+func (s *Server) dndSurfaceGone(surf *surface) {
464+ if d := s.dnd; d != nil && d.target != nil && surf.wl != nil && d.target.surface.Resource == surf.wl.Resource {
465+ s.dndLeave(d)
466+ }
467+}
468+
469+// dndCancelAll cancels the drag and the drops waiting for their finish: the
470+// session is protected.
471+func (s *Server) dndCancelAll() {
472+ if d := s.dnd; d != nil {
473+ s.dndEnd(d, true, false)
474+ }
475+ for _, t := range slices.Clone(s.dndDrops) {
476+ if src := t.d.src; src != nil && src.alive() {
477+ src.cancel()
478+ }
479+ s.dndDropDone(t)
480+ }
481+}
482+
483+// dndOfferGone runs when an offer of a drag is destroyed. With none left on
484+// a live target, nothing is accepted any more. With none left on a dropped
485+// one the transfer is over: a pre-v3 target has finished, any other
486+// cancelled it.
487+func (s *Server) dndOfferGone(o *dndOffer) {
488+ t := o.t
489+ t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o })
490+ if len(t.offers) != 0 {
491+ return
492+ }
493+ switch t.state {
494+ case targetActive:
495+ s.dndResetSource(t)
496+ case targetDropped:
497+ if src := t.d.src; src != nil && src.alive() {
498+ if !t.legacy {
499+ src.cancel()
500+ } else if src.res.Version() >= 3 {
501+ src.res.SendDndFinished()
502+ }
503+ }
504+ s.dndDropDone(t)
505+ }
506+}
507+
508+// update recomputes the action after the target or the source changed
509+// something, and tells both when it changed. After an ask drop the choice is
510+// silent until finish.
511+func (o *dndOffer) update() {
512+ t, src := o.t, o.t.d.src
513+ if t.state == targetDone || t.state == targetDropped && !t.ask || src == nil || !src.alive() {
514+ return
515+ }
516+ action := chooseAction(o.actions&src.srcActions(), o.preferred)
517+ if action != t.action {
518+ t.action = action
519+ if t.state == targetActive && src.res.Version() >= 3 {
520+ src.res.SendAction(action)
521+ }
522+ }
523+ if t.state == targetActive && action != o.sent {
524+ o.sent = action
525+ if o.res.Version() >= 3 {
526+ o.res.SendAction(action)
527+ }
528+ }
529+}
530+
531+func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) {
532+ t := o.t
533+ if t.state != targetActive && !(t.state == targetDropped && t.ask) || !t.d.src.alive() {
534+ return
535+ }
536+ t.accepted = mime
537+ t.d.src.res.SendTarget(mime)
538+}
539+
540+func (o *dndOffer) Receive(_ *wayland.DataOffer, mime string, fd int) {
541+ defer unix.Close(fd)
542+ src := o.t.d.src
543+ if o.server.protected() || o.t.state == targetDone || !src.alive() {
544+ return
545+ }
546+ if slices.Contains(src.mimes, mime) {
547+ src.send(mime, fd)
548+ }
549+}
550+
551+func (*dndOffer) Destroy(*wayland.DataOffer) {}
552+
553+func (o *dndOffer) Finish(r *wayland.DataOffer) {
554+ t := o.t
555+ switch t.state {
556+ case targetDone:
557+ return
558+ case targetActive:
559+ r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop")
560+ return
561+ }
562+ if t.action == 0 || t.action == actionAsk {
563+ r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action")
564+ return
565+ }
566+ if t.accepted == "" {
567+ r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without an accepted MIME type")
568+ return
569+ }
570+ o.server.dndDropDone(t)
571+ if src := t.d.src; src.alive() && src.res.Version() >= 3 {
572+ if t.ask {
573+ src.res.SendAction(t.action)
574+ }
575+ src.res.SendDndFinished()
576+ }
577+}
578+
579+func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) {
580+ if actions&^dndAllActions != 0 {
581+ r.PostError(uint32(wayland.DataOfferErrorInvalidActionMask), "invalid action mask")
582+ return
583+ }
584+ if preferred != 0 && (preferred&(preferred-1) != 0 || preferred&actions == 0) {
585+ r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action")
586+ return
587+ }
588+ if t := o.t; t.state == targetDropped && t.ask && preferred != 0 && t.d.src != nil && preferred&t.d.src.srcActions() == 0 {
589+ r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "final action not allowed by the source")
590+ return
591+ }
592+ o.actions, o.preferred = actions, preferred
593+ o.update()
594+}
A internal/adapters/wayland/dnd_test.go +1154 -0
@@ -0,0 +1,1154 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "errors"
6+ "sync/atomic"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/go-wayland-bindings/server/wayland"
11+ "github.com/bnema/neferwl/internal/adapters/logging"
12+ "github.com/bnema/neferwl/internal/adapters/sessionsecurity"
13+ "github.com/bnema/neferwl/internal/ports"
14+ "github.com/bnema/wlturbo"
15+ "golang.org/x/sys/unix"
16+)
17+
18+const (
19+ actCopy = uint32(wayland.DataDeviceManagerDndActionCopy)
20+ actMove = uint32(wayland.DataDeviceManagerDndActionMove)
21+ actAsk = uint32(wayland.DataDeviceManagerDndActionAsk)
22+)
23+
24+// dndEvent is one data device, offer or source event a test client saw.
25+type dndEvent struct {
26+ name string
27+ mime string
28+ x, y float64
29+ n uint32
30+}
31+
32+type dndDeviceEvents struct {
33+ wlturbo.BaseProxy
34+ c *wlturbo.Display
35+ log chan dndEvent
36+ // offer is the id of the latest data_offer.
37+ offer atomic.Uint32
38+}
39+
40+func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {
41+ switch e.Opcode {
42+ case uint16(wayland.DataDeviceEventDataOffer):
43+ o := &dndOfferEvents{log: p.log}
44+ o.SetID(e.Uint32())
45+ p.offer.Store(o.ID())
46+ registerWireProxy(p.c, o)
47+ case uint16(wayland.DataDeviceEventEnter):
48+ serial, _ := e.Uint32(), e.Uint32()
49+ x, y := e.Fixed().Float64(), e.Fixed().Float64()
50+ p.log <- dndEvent{name: "enter", x: x, y: y, n: e.Uint32() + serial*0}
51+ case uint16(wayland.DataDeviceEventLeave):
52+ p.log <- dndEvent{name: "leave"}
53+ case uint16(wayland.DataDeviceEventMotion):
54+ _ = e.Uint32()
55+ p.log <- dndEvent{name: "motion", x: e.Fixed().Float64(), y: e.Fixed().Float64()}
56+ case uint16(wayland.DataDeviceEventDrop):
57+ p.log <- dndEvent{name: "drop"}
58+ }
59+}
60+
61+type dndOfferEvents struct {
62+ wlturbo.BaseProxy
63+ log chan dndEvent
64+}
65+
66+func (p *dndOfferEvents) Dispatch(e *wlturbo.Event) {
67+ switch e.Opcode {
68+ case uint16(wayland.DataOfferEventOffer):
69+ p.log <- dndEvent{name: "offer", mime: e.String()}
70+ case uint16(wayland.DataOfferEventSourceActions):
71+ p.log <- dndEvent{name: "source_actions", n: e.Uint32()}
72+ case uint16(wayland.DataOfferEventAction):
73+ p.log <- dndEvent{name: "offer_action", n: e.Uint32()}
74+ }
75+}
76+
77+type dndSourceEvents struct {
78+ wlturbo.BaseProxy
79+ log chan dndEvent
80+}
81+
82+func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) {
83+ switch e.Opcode {
84+ case uint16(wayland.DataSourceEventTarget):
85+ p.log <- dndEvent{name: "target", mime: e.String()}
86+ case uint16(wayland.DataSourceEventSend):
87+ mime := e.String()
88+ fd := int(e.Fd())
89+ _, _ = unix.Write(fd, []byte("dnd data"))
90+ _ = unix.Close(fd)
91+ p.log <- dndEvent{name: "send", mime: mime}
92+ case uint16(wayland.DataSourceEventCancelled):
93+ p.log <- dndEvent{name: "cancelled"}
94+ case uint16(wayland.DataSourceEventDndDropPerformed):
95+ p.log <- dndEvent{name: "drop_performed"}
96+ case uint16(wayland.DataSourceEventDndFinished):
97+ p.log <- dndEvent{name: "finished"}
98+ case uint16(wayland.DataSourceEventAction):
99+ p.log <- dndEvent{name: "action", n: e.Uint32()}
100+ }
101+}
102+
103+// dndClient is a client with a toplevel, a pointer and a data device.
104+type dndClient struct {
105+ keys chan uint32
106+ c *wlturbo.Display
107+ manager uint32
108+ device *dndDeviceEvents
109+ serials chan uint32
110+ comp uint32
111+ shm uint32
112+ surf uint32
113+ window ports.WindowID
114+ log chan dndEvent
115+}
116+
117+// keySerials records the serial of each key event.
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+
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 {
136+ t.Helper()
137+ c := protocolClient(t, s, dir)
138+ d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8), keys: make(chan uint32, 8)}
139+ kb := &keySerials{serials: d.keys}
140+ seat := bindVersion(t, c, "wl_seat", 8)
141+ registerProtocol(t, c, seat)
142+ ptr := &pressSerials{serials: d.serials}
143+ ptr.SetID(c.AllocateID())
144+ registerWireProxy(c, ptr)
145+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID())
146+ kb.SetID(c.AllocateID())
147+ registerWireProxy(c, kb)
148+ requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, kb.ID())
149+ d.manager = bindVersion(t, c, "wl_data_device_manager", version)
150+ d.device = &dndDeviceEvents{c: c, log: d.log}
151+ d.device.SetID(c.AllocateID())
152+ registerWireProxy(c, d.device)
153+ requestProtocol(t, c, d.manager, wayland.DataDeviceManagerRequestGetDataDevice, d.device.ID(), seat)
154+ d.comp = bindVersion(t, c, "wl_compositor", 5)
155+ d.shm = bindProtocol(t, c, "wl_shm")
156+ registerProtocol(t, c, d.shm)
157+ w, surf, _ := surfaceMapperAt(t, c, events, 5)()
158+ d.window, d.surf = w.ID, surf
159+ return d
160+}
161+
162+// lastOffer is the id of the latest offer the client got.
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+
172+// next returns the next event the client sees.
173+func (d *dndClient) next(t *testing.T) dndEvent {
174+ t.Helper()
175+ deadline := time.After(2 * time.Second)
176+ for {
177+ if err := d.c.Roundtrip(); err != nil {
178+ t.Fatal(err)
179+ }
180+ select {
181+ case ev := <-d.log:
182+ return ev
183+ case <-deadline:
184+ t.Fatal("no event")
185+ case <-time.After(5 * time.Millisecond):
186+ }
187+ }
188+}
189+
190+// expect waits for the next event and checks its name.
191+func (d *dndClient) expect(t *testing.T, name string) dndEvent {
192+ t.Helper()
193+ ev := d.next(t)
194+ if ev.name != name {
195+ t.Fatalf("event %+v, want %s", ev, name)
196+ }
197+ return ev
198+}
199+
200+// none checks that no event arrives.
201+func (d *dndClient) none(t *testing.T) {
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)
208+ }
209+ select {
210+ case ev := <-d.log:
211+ t.Fatalf("unexpected event %+v", ev)
212+ default:
213+ }
214+}
215+
216+func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 {
217+ t.Helper()
218+ src := &dndSourceEvents{log: d.log}
219+ src.SetID(d.c.AllocateID())
220+ registerWireProxy(d.c, src)
221+ requestProtocol(t, d.c, d.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID())
222+ for _, m := range mimes {
223+ requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestOffer, m)
224+ }
225+ if actions != 0 {
226+ requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestSetActions, actions)
227+ }
228+ return src.ID()
229+}
230+
231+// press makes the client's window press a button and returns the serial.
232+func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 {
233+ t.Helper()
234+ commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1}
235+ commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Grab: true, Time: time.Millisecond}
236+ deadline := time.After(2 * time.Second)
237+ for {
238+ if err := d.c.Roundtrip(); err != nil {
239+ t.Fatal(err)
240+ }
241+ select {
242+ case serial := <-d.serials:
243+ return serial
244+ case <-deadline:
245+ t.Fatal("no button")
246+ case <-time.After(5 * time.Millisecond):
247+ }
248+ }
249+}
250+
251+// press2 returns the next pointer button serial.
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+
309+func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) {
310+ t.Helper()
311+ d.startDragFrom(t, src, d.surf, icon, serial)
312+}
313+
314+// startDragFrom is startDrag with an origin surface.
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)
318+ if err := d.c.Roundtrip(); err != nil {
319+ t.Fatal(err)
320+ }
321+}
322+
323+// buffer makes a w×h ARGB shm buffer.
324+func (d *dndClient) buffer(t *testing.T, w, h int) uint32 {
325+ t.Helper()
326+ fd, err := unix.MemfdCreate("dnd-icon", 0)
327+ if err != nil {
328+ t.Fatal(err)
329+ }
330+ defer unix.Close(fd)
331+ if err := unix.Ftruncate(fd, int64(w*h*4)); err != nil {
332+ t.Fatal(err)
333+ }
334+ pool, buf := d.c.AllocateID(), d.c.AllocateID()
335+ registerProtocol(t, d.c, pool)
336+ registerProtocol(t, d.c, buf)
337+ if err := wireRequest(d.c, d.shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil {
338+ t.Fatal(err)
339+ }
340+ requestProtocol(t, d.c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
341+ return buf
342+}
343+
344+func (d *dndClient) surfaceNew(t *testing.T) uint32 {
345+ t.Helper()
346+ id := d.c.AllocateID()
347+ requestProtocol(t, d.c, d.comp, wayland.CompositorRequestCreateSurface, id)
348+ registerProtocol(t, d.c, id)
349+ return id
350+}
351+
352+// dndNextEvent returns the next client event of type T, skipping others.
353+func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T {
354+ t.Helper()
355+ deadline := time.After(2 * time.Second)
356+ for {
357+ select {
358+ case ev := <-events:
359+ if v, ok := ev.(T); ok {
360+ return v
361+ }
362+ case <-deadline:
363+ t.Fatalf("no %T event", *new(T))
364+ }
365+ }
366+}
367+
368+// dndNoEvent checks that no client event of type T is pending.
369+func dndNoEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) {
370+ t.Helper()
371+ time.Sleep(50 * time.Millisecond)
372+ for {
373+ select {
374+ case ev := <-events:
375+ if _, ok := ev.(T); ok {
376+ t.Fatalf("unexpected %T: %+v", ev, ev)
377+ }
378+ default:
379+ return
380+ }
381+ }
382+}
383+
384+// dndPair is two clients with a drag started from a on a source.
385+type dndPair struct {
386+ s *Server
387+ events chan ports.ClientEvent
388+ commands chan ports.ClientCommand
389+ a, b *dndClient
390+}
391+
392+func newDndPair(t *testing.T) *dndPair {
393+ t.Helper()
394+ s, events, commands, dir := keyboardServer(t)
395+ p := &dndPair{s: s, events: events, commands: commands}
396+ p.a = newDndClient(t, s, dir, events)
397+ p.b = newDndClient(t, s, dir, events)
398+ return p
399+}
400+
401+// start begins a drag of a source offering text/plain, copy and move.
402+func (p *dndPair) start(t *testing.T, icon uint32) uint32 {
403+ t.Helper()
404+ src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
405+ p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
406+ return src
407+}
408+
409+// enter makes b the drag target and returns the offer id.
410+func (p *dndPair) enter(t *testing.T) uint32 {
411+ t.Helper()
412+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond}
413+ for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} {
414+ if ev := p.b.next(t); ev != want {
415+ t.Fatalf("event %+v, want %+v", ev, want)
416+ }
417+ }
418+ ev := p.b.expect(t, "enter")
419+ if ev.x != 5 || ev.y != 6 || ev.n == 0 {
420+ t.Fatalf("enter %+v", ev)
421+ }
422+ return ev.n
423+}
424+
425+// A1: a drag from a to b delivers enter, motion and drop with the offer;
426+// the action is negotiated; the data reaches b; a is told every step.
427+func TestDataDragBetweenClients(t *testing.T) {
428+ p := newDndPair(t)
429+ p.start(t, 0)
430+ if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 {
431+ t.Fatalf("started %+v", v)
432+ }
433+ offer := p.enter(t)
434+
435+ p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond}
436+ if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 {
437+ t.Fatalf("motion %+v", ev)
438+ }
439+
440+ // b accepts and prefers move: both sides learn the action.
441+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
442+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
443+ if ev := p.a.expect(t, "target"); ev.mime != "text/plain" {
444+ t.Fatalf("target %+v", ev)
445+ }
446+ if ev := p.a.expect(t, "action"); ev.n != actMove {
447+ t.Fatalf("action %+v", ev)
448+ }
449+ if ev := p.b.expect(t, "offer_action"); ev.n != actMove {
450+ t.Fatalf("offer action %+v", ev)
451+ }
452+
453+ // Finishing before the drop is a protocol error, so only after it.
454+ p.commands <- ports.DataDragDrop{}
455+ p.b.expect(t, "drop")
456+ p.b.expect(t, "leave")
457+ p.a.expect(t, "drop_performed")
458+
459+ var fds [2]int
460+ if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
461+ t.Fatal(err)
462+ }
463+ defer unix.Close(fds[0])
464+ if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil {
465+ t.Fatal(err)
466+ }
467+ _ = unix.Close(fds[1])
468+ if err := p.b.c.Roundtrip(); err != nil {
469+ t.Fatal(err)
470+ }
471+ if ev := p.a.expect(t, "send"); ev.mime != "text/plain" {
472+ t.Fatalf("send %+v", ev)
473+ }
474+ buf := make([]byte, 16)
475+ n, err := unix.Read(fds[0], buf)
476+ for errors.Is(err, unix.EINTR) {
477+ n, err = unix.Read(fds[0], buf)
478+ }
479+ if err != nil || string(buf[:n]) != "dnd data" {
480+ t.Fatalf("received %q, %v", buf[:max(n, 0)], err)
481+ }
482+
483+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
484+ p.a.expect(t, "finished")
485+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
486+ p.a.none(t)
487+ p.b.none(t)
488+ // core ended the drag: wayland does not echo it.
489+ dndNoEvent[ports.DataDragEnded](t, p.events)
490+}
491+
492+// The action follows wlroots: the preferred one if both allow it, else the
493+// first of copy, move, ask they share, else none.
494+func TestChooseAction(t *testing.T) {
495+ for _, tc := range []struct{ avail, preferred, want uint32 }{
496+ {actCopy | actMove, actMove, actMove},
497+ {actCopy | actMove, actAsk, actCopy},
498+ {actMove | actAsk, 0, actMove},
499+ {actAsk, 0, actAsk},
500+ {0, actCopy, 0},
501+ } {
502+ if got := chooseAction(tc.avail, tc.preferred); got != tc.want {
503+ t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want)
504+ }
505+ }
506+}
507+
508+// A target and a source sharing no action leave the drop refused.
509+func TestDataDragNoCommonAction(t *testing.T) {
510+ p := newDndPair(t)
511+ src := p.a.source(t, actCopy, "text/plain")
512+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
513+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
514+ p.b.expect(t, "offer")
515+ p.b.expect(t, "source_actions")
516+ offer := p.b.expect(t, "enter").n
517+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
518+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove)
519+ p.a.expect(t, "target")
520+ p.commands <- ports.DataDragDrop{}
521+ p.b.expect(t, "leave")
522+ // The refused target no longer accepts.
523+ if ev := p.a.expect(t, "target"); ev.mime != "" {
524+ t.Fatalf("target %+v", ev)
525+ }
526+ p.a.expect(t, "cancelled")
527+}
528+
529+// A2: a release over nothing, or over a target that accepted nothing,
530+// cancels the source; the target gets leave and no drop.
531+func TestDataDragRefusedDrop(t *testing.T) {
532+ t.Run("nothing", func(t *testing.T) {
533+ p := newDndPair(t)
534+ p.start(t, 0)
535+ p.commands <- ports.DataDragDrop{}
536+ p.a.expect(t, "cancelled")
537+ p.b.none(t)
538+ })
539+ t.Run("not accepted", func(t *testing.T) {
540+ p := newDndPair(t)
541+ p.start(t, 0)
542+ p.enter(t)
543+ p.commands <- ports.DataDragDrop{}
544+ p.b.expect(t, "leave")
545+ p.a.expect(t, "cancelled")
546+ p.b.none(t)
547+ })
548+ t.Run("escape", func(t *testing.T) {
549+ p := newDndPair(t)
550+ p.start(t, 0)
551+ p.enter(t)
552+ p.commands <- ports.DataDragCancel{}
553+ p.b.expect(t, "leave")
554+ p.a.expect(t, "cancelled")
555+ dndNoEvent[ports.DataDragEnded](t, p.events)
556+ })
557+}
558+
559+// Moving to another surface leaves the first and enters the second; a
560+// surface of nobody (ID 0) leaves.
561+func TestDataDragLeavesOnFocusChange(t *testing.T) {
562+ p := newDndPair(t)
563+ p.start(t, 0)
564+ p.enter(t)
565+ p.commands <- ports.DataDragFocus{}
566+ p.b.expect(t, "leave")
567+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3}
568+ for range 3 {
569+ p.b.next(t)
570+ }
571+ if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 {
572+ t.Fatalf("enter %+v", ev)
573+ }
574+}
575+
576+// A4: the source dying mid-drag ends it: the target gets leave and core a
577+// DataDragEnded; later commands change nothing.
578+func TestDataDragSourceDestroyed(t *testing.T) {
579+ p := newDndPair(t)
580+ src := p.start(t, 0)
581+ p.enter(t)
582+ requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy)
583+ if err := p.a.c.Roundtrip(); err != nil {
584+ t.Fatal(err)
585+ }
586+ p.b.expect(t, "leave")
587+ dndNextEvent[ports.DataDragEnded](t, p.events)
588+ p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9}
589+ p.commands <- ports.DataDragDrop{}
590+ p.b.none(t)
591+}
592+
593+// A drag whose origin client dies ends the same way.
594+func TestDataDragOriginDies(t *testing.T) {
595+ p := newDndPair(t)
596+ p.start(t, 0)
597+ p.enter(t)
598+ _ = p.a.c.Close()
599+ p.b.expect(t, "leave")
600+ dndNextEvent[ports.DataDragEnded](t, p.events)
601+}
602+
603+// A6: a start_drag without the current press of the origin client is
604+// refused: the source is cancelled and core hears nothing.
605+func TestDataDragInvalidStart(t *testing.T) {
606+ t.Run("stale serial", func(t *testing.T) {
607+ p := newDndPair(t)
608+ serial := p.a.press(t, p.commands)
609+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1)
610+ p.a.expect(t, "cancelled")
611+ dndNoEvent[ports.DataDragStarted](t, p.events)
612+ })
613+ t.Run("other client's press", func(t *testing.T) {
614+ p := newDndPair(t)
615+ p.a.press(t, p.commands)
616+ serial := p.b.press(t, p.commands)
617+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
618+ p.a.expect(t, "cancelled")
619+ dndNoEvent[ports.DataDragStarted](t, p.events)
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+ // Core is told the grab's button: its release drops.
656+ if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 {
657+ t.Fatalf("drag button %#x, want the grab's", ev.Button)
658+ }
659+ })
660+ t.Run("grab release consumed by core", func(t *testing.T) {
661+ // A window drag consumes the release: the next grab press
662+ // still makes the serial a drag can name.
663+ p := newDndPair(t)
664+ stale := p.a.press(t, p.commands)
665+ serial := p.a.press(t, p.commands)
666+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale)
667+ p.a.expect(t, "cancelled")
668+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
669+ dndNextEvent[ports.DataDragStarted](t, p.events)
670+ })
671+ t.Run("grab release of a gone window", func(t *testing.T) {
672+ p := newDndPair(t)
673+ serial := p.a.press(t, p.commands)
674+ p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond}
675+ 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)
677+ p.a.expect(t, "cancelled")
678+ dndNoEvent[ports.DataDragStarted](t, p.events)
679+ })
680+ t.Run("used source", func(t *testing.T) {
681+ p := newDndPair(t)
682+ src := p.start(t, 0)
683+ p.commands <- ports.DataDragCancel{}
684+ p.a.expect(t, "cancelled")
685+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
686+ p.a.expect(t, "cancelled")
687+ })
688+ t.Run("drag in progress", func(t *testing.T) {
689+ p := newDndPair(t)
690+ p.start(t, 0)
691+ dndNextEvent[ports.DataDragStarted](t, p.events)
692+ other := p.a.source(t, actCopy, "text/plain")
693+ p.a.startDrag(t, other, 0, p.a.press(t, p.commands))
694+ p.a.expect(t, "cancelled")
695+ dndNoEvent[ports.DataDragStarted](t, p.events)
696+ })
697+ t.Run("icon with another role", func(t *testing.T) {
698+ p := newDndPair(t)
699+ serial := p.a.press(t, p.commands)
700+ src := p.a.source(t, actCopy, "text/plain")
701+ // 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)
703+ expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole))
704+ dndNoEvent[ports.DataDragStarted](t, p.events)
705+ })
706+}
707+
708+// A nil source is a drag internal to the origin client: only its own
709+// surfaces see it, without offer.
710+func TestDataDragWithoutSource(t *testing.T) {
711+ p := newDndPair(t)
712+ p.a.startDrag(t, 0, 0, p.a.press(t, p.commands))
713+ dndNextEvent[ports.DataDragStarted](t, p.events)
714+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
715+ p.b.none(t)
716+ 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 {
718+ t.Fatalf("enter %+v", ev)
719+ }
720+ p.commands <- ports.DataDragDrop{}
721+ p.a.expect(t, "drop")
722+}
723+
724+// Offer and source requests are validated.
725+func TestDataDragProtocolErrors(t *testing.T) {
726+ t.Run("finish before drop", func(t *testing.T) {
727+ p := newDndPair(t)
728+ p.start(t, 0)
729+ offer := p.enter(t)
730+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
731+ expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish))
732+ })
733+ t.Run("offer action mask", func(t *testing.T) {
734+ p := newDndPair(t)
735+ p.start(t, 0)
736+ offer := p.enter(t)
737+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0))
738+ expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask))
739+ })
740+ t.Run("offer preferred action", func(t *testing.T) {
741+ p := newDndPair(t)
742+ p.start(t, 0)
743+ offer := p.enter(t)
744+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove)
745+ expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction))
746+ })
747+ t.Run("source action mask", func(t *testing.T) {
748+ p := newDndPair(t)
749+ src := p.a.source(t, 0, "text/plain")
750+ requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16))
751+ expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask))
752+ })
753+ t.Run("selection with actions", func(t *testing.T) {
754+ p := newDndPair(t)
755+ src := p.a.source(t, actCopy, "text/plain")
756+ requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0))
757+ expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
758+ })
759+ t.Run("actions after the drag started", func(t *testing.T) {
760+ p := newDndPair(t)
761+ src := p.start(t, 0)
762+ requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy)
763+ expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
764+ })
765+}
766+
767+// A destroyed offer before finish cancels the source.
768+func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) {
769+ p := newDndPair(t)
770+ p.start(t, 0)
771+ offer := p.enter(t)
772+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
773+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
774+ p.a.expect(t, "target")
775+ p.a.expect(t, "action")
776+ p.commands <- ports.DataDragDrop{}
777+ p.b.expect(t, "offer_action")
778+ p.b.expect(t, "drop")
779+ p.b.expect(t, "leave")
780+ p.a.expect(t, "drop_performed")
781+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
782+ p.a.expect(t, "cancelled")
783+}
784+
785+// iconContent reports whether the server still holds content for id.
786+func iconContent(t *testing.T, s *Server, id ports.WindowID) bool {
787+ t.Helper()
788+ var ok bool
789+ if !s.display.Do(func() { _, ok = s.latest[id] }) {
790+ t.Fatal("display stopped")
791+ }
792+ return ok
793+}
794+
795+// The icon is a surface with its own window ID: its size and the
796+// offsets of its commits reach core, its content is published, and both go
797+// when the drag ends.
798+func TestDataDragIcon(t *testing.T) {
799+ p := newDndPair(t)
800+ icon := p.a.surfaceNew(t)
801+ buf := p.a.buffer(t, 8, 6)
802+ // An offset on an ordinary surface is not used.
803+ requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5))
804+ requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit)
805+
806+ src := p.a.source(t, actCopy, "text/plain")
807+ p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
808+ started := dndNextEvent[ports.DataDragStarted](t, p.events)
809+ if started.Icon == 0 {
810+ t.Fatal("no icon ID")
811+ }
812+ id := started.Icon
813+ if iconContent(t, p.s, id) {
814+ t.Fatal("content before the first commit")
815+ }
816+
817+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
818+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4))
819+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
820+ if err := p.a.c.Roundtrip(); err != nil {
821+ t.Fatal(err)
822+ }
823+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) {
824+ t.Fatalf("icon %+v", got)
825+ }
826+ if !iconContent(t, p.s, id) {
827+ t.Fatal("no icon content")
828+ }
829+
830+ // Offsets add up from commit to commit.
831+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2))
832+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
833+ if err := p.a.c.Roundtrip(); err != nil {
834+ t.Fatal(err)
835+ }
836+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) {
837+ t.Fatalf("icon %+v", got)
838+ }
839+ // The icon never takes input.
840+ dndNoEvent[ports.InputRegionChanged](t, p.events)
841+
842+ p.commands <- ports.DataDragCancel{}
843+ p.a.expect(t, "cancelled")
844+ if iconContent(t, p.s, id) {
845+ t.Fatal("icon content kept after the drag")
846+ }
847+}
848+
849+// An icon committed before start_drag shows at once.
850+func TestDataDragIconCommittedEarly(t *testing.T) {
851+ p := newDndPair(t)
852+ icon := p.a.surfaceNew(t)
853+ buf := p.a.buffer(t, 4, 4)
854+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
855+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
856+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
857+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
858+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) {
859+ t.Fatalf("icon %+v", got)
860+ }
861+ if !iconContent(t, p.s, id) {
862+ t.Fatal("no icon content")
863+ }
864+}
865+
866+// Before wl_surface v5 the icon offset rides on attach and adds up.
867+func TestDataDragIconAttachOffset(t *testing.T) {
868+ p := newDndPair(t)
869+ comp4 := bindVersion(t, p.a.c, "wl_compositor", 4)
870+ icon := p.a.c.AllocateID()
871+ requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon)
872+ registerProtocol(t, p.a.c, icon)
873+ buf := p.a.buffer(t, 4, 4)
874+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
875+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
876+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1))
877+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
878+ if err := p.a.c.Roundtrip(); err != nil {
879+ t.Fatal(err)
880+ }
881+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) {
882+ t.Fatalf("icon %+v", got)
883+ }
884+}
885+
886+// Destroying the icon mid-drag hides it; the drag goes on.
887+func TestDataDragIconDestroyed(t *testing.T) {
888+ p := newDndPair(t)
889+ icon := p.a.surfaceNew(t)
890+ buf := p.a.buffer(t, 8, 6)
891+ p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands))
892+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
893+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
894+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
895+ if err := p.a.c.Roundtrip(); err != nil {
896+ t.Fatal(err)
897+ }
898+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 {
899+ t.Fatalf("icon %+v", got)
900+ }
901+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy)
902+ if err := p.a.c.Roundtrip(); err != nil {
903+ t.Fatal(err)
904+ }
905+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) {
906+ t.Fatalf("icon %+v", got)
907+ }
908+ if iconContent(t, p.s, id) {
909+ t.Fatal("icon content kept")
910+ }
911+ p.enter(t)
912+}
913+
914+// lockedPair is a pair of clients on a server that can lock the session.
915+func lockedPair(t *testing.T) (*dndPair, string) {
916+ t.Helper()
917+ gate := &sessionsecurity.Gate{}
918+ events := make(chan ports.ClientEvent, 128)
919+ commands := make(chan ports.ClientCommand, 32)
920+ dir := t.TempDir()
921+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Security: gate, Keymap: keymapText(t)}, Channels{Events: events, Commands: commands, SecurityChanges: make(chan ports.SecurityState, 16), SecurityEvents: make(chan ports.SecurityBackendEvent, 16)}, logging.For(context.Background(), "wayland"))
922+ if err != nil {
923+ t.Fatal(err)
924+ }
925+ ctx, cancel := context.WithCancel(context.Background())
926+ done := make(chan error, 1)
927+ go func() { done <- s.Run(ctx) }()
928+ t.Cleanup(func() {
929+ cancel()
930+ select {
931+ case err := <-done:
932+ if err != nil {
933+ t.Error(err)
934+ }
935+ case <-time.After(3 * time.Second):
936+ t.Error("server did not stop")
937+ }
938+ })
939+ p := &dndPair{s: s, events: events, commands: commands}
940+ p.a = newDndClient(t, s, dir, events)
941+ p.b = newDndClient(t, s, dir, events)
942+ return p, dir
943+}
944+
945+func lockSession(t *testing.T, s *Server, dir string) {
946+ t.Helper()
947+ lc := protocolClient(t, s, dir)
948+ if _, err := bindLockManager(t, lc).Lock(); err != nil {
949+ t.Fatal(err)
950+ }
951+ if err := lc.Roundtrip(); err != nil {
952+ t.Fatal(err)
953+ }
954+}
955+
956+// A7: the session lock cancels a drag at once: target leave, source
957+// cancelled, icon gone.
958+func TestDataDragLockCancels(t *testing.T) {
959+ p, dir := lockedPair(t)
960+ icon := p.a.surfaceNew(t)
961+ buf := p.a.buffer(t, 8, 6)
962+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
963+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
964+ src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
965+ p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
966+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
967+ p.enter(t)
968+
969+ lockSession(t, p.s, dir)
970+ p.b.expect(t, "leave")
971+ p.a.expect(t, "cancelled")
972+ if iconContent(t, p.s, id) {
973+ t.Fatal("icon content kept")
974+ }
975+ // A new drag is refused while protected.
976+ src2 := p.a.source(t, actCopy, "text/plain")
977+ p.a.startDrag(t, src2, 0, uint32(1))
978+ p.a.expect(t, "cancelled")
979+}
980+
981+// GO-006: a dropped transfer waiting for its finish dies with the lock, for
982+// a v3 and a pre-v3 target alike.
983+func TestDataDropLockCancelsPending(t *testing.T) {
984+ for _, version := range []uint32{3, 2} {
985+ p, dir := lockedPair(t)
986+ p.b = newDndClientAt(t, p.s, dir, p.events, version)
987+ p.start(t, 0)
988+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6}
989+ p.b.until(t, "enter")
990+ offer := p.b.lastOffer(t)
991+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
992+ if version >= 3 {
993+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
994+ }
995+ p.a.until(t, "target")
996+ p.commands <- ports.DataDragDrop{}
997+ p.a.until(t, "drop_performed")
998+ lockSession(t, p.s, dir)
999+ p.a.expect(t, "cancelled")
1000+ }
1001+}
1002+
1003+// A subsurface of the focused tree is a valid origin.
1004+func TestDataDragFromSubsurface(t *testing.T) {
1005+ p := newDndPair(t)
1006+ serial := p.a.press(t, p.commands)
1007+ child := p.a.surfaceNew(t)
1008+ subc := bindProtocol(t, p.a.c, "wl_subcompositor")
1009+ requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf)
1010+ p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial)
1011+ dndNextEvent[ports.DataDragStarted](t, p.events)
1012+}
1013+
1014+// GO-003: destroying the offers of the target withdraws the acceptance: the
1015+// release then cancels, with no drop.
1016+func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) {
1017+ p := newDndPair(t)
1018+ p.start(t, 0)
1019+ offer := p.enter(t)
1020+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1021+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1022+ p.a.expect(t, "target")
1023+ p.a.expect(t, "action")
1024+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
1025+ if err := p.b.c.Roundtrip(); err != nil {
1026+ t.Fatal(err)
1027+ }
1028+ if ev := p.a.expect(t, "target"); ev.mime != "" {
1029+ t.Fatalf("target %+v", ev)
1030+ }
1031+ if ev := p.a.expect(t, "action"); ev.n != 0 {
1032+ t.Fatalf("action %+v", ev)
1033+ }
1034+ p.commands <- ports.DataDragDrop{}
1035+ got := p.b.until(t, "leave")
1036+ for _, ev := range got {
1037+ if ev.name == "drop" {
1038+ t.Fatalf("drop after the offer was destroyed: %+v", got)
1039+ }
1040+ }
1041+ p.a.expect(t, "cancelled")
1042+}
1043+
1044+// GO-004: with ask, the target picks the final action after the drop; the
1045+// source hears it just before dnd_finished.
1046+func TestDataDragAsk(t *testing.T) {
1047+ p := newDndPair(t)
1048+ src := p.a.source(t, actCopy|actMove|actAsk, "text/plain")
1049+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1050+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1051+ p.b.until(t, "enter")
1052+ id := p.b.lastOffer(t)
1053+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1054+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1055+ p.a.expect(t, "target")
1056+ if ev := p.a.expect(t, "action"); ev.n != actAsk {
1057+ t.Fatalf("action %+v", ev)
1058+ }
1059+ p.commands <- ports.DataDragDrop{}
1060+ p.b.until(t, "leave")
1061+ p.a.expect(t, "drop_performed")
1062+
1063+ // Finishing while the action is ask is an error; the choice comes first.
1064+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain")
1065+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1066+ 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
1069+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish)
1070+ if ev := p.a.expect(t, "action"); ev.n != actMove {
1071+ t.Fatalf("action %+v", ev)
1072+ }
1073+ p.a.expect(t, "finished")
1074+}
1075+
1076+// A final preferred action outside the source's is an error.
1077+func TestDataDragAskFinalActionNotInSource(t *testing.T) {
1078+ p := newDndPair(t)
1079+ src := p.a.source(t, actCopy|actAsk, "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+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1086+ p.a.expect(t, "target")
1087+ p.a.expect(t, "action")
1088+ p.commands <- ports.DataDragDrop{}
1089+ p.b.until(t, "leave")
1090+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove)
1091+ expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction))
1092+}
1093+
1094+// GO-005: a pre-v3 target copies: a source that does not allow copy is
1095+// cancelled, one that does gets it.
1096+func TestDataDragLegacyTarget(t *testing.T) {
1097+ setup := func(t *testing.T, actions uint32) (*dndPair, uint32) {
1098+ s, events, commands, dir := keyboardServer(t)
1099+ p := &dndPair{s: s, events: events, commands: commands}
1100+ p.a = newDndClient(t, s, dir, events)
1101+ p.b = newDndClientAt(t, s, dir, events, 2)
1102+ src := p.a.source(t, actions, "text/plain")
1103+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1104+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1105+ p.b.until(t, "enter")
1106+ id := p.b.lastOffer(t)
1107+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1108+ p.a.expect(t, "target")
1109+ return p, id
1110+ }
1111+ t.Run("move only", func(t *testing.T) {
1112+ p, _ := setup(t, actMove)
1113+ p.commands <- ports.DataDragDrop{}
1114+ got := p.b.until(t, "leave")
1115+ for _, ev := range got {
1116+ if ev.name == "drop" {
1117+ t.Fatalf("drop for a source without copy: %+v", got)
1118+ }
1119+ }
1120+ p.a.until(t, "cancelled")
1121+ })
1122+ t.Run("copy", func(t *testing.T) {
1123+ p, id := setup(t, actCopy|actMove)
1124+ p.commands <- ports.DataDragDrop{}
1125+ p.b.expect(t, "drop")
1126+ p.b.expect(t, "leave")
1127+ if ev := p.a.expect(t, "action"); ev.n != actCopy {
1128+ t.Fatalf("action %+v", ev)
1129+ }
1130+ p.a.expect(t, "drop_performed")
1131+ // GO-006: destroying the last offer finishes the transfer.
1132+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy)
1133+ p.a.expect(t, "finished")
1134+ })
1135+}
1136+
1137+// GO-007: of several offset requests before one commit, the last counts.
1138+func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) {
1139+ p := newDndPair(t)
1140+ icon := p.a.surfaceNew(t)
1141+ buf := p.a.buffer(t, 8, 6)
1142+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
1143+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
1144+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1145+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1))
1146+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7))
1147+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
1148+ if err := p.a.c.Roundtrip(); err != nil {
1149+ t.Fatal(err)
1150+ }
1151+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) {
1152+ t.Fatalf("icon %+v", got)
1153+ }
1154+}
@@ -0,0 +1,1154 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "errors"
6+ "sync/atomic"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/go-wayland-bindings/server/wayland"
11+ "github.com/bnema/neferwl/internal/adapters/logging"
12+ "github.com/bnema/neferwl/internal/adapters/sessionsecurity"
13+ "github.com/bnema/neferwl/internal/ports"
14+ "github.com/bnema/wlturbo"
15+ "golang.org/x/sys/unix"
16+)
17+
18+const (
19+ actCopy = uint32(wayland.DataDeviceManagerDndActionCopy)
20+ actMove = uint32(wayland.DataDeviceManagerDndActionMove)
21+ actAsk = uint32(wayland.DataDeviceManagerDndActionAsk)
22+)
23+
24+// dndEvent is one data device, offer or source event a test client saw.
25+type dndEvent struct {
26+ name string
27+ mime string
28+ x, y float64
29+ n uint32
30+}
31+
32+type dndDeviceEvents struct {
33+ wlturbo.BaseProxy
34+ c *wlturbo.Display
35+ log chan dndEvent
36+ // offer is the id of the latest data_offer.
37+ offer atomic.Uint32
38+}
39+
40+func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {
41+ switch e.Opcode {
42+ case uint16(wayland.DataDeviceEventDataOffer):
43+ o := &dndOfferEvents{log: p.log}
44+ o.SetID(e.Uint32())
45+ p.offer.Store(o.ID())
46+ registerWireProxy(p.c, o)
47+ case uint16(wayland.DataDeviceEventEnter):
48+ serial, _ := e.Uint32(), e.Uint32()
49+ x, y := e.Fixed().Float64(), e.Fixed().Float64()
50+ p.log <- dndEvent{name: "enter", x: x, y: y, n: e.Uint32() + serial*0}
51+ case uint16(wayland.DataDeviceEventLeave):
52+ p.log <- dndEvent{name: "leave"}
53+ case uint16(wayland.DataDeviceEventMotion):
54+ _ = e.Uint32()
55+ p.log <- dndEvent{name: "motion", x: e.Fixed().Float64(), y: e.Fixed().Float64()}
56+ case uint16(wayland.DataDeviceEventDrop):
57+ p.log <- dndEvent{name: "drop"}
58+ }
59+}
60+
61+type dndOfferEvents struct {
62+ wlturbo.BaseProxy
63+ log chan dndEvent
64+}
65+
66+func (p *dndOfferEvents) Dispatch(e *wlturbo.Event) {
67+ switch e.Opcode {
68+ case uint16(wayland.DataOfferEventOffer):
69+ p.log <- dndEvent{name: "offer", mime: e.String()}
70+ case uint16(wayland.DataOfferEventSourceActions):
71+ p.log <- dndEvent{name: "source_actions", n: e.Uint32()}
72+ case uint16(wayland.DataOfferEventAction):
73+ p.log <- dndEvent{name: "offer_action", n: e.Uint32()}
74+ }
75+}
76+
77+type dndSourceEvents struct {
78+ wlturbo.BaseProxy
79+ log chan dndEvent
80+}
81+
82+func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) {
83+ switch e.Opcode {
84+ case uint16(wayland.DataSourceEventTarget):
85+ p.log <- dndEvent{name: "target", mime: e.String()}
86+ case uint16(wayland.DataSourceEventSend):
87+ mime := e.String()
88+ fd := int(e.Fd())
89+ _, _ = unix.Write(fd, []byte("dnd data"))
90+ _ = unix.Close(fd)
91+ p.log <- dndEvent{name: "send", mime: mime}
92+ case uint16(wayland.DataSourceEventCancelled):
93+ p.log <- dndEvent{name: "cancelled"}
94+ case uint16(wayland.DataSourceEventDndDropPerformed):
95+ p.log <- dndEvent{name: "drop_performed"}
96+ case uint16(wayland.DataSourceEventDndFinished):
97+ p.log <- dndEvent{name: "finished"}
98+ case uint16(wayland.DataSourceEventAction):
99+ p.log <- dndEvent{name: "action", n: e.Uint32()}
100+ }
101+}
102+
103+// dndClient is a client with a toplevel, a pointer and a data device.
104+type dndClient struct {
105+ keys chan uint32
106+ c *wlturbo.Display
107+ manager uint32
108+ device *dndDeviceEvents
109+ serials chan uint32
110+ comp uint32
111+ shm uint32
112+ surf uint32
113+ window ports.WindowID
114+ log chan dndEvent
115+}
116+
117+// keySerials records the serial of each key event.
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+
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 {
136+ t.Helper()
137+ c := protocolClient(t, s, dir)
138+ d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8), keys: make(chan uint32, 8)}
139+ kb := &keySerials{serials: d.keys}
140+ seat := bindVersion(t, c, "wl_seat", 8)
141+ registerProtocol(t, c, seat)
142+ ptr := &pressSerials{serials: d.serials}
143+ ptr.SetID(c.AllocateID())
144+ registerWireProxy(c, ptr)
145+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID())
146+ kb.SetID(c.AllocateID())
147+ registerWireProxy(c, kb)
148+ requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, kb.ID())
149+ d.manager = bindVersion(t, c, "wl_data_device_manager", version)
150+ d.device = &dndDeviceEvents{c: c, log: d.log}
151+ d.device.SetID(c.AllocateID())
152+ registerWireProxy(c, d.device)
153+ requestProtocol(t, c, d.manager, wayland.DataDeviceManagerRequestGetDataDevice, d.device.ID(), seat)
154+ d.comp = bindVersion(t, c, "wl_compositor", 5)
155+ d.shm = bindProtocol(t, c, "wl_shm")
156+ registerProtocol(t, c, d.shm)
157+ w, surf, _ := surfaceMapperAt(t, c, events, 5)()
158+ d.window, d.surf = w.ID, surf
159+ return d
160+}
161+
162+// lastOffer is the id of the latest offer the client got.
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+
172+// next returns the next event the client sees.
173+func (d *dndClient) next(t *testing.T) dndEvent {
174+ t.Helper()
175+ deadline := time.After(2 * time.Second)
176+ for {
177+ if err := d.c.Roundtrip(); err != nil {
178+ t.Fatal(err)
179+ }
180+ select {
181+ case ev := <-d.log:
182+ return ev
183+ case <-deadline:
184+ t.Fatal("no event")
185+ case <-time.After(5 * time.Millisecond):
186+ }
187+ }
188+}
189+
190+// expect waits for the next event and checks its name.
191+func (d *dndClient) expect(t *testing.T, name string) dndEvent {
192+ t.Helper()
193+ ev := d.next(t)
194+ if ev.name != name {
195+ t.Fatalf("event %+v, want %s", ev, name)
196+ }
197+ return ev
198+}
199+
200+// none checks that no event arrives.
201+func (d *dndClient) none(t *testing.T) {
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)
208+ }
209+ select {
210+ case ev := <-d.log:
211+ t.Fatalf("unexpected event %+v", ev)
212+ default:
213+ }
214+}
215+
216+func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 {
217+ t.Helper()
218+ src := &dndSourceEvents{log: d.log}
219+ src.SetID(d.c.AllocateID())
220+ registerWireProxy(d.c, src)
221+ requestProtocol(t, d.c, d.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID())
222+ for _, m := range mimes {
223+ requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestOffer, m)
224+ }
225+ if actions != 0 {
226+ requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestSetActions, actions)
227+ }
228+ return src.ID()
229+}
230+
231+// press makes the client's window press a button and returns the serial.
232+func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 {
233+ t.Helper()
234+ commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1}
235+ commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Grab: true, Time: time.Millisecond}
236+ deadline := time.After(2 * time.Second)
237+ for {
238+ if err := d.c.Roundtrip(); err != nil {
239+ t.Fatal(err)
240+ }
241+ select {
242+ case serial := <-d.serials:
243+ return serial
244+ case <-deadline:
245+ t.Fatal("no button")
246+ case <-time.After(5 * time.Millisecond):
247+ }
248+ }
249+}
250+
251+// press2 returns the next pointer button serial.
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+
309+func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) {
310+ t.Helper()
311+ d.startDragFrom(t, src, d.surf, icon, serial)
312+}
313+
314+// startDragFrom is startDrag with an origin surface.
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)
318+ if err := d.c.Roundtrip(); err != nil {
319+ t.Fatal(err)
320+ }
321+}
322+
323+// buffer makes a w×h ARGB shm buffer.
324+func (d *dndClient) buffer(t *testing.T, w, h int) uint32 {
325+ t.Helper()
326+ fd, err := unix.MemfdCreate("dnd-icon", 0)
327+ if err != nil {
328+ t.Fatal(err)
329+ }
330+ defer unix.Close(fd)
331+ if err := unix.Ftruncate(fd, int64(w*h*4)); err != nil {
332+ t.Fatal(err)
333+ }
334+ pool, buf := d.c.AllocateID(), d.c.AllocateID()
335+ registerProtocol(t, d.c, pool)
336+ registerProtocol(t, d.c, buf)
337+ if err := wireRequest(d.c, d.shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil {
338+ t.Fatal(err)
339+ }
340+ requestProtocol(t, d.c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
341+ return buf
342+}
343+
344+func (d *dndClient) surfaceNew(t *testing.T) uint32 {
345+ t.Helper()
346+ id := d.c.AllocateID()
347+ requestProtocol(t, d.c, d.comp, wayland.CompositorRequestCreateSurface, id)
348+ registerProtocol(t, d.c, id)
349+ return id
350+}
351+
352+// dndNextEvent returns the next client event of type T, skipping others.
353+func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T {
354+ t.Helper()
355+ deadline := time.After(2 * time.Second)
356+ for {
357+ select {
358+ case ev := <-events:
359+ if v, ok := ev.(T); ok {
360+ return v
361+ }
362+ case <-deadline:
363+ t.Fatalf("no %T event", *new(T))
364+ }
365+ }
366+}
367+
368+// dndNoEvent checks that no client event of type T is pending.
369+func dndNoEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) {
370+ t.Helper()
371+ time.Sleep(50 * time.Millisecond)
372+ for {
373+ select {
374+ case ev := <-events:
375+ if _, ok := ev.(T); ok {
376+ t.Fatalf("unexpected %T: %+v", ev, ev)
377+ }
378+ default:
379+ return
380+ }
381+ }
382+}
383+
384+// dndPair is two clients with a drag started from a on a source.
385+type dndPair struct {
386+ s *Server
387+ events chan ports.ClientEvent
388+ commands chan ports.ClientCommand
389+ a, b *dndClient
390+}
391+
392+func newDndPair(t *testing.T) *dndPair {
393+ t.Helper()
394+ s, events, commands, dir := keyboardServer(t)
395+ p := &dndPair{s: s, events: events, commands: commands}
396+ p.a = newDndClient(t, s, dir, events)
397+ p.b = newDndClient(t, s, dir, events)
398+ return p
399+}
400+
401+// start begins a drag of a source offering text/plain, copy and move.
402+func (p *dndPair) start(t *testing.T, icon uint32) uint32 {
403+ t.Helper()
404+ src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
405+ p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
406+ return src
407+}
408+
409+// enter makes b the drag target and returns the offer id.
410+func (p *dndPair) enter(t *testing.T) uint32 {
411+ t.Helper()
412+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond}
413+ for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} {
414+ if ev := p.b.next(t); ev != want {
415+ t.Fatalf("event %+v, want %+v", ev, want)
416+ }
417+ }
418+ ev := p.b.expect(t, "enter")
419+ if ev.x != 5 || ev.y != 6 || ev.n == 0 {
420+ t.Fatalf("enter %+v", ev)
421+ }
422+ return ev.n
423+}
424+
425+// A1: a drag from a to b delivers enter, motion and drop with the offer;
426+// the action is negotiated; the data reaches b; a is told every step.
427+func TestDataDragBetweenClients(t *testing.T) {
428+ p := newDndPair(t)
429+ p.start(t, 0)
430+ if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 {
431+ t.Fatalf("started %+v", v)
432+ }
433+ offer := p.enter(t)
434+
435+ p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond}
436+ if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 {
437+ t.Fatalf("motion %+v", ev)
438+ }
439+
440+ // b accepts and prefers move: both sides learn the action.
441+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
442+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
443+ if ev := p.a.expect(t, "target"); ev.mime != "text/plain" {
444+ t.Fatalf("target %+v", ev)
445+ }
446+ if ev := p.a.expect(t, "action"); ev.n != actMove {
447+ t.Fatalf("action %+v", ev)
448+ }
449+ if ev := p.b.expect(t, "offer_action"); ev.n != actMove {
450+ t.Fatalf("offer action %+v", ev)
451+ }
452+
453+ // Finishing before the drop is a protocol error, so only after it.
454+ p.commands <- ports.DataDragDrop{}
455+ p.b.expect(t, "drop")
456+ p.b.expect(t, "leave")
457+ p.a.expect(t, "drop_performed")
458+
459+ var fds [2]int
460+ if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
461+ t.Fatal(err)
462+ }
463+ defer unix.Close(fds[0])
464+ if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil {
465+ t.Fatal(err)
466+ }
467+ _ = unix.Close(fds[1])
468+ if err := p.b.c.Roundtrip(); err != nil {
469+ t.Fatal(err)
470+ }
471+ if ev := p.a.expect(t, "send"); ev.mime != "text/plain" {
472+ t.Fatalf("send %+v", ev)
473+ }
474+ buf := make([]byte, 16)
475+ n, err := unix.Read(fds[0], buf)
476+ for errors.Is(err, unix.EINTR) {
477+ n, err = unix.Read(fds[0], buf)
478+ }
479+ if err != nil || string(buf[:n]) != "dnd data" {
480+ t.Fatalf("received %q, %v", buf[:max(n, 0)], err)
481+ }
482+
483+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
484+ p.a.expect(t, "finished")
485+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
486+ p.a.none(t)
487+ p.b.none(t)
488+ // core ended the drag: wayland does not echo it.
489+ dndNoEvent[ports.DataDragEnded](t, p.events)
490+}
491+
492+// The action follows wlroots: the preferred one if both allow it, else the
493+// first of copy, move, ask they share, else none.
494+func TestChooseAction(t *testing.T) {
495+ for _, tc := range []struct{ avail, preferred, want uint32 }{
496+ {actCopy | actMove, actMove, actMove},
497+ {actCopy | actMove, actAsk, actCopy},
498+ {actMove | actAsk, 0, actMove},
499+ {actAsk, 0, actAsk},
500+ {0, actCopy, 0},
501+ } {
502+ if got := chooseAction(tc.avail, tc.preferred); got != tc.want {
503+ t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want)
504+ }
505+ }
506+}
507+
508+// A target and a source sharing no action leave the drop refused.
509+func TestDataDragNoCommonAction(t *testing.T) {
510+ p := newDndPair(t)
511+ src := p.a.source(t, actCopy, "text/plain")
512+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
513+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
514+ p.b.expect(t, "offer")
515+ p.b.expect(t, "source_actions")
516+ offer := p.b.expect(t, "enter").n
517+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
518+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove)
519+ p.a.expect(t, "target")
520+ p.commands <- ports.DataDragDrop{}
521+ p.b.expect(t, "leave")
522+ // The refused target no longer accepts.
523+ if ev := p.a.expect(t, "target"); ev.mime != "" {
524+ t.Fatalf("target %+v", ev)
525+ }
526+ p.a.expect(t, "cancelled")
527+}
528+
529+// A2: a release over nothing, or over a target that accepted nothing,
530+// cancels the source; the target gets leave and no drop.
531+func TestDataDragRefusedDrop(t *testing.T) {
532+ t.Run("nothing", func(t *testing.T) {
533+ p := newDndPair(t)
534+ p.start(t, 0)
535+ p.commands <- ports.DataDragDrop{}
536+ p.a.expect(t, "cancelled")
537+ p.b.none(t)
538+ })
539+ t.Run("not accepted", func(t *testing.T) {
540+ p := newDndPair(t)
541+ p.start(t, 0)
542+ p.enter(t)
543+ p.commands <- ports.DataDragDrop{}
544+ p.b.expect(t, "leave")
545+ p.a.expect(t, "cancelled")
546+ p.b.none(t)
547+ })
548+ t.Run("escape", func(t *testing.T) {
549+ p := newDndPair(t)
550+ p.start(t, 0)
551+ p.enter(t)
552+ p.commands <- ports.DataDragCancel{}
553+ p.b.expect(t, "leave")
554+ p.a.expect(t, "cancelled")
555+ dndNoEvent[ports.DataDragEnded](t, p.events)
556+ })
557+}
558+
559+// Moving to another surface leaves the first and enters the second; a
560+// surface of nobody (ID 0) leaves.
561+func TestDataDragLeavesOnFocusChange(t *testing.T) {
562+ p := newDndPair(t)
563+ p.start(t, 0)
564+ p.enter(t)
565+ p.commands <- ports.DataDragFocus{}
566+ p.b.expect(t, "leave")
567+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3}
568+ for range 3 {
569+ p.b.next(t)
570+ }
571+ if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 {
572+ t.Fatalf("enter %+v", ev)
573+ }
574+}
575+
576+// A4: the source dying mid-drag ends it: the target gets leave and core a
577+// DataDragEnded; later commands change nothing.
578+func TestDataDragSourceDestroyed(t *testing.T) {
579+ p := newDndPair(t)
580+ src := p.start(t, 0)
581+ p.enter(t)
582+ requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy)
583+ if err := p.a.c.Roundtrip(); err != nil {
584+ t.Fatal(err)
585+ }
586+ p.b.expect(t, "leave")
587+ dndNextEvent[ports.DataDragEnded](t, p.events)
588+ p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9}
589+ p.commands <- ports.DataDragDrop{}
590+ p.b.none(t)
591+}
592+
593+// A drag whose origin client dies ends the same way.
594+func TestDataDragOriginDies(t *testing.T) {
595+ p := newDndPair(t)
596+ p.start(t, 0)
597+ p.enter(t)
598+ _ = p.a.c.Close()
599+ p.b.expect(t, "leave")
600+ dndNextEvent[ports.DataDragEnded](t, p.events)
601+}
602+
603+// A6: a start_drag without the current press of the origin client is
604+// refused: the source is cancelled and core hears nothing.
605+func TestDataDragInvalidStart(t *testing.T) {
606+ t.Run("stale serial", func(t *testing.T) {
607+ p := newDndPair(t)
608+ serial := p.a.press(t, p.commands)
609+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1)
610+ p.a.expect(t, "cancelled")
611+ dndNoEvent[ports.DataDragStarted](t, p.events)
612+ })
613+ t.Run("other client's press", func(t *testing.T) {
614+ p := newDndPair(t)
615+ p.a.press(t, p.commands)
616+ serial := p.b.press(t, p.commands)
617+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
618+ p.a.expect(t, "cancelled")
619+ dndNoEvent[ports.DataDragStarted](t, p.events)
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+ // Core is told the grab's button: its release drops.
656+ if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 {
657+ t.Fatalf("drag button %#x, want the grab's", ev.Button)
658+ }
659+ })
660+ t.Run("grab release consumed by core", func(t *testing.T) {
661+ // A window drag consumes the release: the next grab press
662+ // still makes the serial a drag can name.
663+ p := newDndPair(t)
664+ stale := p.a.press(t, p.commands)
665+ serial := p.a.press(t, p.commands)
666+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale)
667+ p.a.expect(t, "cancelled")
668+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
669+ dndNextEvent[ports.DataDragStarted](t, p.events)
670+ })
671+ t.Run("grab release of a gone window", func(t *testing.T) {
672+ p := newDndPair(t)
673+ serial := p.a.press(t, p.commands)
674+ p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond}
675+ 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)
677+ p.a.expect(t, "cancelled")
678+ dndNoEvent[ports.DataDragStarted](t, p.events)
679+ })
680+ t.Run("used source", func(t *testing.T) {
681+ p := newDndPair(t)
682+ src := p.start(t, 0)
683+ p.commands <- ports.DataDragCancel{}
684+ p.a.expect(t, "cancelled")
685+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
686+ p.a.expect(t, "cancelled")
687+ })
688+ t.Run("drag in progress", func(t *testing.T) {
689+ p := newDndPair(t)
690+ p.start(t, 0)
691+ dndNextEvent[ports.DataDragStarted](t, p.events)
692+ other := p.a.source(t, actCopy, "text/plain")
693+ p.a.startDrag(t, other, 0, p.a.press(t, p.commands))
694+ p.a.expect(t, "cancelled")
695+ dndNoEvent[ports.DataDragStarted](t, p.events)
696+ })
697+ t.Run("icon with another role", func(t *testing.T) {
698+ p := newDndPair(t)
699+ serial := p.a.press(t, p.commands)
700+ src := p.a.source(t, actCopy, "text/plain")
701+ // 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)
703+ expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole))
704+ dndNoEvent[ports.DataDragStarted](t, p.events)
705+ })
706+}
707+
708+// A nil source is a drag internal to the origin client: only its own
709+// surfaces see it, without offer.
710+func TestDataDragWithoutSource(t *testing.T) {
711+ p := newDndPair(t)
712+ p.a.startDrag(t, 0, 0, p.a.press(t, p.commands))
713+ dndNextEvent[ports.DataDragStarted](t, p.events)
714+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
715+ p.b.none(t)
716+ 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 {
718+ t.Fatalf("enter %+v", ev)
719+ }
720+ p.commands <- ports.DataDragDrop{}
721+ p.a.expect(t, "drop")
722+}
723+
724+// Offer and source requests are validated.
725+func TestDataDragProtocolErrors(t *testing.T) {
726+ t.Run("finish before drop", func(t *testing.T) {
727+ p := newDndPair(t)
728+ p.start(t, 0)
729+ offer := p.enter(t)
730+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
731+ expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish))
732+ })
733+ t.Run("offer action mask", func(t *testing.T) {
734+ p := newDndPair(t)
735+ p.start(t, 0)
736+ offer := p.enter(t)
737+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0))
738+ expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask))
739+ })
740+ t.Run("offer preferred action", func(t *testing.T) {
741+ p := newDndPair(t)
742+ p.start(t, 0)
743+ offer := p.enter(t)
744+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove)
745+ expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction))
746+ })
747+ t.Run("source action mask", func(t *testing.T) {
748+ p := newDndPair(t)
749+ src := p.a.source(t, 0, "text/plain")
750+ requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16))
751+ expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask))
752+ })
753+ t.Run("selection with actions", func(t *testing.T) {
754+ p := newDndPair(t)
755+ src := p.a.source(t, actCopy, "text/plain")
756+ requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0))
757+ expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
758+ })
759+ t.Run("actions after the drag started", func(t *testing.T) {
760+ p := newDndPair(t)
761+ src := p.start(t, 0)
762+ requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy)
763+ expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
764+ })
765+}
766+
767+// A destroyed offer before finish cancels the source.
768+func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) {
769+ p := newDndPair(t)
770+ p.start(t, 0)
771+ offer := p.enter(t)
772+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
773+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
774+ p.a.expect(t, "target")
775+ p.a.expect(t, "action")
776+ p.commands <- ports.DataDragDrop{}
777+ p.b.expect(t, "offer_action")
778+ p.b.expect(t, "drop")
779+ p.b.expect(t, "leave")
780+ p.a.expect(t, "drop_performed")
781+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
782+ p.a.expect(t, "cancelled")
783+}
784+
785+// iconContent reports whether the server still holds content for id.
786+func iconContent(t *testing.T, s *Server, id ports.WindowID) bool {
787+ t.Helper()
788+ var ok bool
789+ if !s.display.Do(func() { _, ok = s.latest[id] }) {
790+ t.Fatal("display stopped")
791+ }
792+ return ok
793+}
794+
795+// The icon is a surface with its own window ID: its size and the
796+// offsets of its commits reach core, its content is published, and both go
797+// when the drag ends.
798+func TestDataDragIcon(t *testing.T) {
799+ p := newDndPair(t)
800+ icon := p.a.surfaceNew(t)
801+ buf := p.a.buffer(t, 8, 6)
802+ // An offset on an ordinary surface is not used.
803+ requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5))
804+ requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit)
805+
806+ src := p.a.source(t, actCopy, "text/plain")
807+ p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
808+ started := dndNextEvent[ports.DataDragStarted](t, p.events)
809+ if started.Icon == 0 {
810+ t.Fatal("no icon ID")
811+ }
812+ id := started.Icon
813+ if iconContent(t, p.s, id) {
814+ t.Fatal("content before the first commit")
815+ }
816+
817+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
818+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4))
819+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
820+ if err := p.a.c.Roundtrip(); err != nil {
821+ t.Fatal(err)
822+ }
823+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) {
824+ t.Fatalf("icon %+v", got)
825+ }
826+ if !iconContent(t, p.s, id) {
827+ t.Fatal("no icon content")
828+ }
829+
830+ // Offsets add up from commit to commit.
831+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2))
832+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
833+ if err := p.a.c.Roundtrip(); err != nil {
834+ t.Fatal(err)
835+ }
836+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) {
837+ t.Fatalf("icon %+v", got)
838+ }
839+ // The icon never takes input.
840+ dndNoEvent[ports.InputRegionChanged](t, p.events)
841+
842+ p.commands <- ports.DataDragCancel{}
843+ p.a.expect(t, "cancelled")
844+ if iconContent(t, p.s, id) {
845+ t.Fatal("icon content kept after the drag")
846+ }
847+}
848+
849+// An icon committed before start_drag shows at once.
850+func TestDataDragIconCommittedEarly(t *testing.T) {
851+ p := newDndPair(t)
852+ icon := p.a.surfaceNew(t)
853+ buf := p.a.buffer(t, 4, 4)
854+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
855+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
856+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
857+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
858+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) {
859+ t.Fatalf("icon %+v", got)
860+ }
861+ if !iconContent(t, p.s, id) {
862+ t.Fatal("no icon content")
863+ }
864+}
865+
866+// Before wl_surface v5 the icon offset rides on attach and adds up.
867+func TestDataDragIconAttachOffset(t *testing.T) {
868+ p := newDndPair(t)
869+ comp4 := bindVersion(t, p.a.c, "wl_compositor", 4)
870+ icon := p.a.c.AllocateID()
871+ requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon)
872+ registerProtocol(t, p.a.c, icon)
873+ buf := p.a.buffer(t, 4, 4)
874+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
875+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
876+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1))
877+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
878+ if err := p.a.c.Roundtrip(); err != nil {
879+ t.Fatal(err)
880+ }
881+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) {
882+ t.Fatalf("icon %+v", got)
883+ }
884+}
885+
886+// Destroying the icon mid-drag hides it; the drag goes on.
887+func TestDataDragIconDestroyed(t *testing.T) {
888+ p := newDndPair(t)
889+ icon := p.a.surfaceNew(t)
890+ buf := p.a.buffer(t, 8, 6)
891+ p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands))
892+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
893+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
894+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
895+ if err := p.a.c.Roundtrip(); err != nil {
896+ t.Fatal(err)
897+ }
898+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 {
899+ t.Fatalf("icon %+v", got)
900+ }
901+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy)
902+ if err := p.a.c.Roundtrip(); err != nil {
903+ t.Fatal(err)
904+ }
905+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) {
906+ t.Fatalf("icon %+v", got)
907+ }
908+ if iconContent(t, p.s, id) {
909+ t.Fatal("icon content kept")
910+ }
911+ p.enter(t)
912+}
913+
914+// lockedPair is a pair of clients on a server that can lock the session.
915+func lockedPair(t *testing.T) (*dndPair, string) {
916+ t.Helper()
917+ gate := &sessionsecurity.Gate{}
918+ events := make(chan ports.ClientEvent, 128)
919+ commands := make(chan ports.ClientCommand, 32)
920+ dir := t.TempDir()
921+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Security: gate, Keymap: keymapText(t)}, Channels{Events: events, Commands: commands, SecurityChanges: make(chan ports.SecurityState, 16), SecurityEvents: make(chan ports.SecurityBackendEvent, 16)}, logging.For(context.Background(), "wayland"))
922+ if err != nil {
923+ t.Fatal(err)
924+ }
925+ ctx, cancel := context.WithCancel(context.Background())
926+ done := make(chan error, 1)
927+ go func() { done <- s.Run(ctx) }()
928+ t.Cleanup(func() {
929+ cancel()
930+ select {
931+ case err := <-done:
932+ if err != nil {
933+ t.Error(err)
934+ }
935+ case <-time.After(3 * time.Second):
936+ t.Error("server did not stop")
937+ }
938+ })
939+ p := &dndPair{s: s, events: events, commands: commands}
940+ p.a = newDndClient(t, s, dir, events)
941+ p.b = newDndClient(t, s, dir, events)
942+ return p, dir
943+}
944+
945+func lockSession(t *testing.T, s *Server, dir string) {
946+ t.Helper()
947+ lc := protocolClient(t, s, dir)
948+ if _, err := bindLockManager(t, lc).Lock(); err != nil {
949+ t.Fatal(err)
950+ }
951+ if err := lc.Roundtrip(); err != nil {
952+ t.Fatal(err)
953+ }
954+}
955+
956+// A7: the session lock cancels a drag at once: target leave, source
957+// cancelled, icon gone.
958+func TestDataDragLockCancels(t *testing.T) {
959+ p, dir := lockedPair(t)
960+ icon := p.a.surfaceNew(t)
961+ buf := p.a.buffer(t, 8, 6)
962+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
963+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
964+ src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
965+ p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
966+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
967+ p.enter(t)
968+
969+ lockSession(t, p.s, dir)
970+ p.b.expect(t, "leave")
971+ p.a.expect(t, "cancelled")
972+ if iconContent(t, p.s, id) {
973+ t.Fatal("icon content kept")
974+ }
975+ // A new drag is refused while protected.
976+ src2 := p.a.source(t, actCopy, "text/plain")
977+ p.a.startDrag(t, src2, 0, uint32(1))
978+ p.a.expect(t, "cancelled")
979+}
980+
981+// GO-006: a dropped transfer waiting for its finish dies with the lock, for
982+// a v3 and a pre-v3 target alike.
983+func TestDataDropLockCancelsPending(t *testing.T) {
984+ for _, version := range []uint32{3, 2} {
985+ p, dir := lockedPair(t)
986+ p.b = newDndClientAt(t, p.s, dir, p.events, version)
987+ p.start(t, 0)
988+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6}
989+ p.b.until(t, "enter")
990+ offer := p.b.lastOffer(t)
991+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
992+ if version >= 3 {
993+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
994+ }
995+ p.a.until(t, "target")
996+ p.commands <- ports.DataDragDrop{}
997+ p.a.until(t, "drop_performed")
998+ lockSession(t, p.s, dir)
999+ p.a.expect(t, "cancelled")
1000+ }
1001+}
1002+
1003+// A subsurface of the focused tree is a valid origin.
1004+func TestDataDragFromSubsurface(t *testing.T) {
1005+ p := newDndPair(t)
1006+ serial := p.a.press(t, p.commands)
1007+ child := p.a.surfaceNew(t)
1008+ subc := bindProtocol(t, p.a.c, "wl_subcompositor")
1009+ requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf)
1010+ p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial)
1011+ dndNextEvent[ports.DataDragStarted](t, p.events)
1012+}
1013+
1014+// GO-003: destroying the offers of the target withdraws the acceptance: the
1015+// release then cancels, with no drop.
1016+func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) {
1017+ p := newDndPair(t)
1018+ p.start(t, 0)
1019+ offer := p.enter(t)
1020+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1021+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1022+ p.a.expect(t, "target")
1023+ p.a.expect(t, "action")
1024+ requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
1025+ if err := p.b.c.Roundtrip(); err != nil {
1026+ t.Fatal(err)
1027+ }
1028+ if ev := p.a.expect(t, "target"); ev.mime != "" {
1029+ t.Fatalf("target %+v", ev)
1030+ }
1031+ if ev := p.a.expect(t, "action"); ev.n != 0 {
1032+ t.Fatalf("action %+v", ev)
1033+ }
1034+ p.commands <- ports.DataDragDrop{}
1035+ got := p.b.until(t, "leave")
1036+ for _, ev := range got {
1037+ if ev.name == "drop" {
1038+ t.Fatalf("drop after the offer was destroyed: %+v", got)
1039+ }
1040+ }
1041+ p.a.expect(t, "cancelled")
1042+}
1043+
1044+// GO-004: with ask, the target picks the final action after the drop; the
1045+// source hears it just before dnd_finished.
1046+func TestDataDragAsk(t *testing.T) {
1047+ p := newDndPair(t)
1048+ src := p.a.source(t, actCopy|actMove|actAsk, "text/plain")
1049+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1050+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1051+ p.b.until(t, "enter")
1052+ id := p.b.lastOffer(t)
1053+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1054+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1055+ p.a.expect(t, "target")
1056+ if ev := p.a.expect(t, "action"); ev.n != actAsk {
1057+ t.Fatalf("action %+v", ev)
1058+ }
1059+ p.commands <- ports.DataDragDrop{}
1060+ p.b.until(t, "leave")
1061+ p.a.expect(t, "drop_performed")
1062+
1063+ // Finishing while the action is ask is an error; the choice comes first.
1064+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain")
1065+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1066+ 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
1069+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish)
1070+ if ev := p.a.expect(t, "action"); ev.n != actMove {
1071+ t.Fatalf("action %+v", ev)
1072+ }
1073+ p.a.expect(t, "finished")
1074+}
1075+
1076+// A final preferred action outside the source's is an error.
1077+func TestDataDragAskFinalActionNotInSource(t *testing.T) {
1078+ p := newDndPair(t)
1079+ src := p.a.source(t, actCopy|actAsk, "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+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1086+ p.a.expect(t, "target")
1087+ p.a.expect(t, "action")
1088+ p.commands <- ports.DataDragDrop{}
1089+ p.b.until(t, "leave")
1090+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove)
1091+ expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction))
1092+}
1093+
1094+// GO-005: a pre-v3 target copies: a source that does not allow copy is
1095+// cancelled, one that does gets it.
1096+func TestDataDragLegacyTarget(t *testing.T) {
1097+ setup := func(t *testing.T, actions uint32) (*dndPair, uint32) {
1098+ s, events, commands, dir := keyboardServer(t)
1099+ p := &dndPair{s: s, events: events, commands: commands}
1100+ p.a = newDndClient(t, s, dir, events)
1101+ p.b = newDndClientAt(t, s, dir, events, 2)
1102+ src := p.a.source(t, actions, "text/plain")
1103+ p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1104+ p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1105+ p.b.until(t, "enter")
1106+ id := p.b.lastOffer(t)
1107+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1108+ p.a.expect(t, "target")
1109+ return p, id
1110+ }
1111+ t.Run("move only", func(t *testing.T) {
1112+ p, _ := setup(t, actMove)
1113+ p.commands <- ports.DataDragDrop{}
1114+ got := p.b.until(t, "leave")
1115+ for _, ev := range got {
1116+ if ev.name == "drop" {
1117+ t.Fatalf("drop for a source without copy: %+v", got)
1118+ }
1119+ }
1120+ p.a.until(t, "cancelled")
1121+ })
1122+ t.Run("copy", func(t *testing.T) {
1123+ p, id := setup(t, actCopy|actMove)
1124+ p.commands <- ports.DataDragDrop{}
1125+ p.b.expect(t, "drop")
1126+ p.b.expect(t, "leave")
1127+ if ev := p.a.expect(t, "action"); ev.n != actCopy {
1128+ t.Fatalf("action %+v", ev)
1129+ }
1130+ p.a.expect(t, "drop_performed")
1131+ // GO-006: destroying the last offer finishes the transfer.
1132+ requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy)
1133+ p.a.expect(t, "finished")
1134+ })
1135+}
1136+
1137+// GO-007: of several offset requests before one commit, the last counts.
1138+func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) {
1139+ p := newDndPair(t)
1140+ icon := p.a.surfaceNew(t)
1141+ buf := p.a.buffer(t, 8, 6)
1142+ p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
1143+ id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
1144+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1145+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1))
1146+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7))
1147+ requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
1148+ if err := p.a.c.Roundtrip(); err != nil {
1149+ t.Fatal(err)
1150+ }
1151+ if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) {
1152+ t.Fatalf("icon %+v", got)
1153+ }
1154+}
M internal/adapters/wayland/input_region.go +2 -1
@@ -82,7 +82,8 @@ func (s *surface) resetInputEmission() {
8282 func (s *surface) emitInput() {
8383 root := s.root()
8484 id := root.windowID()
85- if id == 0 {
85+ if id == 0 || root.kind == roleDragIcon {
86+ // A drag icon takes no input.
8687 return
8788 }
8889 all, rects := root.effectiveInput()
@@ -82,7 +82,8 @@ func (s *surface) resetInputEmission() {
82 func (s *surface) emitInput() {82 func (s *surface) emitInput() {
83 root := s.root()83 root := s.root()
84 id := root.windowID()84 id := root.windowID()
85- if id == 0 {85+ if id == 0 || root.kind == roleDragIcon {
86+ // A drag icon takes no input.
86 return87 return
87 }88 }
88 all, rects := root.effectiveInput()89 all, rects := root.effectiveInput()
M internal/adapters/wayland/seat.go +15 -0
@@ -43,6 +43,13 @@ type seatState struct {
4343 // pressClient: popup grabs must come from it.
4444 press uint32
4545 pressClient server.Client
46+ // grabSerial is the serial of the press that started the implicit
47+ // grab (core marks it), grabButton its button: the only serial
48+ // start_drag accepts (wlr_seat_validate_pointer_grab_serial). The
49+ // grab's release clears it; core may consume that release (a window
50+ // drag), and then the next grab press replaces it.
51+ grabSerial uint32
52+ grabButton uint32
4653 // pressAt and focusAt date the last press and keyboard focus change,
4754 // for xdg-activation tokens.
4855 pressAt, focusAt time.Time
@@ -127,12 +134,20 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
127134 }
128135 case ports.PointerButtonTo:
129136 // Releases may target the implicit-grab window after focus has moved.
137+ if c.Pressed && c.Grab || !c.Pressed && c.Button == s.seat.grabButton {
138+ // A new grab, or the end of this one even when its window is
139+ // gone.
140+ s.seat.grabSerial, s.seat.grabButton = 0, 0
141+ }
130142 if client, pointers, ok := s.pointerTarget(c.ID); ok {
131143 s.serial++
132144 state := uint32(0)
133145 if c.Pressed {
134146 state = 1
135147 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
148+ if c.Grab {
149+ s.seat.grabSerial, s.seat.grabButton = s.serial, c.Button
150+ }
136151 }
137152 for _, p := range pointers {
138153 s.stampPointer(p, c.Time)
@@ -43,6 +43,13 @@ 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+ // grabSerial is the serial of the press that started the implicit
47+ // grab (core marks it), grabButton its button: the only serial
48+ // start_drag accepts (wlr_seat_validate_pointer_grab_serial). The
49+ // grab's release clears it; core may consume that release (a window
50+ // drag), and then the next grab press replaces it.
51+ grabSerial uint32
52+ grabButton uint32
46 // pressAt and focusAt date the last press and keyboard focus change,53 // pressAt and focusAt date the last press and keyboard focus change,
47 // for xdg-activation tokens.54 // for xdg-activation tokens.
48 pressAt, focusAt time.Time55 pressAt, focusAt time.Time
@@ -127,12 +134,20 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
127 }134 }
128 case ports.PointerButtonTo:135 case ports.PointerButtonTo:
129 // Releases may target the implicit-grab window after focus has moved.136 // Releases may target the implicit-grab window after focus has moved.
137+ if c.Pressed && c.Grab || !c.Pressed && c.Button == s.seat.grabButton {
138+ // A new grab, or the end of this one even when its window is
139+ // gone.
140+ s.seat.grabSerial, s.seat.grabButton = 0, 0
141+ }
130 if client, pointers, ok := s.pointerTarget(c.ID); ok {142 if client, pointers, ok := s.pointerTarget(c.ID); ok {
131 s.serial++143 s.serial++
132 state := uint32(0)144 state := uint32(0)
133 if c.Pressed {145 if c.Pressed {
134 state = 1146 state = 1
135 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()147 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
148+ if c.Grab {
149+ s.seat.grabSerial, s.seat.grabButton = s.serial, c.Button
150+ }
136 }151 }
137 for _, p := range pointers {152 for _, p := range pointers {
138 s.stampPointer(p, c.Time)153 s.stampPointer(p, c.Time)
M internal/adapters/wayland/server.go +10 -2
@@ -292,7 +292,12 @@ type Server struct {
292292 cursorQueued bool
293293 cursorReady chan struct{}
294294 // Clipboard and primary selection (clipboard.go).
295- selections [2]*clipSource
295+ selections [2]*clipSource
296+ // Drag and drop (dnd.go): the drag in progress, drops waiting for
297+ // their finish, and each client's data devices.
298+ dnd *dndState
299+ dndDrops []*dndTarget
300+ dataDevices map[server.Client][]*wayland.DataDevice
296301 clipDevices []*clipDevice
297302 dataSources, primarySources, controlSources map[*server.Resource]*clipSource
298303 // Pointer constraints (constraints.go).
@@ -786,13 +791,16 @@ func (s *Server) apply(cmd ports.ClientCommand) {
786791 // Only input/focus commands require an owner envelope. Config relay
787792 // also produces SetKeymap and other epoch-independent commands.
788793 switch cmd.(type) {
789- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey:
794+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey,
795+ ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
790796 return
791797 }
792798 }
793799 switch c := cmd.(type) {
794800 case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
795801 s.applyInput(c)
802+ case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
803+ s.applyDrag(c)
796804 case ports.ConfigurePopup:
797805 if w := s.windows[c.ID]; w != nil && w.popup != nil {
798806 w.popup.configure(c)
@@ -292,7 +292,12 @@ type Server struct {
292 cursorQueued bool292 cursorQueued bool
293 cursorReady chan struct{}293 cursorReady chan struct{}
294 // Clipboard and primary selection (clipboard.go).294 // Clipboard and primary selection (clipboard.go).
295- selections [2]*clipSource295+ selections [2]*clipSource
296+ // Drag and drop (dnd.go): the drag in progress, drops waiting for
297+ // their finish, and each client's data devices.
298+ dnd *dndState
299+ dndDrops []*dndTarget
300+ dataDevices map[server.Client][]*wayland.DataDevice
296 clipDevices []*clipDevice301 clipDevices []*clipDevice
297 dataSources, primarySources, controlSources map[*server.Resource]*clipSource302 dataSources, primarySources, controlSources map[*server.Resource]*clipSource
298 // Pointer constraints (constraints.go).303 // Pointer constraints (constraints.go).
@@ -786,13 +791,16 @@ func (s *Server) apply(cmd ports.ClientCommand) {
786 // Only input/focus commands require an owner envelope. Config relay791 // Only input/focus commands require an owner envelope. Config relay
787 // also produces SetKeymap and other epoch-independent commands.792 // also produces SetKeymap and other epoch-independent commands.
788 switch cmd.(type) {793 switch cmd.(type) {
789- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey:794+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey,
795+ ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
790 return796 return
791 }797 }
792 }798 }
793 switch c := cmd.(type) {799 switch c := cmd.(type) {
794 case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:800 case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
795 s.applyInput(c)801 s.applyInput(c)
802+ case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
803+ s.applyDrag(c)
796 case ports.ConfigurePopup:804 case ports.ConfigurePopup:
797 if w := s.windows[c.ID]; w != nil && w.popup != nil {805 if w := s.windows[c.ID]; w != nil && w.popup != nil {
798 w.popup.configure(c)806 w.popup.configure(c)
M internal/adapters/wayland/session_lock.go +2 -0
@@ -138,6 +138,7 @@ func (l *sessionLock) orphan() {
138138 }
139139
140140 func (s *Server) resetProtectionInput() {
141+ s.dndCancelAll()
141142 s.changeFocus(0)
142143 s.changePointerFocus(0, 0, 0)
143144 s.deactivateText()
@@ -146,6 +147,7 @@ func (s *Server) resetProtectionInput() {
146147 clear(s.seat.grabKeys)
147148 s.seat.modState = ports.ModState{}
148149 s.seat.press, s.seat.pressClient = 0, server.Client{}
150+ s.seat.grabSerial, s.seat.grabButton = 0, 0
149151 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
150152 s.cursorSurface = nil
151153 }
@@ -138,6 +138,7 @@ func (l *sessionLock) orphan() {
138 }138 }
139 139
140 func (s *Server) resetProtectionInput() {140 func (s *Server) resetProtectionInput() {
141+ s.dndCancelAll()
141 s.changeFocus(0)142 s.changeFocus(0)
142 s.changePointerFocus(0, 0, 0)143 s.changePointerFocus(0, 0, 0)
143 s.deactivateText()144 s.deactivateText()
@@ -146,6 +147,7 @@ func (s *Server) resetProtectionInput() {
146 clear(s.seat.grabKeys)147 clear(s.seat.grabKeys)
147 s.seat.modState = ports.ModState{}148 s.seat.modState = ports.ModState{}
148 s.seat.press, s.seat.pressClient = 0, server.Client{}149 s.seat.press, s.seat.pressClient = 0, server.Client{}
150+ s.seat.grabSerial, s.seat.grabButton = 0, 0
149 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}151 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
150 s.cursorSurface = nil152 s.cursorSurface = nil
151 }153 }
M internal/adapters/wayland/surface.go +26 -3
@@ -26,6 +26,8 @@ const (
2626 roleCursor
2727 roleInputPopup
2828 roleSessionLock
29+ // roleDragIcon is the icon of a data drag (dnd.go).
30+ roleDragIcon
2931 )
3032
3133 type surface struct {
@@ -137,6 +139,10 @@ type pendingCommit struct {
137139 // barrier and wait are fifo requests; at the commit timestamp.
138140 barrier, wait bool
139141 at time.Time
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.
145+ dx, dy int32
140146 // damage is in surface (logical) pixels, bufDamage in buffer pixels.
141147 damage, bufDamage []ports.Rect
142148 // feedback are wp_presentation feedbacks for this commit.
@@ -192,6 +198,9 @@ func (s *surface) windowID() ports.WindowID {
192198 if s.lock != nil && s.lock.mapped {
193199 return s.lock.id
194200 }
201+ if s.kind == roleDragIcon && s.server.dnd != nil && s.server.dnd.icon == s {
202+ return s.server.dnd.iconID
203+ }
195204 return 0
196205 }
197206
@@ -343,10 +352,17 @@ func (s *surface) Destroy(*wayland.Surface) {
343352 if c := s.server.constraints[s]; c != nil {
344353 s.server.dropConstraint(c)
345354 }
355+ s.server.dndSurfaceGone(s)
346356 }
347-func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) {
357+
358+// Attach: before wl_surface v5 the offset rides on attach; only a drag
359+// icon uses it.
360+func (s *surface) Attach(r *wayland.Surface, b *wayland.Buffer, dx, dy int32) {
348361 s.next.buffer = b
349362 s.next.attached = true
363+ if r.Version() < 5 {
364+ s.next.dx, s.next.dy = dx, dy
365+ }
350366 }
351367 func (s *surface) Frame(r *wayland.Surface, id uint32) {
352368 cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})
@@ -544,6 +560,9 @@ func (s *surface) applyCommit(u *update) {
544560 }
545561 s.commitConstraint(u.cons, u.region, geometry)
546562 s.emitInput()
563+ if s.kind == roleDragIcon {
564+ s.server.dndIconCommit(s, u.dx, u.dy)
565+ }
547566 if s.xdg != nil && s.xdg.window != nil {
548567 s.xdg.window.afterCommit()
549568 }
@@ -757,5 +776,9 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
757776 }
758777 s.next.scale = int(v)
759778 }
760-func (*surface) Offset(*wayland.Surface, int32, int32) {}
761-func (*surface) GetRelease(*wayland.Surface, uint32) {}
779+
780+// Offset moves the next commit's surface (v5); only a drag icon uses it.
781+func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) {
782+ s.next.dx, s.next.dy = dx, dy
783+}
784+func (*surface) GetRelease(*wayland.Surface, uint32) {}
@@ -26,6 +26,8 @@ const (
26 roleCursor26 roleCursor
27 roleInputPopup27 roleInputPopup
28 roleSessionLock28 roleSessionLock
29+ // roleDragIcon is the icon of a data drag (dnd.go).
30+ roleDragIcon
29 )31 )
30 32
31 type surface struct {33 type surface struct {
@@ -137,6 +139,10 @@ type pendingCommit struct {
137 // barrier and wait are fifo requests; at the commit timestamp.139 // barrier and wait are fifo requests; at the commit timestamp.
138 barrier, wait bool140 barrier, wait bool
139 at time.Time141 at time.Time
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.
145+ dx, dy int32
140 // damage is in surface (logical) pixels, bufDamage in buffer pixels.146 // damage is in surface (logical) pixels, bufDamage in buffer pixels.
141 damage, bufDamage []ports.Rect147 damage, bufDamage []ports.Rect
142 // feedback are wp_presentation feedbacks for this commit.148 // feedback are wp_presentation feedbacks for this commit.
@@ -192,6 +198,9 @@ func (s *surface) windowID() ports.WindowID {
192 if s.lock != nil && s.lock.mapped {198 if s.lock != nil && s.lock.mapped {
193 return s.lock.id199 return s.lock.id
194 }200 }
201+ if s.kind == roleDragIcon && s.server.dnd != nil && s.server.dnd.icon == s {
202+ return s.server.dnd.iconID
203+ }
195 return 0204 return 0
196 }205 }
197 206
@@ -343,10 +352,17 @@ func (s *surface) Destroy(*wayland.Surface) {
343 if c := s.server.constraints[s]; c != nil {352 if c := s.server.constraints[s]; c != nil {
344 s.server.dropConstraint(c)353 s.server.dropConstraint(c)
345 }354 }
355+ s.server.dndSurfaceGone(s)
346 }356 }
347-func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) {357+
358+// Attach: before wl_surface v5 the offset rides on attach; only a drag
359+// icon uses it.
360+func (s *surface) Attach(r *wayland.Surface, b *wayland.Buffer, dx, dy int32) {
348 s.next.buffer = b361 s.next.buffer = b
349 s.next.attached = true362 s.next.attached = true
363+ if r.Version() < 5 {
364+ s.next.dx, s.next.dy = dx, dy
365+ }
350 }366 }
351 func (s *surface) Frame(r *wayland.Surface, id uint32) {367 func (s *surface) Frame(r *wayland.Surface, id uint32) {
352 cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})368 cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})
@@ -544,6 +560,9 @@ func (s *surface) applyCommit(u *update) {
544 }560 }
545 s.commitConstraint(u.cons, u.region, geometry)561 s.commitConstraint(u.cons, u.region, geometry)
546 s.emitInput()562 s.emitInput()
563+ if s.kind == roleDragIcon {
564+ s.server.dndIconCommit(s, u.dx, u.dy)
565+ }
547 if s.xdg != nil && s.xdg.window != nil {566 if s.xdg != nil && s.xdg.window != nil {
548 s.xdg.window.afterCommit()567 s.xdg.window.afterCommit()
549 }568 }
@@ -757,5 +776,9 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
757 }776 }
758 s.next.scale = int(v)777 s.next.scale = int(v)
759 }778 }
760-func (*surface) Offset(*wayland.Surface, int32, int32) {}779+
761-func (*surface) GetRelease(*wayland.Surface, uint32) {}780+// Offset moves the next commit's surface (v5); only a drag icon uses it.
781+func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) {
782+ s.next.dx, s.next.dy = dx, dy
783+}
784+func (*surface) GetRelease(*wayland.Surface, uint32) {}
M internal/core/client.go +21 -0
@@ -84,6 +84,24 @@ func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish,
8484 if c.clientDrag(ctx, v) != nil {
8585 return false, true
8686 }
87+ case ports.DataDragStarted:
88+ if c.startDataDrag(ctx, v) != nil {
89+ return false, true
90+ }
91+ return false, false
92+ case ports.DataDragIcon:
93+ if !c.setDataDragIcon(v) {
94+ return false, false
95+ }
96+ case ports.DataDragEnded:
97+ if c.dataDrag == nil {
98+ return false, false
99+ }
100+ c.abortDataDrag()
101+ if c.finishDataDrag(ctx) != nil {
102+ return false, true
103+ }
104+ return false, false
87105 case ports.WindowFullscreenRequest:
88106 if c.fullscreenRequest(ctx, v) != nil {
89107 return false, true
@@ -114,6 +132,9 @@ func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error {
114132 if c.drag != nil && c.drag.id == v.ID {
115133 c.abortDrag()
116134 }
135+ if err := c.dataDragGone(ctx, v.ID); err != nil {
136+ return err
137+ }
117138 c.unmapWindow(v)
118139 c.releaseSlots()
119140 if c.pointer == v.ID {
@@ -84,6 +84,24 @@ func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish,
84 if c.clientDrag(ctx, v) != nil {84 if c.clientDrag(ctx, v) != nil {
85 return false, true85 return false, true
86 }86 }
87+ case ports.DataDragStarted:
88+ if c.startDataDrag(ctx, v) != nil {
89+ return false, true
90+ }
91+ return false, false
92+ case ports.DataDragIcon:
93+ if !c.setDataDragIcon(v) {
94+ return false, false
95+ }
96+ case ports.DataDragEnded:
97+ if c.dataDrag == nil {
98+ return false, false
99+ }
100+ c.abortDataDrag()
101+ if c.finishDataDrag(ctx) != nil {
102+ return false, true
103+ }
104+ return false, false
87 case ports.WindowFullscreenRequest:105 case ports.WindowFullscreenRequest:
88 if c.fullscreenRequest(ctx, v) != nil {106 if c.fullscreenRequest(ctx, v) != nil {
89 return false, true107 return false, true
@@ -114,6 +132,9 @@ func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error {
114 if c.drag != nil && c.drag.id == v.ID {132 if c.drag != nil && c.drag.id == v.ID {
115 c.abortDrag()133 c.abortDrag()
116 }134 }
135+ if err := c.dataDragGone(ctx, v.ID); err != nil {
136+ return err
137+ }
117 c.unmapWindow(v)138 c.unmapWindow(v)
118 c.releaseSlots()139 c.releaseSlots()
119 if c.pointer == v.ID {140 if c.pointer == v.ID {
M internal/core/clientgesture.go +1 -1
@@ -38,7 +38,7 @@ func (c *Core) gestureBegin(ctx context.Context, kind ports.GestureKind, fingers
3838 return err
3939 }
4040 c.gesture = clientGesture{on: true, kind: kind, at: at}
41- if c.pointer == 0 || c.drag != nil {
41+ if c.pointer == 0 || c.drag != nil || c.dataDrag != nil {
4242 return nil
4343 }
4444 c.gesture.id = c.pointer
@@ -38,7 +38,7 @@ func (c *Core) gestureBegin(ctx context.Context, kind ports.GestureKind, fingers
38 return err38 return err
39 }39 }
40 c.gesture = clientGesture{on: true, kind: kind, at: at}40 c.gesture = clientGesture{on: true, kind: kind, at: at}
41- if c.pointer == 0 || c.drag != nil {41+ if c.pointer == 0 || c.drag != nil || c.dataDrag != nil {
42 return nil42 return nil
43 }43 }
44 c.gesture.id = c.pointer44 c.gesture.id = c.pointer
M internal/core/core.go +7 -1
@@ -101,7 +101,9 @@ type Core struct {
101101 // buttons whose release no client must see. mods are the modifiers
102102 // held, cmdMod the one keyboard.cmd names. lastButton is the latest
103103 // press a client saw, the one xdg_toplevel.move's serial names.
104- drag *dragState
104+ drag *dragState
105+ // dataDrag is the client data drag in progress (datadrag.go).
106+ dataDrag *dataDragState
105107 swallow map[uint32]bool
106108 lastButton uint32
107109 mods ports.Mods
@@ -760,6 +762,10 @@ func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error {
760762 // rehit points the pointer at what now lies under a still cursor, as
761763 // windows slide under it. A held button keeps its grab.
762764 func (c *Core) rehit(ctx context.Context) error {
765+ if c.dataDrag != nil {
766+ // The drag target follows the layout too.
767+ return c.dataDragRetarget(ctx, c.motionTime)
768+ }
763769 if c.grab != 0 || c.drag != nil {
764770 return nil
765771 }
@@ -101,7 +101,9 @@ type Core struct {
101 // buttons whose release no client must see. mods are the modifiers101 // buttons whose release no client must see. mods are the modifiers
102 // held, cmdMod the one keyboard.cmd names. lastButton is the latest102 // held, cmdMod the one keyboard.cmd names. lastButton is the latest
103 // press a client saw, the one xdg_toplevel.move's serial names.103 // press a client saw, the one xdg_toplevel.move's serial names.
104- drag *dragState104+ drag *dragState
105+ // dataDrag is the client data drag in progress (datadrag.go).
106+ dataDrag *dataDragState
105 swallow map[uint32]bool107 swallow map[uint32]bool
106 lastButton uint32108 lastButton uint32
107 mods ports.Mods109 mods ports.Mods
@@ -760,6 +762,10 @@ func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error {
760 // rehit points the pointer at what now lies under a still cursor, as762 // rehit points the pointer at what now lies under a still cursor, as
761 // windows slide under it. A held button keeps its grab.763 // windows slide under it. A held button keeps its grab.
762 func (c *Core) rehit(ctx context.Context) error {764 func (c *Core) rehit(ctx context.Context) error {
765+ if c.dataDrag != nil {
766+ // The drag target follows the layout too.
767+ return c.dataDragRetarget(ctx, c.motionTime)
768+ }
763 if c.grab != 0 || c.drag != nil {769 if c.grab != 0 || c.drag != nil {
764 return nil770 return nil
765 }771 }
M internal/core/core_test.go +1 -1
@@ -307,7 +307,7 @@ func TestPointerFocusAndGrab(t *testing.T) {
307307 t.Fatal(v)
308308 }
309309 input <- ports.PointerButton{Button: 0x110, Pressed: true}
310- if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) {
310+ if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true, Grab: true}) {
311311 t.Fatal(v)
312312 }
313313 scene(t, scenes)
@@ -307,7 +307,7 @@ func TestPointerFocusAndGrab(t *testing.T) {
307 t.Fatal(v)307 t.Fatal(v)
308 }308 }
309 input <- ports.PointerButton{Button: 0x110, Pressed: true}309 input <- ports.PointerButton{Button: 0x110, Pressed: true}
310- if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) {310+ if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true, Grab: true}) {
311 t.Fatal(v)311 t.Fatal(v)
312 }312 }
313 scene(t, scenes)313 scene(t, scenes)
A internal/core/datadrag.go +206 -0
@@ -0,0 +1,206 @@
1+package core
2+
3+import (
4+ "context"
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// Data drag: a client drag and drop (wl_data_device.start_drag). Wayland
11+// validated the request against the client's press and tells core; core
12+// owns the pointer for the length of the drag, as for a window drag
13+// (drag.go):
14+//
15+// - the origin client gets the pointer leave and no pointer event after;
16+// nobody else gets one: the surface under the pointer is the drag
17+// target, told to wayland as DataDragFocus and DataDragMotion;
18+// - releasing the drag button drops (DataDragDrop), Escape cancels
19+// (DataDragCancel); neither reaches a client;
20+// - scrolling and touchpad gestures go nowhere, compositor swipes
21+// included; binds still run, Escape cancels the drag before the
22+// switcher; the keyboard keeps its
23+// focus, clients read the modifiers that choose the action from it;
24+// - the drag icon, a client surface wayland measures (DataDragIcon), is
25+// drawn at the cursor over everything.
26+//
27+// Layout changes under a still cursor reroute the drag (rehit), and the
28+// drop re-hits.
29+
30+// dataDragState is the data drag in progress.
31+type dataDragState struct {
32+ // button is the pointer button that holds the drag.
33+ button uint32
34+ // target is the surface the drag was last routed to (0: none), at
35+ // at in its coordinates.
36+ target WindowID
37+ at [2]float64
38+ icon dataDragIcon
39+}
40+
41+// dataDragIcon is the icon surface: its top left corner relative to the
42+// cursor and its size, logical px. Hidden while W or H is 0.
43+type dataDragIcon struct {
44+ id WindowID
45+ x, y, w, h int
46+}
47+
48+// startDataDrag answers the start_drag wayland validated: it takes the
49+// pointer if a client holds a button, else cancels the drag. A drag
50+// during a window drag or a locked session is refused.
51+func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error {
52+ button := v.Button
53+ if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] {
54+ return c.command(ctx, ports.DataDragCancel{})
55+ }
56+ c.dataDrag = &dataDragState{button: button, icon: dataDragIcon{id: v.Icon}}
57+ // Other held buttons release without a client to tell.
58+ for b := range c.buttons {
59+ if b != button {
60+ c.swallow[b] = true
61+ }
62+ }
63+ c.grab = 0
64+ // A compositor swipe in progress lets go of what it moved.
65+ if c.swipe != nil {
66+ c.dropSwipe()
67+ c.swipe = nil
68+ }
69+ // The origin gets its pointer leave, as the drag takes the pointer.
70+ if c.pointer != 0 {
71+ c.pointer = 0
72+ if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
73+ return err
74+ }
75+ }
76+ // The surface under the pointer, the origin's usually, is the first
77+ // target.
78+ return c.dataDragRoute(ctx, c.motionTime)
79+}
80+
81+// dataDragRoute sends the surface under the cursor as the drag target: a
82+// focus when it changed, else a motion.
83+func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error {
84+ d := c.dataDrag
85+ id, x, y := c.hit(c.cursorX, c.cursorY)
86+ d.at = [2]float64{x, y}
87+ switch {
88+ case id != d.target:
89+ d.target = id
90+ return c.command(ctx, ports.DataDragFocus{ID: id, X: x, Y: y, Time: at})
91+ case id != 0:
92+ return c.command(ctx, ports.DataDragMotion{ID: id, X: x, Y: y, Time: at})
93+ }
94+ return nil
95+}
96+
97+// dataDragRetarget re-routes a still cursor: the target is told only when
98+// it or the point in it changed.
99+func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error {
100+ d := c.dataDrag
101+ if id, x, y := c.hit(c.cursorX, c.cursorY); id == d.target && (id == 0 || d.at == [2]float64{x, y}) {
102+ return nil
103+ }
104+ return c.dataDragRoute(ctx, at)
105+}
106+
107+// dataDragMotion follows the cursor: the target gets the motion and the
108+// icon moves with it.
109+func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error {
110+ if err := c.dataDragRoute(ctx, at); err != nil {
111+ return err
112+ }
113+ if c.dataDrag.icon.visible() {
114+ return c.publish(ctx)
115+ }
116+ return nil
117+}
118+
119+// dataDragButton handles a button event of a data drag: the release of
120+// the drag button drops; the other buttons do nothing, their releases
121+// neither.
122+func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error {
123+ if !v.Pressed {
124+ if v.Button != c.dataDrag.button {
125+ return nil
126+ }
127+ delete(c.buttons, v.Button)
128+ // A bind may have moved the layout under a still cursor: the
129+ // drop goes to what lies there now.
130+ if err := c.dataDragRetarget(ctx, v.Time); err != nil {
131+ return err
132+ }
133+ c.dataDrag = nil
134+ if err := c.command(ctx, ports.DataDragDrop{}); err != nil {
135+ return err
136+ }
137+ return c.finishDataDrag(ctx)
138+ }
139+ c.buttons[v.Button] = true
140+ c.swallow[v.Button] = true
141+ return nil
142+}
143+
144+// cancelDataDrag ends the drag on Escape.
145+func (c *Core) cancelDataDrag(ctx context.Context) error {
146+ c.abortDataDrag()
147+ if err := c.command(ctx, ports.DataDragCancel{}); err != nil {
148+ return err
149+ }
150+ return c.finishDataDrag(ctx)
151+}
152+
153+// abortDataDrag ends the drag where it is; the held button's release is
154+// dropped.
155+func (c *Core) abortDataDrag() {
156+ if d := c.dataDrag; d != nil {
157+ c.swallow[d.button] = true
158+ c.dataDrag = nil
159+ }
160+}
161+
162+// finishDataDrag redraws without the icon and points the pointer at what
163+// lies under the cursor.
164+func (c *Core) finishDataDrag(ctx context.Context) error {
165+ if err := c.publish(ctx); err != nil {
166+ return err
167+ }
168+ return c.rehit(ctx)
169+}
170+
171+// dataDragGone drops the target that was unmapped: it gets its leave.
172+func (c *Core) dataDragGone(ctx context.Context, id WindowID) error {
173+ if d := c.dataDrag; d != nil && d.target == id {
174+ d.target = 0
175+ return c.command(ctx, ports.DataDragFocus{Time: c.motionTime})
176+ }
177+ return nil
178+}
179+
180+// setDataDragIcon records the icon geometry wayland measured. It reports
181+// whether the scene changes.
182+func (c *Core) setDataDragIcon(v ports.DataDragIcon) bool {
183+ d := c.dataDrag
184+ if d == nil || d.icon.id == 0 || v.ID != d.icon.id {
185+ return false
186+ }
187+ icon := dataDragIcon{id: v.ID, x: v.X, y: v.Y, w: max(v.W, 0), h: max(v.H, 0)}
188+ if icon == d.icon {
189+ return false
190+ }
191+ d.icon = icon
192+ return true
193+}
194+
195+func (i dataDragIcon) visible() bool { return i.id != 0 && i.w > 0 && i.h > 0 }
196+
197+// dataDragIconOn is the icon as a window of screen sc (index i) when the
198+// cursor is on it.
199+func (c *Core) dataDragIconOn(i int, sc *screen) (ports.SceneWindow, bool) {
200+ d := c.dataDrag
201+ if d == nil || !d.icon.visible() || c.screenAt(c.cursorX, c.cursorY) != i {
202+ return ports.SceneWindow{}, false
203+ }
204+ x, y := int(c.cursorX)-sc.x+d.icon.x, int(c.cursorY)-sc.y+d.icon.y
205+ return ports.SceneWindow{ID: d.icon.id, Rect: Rect{X: x, Y: y, W: d.icon.w, H: d.icon.h}, Popup: true, OverLayers: true}, true
206+}
@@ -0,0 +1,206 @@
1+package core
2+
3+import (
4+ "context"
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// Data drag: a client drag and drop (wl_data_device.start_drag). Wayland
11+// validated the request against the client's press and tells core; core
12+// owns the pointer for the length of the drag, as for a window drag
13+// (drag.go):
14+//
15+// - the origin client gets the pointer leave and no pointer event after;
16+// nobody else gets one: the surface under the pointer is the drag
17+// target, told to wayland as DataDragFocus and DataDragMotion;
18+// - releasing the drag button drops (DataDragDrop), Escape cancels
19+// (DataDragCancel); neither reaches a client;
20+// - scrolling and touchpad gestures go nowhere, compositor swipes
21+// included; binds still run, Escape cancels the drag before the
22+// switcher; the keyboard keeps its
23+// focus, clients read the modifiers that choose the action from it;
24+// - the drag icon, a client surface wayland measures (DataDragIcon), is
25+// drawn at the cursor over everything.
26+//
27+// Layout changes under a still cursor reroute the drag (rehit), and the
28+// drop re-hits.
29+
30+// dataDragState is the data drag in progress.
31+type dataDragState struct {
32+ // button is the pointer button that holds the drag.
33+ button uint32
34+ // target is the surface the drag was last routed to (0: none), at
35+ // at in its coordinates.
36+ target WindowID
37+ at [2]float64
38+ icon dataDragIcon
39+}
40+
41+// dataDragIcon is the icon surface: its top left corner relative to the
42+// cursor and its size, logical px. Hidden while W or H is 0.
43+type dataDragIcon struct {
44+ id WindowID
45+ x, y, w, h int
46+}
47+
48+// startDataDrag answers the start_drag wayland validated: it takes the
49+// pointer if a client holds a button, else cancels the drag. A drag
50+// during a window drag or a locked session is refused.
51+func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error {
52+ button := v.Button
53+ if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] {
54+ return c.command(ctx, ports.DataDragCancel{})
55+ }
56+ c.dataDrag = &dataDragState{button: button, icon: dataDragIcon{id: v.Icon}}
57+ // Other held buttons release without a client to tell.
58+ for b := range c.buttons {
59+ if b != button {
60+ c.swallow[b] = true
61+ }
62+ }
63+ c.grab = 0
64+ // A compositor swipe in progress lets go of what it moved.
65+ if c.swipe != nil {
66+ c.dropSwipe()
67+ c.swipe = nil
68+ }
69+ // The origin gets its pointer leave, as the drag takes the pointer.
70+ if c.pointer != 0 {
71+ c.pointer = 0
72+ if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
73+ return err
74+ }
75+ }
76+ // The surface under the pointer, the origin's usually, is the first
77+ // target.
78+ return c.dataDragRoute(ctx, c.motionTime)
79+}
80+
81+// dataDragRoute sends the surface under the cursor as the drag target: a
82+// focus when it changed, else a motion.
83+func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error {
84+ d := c.dataDrag
85+ id, x, y := c.hit(c.cursorX, c.cursorY)
86+ d.at = [2]float64{x, y}
87+ switch {
88+ case id != d.target:
89+ d.target = id
90+ return c.command(ctx, ports.DataDragFocus{ID: id, X: x, Y: y, Time: at})
91+ case id != 0:
92+ return c.command(ctx, ports.DataDragMotion{ID: id, X: x, Y: y, Time: at})
93+ }
94+ return nil
95+}
96+
97+// dataDragRetarget re-routes a still cursor: the target is told only when
98+// it or the point in it changed.
99+func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error {
100+ d := c.dataDrag
101+ if id, x, y := c.hit(c.cursorX, c.cursorY); id == d.target && (id == 0 || d.at == [2]float64{x, y}) {
102+ return nil
103+ }
104+ return c.dataDragRoute(ctx, at)
105+}
106+
107+// dataDragMotion follows the cursor: the target gets the motion and the
108+// icon moves with it.
109+func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error {
110+ if err := c.dataDragRoute(ctx, at); err != nil {
111+ return err
112+ }
113+ if c.dataDrag.icon.visible() {
114+ return c.publish(ctx)
115+ }
116+ return nil
117+}
118+
119+// dataDragButton handles a button event of a data drag: the release of
120+// the drag button drops; the other buttons do nothing, their releases
121+// neither.
122+func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error {
123+ if !v.Pressed {
124+ if v.Button != c.dataDrag.button {
125+ return nil
126+ }
127+ delete(c.buttons, v.Button)
128+ // A bind may have moved the layout under a still cursor: the
129+ // drop goes to what lies there now.
130+ if err := c.dataDragRetarget(ctx, v.Time); err != nil {
131+ return err
132+ }
133+ c.dataDrag = nil
134+ if err := c.command(ctx, ports.DataDragDrop{}); err != nil {
135+ return err
136+ }
137+ return c.finishDataDrag(ctx)
138+ }
139+ c.buttons[v.Button] = true
140+ c.swallow[v.Button] = true
141+ return nil
142+}
143+
144+// cancelDataDrag ends the drag on Escape.
145+func (c *Core) cancelDataDrag(ctx context.Context) error {
146+ c.abortDataDrag()
147+ if err := c.command(ctx, ports.DataDragCancel{}); err != nil {
148+ return err
149+ }
150+ return c.finishDataDrag(ctx)
151+}
152+
153+// abortDataDrag ends the drag where it is; the held button's release is
154+// dropped.
155+func (c *Core) abortDataDrag() {
156+ if d := c.dataDrag; d != nil {
157+ c.swallow[d.button] = true
158+ c.dataDrag = nil
159+ }
160+}
161+
162+// finishDataDrag redraws without the icon and points the pointer at what
163+// lies under the cursor.
164+func (c *Core) finishDataDrag(ctx context.Context) error {
165+ if err := c.publish(ctx); err != nil {
166+ return err
167+ }
168+ return c.rehit(ctx)
169+}
170+
171+// dataDragGone drops the target that was unmapped: it gets its leave.
172+func (c *Core) dataDragGone(ctx context.Context, id WindowID) error {
173+ if d := c.dataDrag; d != nil && d.target == id {
174+ d.target = 0
175+ return c.command(ctx, ports.DataDragFocus{Time: c.motionTime})
176+ }
177+ return nil
178+}
179+
180+// setDataDragIcon records the icon geometry wayland measured. It reports
181+// whether the scene changes.
182+func (c *Core) setDataDragIcon(v ports.DataDragIcon) bool {
183+ d := c.dataDrag
184+ if d == nil || d.icon.id == 0 || v.ID != d.icon.id {
185+ return false
186+ }
187+ icon := dataDragIcon{id: v.ID, x: v.X, y: v.Y, w: max(v.W, 0), h: max(v.H, 0)}
188+ if icon == d.icon {
189+ return false
190+ }
191+ d.icon = icon
192+ return true
193+}
194+
195+func (i dataDragIcon) visible() bool { return i.id != 0 && i.w > 0 && i.h > 0 }
196+
197+// dataDragIconOn is the icon as a window of screen sc (index i) when the
198+// cursor is on it.
199+func (c *Core) dataDragIconOn(i int, sc *screen) (ports.SceneWindow, bool) {
200+ d := c.dataDrag
201+ if d == nil || !d.icon.visible() || c.screenAt(c.cursorX, c.cursorY) != i {
202+ return ports.SceneWindow{}, false
203+ }
204+ x, y := int(c.cursorX)-sc.x+d.icon.x, int(c.cursorY)-sc.y+d.icon.y
205+ return ports.SceneWindow{ID: d.icon.id, Rect: Rect{X: x, Y: y, W: d.icon.w, H: d.icon.h}, Popup: true, OverLayers: true}, true
206+}
A internal/core/datadrag_test.go +577 -0
@@ -0,0 +1,577 @@
1+package core_test
2+
3+import (
4+ "slices"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+const dndIcon ports.WindowID = 900
12+
13+// dndRig is a drag rig with tiles 1 and 2 side by side and the pointer
14+// on 1 with its button held, as a client that is about to start_drag.
15+type dndRig struct {
16+ *dragRig
17+ one, two ports.Rect
18+}
19+
20+func startDnd(t *testing.T, outs ...ports.OutputInfo) *dndRig {
21+ t.Helper()
22+ if len(outs) == 0 {
23+ outs = []ports.OutputInfo{dragOut}
24+ }
25+ r := startDragRig(t, nil, outs...)
26+ r.client <- ports.WindowMapped{ID: 1}
27+ r.client <- ports.WindowMapped{ID: 2}
28+ set := r.settle(t)
29+ d := &dndRig{dragRig: r, one: dragRect(t, set, outs[0].Name, 1), two: dragRect(t, set, outs[0].Name, 2)}
30+ r.moveTo(float64(d.one.X+20), float64(d.one.Y+30), 1)
31+ r.in(ports.PointerButton{Button: 0x110, Pressed: true})
32+ r.settle(t)
33+ r.drain()
34+ return d
35+}
36+
37+// dataDragCommands keeps what the data drag sends and the pointer events
38+// no client may see during it.
39+func dataDragCommands(cmds []ports.ClientCommand) []ports.ClientCommand {
40+ var out []ports.ClientCommand
41+ for _, v := range cmds {
42+ switch v.(type) {
43+ case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel,
44+ ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo,
45+ ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
46+ out = append(out, v)
47+ }
48+ }
49+ return out
50+}
51+
52+func (d *dndRig) start(t *testing.T, icon ports.WindowID) []ports.ClientCommand {
53+ t.Helper()
54+ d.client <- ports.DataDragStarted{Button: 0x110, Icon: icon}
55+ d.settle(t)
56+ return dataDragCommands(d.drain())
57+}
58+
59+func wantCommands(t *testing.T, got []ports.ClientCommand, want ...ports.ClientCommand) {
60+ t.Helper()
61+ if !slices.Equal(got, want) {
62+ t.Fatalf("commands %#v, want %#v", got, want)
63+ }
64+}
65+
66+func TestDataDragRoutesToSurfaceUnderPointer(t *testing.T) {
67+ d := startDnd(t)
68+ // The origin loses the pointer and is the first target.
69+ wantCommands(t, d.start(t, 0),
70+ ports.PointerFocus{},
71+ ports.DataDragFocus{ID: 1, X: 20, Y: 30, Time: time.Millisecond})
72+ // Motion over the same surface.
73+ d.moveTo(float64(d.one.X+25), float64(d.one.Y+35), 2)
74+ d.settle(t)
75+ wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 1, X: 25, Y: 35, Time: 2 * time.Millisecond})
76+ // Over the other window: it is the new target.
77+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
78+ d.settle(t)
79+ wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond})
80+ d.moveTo(float64(d.two.X+7), float64(d.two.Y+8), 4)
81+ d.settle(t)
82+ wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 2, X: 7, Y: 8, Time: 4 * time.Millisecond})
83+ // Over nothing: no target.
84+ d.moveTo(float64(d.two.X+d.two.W+5), float64(d.two.Y+8), 5)
85+ d.settle(t)
86+ wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{Time: 5 * time.Millisecond})
87+ d.moveTo(float64(d.two.X+d.two.W+6), float64(d.two.Y+8), 6)
88+ d.settle(t)
89+ wantCommands(t, dataDragCommands(d.drain()))
90+}
91+
92+func TestDataDragDropsOnRelease(t *testing.T) {
93+ d := startDnd(t)
94+ d.start(t, 0)
95+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
96+ d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond})
97+ d.settle(t)
98+ // The release is the drop; the pointer then points at the window under
99+ // the cursor, not at the origin that grabbed it.
100+ wantCommands(t, dataDragCommands(d.drain()),
101+ ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond},
102+ ports.DataDragDrop{},
103+ ports.PointerFocus{ID: 2, X: 5, Y: 6})
104+ // The drag is over: motion is plain pointer motion again.
105+ d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5)
106+ d.settle(t)
107+ wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond})
108+ // The next press is an ordinary click.
109+ d.in(ports.PointerButton{Button: 0x110, Pressed: true})
110+ d.settle(t)
111+ wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true, Grab: true})
112+}
113+
114+func TestDataDragSendsNothingToClients(t *testing.T) {
115+ d := startDnd(t)
116+ d.start(t, 0)
117+ d.in(ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}})
118+ d.in(ports.PinchBegin{Fingers: 2, Time: time.Second})
119+ d.in(ports.PinchUpdate{Scale: 2, Time: time.Second})
120+ d.in(ports.PinchEnd{Time: time.Second})
121+ d.in(ports.HoldBegin{Fingers: 2, Time: time.Second})
122+ d.in(ports.HoldEnd{Time: time.Second})
123+ d.in(ports.SwipeBegin{Fingers: 2, Time: time.Second})
124+ d.in(ports.SwipeEnd{Time: time.Second})
125+ // Other buttons neither press nor release anywhere.
126+ d.in(ports.PointerButton{Button: 0x111, Pressed: true})
127+ d.in(ports.PointerButton{Button: 0x111})
128+ d.settle(t)
129+ for _, v := range dataDragCommands(d.drain()) {
130+ switch v.(type) {
131+ case ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.PointerMotionTo, ports.DataDragDrop, ports.DataDragCancel:
132+ t.Fatalf("sent %#v", v)
133+ }
134+ }
135+ // Still dragging: the drag button's release drops.
136+ d.in(ports.PointerButton{Button: 0x110})
137+ d.settle(t)
138+ if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
139+ t.Fatalf("commands %#v", got)
140+ }
141+}
142+
143+func TestDataDragKeyboardKeepsForwarding(t *testing.T) {
144+ d := startDnd(t)
145+ d.start(t, 0)
146+ d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42, Pressed: true, Mods: ports.ModShift})
147+ d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42})
148+ d.settle(t)
149+ n := 0
150+ for _, v := range d.drain() {
151+ if _, ok := v.(ports.ForwardKey); ok {
152+ n++
153+ }
154+ }
155+ if n != 2 {
156+ t.Fatalf("%d keys forwarded, want 2", n)
157+ }
158+}
159+
160+func TestDataDragEscapeCancels(t *testing.T) {
161+ d := startDnd(t)
162+ d.start(t, 0)
163+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
164+ d.settle(t)
165+ d.drain()
166+ d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Pressed: true})
167+ d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1})
168+ d.in(ports.PointerButton{Button: 0x110})
169+ d.settle(t)
170+ cmds := d.drain()
171+ wantCommands(t, dataDragCommands(cmds),
172+ ports.DataDragCancel{},
173+ ports.PointerFocus{ID: 2, X: 5, Y: 6})
174+ for _, v := range cmds {
175+ if k, ok := v.(ports.ForwardKey); ok {
176+ t.Fatalf("client saw %#v", k)
177+ }
178+ }
179+ // The press that started the drag is over.
180+ d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5)
181+ d.settle(t)
182+ wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond})
183+}
184+
185+func TestDataDragEndedByWayland(t *testing.T) {
186+ d := startDnd(t)
187+ d.start(t, dndIcon)
188+ d.client <- ports.DataDragIcon{ID: dndIcon, X: 1, Y: 2, W: 16, H: 16}
189+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
190+ d.settle(t)
191+ d.drain()
192+ // The source died: no command, the icon goes and the pointer is
193+ // pointed at the window under the cursor.
194+ d.client <- ports.DataDragEnded{}
195+ set := d.settle(t)
196+ if _, ok := windowIn(set, "A", dndIcon); ok {
197+ t.Fatal("icon still drawn")
198+ }
199+ wantCommands(t, dataDragCommands(d.drain()),
200+ ports.PointerFocus{ID: 2, X: 5, Y: 6})
201+ // The held button's release reaches nobody.
202+ d.in(ports.PointerButton{Button: 0x110})
203+ d.settle(t)
204+ wantCommands(t, dataDragCommands(d.drain()))
205+ // An end with no drag is ignored.
206+ d.client <- ports.DataDragEnded{}
207+ d.settle(t)
208+ wantCommands(t, dataDragCommands(d.drain()))
209+}
210+
211+func TestDataDragRefused(t *testing.T) {
212+ t.Run("no press", func(t *testing.T) {
213+ d := startDnd(t)
214+ d.in(ports.PointerButton{Button: 0x110})
215+ d.settle(t)
216+ d.drain()
217+ wantCommands(t, d.start(t, 0), ports.DataDragCancel{})
218+ // No drag started: motion is plain.
219+ d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 2)
220+ d.settle(t)
221+ wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 2 * time.Millisecond})
222+ })
223+ t.Run("window drag", func(t *testing.T) {
224+ r := startDragRig(t, nil, dragOut)
225+ r.client <- ports.WindowMapped{ID: 1}
226+ r.client <- ports.WindowMapped{ID: 2}
227+ set := r.settle(t)
228+ one := dragRect(t, set, "A", 1)
229+ r.moveTo(float64(one.X+20), float64(one.Y+30), 1)
230+ r.settle(t)
231+ r.press(t, 0x110, ports.ModSuper)
232+ r.settle(t)
233+ r.drain()
234+ r.client <- ports.DataDragStarted{Button: 0x110}
235+ r.settle(t)
236+ wantCommands(t, r.drain(), ports.DataDragCancel{})
237+ // The window drag goes on.
238+ r.moveTo(float64(one.X+200), float64(one.Y+30), 2)
239+ r.in(ports.PointerButton{Button: 0x110})
240+ r.settle(t)
241+ wantCommands(t, dataDragCommands(r.drain()), ports.PointerFocus{ID: 1, X: 200, Y: 30})
242+ })
243+ t.Run("twice", func(t *testing.T) {
244+ d := startDnd(t)
245+ d.start(t, 0)
246+ wantCommands(t, d.start(t, 0), ports.DataDragCancel{})
247+ })
248+ t.Run("locked", func(t *testing.T) {
249+ d := startDnd(t)
250+ locked := ports.SecurityState{Generation: 1, Protected: true}
251+ d.epoch.Store(locked)
252+ d.client <- ports.SessionLockChanged{State: locked}
253+ d.settle(t)
254+ d.drain()
255+ d.client <- ports.DataDragStarted{Button: 0x110}
256+ d.settle(t)
257+ var got []ports.ClientCommand
258+ for _, v := range d.drain() {
259+ switch v.(type) {
260+ case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
261+ got = append(got, v)
262+ }
263+ }
264+ wantCommands(t, got, ports.DataDragCancel{})
265+ })
266+}
267+
268+func TestDataDragCommandsAreSecurityWrapped(t *testing.T) {
269+ d := startDnd(t)
270+ d.client <- ports.DataDragStarted{Button: 0x110}
271+ d.settle(t)
272+ n := 0
273+ for {
274+ select {
275+ case v := <-d.commands:
276+ s, ok := v.(ports.SecurityCommand)
277+ if !ok {
278+ t.Fatalf("unwrapped %#v", v)
279+ }
280+ if _, ok := s.Command.(ports.DataDragFocus); ok {
281+ n++
282+ }
283+ continue
284+ default:
285+ }
286+ break
287+ }
288+ if n != 1 {
289+ t.Fatalf("%d DataDragFocus", n)
290+ }
291+}
292+
293+func TestDataDragLockCancelsIt(t *testing.T) {
294+ d := startDnd(t)
295+ d.start(t, dndIcon)
296+ d.client <- ports.DataDragIcon{ID: dndIcon, W: 16, H: 16}
297+ d.settle(t)
298+ locked := ports.SecurityState{Generation: 1, Protected: true}
299+ d.epoch.Store(locked)
300+ d.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 1000, Height: 600}}}
301+ set := d.settle(t)
302+ if _, ok := windowIn(set, "A", dndIcon); ok {
303+ t.Fatal("icon drawn while locked")
304+ }
305+ d.drain()
306+ d.in(ports.PointerMotion{X: 100, Y: 100, Time: time.Second})
307+ d.settle(t)
308+ for _, v := range d.drain() {
309+ switch v.(type) {
310+ case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop:
311+ t.Fatalf("data drag alive while locked: %#v", v)
312+ }
313+ }
314+ // After the unlock, the old drag is gone: plain pointer events.
315+ unlocked := ports.SecurityState{Generation: 2}
316+ d.epoch.Store(unlocked)
317+ d.client <- ports.SessionLockChanged{State: unlocked}
318+ d.settle(t)
319+ d.drain()
320+ d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 9)
321+ d.settle(t)
322+ got := dataDragCommands(d.drain())
323+ wantCommands(t, got, ports.PointerFocus{ID: 1, X: 21, Y: 30}, ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 9 * time.Millisecond})
324+}
325+
326+func TestDataDragTargetUnmapped(t *testing.T) {
327+ d := startDnd(t)
328+ d.start(t, 0)
329+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
330+ d.settle(t)
331+ d.drain()
332+ d.client <- ports.WindowUnmapped{ID: 2}
333+ d.settle(t)
334+ cmds := dataDragCommands(d.drain())
335+ if len(cmds) == 0 || cmds[0] != (ports.DataDragFocus{Time: 3 * time.Millisecond}) {
336+ t.Fatalf("commands %#v", cmds)
337+ }
338+}
339+
340+func TestDataDragIconIsLastWindowAtCursor(t *testing.T) {
341+ d := startDnd(t)
342+ d.start(t, dndIcon)
343+ // Not drawn until wayland measured it.
344+ if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
345+ t.Fatal("icon drawn before its size")
346+ }
347+ cx, cy := d.one.X+20, d.one.Y+30
348+ d.client <- ports.DataDragIcon{ID: dndIcon, X: -4, Y: -6, W: 32, H: 24}
349+ set := d.settle(t)
350+ s := dragScene(set, "A")
351+ last := s.Windows[len(s.Windows)-1]
352+ want := ports.SceneWindow{ID: dndIcon, Rect: ports.Rect{X: cx - 4, Y: cy - 6, W: 32, H: 24}, Popup: true, OverLayers: true}
353+ if last != want {
354+ t.Fatalf("last window %+v, want %+v", last, want)
355+ }
356+ // It follows the cursor; the scene changes.
357+ d.moveTo(float64(cx+100), float64(cy+50), 2)
358+ moved := dragScene(d.settle(t), "A")
359+ if moved.SameAs(s) || moved.Seq == s.Seq {
360+ t.Fatal("scene did not change with the icon")
361+ }
362+ if got := moved.Windows[len(moved.Windows)-1]; got.Rect.X != cx+96 || got.Rect.Y != cy+44 || got.ID != dndIcon {
363+ t.Fatalf("moved icon %+v", got)
364+ }
365+ // Under the icon, the surface below is still the target.
366+ d.drain()
367+ d.moveTo(float64(cx+101), float64(cy+50), 3)
368+ d.settle(t)
369+ for _, v := range d.drain() {
370+ if f, ok := v.(ports.DataDragFocus); ok && f.ID == dndIcon {
371+ t.Fatalf("icon was targeted: %#v", f)
372+ }
373+ }
374+ // A commit that changes the offset moves it; a zero size hides it.
375+ d.client <- ports.DataDragIcon{ID: dndIcon, X: 0, Y: 0, W: 32, H: 24}
376+ if got := dragScene(d.settle(t), "A"); got.Windows[len(got.Windows)-1].Rect.X != cx+101 {
377+ t.Fatalf("offset ignored: %+v", got.Windows[len(got.Windows)-1])
378+ }
379+ d.client <- ports.DataDragIcon{ID: dndIcon}
380+ if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
381+ t.Fatal("hidden icon drawn")
382+ }
383+ // Geometry of another surface is not the icon's.
384+ d.client <- ports.DataDragIcon{ID: dndIcon + 1, W: 10, H: 10}
385+ if _, ok := windowIn(d.settle(t), "A", dndIcon+1); ok {
386+ t.Fatal("foreign icon drawn")
387+ }
388+ // The drop removes it.
389+ d.client <- ports.DataDragIcon{ID: dndIcon, W: 32, H: 24}
390+ d.settle(t)
391+ d.in(ports.PointerButton{Button: 0x110})
392+ set = d.settle(t)
393+ if _, ok := windowIn(set, "A", dndIcon); ok {
394+ t.Fatal("icon drawn after the drop")
395+ }
396+}
397+
398+func TestDataDragIconOnScreenUnderCursor(t *testing.T) {
399+ other := ports.OutputInfo{Name: "B", Width: 800, Height: 600}
400+ d := startDnd(t, dragOut, other)
401+ d.start(t, dndIcon)
402+ d.client <- ports.DataDragIcon{ID: dndIcon, X: 2, Y: 3, W: 10, H: 10}
403+ d.moveTo(1000+100, 50, 2)
404+ set := d.settle(t)
405+ if _, ok := windowIn(set, "A", dndIcon); ok {
406+ t.Fatal("icon on the other screen")
407+ }
408+ w, ok := windowIn(set, "B", dndIcon)
409+ if !ok {
410+ t.Fatal("no icon on B")
411+ }
412+ if w.Rect != (ports.Rect{X: 102, Y: 53, W: 10, H: 10}) {
413+ t.Fatalf("icon %+v", w.Rect)
414+ }
415+}
416+
417+func TestDataDragNoIconIgnoresIconEvents(t *testing.T) {
418+ d := startDnd(t)
419+ d.start(t, 0)
420+ d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10}
421+ if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
422+ t.Fatal("icon drawn for a drag without one")
423+ }
424+ // Nor with no drag at all.
425+ d.in(ports.PointerButton{Button: 0x110})
426+ d.settle(t)
427+ d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10}
428+ if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
429+ t.Fatal("icon drawn without a drag")
430+ }
431+}
432+
433+func TestDataDragFocusScreenFollowsPointer(t *testing.T) {
434+ r := startDragRig(t, nil, dragOut, ports.OutputInfo{Name: "B", Width: 1000, Height: 600})
435+ r.client <- ports.WindowMapped{ID: 1}
436+ r.settle(t)
437+ r.moveTo(1500, 300, 1)
438+ r.settle(t)
439+ r.client <- ports.WindowMapped{ID: 3}
440+ set := r.settle(t)
441+ one := dragRect(t, set, "A", 1)
442+ r.moveTo(float64(one.X+20), float64(one.Y+30), 2)
443+ set = r.settle(t)
444+ if w, _ := windowIn(set, "A", 1); !w.Focused {
445+ t.Fatal("1 not focused")
446+ }
447+ r.in(ports.PointerButton{Button: 0x110, Pressed: true})
448+ r.settle(t)
449+ r.client <- ports.DataDragStarted{Button: 0x110}
450+ r.moveTo(1500, 300, 3)
451+ set = r.settle(t)
452+ if w, _ := windowIn(set, "B", 3); !w.Focused {
453+ t.Fatal("the screen under the drag did not take the focus")
454+ }
455+ three := dragRect(t, set, "B", 3)
456+ var focus []ports.DataDragFocus
457+ for _, v := range r.drain() {
458+ if f, ok := v.(ports.DataDragFocus); ok {
459+ focus = append(focus, f)
460+ }
461+ }
462+ if n := len(focus); n != 2 || focus[1].ID != 3 || focus[1].X != float64(1500-1000-three.X) {
463+ t.Fatalf("focus %+v", focus)
464+ }
465+}
466+
467+func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) {
468+ d := startDnd(t)
469+ d.start(t, 0)
470+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
471+ d.settle(t)
472+ d.drain()
473+ // cmd+2 shows an empty workspace under the still cursor: the target
474+ // leaves, and the release drops on nothing.
475+ d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true})
476+ d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper})
477+ d.settle(t)
478+ got := dataDragCommands(d.drain())
479+ if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) {
480+ t.Fatalf("no leave after the switch: %#v", got)
481+ }
482+ d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond})
483+ d.settle(t)
484+ got = dataDragCommands(d.drain())
485+ if len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
486+ t.Fatalf("commands %#v", got)
487+ }
488+ for _, v := range got {
489+ if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 {
490+ t.Fatalf("hidden window targeted: %#v", got)
491+ }
492+ }
493+}
494+
495+func TestDataDragTargetMovedUnderStillCursor(t *testing.T) {
496+ d := startDnd(t)
497+ d.start(t, 0)
498+ // Over 2, focused by a click-free keyboard move, then cmd+f maximizes
499+ // its column: still the target under the cursor, at another point.
500+ d.moveTo(float64(d.two.X+d.two.W-20), float64(d.two.Y+30), 2)
501+ d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper, Pressed: true})
502+ d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper})
503+ d.settle(t)
504+ d.drain()
505+ d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true})
506+ d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper})
507+ set := d.settle(t)
508+ wide := dragRect(t, set, dragOut.Name, 2)
509+ if wide.X == d.two.X {
510+ t.Fatalf("the column did not move: %v", wide)
511+ }
512+ x, y := float64(d.two.X+d.two.W-20-wide.X), float64(d.two.Y+30-wide.Y)
513+ got := dataDragCommands(d.drain())
514+ if !slices.Contains(got, ports.ClientCommand(ports.DataDragMotion{ID: 2, X: x, Y: y, Time: 2 * time.Millisecond})) {
515+ t.Fatalf("no motion to %v,%v: %#v", x, y, got)
516+ }
517+}
518+
519+func TestDataDragDropsOnItsOwnButton(t *testing.T) {
520+ d := startDnd(t)
521+ // Right pressed after left: the drag is left's, the grab's.
522+ d.in(ports.PointerButton{Button: 0x111, Pressed: true})
523+ d.settle(t)
524+ d.drain()
525+ d.start(t, 0)
526+ d.in(ports.PointerButton{Button: 0x111})
527+ d.settle(t)
528+ if got := dataDragCommands(d.drain()); slices.Contains(got, ports.ClientCommand(ports.DataDragDrop{})) {
529+ t.Fatalf("right's release dropped: %#v", got)
530+ }
531+ d.in(ports.PointerButton{Button: 0x110})
532+ d.settle(t)
533+ if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
534+ t.Fatalf("left's release did not drop: %#v", got)
535+ }
536+}
537+
538+func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) {
539+ d := startDnd(t)
540+ d.start(t, 0)
541+ d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true})
542+ d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true})
543+ d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper})
544+ d.settle(t)
545+ d.drain()
546+ d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true})
547+ d.settle(t)
548+ got := dataDragCommands(d.drain())
549+ if len(got) == 0 || got[0] != (ports.DataDragCancel{}) {
550+ t.Fatalf("Escape did not cancel the drag: %#v", got)
551+ }
552+ d.in(ports.PointerButton{Button: 0x110})
553+ d.settle(t)
554+ if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) {
555+ t.Fatal("dropped after Escape")
556+ }
557+}
558+
559+func TestDataDragIgnoresCompositorSwipes(t *testing.T) {
560+ d := startDnd(t)
561+ d.start(t, 0)
562+ before := d.settle(t)
563+ // Flicks that page columns and workspaces, as navigation would.
564+ for _, fingers := range []int{3, 4} {
565+ at := time.Second
566+ d.in(ports.SwipeBegin{Fingers: fingers, Time: at})
567+ for range 10 {
568+ at += 8 * time.Millisecond
569+ d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at})
570+ }
571+ d.in(ports.SwipeEnd{Time: at})
572+ }
573+ after := d.settle(t)
574+ if !slices.Equal(before[0].Windows, after[0].Windows) {
575+ t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows)
576+ }
577+}
@@ -0,0 +1,577 @@
1+package core_test
2+
3+import (
4+ "slices"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+const dndIcon ports.WindowID = 900
12+
13+// dndRig is a drag rig with tiles 1 and 2 side by side and the pointer
14+// on 1 with its button held, as a client that is about to start_drag.
15+type dndRig struct {
16+ *dragRig
17+ one, two ports.Rect
18+}
19+
20+func startDnd(t *testing.T, outs ...ports.OutputInfo) *dndRig {
21+ t.Helper()
22+ if len(outs) == 0 {
23+ outs = []ports.OutputInfo{dragOut}
24+ }
25+ r := startDragRig(t, nil, outs...)
26+ r.client <- ports.WindowMapped{ID: 1}
27+ r.client <- ports.WindowMapped{ID: 2}
28+ set := r.settle(t)
29+ d := &dndRig{dragRig: r, one: dragRect(t, set, outs[0].Name, 1), two: dragRect(t, set, outs[0].Name, 2)}
30+ r.moveTo(float64(d.one.X+20), float64(d.one.Y+30), 1)
31+ r.in(ports.PointerButton{Button: 0x110, Pressed: true})
32+ r.settle(t)
33+ r.drain()
34+ return d
35+}
36+
37+// dataDragCommands keeps what the data drag sends and the pointer events
38+// no client may see during it.
39+func dataDragCommands(cmds []ports.ClientCommand) []ports.ClientCommand {
40+ var out []ports.ClientCommand
41+ for _, v := range cmds {
42+ switch v.(type) {
43+ case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel,
44+ ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo,
45+ ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
46+ out = append(out, v)
47+ }
48+ }
49+ return out
50+}
51+
52+func (d *dndRig) start(t *testing.T, icon ports.WindowID) []ports.ClientCommand {
53+ t.Helper()
54+ d.client <- ports.DataDragStarted{Button: 0x110, Icon: icon}
55+ d.settle(t)
56+ return dataDragCommands(d.drain())
57+}
58+
59+func wantCommands(t *testing.T, got []ports.ClientCommand, want ...ports.ClientCommand) {
60+ t.Helper()
61+ if !slices.Equal(got, want) {
62+ t.Fatalf("commands %#v, want %#v", got, want)
63+ }
64+}
65+
66+func TestDataDragRoutesToSurfaceUnderPointer(t *testing.T) {
67+ d := startDnd(t)
68+ // The origin loses the pointer and is the first target.
69+ wantCommands(t, d.start(t, 0),
70+ ports.PointerFocus{},
71+ ports.DataDragFocus{ID: 1, X: 20, Y: 30, Time: time.Millisecond})
72+ // Motion over the same surface.
73+ d.moveTo(float64(d.one.X+25), float64(d.one.Y+35), 2)
74+ d.settle(t)
75+ wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 1, X: 25, Y: 35, Time: 2 * time.Millisecond})
76+ // Over the other window: it is the new target.
77+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
78+ d.settle(t)
79+ wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond})
80+ d.moveTo(float64(d.two.X+7), float64(d.two.Y+8), 4)
81+ d.settle(t)
82+ wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 2, X: 7, Y: 8, Time: 4 * time.Millisecond})
83+ // Over nothing: no target.
84+ d.moveTo(float64(d.two.X+d.two.W+5), float64(d.two.Y+8), 5)
85+ d.settle(t)
86+ wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{Time: 5 * time.Millisecond})
87+ d.moveTo(float64(d.two.X+d.two.W+6), float64(d.two.Y+8), 6)
88+ d.settle(t)
89+ wantCommands(t, dataDragCommands(d.drain()))
90+}
91+
92+func TestDataDragDropsOnRelease(t *testing.T) {
93+ d := startDnd(t)
94+ d.start(t, 0)
95+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
96+ d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond})
97+ d.settle(t)
98+ // The release is the drop; the pointer then points at the window under
99+ // the cursor, not at the origin that grabbed it.
100+ wantCommands(t, dataDragCommands(d.drain()),
101+ ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond},
102+ ports.DataDragDrop{},
103+ ports.PointerFocus{ID: 2, X: 5, Y: 6})
104+ // The drag is over: motion is plain pointer motion again.
105+ d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5)
106+ d.settle(t)
107+ wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond})
108+ // The next press is an ordinary click.
109+ d.in(ports.PointerButton{Button: 0x110, Pressed: true})
110+ d.settle(t)
111+ wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true, Grab: true})
112+}
113+
114+func TestDataDragSendsNothingToClients(t *testing.T) {
115+ d := startDnd(t)
116+ d.start(t, 0)
117+ d.in(ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}})
118+ d.in(ports.PinchBegin{Fingers: 2, Time: time.Second})
119+ d.in(ports.PinchUpdate{Scale: 2, Time: time.Second})
120+ d.in(ports.PinchEnd{Time: time.Second})
121+ d.in(ports.HoldBegin{Fingers: 2, Time: time.Second})
122+ d.in(ports.HoldEnd{Time: time.Second})
123+ d.in(ports.SwipeBegin{Fingers: 2, Time: time.Second})
124+ d.in(ports.SwipeEnd{Time: time.Second})
125+ // Other buttons neither press nor release anywhere.
126+ d.in(ports.PointerButton{Button: 0x111, Pressed: true})
127+ d.in(ports.PointerButton{Button: 0x111})
128+ d.settle(t)
129+ for _, v := range dataDragCommands(d.drain()) {
130+ switch v.(type) {
131+ case ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.PointerMotionTo, ports.DataDragDrop, ports.DataDragCancel:
132+ t.Fatalf("sent %#v", v)
133+ }
134+ }
135+ // Still dragging: the drag button's release drops.
136+ d.in(ports.PointerButton{Button: 0x110})
137+ d.settle(t)
138+ if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
139+ t.Fatalf("commands %#v", got)
140+ }
141+}
142+
143+func TestDataDragKeyboardKeepsForwarding(t *testing.T) {
144+ d := startDnd(t)
145+ d.start(t, 0)
146+ d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42, Pressed: true, Mods: ports.ModShift})
147+ d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42})
148+ d.settle(t)
149+ n := 0
150+ for _, v := range d.drain() {
151+ if _, ok := v.(ports.ForwardKey); ok {
152+ n++
153+ }
154+ }
155+ if n != 2 {
156+ t.Fatalf("%d keys forwarded, want 2", n)
157+ }
158+}
159+
160+func TestDataDragEscapeCancels(t *testing.T) {
161+ d := startDnd(t)
162+ d.start(t, 0)
163+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
164+ d.settle(t)
165+ d.drain()
166+ d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Pressed: true})
167+ d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1})
168+ d.in(ports.PointerButton{Button: 0x110})
169+ d.settle(t)
170+ cmds := d.drain()
171+ wantCommands(t, dataDragCommands(cmds),
172+ ports.DataDragCancel{},
173+ ports.PointerFocus{ID: 2, X: 5, Y: 6})
174+ for _, v := range cmds {
175+ if k, ok := v.(ports.ForwardKey); ok {
176+ t.Fatalf("client saw %#v", k)
177+ }
178+ }
179+ // The press that started the drag is over.
180+ d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5)
181+ d.settle(t)
182+ wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond})
183+}
184+
185+func TestDataDragEndedByWayland(t *testing.T) {
186+ d := startDnd(t)
187+ d.start(t, dndIcon)
188+ d.client <- ports.DataDragIcon{ID: dndIcon, X: 1, Y: 2, W: 16, H: 16}
189+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
190+ d.settle(t)
191+ d.drain()
192+ // The source died: no command, the icon goes and the pointer is
193+ // pointed at the window under the cursor.
194+ d.client <- ports.DataDragEnded{}
195+ set := d.settle(t)
196+ if _, ok := windowIn(set, "A", dndIcon); ok {
197+ t.Fatal("icon still drawn")
198+ }
199+ wantCommands(t, dataDragCommands(d.drain()),
200+ ports.PointerFocus{ID: 2, X: 5, Y: 6})
201+ // The held button's release reaches nobody.
202+ d.in(ports.PointerButton{Button: 0x110})
203+ d.settle(t)
204+ wantCommands(t, dataDragCommands(d.drain()))
205+ // An end with no drag is ignored.
206+ d.client <- ports.DataDragEnded{}
207+ d.settle(t)
208+ wantCommands(t, dataDragCommands(d.drain()))
209+}
210+
211+func TestDataDragRefused(t *testing.T) {
212+ t.Run("no press", func(t *testing.T) {
213+ d := startDnd(t)
214+ d.in(ports.PointerButton{Button: 0x110})
215+ d.settle(t)
216+ d.drain()
217+ wantCommands(t, d.start(t, 0), ports.DataDragCancel{})
218+ // No drag started: motion is plain.
219+ d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 2)
220+ d.settle(t)
221+ wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 2 * time.Millisecond})
222+ })
223+ t.Run("window drag", func(t *testing.T) {
224+ r := startDragRig(t, nil, dragOut)
225+ r.client <- ports.WindowMapped{ID: 1}
226+ r.client <- ports.WindowMapped{ID: 2}
227+ set := r.settle(t)
228+ one := dragRect(t, set, "A", 1)
229+ r.moveTo(float64(one.X+20), float64(one.Y+30), 1)
230+ r.settle(t)
231+ r.press(t, 0x110, ports.ModSuper)
232+ r.settle(t)
233+ r.drain()
234+ r.client <- ports.DataDragStarted{Button: 0x110}
235+ r.settle(t)
236+ wantCommands(t, r.drain(), ports.DataDragCancel{})
237+ // The window drag goes on.
238+ r.moveTo(float64(one.X+200), float64(one.Y+30), 2)
239+ r.in(ports.PointerButton{Button: 0x110})
240+ r.settle(t)
241+ wantCommands(t, dataDragCommands(r.drain()), ports.PointerFocus{ID: 1, X: 200, Y: 30})
242+ })
243+ t.Run("twice", func(t *testing.T) {
244+ d := startDnd(t)
245+ d.start(t, 0)
246+ wantCommands(t, d.start(t, 0), ports.DataDragCancel{})
247+ })
248+ t.Run("locked", func(t *testing.T) {
249+ d := startDnd(t)
250+ locked := ports.SecurityState{Generation: 1, Protected: true}
251+ d.epoch.Store(locked)
252+ d.client <- ports.SessionLockChanged{State: locked}
253+ d.settle(t)
254+ d.drain()
255+ d.client <- ports.DataDragStarted{Button: 0x110}
256+ d.settle(t)
257+ var got []ports.ClientCommand
258+ for _, v := range d.drain() {
259+ switch v.(type) {
260+ case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
261+ got = append(got, v)
262+ }
263+ }
264+ wantCommands(t, got, ports.DataDragCancel{})
265+ })
266+}
267+
268+func TestDataDragCommandsAreSecurityWrapped(t *testing.T) {
269+ d := startDnd(t)
270+ d.client <- ports.DataDragStarted{Button: 0x110}
271+ d.settle(t)
272+ n := 0
273+ for {
274+ select {
275+ case v := <-d.commands:
276+ s, ok := v.(ports.SecurityCommand)
277+ if !ok {
278+ t.Fatalf("unwrapped %#v", v)
279+ }
280+ if _, ok := s.Command.(ports.DataDragFocus); ok {
281+ n++
282+ }
283+ continue
284+ default:
285+ }
286+ break
287+ }
288+ if n != 1 {
289+ t.Fatalf("%d DataDragFocus", n)
290+ }
291+}
292+
293+func TestDataDragLockCancelsIt(t *testing.T) {
294+ d := startDnd(t)
295+ d.start(t, dndIcon)
296+ d.client <- ports.DataDragIcon{ID: dndIcon, W: 16, H: 16}
297+ d.settle(t)
298+ locked := ports.SecurityState{Generation: 1, Protected: true}
299+ d.epoch.Store(locked)
300+ d.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 1000, Height: 600}}}
301+ set := d.settle(t)
302+ if _, ok := windowIn(set, "A", dndIcon); ok {
303+ t.Fatal("icon drawn while locked")
304+ }
305+ d.drain()
306+ d.in(ports.PointerMotion{X: 100, Y: 100, Time: time.Second})
307+ d.settle(t)
308+ for _, v := range d.drain() {
309+ switch v.(type) {
310+ case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop:
311+ t.Fatalf("data drag alive while locked: %#v", v)
312+ }
313+ }
314+ // After the unlock, the old drag is gone: plain pointer events.
315+ unlocked := ports.SecurityState{Generation: 2}
316+ d.epoch.Store(unlocked)
317+ d.client <- ports.SessionLockChanged{State: unlocked}
318+ d.settle(t)
319+ d.drain()
320+ d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 9)
321+ d.settle(t)
322+ got := dataDragCommands(d.drain())
323+ wantCommands(t, got, ports.PointerFocus{ID: 1, X: 21, Y: 30}, ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 9 * time.Millisecond})
324+}
325+
326+func TestDataDragTargetUnmapped(t *testing.T) {
327+ d := startDnd(t)
328+ d.start(t, 0)
329+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
330+ d.settle(t)
331+ d.drain()
332+ d.client <- ports.WindowUnmapped{ID: 2}
333+ d.settle(t)
334+ cmds := dataDragCommands(d.drain())
335+ if len(cmds) == 0 || cmds[0] != (ports.DataDragFocus{Time: 3 * time.Millisecond}) {
336+ t.Fatalf("commands %#v", cmds)
337+ }
338+}
339+
340+func TestDataDragIconIsLastWindowAtCursor(t *testing.T) {
341+ d := startDnd(t)
342+ d.start(t, dndIcon)
343+ // Not drawn until wayland measured it.
344+ if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
345+ t.Fatal("icon drawn before its size")
346+ }
347+ cx, cy := d.one.X+20, d.one.Y+30
348+ d.client <- ports.DataDragIcon{ID: dndIcon, X: -4, Y: -6, W: 32, H: 24}
349+ set := d.settle(t)
350+ s := dragScene(set, "A")
351+ last := s.Windows[len(s.Windows)-1]
352+ want := ports.SceneWindow{ID: dndIcon, Rect: ports.Rect{X: cx - 4, Y: cy - 6, W: 32, H: 24}, Popup: true, OverLayers: true}
353+ if last != want {
354+ t.Fatalf("last window %+v, want %+v", last, want)
355+ }
356+ // It follows the cursor; the scene changes.
357+ d.moveTo(float64(cx+100), float64(cy+50), 2)
358+ moved := dragScene(d.settle(t), "A")
359+ if moved.SameAs(s) || moved.Seq == s.Seq {
360+ t.Fatal("scene did not change with the icon")
361+ }
362+ if got := moved.Windows[len(moved.Windows)-1]; got.Rect.X != cx+96 || got.Rect.Y != cy+44 || got.ID != dndIcon {
363+ t.Fatalf("moved icon %+v", got)
364+ }
365+ // Under the icon, the surface below is still the target.
366+ d.drain()
367+ d.moveTo(float64(cx+101), float64(cy+50), 3)
368+ d.settle(t)
369+ for _, v := range d.drain() {
370+ if f, ok := v.(ports.DataDragFocus); ok && f.ID == dndIcon {
371+ t.Fatalf("icon was targeted: %#v", f)
372+ }
373+ }
374+ // A commit that changes the offset moves it; a zero size hides it.
375+ d.client <- ports.DataDragIcon{ID: dndIcon, X: 0, Y: 0, W: 32, H: 24}
376+ if got := dragScene(d.settle(t), "A"); got.Windows[len(got.Windows)-1].Rect.X != cx+101 {
377+ t.Fatalf("offset ignored: %+v", got.Windows[len(got.Windows)-1])
378+ }
379+ d.client <- ports.DataDragIcon{ID: dndIcon}
380+ if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
381+ t.Fatal("hidden icon drawn")
382+ }
383+ // Geometry of another surface is not the icon's.
384+ d.client <- ports.DataDragIcon{ID: dndIcon + 1, W: 10, H: 10}
385+ if _, ok := windowIn(d.settle(t), "A", dndIcon+1); ok {
386+ t.Fatal("foreign icon drawn")
387+ }
388+ // The drop removes it.
389+ d.client <- ports.DataDragIcon{ID: dndIcon, W: 32, H: 24}
390+ d.settle(t)
391+ d.in(ports.PointerButton{Button: 0x110})
392+ set = d.settle(t)
393+ if _, ok := windowIn(set, "A", dndIcon); ok {
394+ t.Fatal("icon drawn after the drop")
395+ }
396+}
397+
398+func TestDataDragIconOnScreenUnderCursor(t *testing.T) {
399+ other := ports.OutputInfo{Name: "B", Width: 800, Height: 600}
400+ d := startDnd(t, dragOut, other)
401+ d.start(t, dndIcon)
402+ d.client <- ports.DataDragIcon{ID: dndIcon, X: 2, Y: 3, W: 10, H: 10}
403+ d.moveTo(1000+100, 50, 2)
404+ set := d.settle(t)
405+ if _, ok := windowIn(set, "A", dndIcon); ok {
406+ t.Fatal("icon on the other screen")
407+ }
408+ w, ok := windowIn(set, "B", dndIcon)
409+ if !ok {
410+ t.Fatal("no icon on B")
411+ }
412+ if w.Rect != (ports.Rect{X: 102, Y: 53, W: 10, H: 10}) {
413+ t.Fatalf("icon %+v", w.Rect)
414+ }
415+}
416+
417+func TestDataDragNoIconIgnoresIconEvents(t *testing.T) {
418+ d := startDnd(t)
419+ d.start(t, 0)
420+ d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10}
421+ if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
422+ t.Fatal("icon drawn for a drag without one")
423+ }
424+ // Nor with no drag at all.
425+ d.in(ports.PointerButton{Button: 0x110})
426+ d.settle(t)
427+ d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10}
428+ if _, ok := windowIn(d.settle(t), "A", dndIcon); ok {
429+ t.Fatal("icon drawn without a drag")
430+ }
431+}
432+
433+func TestDataDragFocusScreenFollowsPointer(t *testing.T) {
434+ r := startDragRig(t, nil, dragOut, ports.OutputInfo{Name: "B", Width: 1000, Height: 600})
435+ r.client <- ports.WindowMapped{ID: 1}
436+ r.settle(t)
437+ r.moveTo(1500, 300, 1)
438+ r.settle(t)
439+ r.client <- ports.WindowMapped{ID: 3}
440+ set := r.settle(t)
441+ one := dragRect(t, set, "A", 1)
442+ r.moveTo(float64(one.X+20), float64(one.Y+30), 2)
443+ set = r.settle(t)
444+ if w, _ := windowIn(set, "A", 1); !w.Focused {
445+ t.Fatal("1 not focused")
446+ }
447+ r.in(ports.PointerButton{Button: 0x110, Pressed: true})
448+ r.settle(t)
449+ r.client <- ports.DataDragStarted{Button: 0x110}
450+ r.moveTo(1500, 300, 3)
451+ set = r.settle(t)
452+ if w, _ := windowIn(set, "B", 3); !w.Focused {
453+ t.Fatal("the screen under the drag did not take the focus")
454+ }
455+ three := dragRect(t, set, "B", 3)
456+ var focus []ports.DataDragFocus
457+ for _, v := range r.drain() {
458+ if f, ok := v.(ports.DataDragFocus); ok {
459+ focus = append(focus, f)
460+ }
461+ }
462+ if n := len(focus); n != 2 || focus[1].ID != 3 || focus[1].X != float64(1500-1000-three.X) {
463+ t.Fatalf("focus %+v", focus)
464+ }
465+}
466+
467+func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) {
468+ d := startDnd(t)
469+ d.start(t, 0)
470+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
471+ d.settle(t)
472+ d.drain()
473+ // cmd+2 shows an empty workspace under the still cursor: the target
474+ // leaves, and the release drops on nothing.
475+ d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true})
476+ d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper})
477+ d.settle(t)
478+ got := dataDragCommands(d.drain())
479+ if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) {
480+ t.Fatalf("no leave after the switch: %#v", got)
481+ }
482+ d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond})
483+ d.settle(t)
484+ got = dataDragCommands(d.drain())
485+ if len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
486+ t.Fatalf("commands %#v", got)
487+ }
488+ for _, v := range got {
489+ if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 {
490+ t.Fatalf("hidden window targeted: %#v", got)
491+ }
492+ }
493+}
494+
495+func TestDataDragTargetMovedUnderStillCursor(t *testing.T) {
496+ d := startDnd(t)
497+ d.start(t, 0)
498+ // Over 2, focused by a click-free keyboard move, then cmd+f maximizes
499+ // its column: still the target under the cursor, at another point.
500+ d.moveTo(float64(d.two.X+d.two.W-20), float64(d.two.Y+30), 2)
501+ d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper, Pressed: true})
502+ d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper})
503+ d.settle(t)
504+ d.drain()
505+ d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true})
506+ d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper})
507+ set := d.settle(t)
508+ wide := dragRect(t, set, dragOut.Name, 2)
509+ if wide.X == d.two.X {
510+ t.Fatalf("the column did not move: %v", wide)
511+ }
512+ x, y := float64(d.two.X+d.two.W-20-wide.X), float64(d.two.Y+30-wide.Y)
513+ got := dataDragCommands(d.drain())
514+ if !slices.Contains(got, ports.ClientCommand(ports.DataDragMotion{ID: 2, X: x, Y: y, Time: 2 * time.Millisecond})) {
515+ t.Fatalf("no motion to %v,%v: %#v", x, y, got)
516+ }
517+}
518+
519+func TestDataDragDropsOnItsOwnButton(t *testing.T) {
520+ d := startDnd(t)
521+ // Right pressed after left: the drag is left's, the grab's.
522+ d.in(ports.PointerButton{Button: 0x111, Pressed: true})
523+ d.settle(t)
524+ d.drain()
525+ d.start(t, 0)
526+ d.in(ports.PointerButton{Button: 0x111})
527+ d.settle(t)
528+ if got := dataDragCommands(d.drain()); slices.Contains(got, ports.ClientCommand(ports.DataDragDrop{})) {
529+ t.Fatalf("right's release dropped: %#v", got)
530+ }
531+ d.in(ports.PointerButton{Button: 0x110})
532+ d.settle(t)
533+ if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
534+ t.Fatalf("left's release did not drop: %#v", got)
535+ }
536+}
537+
538+func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) {
539+ d := startDnd(t)
540+ d.start(t, 0)
541+ d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true})
542+ d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true})
543+ d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper})
544+ d.settle(t)
545+ d.drain()
546+ d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true})
547+ d.settle(t)
548+ got := dataDragCommands(d.drain())
549+ if len(got) == 0 || got[0] != (ports.DataDragCancel{}) {
550+ t.Fatalf("Escape did not cancel the drag: %#v", got)
551+ }
552+ d.in(ports.PointerButton{Button: 0x110})
553+ d.settle(t)
554+ if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) {
555+ t.Fatal("dropped after Escape")
556+ }
557+}
558+
559+func TestDataDragIgnoresCompositorSwipes(t *testing.T) {
560+ d := startDnd(t)
561+ d.start(t, 0)
562+ before := d.settle(t)
563+ // Flicks that page columns and workspaces, as navigation would.
564+ for _, fingers := range []int{3, 4} {
565+ at := time.Second
566+ d.in(ports.SwipeBegin{Fingers: fingers, Time: at})
567+ for range 10 {
568+ at += 8 * time.Millisecond
569+ d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at})
570+ }
571+ d.in(ports.SwipeEnd{Time: at})
572+ }
573+ after := d.settle(t)
574+ if !slices.Equal(before[0].Windows, after[0].Windows) {
575+ t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows)
576+ }
577+}
M internal/core/input.go +30 -4
@@ -26,6 +26,10 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
2626 case ports.PointerAxis:
2727 return c.pointerAxis(ctx, v)
2828 case ports.SwipeBegin:
29+ if c.dataDrag != nil && !forwardsSwipe(v.Fingers) {
30+ // The drag owns the pointer: no navigation under it.
31+ return nil
32+ }
2933 if forwardsSwipe(v.Fingers) {
3034 return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)
3135 }
@@ -98,6 +102,11 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
98102 // The dragged window and the others get no pointer events.
99103 return c.dragMotion(ctx, v.Time)
100104 }
105+ if c.dataDrag != nil {
106+ // A data drag goes to the surface under the pointer, by its own
107+ // commands.
108+ return c.dataDragMotion(ctx, v.Time)
109+ }
101110 id, x, y := c.hit(c.cursorX, c.cursorY)
102111 // Layout changes are intentionally re-hit-tested only on motion.
103112 if id != c.pointer {
@@ -123,6 +132,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
123132 delete(c.buttons, v.Button)
124133 return nil
125134 }
135+ if c.dataDrag != nil {
136+ return c.dataDragButton(ctx, v)
137+ }
126138 if d := c.drag; d != nil {
127139 if !v.Pressed && v.Button == d.button {
128140 delete(c.buttons, v.Button)
@@ -160,8 +172,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
160172 return err
161173 }
162174 }
175+ grab := v.Pressed && c.held() == 0
163176 if v.Pressed {
164- if c.held() == 0 {
177+ if grab {
165178 c.grab = id
166179 }
167180 c.buttons[v.Button] = true
@@ -170,7 +183,7 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
170183 delete(c.buttons, v.Button)
171184 }
172185 if id != 0 {
173- if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
186+ if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil {
174187 return err
175188 }
176189 // A click on an on-demand layer gives it the keyboard.
@@ -226,7 +239,7 @@ func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
226239 }
227240 // Scroll goes to the window under the pointer, which has the
228241 // pointer focus even mid-drag. None during a window drag.
229- if c.pointer != 0 && c.drag == nil {
242+ if c.pointer != 0 && c.drag == nil && c.dataDrag == nil {
230243 if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
231244 return err
232245 }
@@ -244,6 +257,12 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
244257 return err
245258 }
246259 }
260+ if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
261+ // Escape cancels the data drag first, even over the switcher;
262+ // its release goes nowhere either.
263+ c.pressed[heldKey(key)] = true
264+ return c.cancelDataDrag(ctx)
265+ }
247266 if c.switching() && key.Pressed && key.Keysym == "Escape" {
248267 // Escape cancels the switcher; its release goes nowhere either.
249268 c.pressed[heldKey(key)] = true
@@ -372,7 +391,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error {
372391 return err
373392 }
374393 }
375- return c.publish(ctx)
394+ if err := c.publish(ctx); err != nil {
395+ return err
396+ }
397+ if c.dataDrag != nil {
398+ // A data drag follows the layout under the still cursor.
399+ return c.rehit(ctx)
400+ }
401+ return nil
376402 }
377403
378404 // runSpawn starts the programs of a spawn effect of an action applied to w:
@@ -26,6 +26,10 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
26 case ports.PointerAxis:26 case ports.PointerAxis:
27 return c.pointerAxis(ctx, v)27 return c.pointerAxis(ctx, v)
28 case ports.SwipeBegin:28 case ports.SwipeBegin:
29+ if c.dataDrag != nil && !forwardsSwipe(v.Fingers) {
30+ // The drag owns the pointer: no navigation under it.
31+ return nil
32+ }
29 if forwardsSwipe(v.Fingers) {33 if forwardsSwipe(v.Fingers) {
30 return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)34 return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)
31 }35 }
@@ -98,6 +102,11 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
98 // The dragged window and the others get no pointer events.102 // The dragged window and the others get no pointer events.
99 return c.dragMotion(ctx, v.Time)103 return c.dragMotion(ctx, v.Time)
100 }104 }
105+ if c.dataDrag != nil {
106+ // A data drag goes to the surface under the pointer, by its own
107+ // commands.
108+ return c.dataDragMotion(ctx, v.Time)
109+ }
101 id, x, y := c.hit(c.cursorX, c.cursorY)110 id, x, y := c.hit(c.cursorX, c.cursorY)
102 // Layout changes are intentionally re-hit-tested only on motion.111 // Layout changes are intentionally re-hit-tested only on motion.
103 if id != c.pointer {112 if id != c.pointer {
@@ -123,6 +132,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
123 delete(c.buttons, v.Button)132 delete(c.buttons, v.Button)
124 return nil133 return nil
125 }134 }
135+ if c.dataDrag != nil {
136+ return c.dataDragButton(ctx, v)
137+ }
126 if d := c.drag; d != nil {138 if d := c.drag; d != nil {
127 if !v.Pressed && v.Button == d.button {139 if !v.Pressed && v.Button == d.button {
128 delete(c.buttons, v.Button)140 delete(c.buttons, v.Button)
@@ -160,8 +172,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
160 return err172 return err
161 }173 }
162 }174 }
175+ grab := v.Pressed && c.held() == 0
163 if v.Pressed {176 if v.Pressed {
164- if c.held() == 0 {177+ if grab {
165 c.grab = id178 c.grab = id
166 }179 }
167 c.buttons[v.Button] = true180 c.buttons[v.Button] = true
@@ -170,7 +183,7 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
170 delete(c.buttons, v.Button)183 delete(c.buttons, v.Button)
171 }184 }
172 if id != 0 {185 if id != 0 {
173- if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {186+ if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil {
174 return err187 return err
175 }188 }
176 // A click on an on-demand layer gives it the keyboard.189 // A click on an on-demand layer gives it the keyboard.
@@ -226,7 +239,7 @@ func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
226 }239 }
227 // Scroll goes to the window under the pointer, which has the240 // Scroll goes to the window under the pointer, which has the
228 // pointer focus even mid-drag. None during a window drag.241 // pointer focus even mid-drag. None during a window drag.
229- if c.pointer != 0 && c.drag == nil {242+ if c.pointer != 0 && c.drag == nil && c.dataDrag == nil {
230 if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {243 if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
231 return err244 return err
232 }245 }
@@ -244,6 +257,12 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
244 return err257 return err
245 }258 }
246 }259 }
260+ if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
261+ // Escape cancels the data drag first, even over the switcher;
262+ // its release goes nowhere either.
263+ c.pressed[heldKey(key)] = true
264+ return c.cancelDataDrag(ctx)
265+ }
247 if c.switching() && key.Pressed && key.Keysym == "Escape" {266 if c.switching() && key.Pressed && key.Keysym == "Escape" {
248 // Escape cancels the switcher; its release goes nowhere either.267 // Escape cancels the switcher; its release goes nowhere either.
249 c.pressed[heldKey(key)] = true268 c.pressed[heldKey(key)] = true
@@ -372,7 +391,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error {
372 return err391 return err
373 }392 }
374 }393 }
375- return c.publish(ctx)394+ if err := c.publish(ctx); err != nil {
395+ return err
396+ }
397+ if c.dataDrag != nil {
398+ // A data drag follows the layout under the still cursor.
399+ return c.rehit(ctx)
400+ }
401+ return nil
376 }402 }
377 403
378 // runSpawn starts the programs of a spawn effect of an action applied to w:404 // runSpawn starts the programs of a spawn effect of an action applied to w:
M internal/core/scene.go +5 -1
@@ -21,7 +21,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
2121 // layout is what is drawn; settled (same indexes) is where the
2222 // windows are going, and alone sizes the configures.
2323 layout, settled := sc.shown, sc.settledLayout
24- scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)}
24+ scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)}
2525 if !sc.mon.previewing() {
2626 // Tiles stop at the panels; the overview lays out previews over
2727 // the whole output.
@@ -126,6 +126,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
126126 }
127127 }
128128 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
129+ if icon, ok := c.dataDragIconOn(i, sc); ok {
130+ // The drag icon is over everything, popups and layers included.
131+ scene.Windows = append(scene.Windows, icon)
132+ }
129133 scene.CaptureIndicators = c.captureIndicators(sc)
130134 scene.Capture = c.captureSceneFor(sc, capture)
131135 // The springs of this frame are advanced: a scene shown while one still
@@ -21,7 +21,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
21 // layout is what is drawn; settled (same indexes) is where the21 // layout is what is drawn; settled (same indexes) is where the
22 // windows are going, and alone sizes the configures.22 // windows are going, and alone sizes the configures.
23 layout, settled := sc.shown, sc.settledLayout23 layout, settled := sc.shown, sc.settledLayout
24- scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)}24+ scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)}
25 if !sc.mon.previewing() {25 if !sc.mon.previewing() {
26 // Tiles stop at the panels; the overview lays out previews over26 // Tiles stop at the panels; the overview lays out previews over
27 // the whole output.27 // the whole output.
@@ -126,6 +126,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
126 }126 }
127 }127 }
128 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)128 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
129+ if icon, ok := c.dataDragIconOn(i, sc); ok {
130+ // The drag icon is over everything, popups and layers included.
131+ scene.Windows = append(scene.Windows, icon)
132+ }
129 scene.CaptureIndicators = c.captureIndicators(sc)133 scene.CaptureIndicators = c.captureIndicators(sc)
130 scene.Capture = c.captureSceneFor(sc, capture)134 scene.Capture = c.captureSceneFor(sc, capture)
131 // The springs of this frame are advanced: a scene shown while one still135 // The springs of this frame are advanced: a scene shown while one still
M internal/core/security.go +2 -0
@@ -33,6 +33,8 @@ func (c *Core) syncSecurity() bool {
3333 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}
3434 c.pointer, c.grab, c.pointerOutput = 0, 0, ""
3535 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0
36+ // Wayland cancels its own drag when the epoch changes.
37+ c.dataDrag = nil
3638 c.pointerAt, c.motionTime = [2]float64{}, 0
3739 c.keyboard = keyboard{}
3840 c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{}
@@ -33,6 +33,8 @@ func (c *Core) syncSecurity() bool {
33 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}33 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}
34 c.pointer, c.grab, c.pointerOutput = 0, 0, ""34 c.pointer, c.grab, c.pointerOutput = 0, 0, ""
35 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 035 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0
36+ // Wayland cancels its own drag when the epoch changes.
37+ c.dataDrag = nil
36 c.pointerAt, c.motionTime = [2]float64{}, 038 c.pointerAt, c.motionTime = [2]float64{}, 0
37 c.keyboard = keyboard{}39 c.keyboard = keyboard{}
38 c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{}40 c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{}
M internal/ports/client_commands.go +3 -0
@@ -178,6 +178,9 @@ type PointerButtonTo struct {
178178 ID WindowID
179179 Button uint32
180180 Pressed bool
181+ // Grab marks the press that starts the implicit grab: no other button
182+ // was held. start_drag must name its serial.
183+ Grab bool
181184 // Time is the device timestamp (CLOCK_MONOTONIC).
182185 Time time.Duration
183186 }
@@ -178,6 +178,9 @@ type PointerButtonTo struct {
178 ID WindowID178 ID WindowID
179 Button uint32179 Button uint32
180 Pressed bool180 Pressed bool
181+ // Grab marks the press that starts the implicit grab: no other button
182+ // was held. start_drag must name its serial.
183+ Grab bool
181 // Time is the device timestamp (CLOCK_MONOTONIC).184 // Time is the device timestamp (CLOCK_MONOTONIC).
182 Time time.Duration185 Time time.Duration
183 }186 }
A internal/ports/dnd.go +69 -0
@@ -0,0 +1,69 @@
1+package ports
2+
3+import "time"
4+
5+// Data drag and drop (wl_data_device.start_drag). Wayland validates the
6+// client's request and tells core; core owns the pointer during the drag:
7+// it hit-tests, routes the drag to the surface under the pointer, drops on
8+// the release and cancels on Escape. Wayland speaks the protocol to the
9+// source and the targets. The icon is a client surface core draws over
10+// everything at the cursor.
11+
12+// DataDragStarted carries wayland → core a client drag answering the
13+// client's current press. Button is the button of that press, whose
14+// release drops; Icon is the drag icon's ID (0: none). Core answers with
15+// DataDragCancel when it refuses the drag.
16+type DataDragStarted struct {
17+ Button uint32
18+ Icon WindowID
19+}
20+
21+func (DataDragStarted) clientEvent() {}
22+
23+// DataDragIcon carries wayland → core the drag icon's geometry after a
24+// commit: its top left corner relative to the cursor hotspot, and its
25+// size, in logical pixels. W or H 0 hides it.
26+type DataDragIcon struct {
27+ ID WindowID
28+ X, Y, W, H int
29+}
30+
31+func (DataDragIcon) clientEvent() {}
32+
33+// DataDragEnded carries wayland → core the end of a drag core did not
34+// end itself: the source died or the drag was cancelled.
35+type DataDragEnded struct{}
36+
37+func (DataDragEnded) clientEvent() {}
38+
39+// DataDragFocus carries core → wayland the surface under the pointer
40+// during a drag (0: none), at surface-local X, Y: the old target gets
41+// leave, the new one enter with an offer.
42+type DataDragFocus struct {
43+ ID WindowID
44+ X, Y float64
45+ Time time.Duration
46+}
47+
48+func (DataDragFocus) clientCommand() {}
49+
50+// DataDragMotion carries core → wayland the pointer over the drag target,
51+// in its surface coordinates.
52+type DataDragMotion struct {
53+ ID WindowID
54+ X, Y float64
55+ Time time.Duration
56+}
57+
58+func (DataDragMotion) clientCommand() {}
59+
60+// DataDragDrop carries core → wayland the release of the drag button: the
61+// target gets drop when it accepted the offer, else the drag is cancelled.
62+type DataDragDrop struct{}
63+
64+func (DataDragDrop) clientCommand() {}
65+
66+// DataDragCancel carries core → wayland a cancelled or refused drag.
67+type DataDragCancel struct{}
68+
69+func (DataDragCancel) clientCommand() {}
@@ -0,0 +1,69 @@
1+package ports
2+
3+import "time"
4+
5+// Data drag and drop (wl_data_device.start_drag). Wayland validates the
6+// client's request and tells core; core owns the pointer during the drag:
7+// it hit-tests, routes the drag to the surface under the pointer, drops on
8+// the release and cancels on Escape. Wayland speaks the protocol to the
9+// source and the targets. The icon is a client surface core draws over
10+// everything at the cursor.
11+
12+// DataDragStarted carries wayland → core a client drag answering the
13+// client's current press. Button is the button of that press, whose
14+// release drops; Icon is the drag icon's ID (0: none). Core answers with
15+// DataDragCancel when it refuses the drag.
16+type DataDragStarted struct {
17+ Button uint32
18+ Icon WindowID
19+}
20+
21+func (DataDragStarted) clientEvent() {}
22+
23+// DataDragIcon carries wayland → core the drag icon's geometry after a
24+// commit: its top left corner relative to the cursor hotspot, and its
25+// size, in logical pixels. W or H 0 hides it.
26+type DataDragIcon struct {
27+ ID WindowID
28+ X, Y, W, H int
29+}
30+
31+func (DataDragIcon) clientEvent() {}
32+
33+// DataDragEnded carries wayland → core the end of a drag core did not
34+// end itself: the source died or the drag was cancelled.
35+type DataDragEnded struct{}
36+
37+func (DataDragEnded) clientEvent() {}
38+
39+// DataDragFocus carries core → wayland the surface under the pointer
40+// during a drag (0: none), at surface-local X, Y: the old target gets
41+// leave, the new one enter with an offer.
42+type DataDragFocus struct {
43+ ID WindowID
44+ X, Y float64
45+ Time time.Duration
46+}
47+
48+func (DataDragFocus) clientCommand() {}
49+
50+// DataDragMotion carries core → wayland the pointer over the drag target,
51+// in its surface coordinates.
52+type DataDragMotion struct {
53+ ID WindowID
54+ X, Y float64
55+ Time time.Duration
56+}
57+
58+func (DataDragMotion) clientCommand() {}
59+
60+// DataDragDrop carries core → wayland the release of the drag button: the
61+// target gets drop when it accepted the offer, else the drag is cancelled.
62+type DataDragDrop struct{}
63+
64+func (DataDragDrop) clientCommand() {}
65+
66+// DataDragCancel carries core → wayland a cancelled or refused drag.
67+type DataDragCancel struct{}
68+
69+func (DataDragCancel) clientCommand() {}

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 25

Tags 10

↑↓ moveenter openesc close