dnd.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close