feat/hints a471fa4

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; 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}

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