v0.1.0 d9218d6

xdg.go RawHistoryFolder

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

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