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, owned by acceptor.
62 // action is the shared result of the latest set_actions request,
63 // broadcast to every offer so drop and finish use the same result.
64 accepted string
65 acceptor *dndOffer
66 action uint32
67 state int
68 // legacy: no device speaks wl_data_offer v3: the target copies and
69 // nothing finishes it; destroying its offers does.
70 legacy bool
71 // ask: the drop came with the ask action; the target picks the final
72 // one after it.
73 ask bool
74}
75
76// dndOffer is a wl_data_offer of a drag.
77type dndOffer struct {
78 server *Server
79 t *dndTarget
80 res *wayland.DataOffer
81 // actions and preferred are the target's set_actions; sent is the last
82 // action told to this offer.
83 actions, preferred, sent uint32
84}
85
86// srcActions is what the source allows; a source that never said (a
87// pre-v3 client) only copies.
88func (src *clipSource) srcActions() uint32 {
89 if src.actionsSet {
90 return src.actions
91 }
92 return uint32(wayland.DataDeviceManagerDndActionCopy)
93}
94
95// chooseAction picks the action like wlroots: the preferred one if both
96// allow it, else the first of copy, move, ask.
97func chooseAction(avail, preferred uint32) uint32 {
98 if avail == 0 {
99 return 0
100 }
101 if preferred&avail != 0 {
102 return preferred
103 }
104 for _, a := range [...]uint32{uint32(wayland.DataDeviceManagerDndActionCopy), uint32(wayland.DataDeviceManagerDndActionMove), uint32(wayland.DataDeviceManagerDndActionAsk)} {
105 if avail&a != 0 {
106 return a
107 }
108 }
109 return 0
110}
111
112// addDataDevice tracks a client's data devices, the drag targets.
113func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) {
114 if s.dataDevices == nil {
115 s.dataDevices = map[server.Client][]*wayland.DataDevice{}
116 }
117 s.dataDevices[c] = append(s.dataDevices[c], dev)
118 dev.OnDestroy = func() {
119 // Copy on write: a drag may be iterating the old list.
120 list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev })
121 if len(list) == 0 {
122 delete(s.dataDevices, c)
123 if d := s.dnd; d != nil && d.target != nil && d.target.surface.Client() == c {
124 s.dndLeave(d)
125 }
126 // An internal drag has nobody left to tell.
127 if d := s.dnd; d != nil && d.client == c && d.src == nil {
128 s.dndEnd(d, false, true)
129 }
130 } else {
131 s.dataDevices[c] = list
132 }
133 }
134}
135
136// startDrag handles wl_data_device.start_drag.
137func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) {
138 var cs *clipSource
139 if srcRes != nil {
140 if cs = s.dataSources[srcRes.Resource]; cs == nil {
141 return
142 }
143 }
144 var iconSurf *surface
145 if icon != nil {
146 if iconSurf = s.surfaces[icon.Resource]; iconSurf == nil {
147 return
148 }
149 if iconSurf.kind != roleNone && iconSurf.kind != roleDragIcon {
150 r.PostError(uint32(wayland.DataDeviceErrorRole), "icon surface already has another role")
151 return
152 }
153 }
154 client := r.Client()
155 valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client &&
156 s.validGrab(origin, serial)
157 if cs != nil && (cs.used || !valid) {
158 // A used source is the selection, a drag source or already
159 // cancelled: only a source nobody holds is cancelled again.
160 if !s.sourceBusy(cs) {
161 cs.used = true
162 if cs.alive() {
163 cs.cancel()
164 }
165 }
166 return
167 }
168 if !valid {
169 return
170 }
171 d := &dndState{src: cs, client: client, icon: iconSurf}
172 if cs != nil {
173 cs.used = true
174 }
175 if iconSurf != nil {
176 d.iconID = s.nextWindow
177 s.nextWindow++
178 iconSurf.kind = roleDragIcon
179 iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) }
180 }
181 s.dnd = d
182 // Core consumes the releases of a drag: the grab ends here.
183 button := s.seat.grabButton
184 s.seat.grabSerial, s.seat.grabButton = 0, 0
185 s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started")
186 s.emit(ports.DataDragStarted{Button: button, Icon: d.iconID})
187 if iconSurf != nil && iconSurf.has {
188 // The icon was committed before the drag: show what it holds.
189 iconSurf.committed = damage{full: true}
190 iconSurf.redraw()
191 s.dndIconSend(d)
192 }
193}
194
195// validGrab reports whether serial is the press of the implicit grab still
196// held, on the surface with pointer focus (or a subsurface of its tree).
197func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool {
198 if serial == 0 || serial != s.seat.grabSerial {
199 return false
200 }
201 focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0)
202 if focus == nil {
203 return false
204 }
205 o, f := s.surfaces[origin.Resource], s.surfaces[focus.Resource]
206 return o != nil && f != nil && o.root() == f.root()
207}
208
209// sourceBusy reports whether src is the selection, the source of the drag
210// or of a drop waiting for its finish.
211func (s *Server) sourceBusy(src *clipSource) bool {
212 if s.selections[selClipboard] == src || s.dnd != nil && s.dnd.src == src {
213 return true
214 }
215 return slices.ContainsFunc(s.dndDrops, func(t *dndTarget) bool { return t.d.src == src })
216}
217
218// dndIconRole runs on each commit of the icon surface, and on its
219// destruction.
220func (s *Server) dndIconRole(d *dndState, icon *surface) {
221 if !icon.destroyed || d.icon != icon {
222 return
223 }
224 // The drag goes on without icon.
225 id := d.iconID
226 d.icon, d.iconID = nil, 0
227 s.emit(ports.DataDragIcon{ID: id})
228 s.emitContent(ports.SurfaceContent{ID: id}, damage{full: true})
229}
230
231// dndIconCommit follows a commit of the active icon: its offsets add up
232// and its geometry goes to core.
233func (s *Server) dndIconCommit(icon *surface, dx, dy int32) {
234 d := s.dnd
235 if d == nil || d.icon != icon {
236 return
237 }
238 d.iconX += int(dx)
239 d.iconY += int(dy)
240 s.dndIconSend(d)
241}
242
243func (s *Server) dndIconSend(d *dndState) {
244 ic := ports.DataDragIcon{ID: d.iconID, X: d.iconX, Y: d.iconY}
245 if d.icon != nil && d.icon.has {
246 ic.W, ic.H = d.icon.content.LogicalW, d.icon.content.LogicalH
247 }
248 if ic != d.iconSent {
249 d.iconSent = ic
250 s.emit(ic)
251 }
252}
253
254// applyDrag applies the commands core sends during a drag.
255func (s *Server) applyDrag(cmd ports.ClientCommand) {
256 d := s.dnd
257 if d == nil {
258 return
259 }
260 switch c := cmd.(type) {
261 case ports.DataDragCancel:
262 s.dndEnd(d, true, false)
263 case ports.DataDragDrop:
264 s.dndDrop(d)
265 case ports.DataDragFocus:
266 s.dndFocus(d, c)
267 case ports.DataDragMotion:
268 if t := d.target; t != nil && !s.protected() {
269 surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y)
270 if surf == nil || surf.Resource != t.surface.Resource {
271 return
272 }
273 for _, dev := range t.devices {
274 if dev.Resource.Alive() {
275 dev.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y))
276 }
277 }
278 }
279 }
280}
281
282// dndFocus moves the drag to the surface core names: the old target gets
283// leave, the new one enter with an offer.
284func (s *Server) dndFocus(d *dndState, c ports.DataDragFocus) {
285 s.dndLeave(d)
286 if c.ID == 0 || s.protected() {
287 return
288 }
289 surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y)
290 if surf == nil {
291 return
292 }
293 client := surf.Client()
294 // Without a source the drag is internal to the origin client.
295 if d.src == nil && client != d.client {
296 return
297 }
298 devices := s.dataDevices[client]
299 if len(devices) == 0 {
300 return
301 }
302 t := &dndTarget{d: d, surface: surf, devices: devices, legacy: true}
303 d.target = t
304 for _, dev := range devices {
305 if !dev.Resource.Alive() {
306 continue
307 }
308 if dev.Version() >= 3 {
309 t.legacy = false
310 }
311 var offer *wayland.DataOffer
312 if d.src != nil {
313 o := &dndOffer{server: s, t: t}
314 res, err := wayland.NewDataOffer(dev.Client(), dev.Version(), 0, o)
315 if err != nil {
316 continue
317 }
318 o.res = res
319 res.OnDestroy = func() { s.dndOfferGone(o) }
320 t.offers = append(t.offers, o)
321 dev.SendDataOffer(res)
322 for _, mime := range d.src.mimes {
323 res.SendOffer(mime)
324 }
325 if dev.Version() >= 3 {
326 res.SendSourceActions(d.src.srcActions())
327 }
328 offer = res
329 }
330 s.serial++
331 dev.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y), offer)
332 }
333}
334
335// dndLeave sends leave to the target of the drag and forgets it: its
336// offers become inert.
337func (s *Server) dndLeave(d *dndState) {
338 t := d.target
339 if t == nil {
340 return
341 }
342 d.target = nil
343 t.state = targetDone
344 s.dndSendLeave(t)
345 s.dndResetSource(t)
346}
347
348func (s *Server) dndSendLeave(t *dndTarget) {
349 for _, dev := range t.devices {
350 if dev.Resource.Alive() {
351 dev.SendLeave()
352 }
353 }
354}
355
356// dndResetSource tells the source nothing accepts or acts any more.
357func (s *Server) dndResetSource(t *dndTarget) {
358 src := t.d.src
359 if src == nil || !src.alive() {
360 return
361 }
362 if t.accepted != "" {
363 src.res.SendTarget("")
364 }
365 if t.action != 0 && src.res.Version() >= 3 {
366 src.res.SendAction(0)
367 }
368 t.accepted, t.action = "", 0
369}
370
371// dndDrop handles the release of the drag button: a target that accepted
372// and agreed on an action gets drop, then leave (its offers stay usable until
373// finish); else the drag is cancelled.
374func (s *Server) dndDrop(d *dndState) {
375 t := d.target
376 if t == nil {
377 s.dndEnd(d, true, false)
378 return
379 }
380 if d.src == nil {
381 // Internal drag: the origin client decides alone.
382 d.target = nil
383 t.state = targetDone
384 s.dndSendDrop(t)
385 s.dndSendLeave(t)
386 s.dndEnd(d, false, false)
387 return
388 }
389 if t.legacy {
390 // A pre-v3 target cannot negotiate: it copies.
391 action := chooseAction(uint32(wayland.DataDeviceManagerDndActionCopy)&d.src.srcActions(), 0)
392 if action != t.action {
393 t.action = action
394 if action != 0 && d.src.alive() && d.src.res.Version() >= 3 {
395 d.src.res.SendAction(action)
396 }
397 }
398 }
399 if !t.legacy && t.accepted == "" || t.action == 0 {
400 s.dndEnd(d, true, false)
401 return
402 }
403 d.target = nil
404 t.state = targetDropped
405 t.ask = t.action == actionAsk
406 s.dndDrops = append(s.dndDrops, t)
407 s.dndSendDrop(t)
408 s.dndSendLeave(t)
409 if d.src.alive() && d.src.res.Version() >= 3 {
410 d.src.res.SendDndDropPerformed()
411 }
412 s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped")
413 s.dndEnd(d, false, false)
414}
415
416func (s *Server) dndSendDrop(t *dndTarget) {
417 for _, dev := range t.devices {
418 if dev.Resource.Alive() {
419 dev.SendDrop()
420 }
421 }
422}
423
424// dndEnd ends the drag d: the target gets leave, the source cancelled if
425// cancel (it is not once dropped), the icon goes. ended tells core about an
426// end it did not cause.
427func (s *Server) dndEnd(d *dndState, cancel, ended bool) {
428 s.dndLeave(d)
429 if cancel && d.src != nil && d.src.alive() {
430 d.src.cancel()
431 }
432 if s.dnd == d {
433 s.dnd = nil
434 }
435 if d.icon != nil {
436 d.icon.role = nil
437 d.icon = nil
438 }
439 if d.iconID != 0 {
440 s.emitContent(ports.SurfaceContent{ID: d.iconID}, damage{full: true})
441 d.iconID = 0
442 }
443 if ended {
444 s.log.Info().Msg("data drag ended")
445 s.emit(ports.DataDragEnded{})
446 }
447}
448
449// dndSourceGone ends the drags of a destroyed source.
450func (s *Server) dndSourceGone(src *clipSource) {
451 if d := s.dnd; d != nil && d.src == src {
452 s.dndEnd(d, false, true)
453 }
454 for _, t := range slices.Clone(s.dndDrops) {
455 if t.d.src == src {
456 s.dndDropDone(t)
457 }
458 }
459}
460
461// dndDropDone forgets a dropped target.
462func (s *Server) dndDropDone(t *dndTarget) {
463 t.state = targetDone
464 s.dndDrops = slices.DeleteFunc(s.dndDrops, func(x *dndTarget) bool { return x == t })
465}
466
467// dndSurfaceGone clears the drag target when its surface is destroyed.
468func (s *Server) dndSurfaceGone(surf *surface) {
469 if d := s.dnd; d != nil && d.target != nil && surf.wl != nil && d.target.surface.Resource == surf.wl.Resource {
470 s.dndLeave(d)
471 }
472}
473
474// dndCancelAll cancels the drag and the drops waiting for their finish: the
475// session is protected.
476func (s *Server) dndCancelAll() {
477 if d := s.dnd; d != nil {
478 s.dndEnd(d, true, false)
479 }
480 for _, t := range slices.Clone(s.dndDrops) {
481 if src := t.d.src; src != nil && src.alive() {
482 src.cancel()
483 }
484 s.dndDropDone(t)
485 }
486}
487
488// dndOfferGone runs when an offer of a drag is destroyed. With none left on
489// a live target, nothing is accepted any more. With none left on a dropped
490// one the transfer is over: a pre-v3 target has finished, any other
491// cancelled it.
492func (s *Server) dndOfferGone(o *dndOffer) {
493 t := o.t
494 t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o })
495 if t.state == targetActive && t.acceptor == o {
496 t.acceptor = nil
497 s.dndResetSource(t)
498 for _, offer := range t.offers {
499 if offer.sent != 0 && offer.res.Version() >= 3 {
500 offer.sent = 0
501 offer.res.SendAction(0)
502 }
503 }
504 }
505 if len(t.offers) != 0 {
506 return
507 }
508 switch t.state {
509 case targetActive:
510 s.dndResetSource(t)
511 case targetDropped:
512 if src := t.d.src; src != nil && src.alive() {
513 if !t.legacy {
514 src.cancel()
515 } else if src.res.Version() >= 3 {
516 src.res.SendDndFinished()
517 }
518 }
519 s.dndDropDone(t)
520 }
521}
522
523// update recomputes the action after the target or the source changed
524// something, and tells both when it changed. After an ask drop the choice is
525// silent until finish.
526func (o *dndOffer) update() {
527 t, src := o.t, o.t.d.src
528 if t.state == targetDone || t.state == targetDropped && !t.ask || src == nil || !src.alive() {
529 return
530 }
531 action := chooseAction(o.actions&src.srcActions(), o.preferred)
532 if action != t.action {
533 t.action = action
534 if t.state == targetActive && src.res.Version() >= 3 {
535 src.res.SendAction(action)
536 }
537 }
538 if t.state == targetActive {
539 for _, offer := range t.offers {
540 if action != offer.sent {
541 offer.sent = action
542 if offer.res.Version() >= 3 {
543 offer.res.SendAction(action)
544 }
545 }
546 }
547 }
548}
549
550func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) {
551 t := o.t
552 if t.state != targetActive && !(t.state == targetDropped && t.ask) || !t.d.src.alive() {
553 return
554 }
555 t.accepted = mime
556 t.acceptor = o
557 t.d.src.res.SendTarget(mime)
558}
559
560func (o *dndOffer) Receive(_ *wayland.DataOffer, mime string, fd int) {
561 defer unix.Close(fd)
562 src := o.t.d.src
563 if o.server.protected() || o.t.state == targetDone || !src.alive() {
564 return
565 }
566 if slices.Contains(src.mimes, mime) {
567 src.send(mime, fd)
568 }
569}
570
571func (*dndOffer) Destroy(*wayland.DataOffer) {}
572
573func (o *dndOffer) Finish(r *wayland.DataOffer) {
574 t := o.t
575 switch t.state {
576 case targetDone:
577 return
578 case targetActive:
579 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop")
580 return
581 }
582 if t.action == 0 || t.action == actionAsk {
583 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action")
584 return
585 }
586 if t.accepted == "" {
587 r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without an accepted MIME type")
588 return
589 }
590 o.server.dndDropDone(t)
591 if src := t.d.src; src.alive() && src.res.Version() >= 3 {
592 if t.ask {
593 src.res.SendAction(t.action)
594 }
595 src.res.SendDndFinished()
596 }
597}
598
599func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) {
600 if actions&^dndAllActions != 0 {
601 r.PostError(uint32(wayland.DataOfferErrorInvalidActionMask), "invalid action mask")
602 return
603 }
604 if preferred != 0 && (preferred&(preferred-1) != 0 || preferred&actions == 0) {
605 r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action")
606 return
607 }
608 if t := o.t; t.state == targetDropped && t.ask && preferred != 0 && t.d.src != nil && preferred&t.d.src.srcActions() == 0 {
609 r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "final action not allowed by the source")
610 return
611 }
612 o.actions, o.preferred = actions, preferred
613 o.update()
614}