feat/data-dnd e05cb15

xdg.go RawHistoryFolder

1package wayland
2
3import (
4	"encoding/binary"
5
6	"github.com/bnema/go-wayland-bindings/server/wayland"
7	"github.com/bnema/go-wayland-bindings/server/xdgshell"
8	"github.com/bnema/neferwl/internal/ports"
9	"github.com/bnema/purego-libwayland/server"
10)
11
12func registerXDG(d *server.Display, s *Server) error {
13	return xdgshell.NewWmBaseGlobal(d, 6, func(c server.Client, v, id uint32) { xdgshell.NewWmBase(c, int32(v), id, wm{s}) })
14}
15
16type wm struct{ server *Server }
17
18func (wm) Destroy(*xdgshell.WmBase)      {}
19func (wm) Pong(*xdgshell.WmBase, uint32) {}
20func (m wm) CreatePositioner(r *xdgshell.WmBase, id uint32) {
21	p := &positioner{}
22	if res, err := xdgshell.NewPositioner(r.Client(), r.Version(), id, p); err == nil {
23		m.server.positioners[res.Resource] = p
24		res.OnDestroy = func() { delete(m.server.positioners, res.Resource) }
25	}
26}
27func (m wm) GetXdgSurface(r *xdgshell.WmBase, id uint32, w *wayland.Surface) {
28	if w == nil {
29		return
30	}
31	state := m.server.surfaces[w.Resource]
32	if state == nil {
33		return
34	}
35	if state.kind != roleNone && state.kind != roleXDG || state.xdg != nil {
36		r.PostError(uint32(xdgshell.WmBaseErrorRole), "surface already has role")
37		return
38	}
39	x := &xdgSurface{wm: r, server: m.server, surface: state}
40	resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x)
41	if err == nil {
42		x.resource = resource
43		state.kind = roleXDG
44		state.xdg = x
45		resource.OnDestroy = func() { state.xdg = nil; state.role = nil }
46	}
47
48}
49
50// positioner is an xdg_positioner: popups copy it when they use it.
51type positioner struct {
52	p                  ports.Positioner
53	sizeSet, anchorSet bool
54}
55
56func (*positioner) Destroy(*xdgshell.Positioner) {}
57func (p *positioner) SetSize(r *xdgshell.Positioner, w, h int32) {
58	if w <= 0 || h <= 0 {
59		r.PostError(uint32(xdgshell.PositionerErrorInvalidInput), "size must be positive")
60		return
61	}
62	p.p.Width, p.p.Height, p.sizeSet = int(w), int(h), true
63}
64func (p *positioner) SetAnchorRect(r *xdgshell.Positioner, x, y, w, h int32) {
65	if w < 0 || h < 0 {
66		r.PostError(uint32(xdgshell.PositionerErrorInvalidInput), "anchor rect size must not be negative")
67		return
68	}
69	p.p.AnchorRect, p.anchorSet = ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}, true
70}
71func (p *positioner) SetAnchor(r *xdgshell.Positioner, v uint32) {
72	if v > ports.EdgeBottomRight {
73		r.PostError(uint32(xdgshell.PositionerErrorInvalidInput), "invalid anchor")
74		return
75	}
76	p.p.Anchor = v
77}
78func (p *positioner) SetGravity(r *xdgshell.Positioner, v uint32) {
79	if v > ports.EdgeBottomRight {
80		r.PostError(uint32(xdgshell.PositionerErrorInvalidInput), "invalid gravity")
81		return
82	}
83	p.p.Gravity = v
84}
85func (p *positioner) SetConstraintAdjustment(_ *xdgshell.Positioner, v uint32) { p.p.Adjust = v }
86func (p *positioner) SetOffset(_ *xdgshell.Positioner, x, y int32) {
87	p.p.OffsetX, p.p.OffsetY = int(x), int(y)
88}
89
90// Reactive popups, parent size and parent configure hints are not used:
91// core places popups when they are created or repositioned.
92func (*positioner) SetReactive(*xdgshell.Positioner)                 {}
93func (*positioner) SetParentSize(*xdgshell.Positioner, int32, int32) {}
94func (*positioner) SetParentConfigure(*xdgshell.Positioner, uint32)  {}
95
96// positionerOf returns a complete positioner, or posts invalid_positioner.
97func (s *Server) positionerOf(wm *xdgshell.WmBase, p *xdgshell.Positioner) (ports.Positioner, bool) {
98	if p != nil && p.Resource != nil {
99		if state := s.positioners[p.Resource]; state != nil && state.sizeSet && state.anchorSet {
100			return state.p, true
101		}
102	}
103	if wm != nil {
104		wm.PostError(uint32(xdgshell.WmBaseErrorInvalidPositioner), "incomplete positioner")
105	}
106	return ports.Positioner{}, false
107}
108
109type xdgSurface struct {
110	wm         *xdgshell.WmBase
111	resource   *xdgshell.Surface
112	server     *Server
113	surface    *surface
114	configured bool
115	acked      bool
116	window     *window
117	serials    []uint32
118	// geometry is the committed window geometry: the visible window inside
119	// client shadows. Empty means the whole surface.
120	geometry, pendingGeometry ports.Rect
121}
122
123func (x *xdgSurface) Destroy(r *xdgshell.Surface) {
124	if x.window != nil {
125		r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "role object still exists")
126	}
127}
128
129// window is an xdg_toplevel, or an xdg_popup when popup is set.
130type window struct {
131	id           ports.WindowID
132	toplevel     *xdgshell.Toplevel
133	popup        *popup
134	xdg          *xdgSurface
135	appID, title string
136	mapped       bool
137	// gen counts the mappings: an unmap and a remap is a new toplevel to
138	// ext-foreign-toplevel-list (identifier) and to its capture sources.
139	gen uint32
140	// parent is set_parent (dialogs); min and max are the size hints.
141	parent *window
142	// dialog is set while an xdg_dialog_v1 object exists for the
143	// toplevel; modal is its state.
144	dialog, modal  bool
145	minW, minH     int32
146	maxW, maxH     int32
147	floating       bool
148	floatW, floatH int // size last sent to core
149	// last is the most recent core configure, resent when decoration mode changes.
150	last    ports.ConfigureWindow
151	hasLast bool
152}
153
154// sendConfigure sends w.last as xdg_toplevel + xdg_surface configure.
155func (w *window) sendConfigure() {
156	c := w.last
157	w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version()))
158	s := w.xdg.server
159	s.serial++
160	w.xdg.resource.SendConfigure(s.serial)
161	w.xdg.serials = append(w.xdg.serials, s.serial)
162}
163
164// toplevelStates encodes the xdg_toplevel states of configure c.
165func toplevelStates(c ports.ConfigureWindow, version int32) []byte {
166	var states []byte
167	add := func(st xdgshell.ToplevelState) {
168		states = binary.LittleEndian.AppendUint32(states, uint32(st))
169	}
170	if c.Fullscreen {
171		add(xdgshell.ToplevelStateFullscreen)
172	}
173	if c.Activated {
174		add(xdgshell.ToplevelStateActivated)
175	}
176	if !c.Visible && !c.Captured && version >= 6 {
177		add(xdgshell.ToplevelStateSuspended)
178	}
179	if c.Fullscreen || c.Floating {
180		return states
181	}
182	// Tiled windows must use exactly this size. Maximized is the strict
183	// size state every client honours: Wine ignores a size change that only
184	// carries tiled states on a window it considers monitor-sized.
185	add(xdgshell.ToplevelStateMaximized)
186	if version >= 2 {
187		for _, st := range []xdgshell.ToplevelState{xdgshell.ToplevelStateTiledLeft, xdgshell.ToplevelStateTiledRight, xdgshell.ToplevelStateTiledTop, xdgshell.ToplevelStateTiledBottom} {
188			add(st)
189		}
190	}
191	return states
192}
193
194func (w *window) unmap() {
195	if w.xdg.server.seat.pointerFocus == w.id {
196		w.xdg.server.changePointerFocus(0, 0, 0)
197	}
198	if w.xdg.server.seat.focused == w.id {
199		w.xdg.server.changeFocus(0)
200	}
201	if !w.mapped {
202		return
203	}
204	w.mapped = false
205	w.xdg.server.toplevelClosed(w.id)
206	w.xdg.surface.resetInputEmission()
207	w.xdg.configured = false
208	w.xdg.acked = false
209	w.xdg.serials = nil
210	w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
211	w.xdg.server.syncInhibitors()
212	w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true})
213}
214func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
215	if x.window != nil {
216		r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed")
217		return
218	}
219	w := &window{id: x.server.nextWindow, xdg: x}
220	t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{w})
221	if err != nil {
222		return
223	}
224	w.toplevel = t
225	x.window = w
226	x.server.nextWindow++
227	x.server.windows[w.id] = w
228	x.surface.role = func(buffer bool) {
229		if x.surface.destroyed {
230			w.unmap()
231			return
232		}
233		if !buffer && !w.mapped && !x.configured {
234			x.configured = true
235			x.server.serial++
236			t.SendConfigure(0, 0, nil)
237			r.SendConfigure(x.server.serial)
238			x.serials = append(x.serials, x.server.serial)
239		} else if buffer && !w.mapped && x.acked {
240			w.mapped = true
241			w.gen++
242			slot := x.server.slotToken(r.Client())
243			// Its size comes with the commit, in WindowResized (afterCommit).
244			w.floating = w.floats()
245			var parent ports.WindowID
246			if w.parent != nil && w.parent.mapped {
247				parent = w.parent.id
248			}
249			x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, Slot: slot, PID: r.Client().PID(), Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Modal: w.modal, Remap: w.gen > 1})
250			x.server.syncInhibitors()
251			x.server.toplevelChanged(w)
252			x.server.castInventoryChanged()
253			x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped")
254		} else if !buffer && w.mapped {
255			w.unmap()
256		}
257	}
258	t.OnDestroy = func() {
259		w.unmap()
260		x.server.foreignToplevelGone(w)
261		x.surface.dropQueue()
262		x.surface.current, x.surface.next.buffer = nil, nil
263		x.surface.next.attached = false
264		delete(x.server.windows, w.id)
265		x.window = nil
266		x.surface.role = nil
267		x.configured = false
268		x.acked = false
269		x.serials = nil
270		x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{}
271	}
272}
273func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, parent *xdgshell.Surface, pos *xdgshell.Positioner) {
274	if x.window != nil {
275		r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed")
276		return
277	}
278	x.server.newPopup(x, r, id, parent, pos)
279}
280
281// SetWindowGeometry sets the visible window inside the surface; it
282// applies on the next commit.
283func (x *xdgSurface) SetWindowGeometry(r *xdgshell.Surface, gx, gy, gw, gh int32) {
284	if gw <= 0 || gh <= 0 {
285		r.PostError(uint32(xdgshell.SurfaceErrorInvalidSize), "invalid window geometry")
286		return
287	}
288	x.pendingGeometry = ports.Rect{X: int(gx), Y: int(gy), W: int(gw), H: int(gh)}
289	id := uint64(0)
290	if x.window != nil {
291		id = uint64(x.window.id)
292	}
293	x.server.log.Debug().Uint64("id", id).Int32("x", gx).Int32("y", gy).Int32("w", gw).Int32("h", gh).Msg("window geometry")
294}
295func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {
296	for i, issued := range x.serials {
297		if issued == serial {
298			x.serials = x.serials[i+1:]
299			x.acked = true
300			return
301		}
302	}
303	r.PostError(uint32(xdgshell.SurfaceErrorInvalidSerial), "unknown configure serial")
304}
305
306type top struct{ w *window }
307
308func (top) Destroy(*xdgshell.Toplevel) {}
309func (t top) SetParent(_ *xdgshell.Toplevel, parent *xdgshell.Toplevel) {
310	var p *window
311	if parent != nil {
312		for _, w := range t.w.xdg.server.windows {
313			if w.toplevel != nil && w.toplevel.Resource == parent.Resource && w != t.w {
314				p = w
315			}
316		}
317	}
318	t.w.setParent(p)
319}
320
321// setParent changes the dialog parent; core follows it once mapped (the
322// map carries the parent of the first frame).
323func (w *window) setParent(p *window) {
324	if w.parent == p {
325		return
326	}
327	w.parent = p
328	if !w.mapped {
329		return
330	}
331	var id ports.WindowID
332	if p != nil && p.mapped {
333		id = p.id
334	}
335	w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id})
336}
337func (t top) SetTitle(_ *xdgshell.Toplevel, title string) {
338	if title == t.w.title {
339		return
340	}
341	t.w.title = title
342	t.w.xdg.server.toplevelChanged(t.w)
343	if t.w.mapped {
344		t.w.xdg.server.castInventoryChanged()
345	}
346}
347func (t top) SetAppId(_ *xdgshell.Toplevel, appID string) {
348	if appID == t.w.appID {
349		return
350	}
351	t.w.appID = appID
352	if t.w.mapped {
353		t.w.xdg.server.emit(ports.WindowAppID{ID: t.w.id, AppID: appID})
354		t.w.xdg.server.toplevelChanged(t.w)
355		t.w.xdg.server.castInventoryChanged()
356	}
357}
358func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {}
359
360// Move and Resize start a pointer drag in core when they answer the
361// client's current press; stale serials are ignored.
362func (t top) Move(r *xdgshell.Toplevel, _ *wayland.Seat, serial uint32) {
363	t.moveRequest(r, serial, false, 0)
364}
365func (t top) Resize(r *xdgshell.Toplevel, _ *wayland.Seat, serial, edges uint32) {
366	if edges > uint32(xdgshell.ToplevelResizeEdgeBottomRight) || edges&3 == 3 || edges&12 == 12 {
367		r.PostError(uint32(xdgshell.ToplevelErrorInvalidResizeEdge), "invalid resize edge")
368		return
369	}
370	t.moveRequest(r, serial, true, ports.ResizeEdges(edges))
371}
372func (t top) moveRequest(r *xdgshell.Toplevel, serial uint32, resize bool, edges ports.ResizeEdges) {
373	s := t.w.xdg.server
374	if !t.w.mapped || serial != s.seat.press || s.seat.pressClient != r.Client() {
375		return
376	}
377	s.emit(ports.WindowMoveRequest{ID: t.w.id, Resize: resize, Edges: edges})
378}
379func (t top) SetMaxSize(r *xdgshell.Toplevel, w, h int32) {
380	if w < 0 || h < 0 {
381		r.PostError(uint32(xdgshell.ToplevelErrorInvalidSize), "negative max size")
382		return
383	}
384	t.w.maxW, t.w.maxH = w, h
385}
386func (t top) SetMinSize(r *xdgshell.Toplevel, w, h int32) {
387	if w < 0 || h < 0 {
388		r.PostError(uint32(xdgshell.ToplevelErrorInvalidSize), "negative min size")
389		return
390	}
391	t.w.minW, t.w.minH = w, h
392}
393func (top) SetMaximized(*xdgshell.Toplevel)   {}
394func (top) UnsetMaximized(*xdgshell.Toplevel) {}
395func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) {
396	if t.w.mapped {
397		t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true})
398	}
399}
400func (t top) UnsetFullscreen(*xdgshell.Toplevel) {
401	if t.w.mapped {
402		t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id})
403	}
404}
405func (top) SetMinimized(*xdgshell.Toplevel) {}
406
407// afterCommit runs once the commit applied content and geometry: a
408// floating window keeps the size it draws.
409func (w *window) afterCommit() {
410	if !w.mapped || !w.floating {
411		return
412	}
413	if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) {
414		w.floatW, w.floatH = fw, fh
415		w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh})
416	}
417}
418
419// floats reports whether the toplevel should float over the columns, as
420// on niri: dialogs (a parent) and fixed-size windows such as splash
421// screens.
422func (w *window) floats() bool {
423	fixed := w.minW > 0 && w.minH > 0 && w.minW == w.maxW && w.minH == w.maxH
424	return w.parent != nil || fixed
425}
426
427// size is the window geometry size, else the surface size, in logical pixels.
428func (w *window) size() (int, int) {
429	if g := w.xdg.geometry; g.W > 0 && g.H > 0 {
430		return g.W, g.H
431	}
432	c := w.xdg.surface.content
433	return c.LogicalW, c.LogicalH
434}
435
436// surfacePoint turns window coordinates (core's, from the geometry origin)
437// into surface coordinates.
438func (w *window) surfacePoint(x, y float64) (float64, float64) {
439	g := w.xdg.geometry
440	return x + float64(g.X), y + float64(g.Y)
441}
442
443func (x *xdgSurface) surfaceResource() *wayland.Surface {
444	for resource, state := range x.server.surfaces {
445		if state == x.surface && resource.Alive() {
446			return wayland.WrapSurface(resource)
447		}
448	}
449	return nil
450}

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