v0.2.0 13c6a90

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	// parent is set_parent (dialogs); min and max are the size hints.
138	parent         *window
139	minW, minH     int32
140	maxW, maxH     int32
141	floating       bool
142	floatW, floatH int // size last sent to core
143	// last is the most recent core configure, resent when decoration mode changes.
144	last    ports.ConfigureWindow
145	hasLast bool
146}
147
148// sendConfigure sends w.last as xdg_toplevel + xdg_surface configure.
149func (w *window) sendConfigure() {
150	c := w.last
151	w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version()))
152	s := w.xdg.server
153	s.serial++
154	w.xdg.resource.SendConfigure(s.serial)
155	w.xdg.serials = append(w.xdg.serials, s.serial)
156}
157
158// toplevelStates encodes the xdg_toplevel states of configure c.
159func toplevelStates(c ports.ConfigureWindow, version int32) []byte {
160	var states []byte
161	add := func(st xdgshell.ToplevelState) {
162		states = binary.LittleEndian.AppendUint32(states, uint32(st))
163	}
164	if c.Fullscreen {
165		add(xdgshell.ToplevelStateFullscreen)
166	}
167	if c.Activated {
168		add(xdgshell.ToplevelStateActivated)
169	}
170	if !c.Visible && !c.Captured && version >= 6 {
171		add(xdgshell.ToplevelStateSuspended)
172	}
173	if c.Fullscreen || c.Floating {
174		return states
175	}
176	// Tiled windows must use exactly this size. Maximized is the strict
177	// size state every client honours: Wine ignores a size change that only
178	// carries tiled states on a window it considers monitor-sized.
179	add(xdgshell.ToplevelStateMaximized)
180	if version >= 2 {
181		for _, st := range []xdgshell.ToplevelState{xdgshell.ToplevelStateTiledLeft, xdgshell.ToplevelStateTiledRight, xdgshell.ToplevelStateTiledTop, xdgshell.ToplevelStateTiledBottom} {
182			add(st)
183		}
184	}
185	return states
186}
187
188func (w *window) unmap() {
189	if w.xdg.server.seat.pointerFocus == w.id {
190		w.xdg.server.changePointerFocus(0, 0, 0)
191	}
192	if w.xdg.server.seat.focused == w.id {
193		w.xdg.server.changeFocus(0)
194	}
195	if !w.mapped {
196		return
197	}
198	w.mapped = false
199	w.xdg.server.toplevelClosed(w.id)
200	w.xdg.surface.resetInputEmission()
201	w.xdg.configured = false
202	w.xdg.acked = false
203	w.xdg.serials = nil
204	w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
205	w.xdg.server.syncInhibitors()
206	w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true})
207}
208func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
209	if x.window != nil {
210		r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed")
211		return
212	}
213	w := &window{id: x.server.nextWindow, xdg: x}
214	t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{w})
215	if err != nil {
216		return
217	}
218	w.toplevel = t
219	x.window = w
220	x.server.nextWindow++
221	x.server.windows[w.id] = w
222	x.surface.role = func(buffer bool) {
223		if x.surface.destroyed {
224			w.unmap()
225			return
226		}
227		if !buffer && !w.mapped && !x.configured {
228			x.configured = true
229			x.server.serial++
230			t.SendConfigure(0, 0, nil)
231			r.SendConfigure(x.server.serial)
232			x.serials = append(x.serials, x.server.serial)
233		} else if buffer && !w.mapped && x.acked {
234			w.mapped = true
235			slot := x.server.slotToken(r.Client())
236			// Its size comes with the commit, in WindowResized (afterCommit).
237			w.floating = w.floats()
238			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})
239			x.server.syncInhibitors()
240			x.server.toplevelChanged(w)
241			x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped")
242		} else if !buffer && w.mapped {
243			w.unmap()
244		}
245	}
246	t.OnDestroy = func() {
247		w.unmap()
248		x.surface.dropQueue()
249		x.surface.current, x.surface.next.buffer = nil, nil
250		x.surface.next.attached = false
251		delete(x.server.windows, w.id)
252		x.window = nil
253		x.surface.role = nil
254		x.configured = false
255		x.acked = false
256		x.serials = nil
257		x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{}
258	}
259}
260func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, parent *xdgshell.Surface, pos *xdgshell.Positioner) {
261	if x.window != nil {
262		r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed")
263		return
264	}
265	x.server.newPopup(x, r, id, parent, pos)
266}
267
268// SetWindowGeometry sets the visible window inside the surface; it
269// applies on the next commit.
270func (x *xdgSurface) SetWindowGeometry(r *xdgshell.Surface, gx, gy, gw, gh int32) {
271	if gw <= 0 || gh <= 0 {
272		r.PostError(uint32(xdgshell.SurfaceErrorInvalidSize), "invalid window geometry")
273		return
274	}
275	x.pendingGeometry = ports.Rect{X: int(gx), Y: int(gy), W: int(gw), H: int(gh)}
276	id := uint64(0)
277	if x.window != nil {
278		id = uint64(x.window.id)
279	}
280	x.server.log.Debug().Uint64("id", id).Int32("x", gx).Int32("y", gy).Int32("w", gw).Int32("h", gh).Msg("window geometry")
281}
282func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {
283	for i, issued := range x.serials {
284		if issued == serial {
285			x.serials = x.serials[i+1:]
286			x.acked = true
287			return
288		}
289	}
290	r.PostError(uint32(xdgshell.SurfaceErrorInvalidSerial), "unknown configure serial")
291}
292
293type top struct{ w *window }
294
295func (top) Destroy(*xdgshell.Toplevel) {}
296func (t top) SetParent(_ *xdgshell.Toplevel, parent *xdgshell.Toplevel) {
297	t.w.parent = nil
298	if parent == nil {
299		return
300	}
301	for _, w := range t.w.xdg.server.windows {
302		if w.toplevel != nil && w.toplevel.Resource == parent.Resource && w != t.w {
303			t.w.parent = w
304		}
305	}
306}
307func (t top) SetTitle(_ *xdgshell.Toplevel, title string) {
308	if title == t.w.title {
309		return
310	}
311	t.w.title = title
312	t.w.xdg.server.toplevelChanged(t.w)
313}
314func (t top) SetAppId(_ *xdgshell.Toplevel, appID string) {
315	if appID == t.w.appID {
316		return
317	}
318	t.w.appID = appID
319	if t.w.mapped {
320		t.w.xdg.server.emit(ports.WindowAppID{ID: t.w.id, AppID: appID})
321		t.w.xdg.server.toplevelChanged(t.w)
322	}
323}
324func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {}
325
326// Move and Resize start a pointer drag in core when they answer the
327// client's current press; stale serials are ignored.
328func (t top) Move(r *xdgshell.Toplevel, _ *wayland.Seat, serial uint32) {
329	t.moveRequest(r, serial, false, 0)
330}
331func (t top) Resize(r *xdgshell.Toplevel, _ *wayland.Seat, serial, edges uint32) {
332	if edges > uint32(xdgshell.ToplevelResizeEdgeBottomRight) || edges&3 == 3 || edges&12 == 12 {
333		r.PostError(uint32(xdgshell.ToplevelErrorInvalidResizeEdge), "invalid resize edge")
334		return
335	}
336	t.moveRequest(r, serial, true, ports.ResizeEdges(edges))
337}
338func (t top) moveRequest(r *xdgshell.Toplevel, serial uint32, resize bool, edges ports.ResizeEdges) {
339	s := t.w.xdg.server
340	if !t.w.mapped || serial != s.seat.press || s.seat.pressClient != r.Client() {
341		return
342	}
343	s.emit(ports.WindowMoveRequest{ID: t.w.id, Resize: resize, Edges: edges})
344}
345func (t top) SetMaxSize(r *xdgshell.Toplevel, w, h int32) {
346	if w < 0 || h < 0 {
347		r.PostError(uint32(xdgshell.ToplevelErrorInvalidSize), "negative max size")
348		return
349	}
350	t.w.maxW, t.w.maxH = w, h
351}
352func (t top) SetMinSize(r *xdgshell.Toplevel, w, h int32) {
353	if w < 0 || h < 0 {
354		r.PostError(uint32(xdgshell.ToplevelErrorInvalidSize), "negative min size")
355		return
356	}
357	t.w.minW, t.w.minH = w, h
358}
359func (top) SetMaximized(*xdgshell.Toplevel)   {}
360func (top) UnsetMaximized(*xdgshell.Toplevel) {}
361func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) {
362	if t.w.mapped {
363		t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true})
364	}
365}
366func (t top) UnsetFullscreen(*xdgshell.Toplevel) {
367	if t.w.mapped {
368		t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id})
369	}
370}
371func (top) SetMinimized(*xdgshell.Toplevel) {}
372
373// afterCommit runs once the commit applied content and geometry: a
374// floating window keeps the size it draws.
375func (w *window) afterCommit() {
376	if !w.mapped || !w.floating {
377		return
378	}
379	if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) {
380		w.floatW, w.floatH = fw, fh
381		w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh})
382	}
383}
384
385// floats reports whether the toplevel should float over the columns, as
386// on niri: dialogs (a parent) and fixed-size windows such as splash
387// screens.
388func (w *window) floats() bool {
389	fixed := w.minW > 0 && w.minH > 0 && w.minW == w.maxW && w.minH == w.maxH
390	return w.parent != nil || fixed
391}
392
393// size is the window geometry size, else the surface size, in logical pixels.
394func (w *window) size() (int, int) {
395	if g := w.xdg.geometry; g.W > 0 && g.H > 0 {
396		return g.W, g.H
397	}
398	c := w.xdg.surface.content
399	return c.LogicalW, c.LogicalH
400}
401
402// surfacePoint turns window coordinates (core's, from the geometry origin)
403// into surface coordinates.
404func (w *window) surfacePoint(x, y float64) (float64, float64) {
405	g := w.xdg.geometry
406	return x + float64(g.X), y + float64(g.Y)
407}
408
409func (x *xdgSurface) surfaceResource() *wayland.Surface {
410	for resource, state := range x.server.surfaces {
411		if state == x.surface && resource.Alive() {
412			return wayland.WrapSurface(resource)
413		}
414	}
415	return nil
416}

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