main f2d166b

datadrag.go RawHistoryFolder

1package core
2
3import (
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.
31type 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.
43type 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.
51func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error {
52	button := v.Button
53	if c.dataDrag != nil || c.pointerBusy() || 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.
83func (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// dataDragHitChanged excludes notifications that only change metadata,
98// capture state or the drag icon. The type switch allocates nothing.
99func dataDragHitChanged(ev ports.ClientEvent) bool {
100	switch ev.(type) {
101	case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped,
102		ports.WindowResized, ports.PopupRequest, ports.PopupMapped,
103		ports.OutputPower, ports.WindowParent, ports.WindowModal,
104		ports.WindowUnmapped, ports.PointerWarp, ports.WindowMoveRequest,
105		ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate:
106		return true
107	default:
108		return false
109	}
110}
111
112// dataDragRetarget re-routes a still cursor: the target is told only when
113// it or the point in it changed.
114func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error {
115	d := c.dataDrag
116	if id, x, y := c.hit(c.cursorX, c.cursorY); id == d.target && (id == 0 || d.at == [2]float64{x, y}) {
117		return nil
118	}
119	return c.dataDragRoute(ctx, at)
120}
121
122// dataDragMotion follows the cursor: the target gets the motion and the
123// icon moves with it.
124func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error {
125	if err := c.dataDragRoute(ctx, at); err != nil {
126		return err
127	}
128	if c.dataDrag.icon.visible() {
129		return c.publish(ctx)
130	}
131	return nil
132}
133
134// dataDragButton handles a button event of a data drag: the release of
135// the drag button drops; the other buttons do nothing, their releases
136// neither.
137func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error {
138	if !v.Pressed {
139		if v.Button != c.dataDrag.button {
140			return nil
141		}
142		delete(c.buttons, v.Button)
143		// A bind may have moved the layout under a still cursor: the
144		// drop goes to what lies there now.
145		if err := c.dataDragRetarget(ctx, v.Time); err != nil {
146			return err
147		}
148		c.dataDrag = nil
149		if err := c.command(ctx, ports.DataDragDrop{}); err != nil {
150			return err
151		}
152		return c.finishDataDrag(ctx)
153	}
154	c.buttons[v.Button] = true
155	c.swallow[v.Button] = true
156	return nil
157}
158
159// cancelDataDrag ends the drag on Escape.
160func (c *Core) cancelDataDrag(ctx context.Context) error {
161	c.abortDataDrag()
162	if err := c.command(ctx, ports.DataDragCancel{}); err != nil {
163		return err
164	}
165	return c.finishDataDrag(ctx)
166}
167
168// abortDataDrag ends the drag where it is; the held button's release is
169// dropped.
170func (c *Core) abortDataDrag() {
171	if d := c.dataDrag; d != nil {
172		c.swallow[d.button] = true
173		c.dataDrag = nil
174	}
175}
176
177// finishDataDrag redraws without the icon and points the pointer at what
178// lies under the cursor.
179func (c *Core) finishDataDrag(ctx context.Context) error {
180	if err := c.publish(ctx); err != nil {
181		return err
182	}
183	return c.rehit(ctx)
184}
185
186// dataDragGone drops the target that was unmapped: it gets its leave.
187func (c *Core) dataDragGone(ctx context.Context, id WindowID) error {
188	if d := c.dataDrag; d != nil && d.target == id {
189		d.target = 0
190		return c.command(ctx, ports.DataDragFocus{Time: c.motionTime})
191	}
192	return nil
193}
194
195// setDataDragIcon records the icon geometry wayland measured. It reports
196// whether the scene changes.
197func (c *Core) setDataDragIcon(v ports.DataDragIcon) bool {
198	d := c.dataDrag
199	if d == nil || d.icon.id == 0 || v.ID != d.icon.id {
200		return false
201	}
202	icon := dataDragIcon{id: v.ID, x: v.X, y: v.Y, w: max(v.W, 0), h: max(v.H, 0)}
203	if icon == d.icon {
204		return false
205	}
206	d.icon = icon
207	return true
208}
209
210func (i dataDragIcon) visible() bool { return i.id != 0 && i.w > 0 && i.h > 0 }
211
212// dataDragIconOn is the icon as a window of screen sc (index i) when the
213// cursor is on it.
214func (c *Core) dataDragIconOn(i int, sc *screen) (ports.SceneWindow, bool) {
215	d := c.dataDrag
216	if d == nil || !d.icon.visible() || c.screenAt(c.cursorX, c.cursorY) != i {
217		return ports.SceneWindow{}, false
218	}
219	x, y := int(c.cursorX)-sc.x+d.icon.x, int(c.cursorY)-sc.y+d.icon.y
220	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
221}

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