1package wayland
2
3import (
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.
27const actionAsk = uint32(wayland.DataDeviceManagerDndActionAsk)
28
29const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk)
30
31// dndState is the drag in progress.
32type 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
47const (
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.
56type 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.
75type 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.
86func (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.
95func 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.
111func (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.
132func (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).
192func (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.
206func (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.
215func (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.
228func (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
238func (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.
250func (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.
279func (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.
332func (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
343func (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.
352func (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.
369func (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
411func (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.
422func (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.
445func (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.
457func (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.
463func (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.
471func (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.
487func (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.
511func (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
531func (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
540func (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
551func (*dndOffer) Destroy(*wayland.DataOffer) {}
552
553func (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
579func (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}