v0.3.0 cf370a0

layer.go RawHistoryFolder

1package wayland
2
3import (
4	"sort"
5
6	"github.com/bnema/go-wayland-bindings/server/wayland"
7	"github.com/bnema/go-wayland-bindings/server/wlrlayershell"
8	"github.com/bnema/go-wayland-bindings/server/xdgshell"
9	"github.com/bnema/neferwl/internal/ports"
10	"github.com/bnema/purego-libwayland/server"
11)
12
13type layerShell struct{ server *Server }
14type layerState struct {
15	width, height, anchor uint32
16	zone                  int32
17	margin                [4]int32
18	keyboard              uint32
19	layer                 ports.Layer
20}
21type layerSurface struct {
22	resource                  *wlrlayershell.ZwlrLayerSurfaceV1
23	shell                     layerShell
24	surface                   *surface
25	id                        ports.WindowID
26	namespace                 string
27	pending, current          layerState
28	configured, acked, mapped bool
29	serials                   []uint32
30	// output is the output the client asked for, else the focused one at
31	// creation. The surface is closed when it is unplugged.
32	output *output
33	closed bool // output gone: commits are ignored until destroy
34	// capture is the exclusion this surface is attached to.
35	capture    *exclusion
36	attachment *layerAttachment
37}
38
39func registerLayer(d *server.Display, s *Server) error {
40	return wlrlayershell.NewZwlrLayerShellV1Global(d, 4, func(c server.Client, v, id uint32) {
41		_, _ = wlrlayershell.NewZwlrLayerShellV1(c, int32(v), id, layerShell{s})
42	})
43}
44func (h layerShell) Destroy(*wlrlayershell.ZwlrLayerShellV1) {}
45func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32, w *wayland.Surface, wl *wayland.Output, layer uint32, namespace string) {
46	if w == nil || h.server.surfaces[w.Resource] == nil {
47		return
48	}
49	state := h.server.surfaces[w.Resource]
50	if state.kind != roleNone || (state.next.attached && state.next.buffer != nil) || state.current != nil {
51		r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")
52		return
53	}
54	if layer > 3 {
55		r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorInvalidLayer), "invalid layer")
56		return
57	}
58	out := h.server.outputOf(wl)
59	gone := wl != nil && out == nil // its output was unplugged
60	if wl == nil {
61		out = h.server.outputByName("")
62	}
63	l := &layerSurface{shell: h, surface: state, id: h.server.nextWindow, namespace: namespace, output: out}
64	l.pending.layer = ports.Layer(layer)
65	resource, err := wlrlayershell.NewZwlrLayerSurfaceV1(r.Client(), r.Version(), id, l)
66	if err != nil {
67		return
68	}
69	l.resource = resource
70	h.server.nextWindow++
71	state.kind = roleLayer
72	state.layer = l
73	state.role = l.commit
74	h.server.layers[l.id] = l
75	if gone {
76		l.close()
77	} else if out != nil {
78		state.sendTreeScale()
79	}
80	resource.OnDestroy = func() {
81		l.unmap()
82		l.captureDetach()
83		delete(h.server.layers, l.id)
84		state.layer = nil
85		state.role = nil
86		state.dropQueue()
87		state.current, state.next.buffer = nil, nil
88		state.next.attached = false
89	}
90}
91func (l *layerSurface) SetSize(_ *wlrlayershell.ZwlrLayerSurfaceV1, w, h uint32) {
92	l.pending.width, l.pending.height = w, h
93}
94func (l *layerSurface) SetAnchor(r *wlrlayershell.ZwlrLayerSurfaceV1, a uint32) {
95	if a > 15 {
96		r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidAnchor), "invalid anchor")
97		return
98	}
99	l.pending.anchor = a
100}
101func (l *layerSurface) SetExclusiveZone(_ *wlrlayershell.ZwlrLayerSurfaceV1, z int32) {
102	l.pending.zone = z
103}
104func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, left int32) {
105	l.pending.margin = [4]int32{t, r, b, left}
106}
107
108func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) {
109	if k > 2 {
110		r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity")
111		return
112	}
113	if l.capture != nil {
114		// A capture HUD never takes the keyboard, whatever it asks for.
115		k = 0
116	}
117	l.pending.keyboard = k
118}
119
120// GetPopup makes the layer surface the parent of an xdg_popup created
121// without one. Core places it relative to the layer.
122func (l *layerSurface) GetPopup(_ *wlrlayershell.ZwlrLayerSurfaceV1, r *xdgshell.Popup) {
123	if r == nil {
124		return
125	}
126	for _, w := range l.shell.server.windows {
127		if p := w.popup; p != nil && p.resource.Resource == r.Resource {
128			if p.parent == nil && !p.sent && !p.done {
129				p.layer = l
130			}
131			return
132		}
133	}
134}
135
136// surfaceResource is the layer's live wl_surface, if any.
137func (l *layerSurface) surfaceResource() *wayland.Surface {
138	for resource, state := range l.shell.server.surfaces {
139		if state == l.surface && resource.Alive() {
140			return wayland.WrapSurface(resource)
141		}
142	}
143	return nil
144}
145func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) {
146	for i, v := range l.serials {
147		if v == serial {
148			l.serials = l.serials[i+1:]
149			l.acked = true
150			return
151		}
152	}
153	r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "unknown configure serial")
154}
155func (*layerSurface) Destroy(*wlrlayershell.ZwlrLayerSurfaceV1) {}
156func (l *layerSurface) SetLayer(r *wlrlayershell.ZwlrLayerSurfaceV1, v uint32) {
157	if v > 3 {
158		r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "invalid layer")
159		return
160	}
161	l.pending.layer = ports.Layer(v)
162}
163
164// Exclusive edge is not implemented; layer-shell is capped at v4.
165func (*layerSurface) SetExclusiveEdge(*wlrlayershell.ZwlrLayerSurfaceV1, uint32) {}
166func (l *layerSurface) unmap() {
167	if !l.mapped {
168		return
169	}
170	if s := l.shell.server; s.seat.focused == l.id {
171		s.changeFocus(0)
172	}
173	if s := l.shell.server; s.seat.pointerFocus == l.id {
174		s.changePointerFocus(0, 0, 0)
175	}
176	l.mapped = false
177	l.surface.resetInputEmission()
178	l.configured = false
179	l.acked = false
180	l.serials = nil
181	l.shell.server.layerChanged()
182	l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}, damage{full: true})
183}
184
185// close tells the client its output is gone; the surface stays unmapped
186// until the client destroys it.
187func (l *layerSurface) close() {
188	if l.closed {
189		return
190	}
191	l.unmap()
192	l.output = nil
193	l.closed = true
194	if l.resource.Resource.Alive() {
195		l.resource.SendClosed()
196	}
197}
198
199func (s *Server) layerChanged() {
200	list := make([]ports.LayerSurface, 0)
201	for _, l := range s.layers {
202		if !l.mapped {
203			continue
204		}
205		v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace, Keyboard: l.current.keyboard}
206		if l.capture != nil {
207			// A capture HUD never takes the keyboard.
208			v.Keyboard = 0
209		}
210		if l.output != nil {
211			v.Output = l.output.name()
212		}
213		if l.surface.current != nil {
214			if b := s.buffers[l.surface.current.Resource]; b != nil {
215				v.Width, v.Height = l.surface.logicalSize(b.size())
216			}
217		}
218		list = append(list, v)
219	}
220	sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID })
221	s.emit(ports.LayerChanged{Layers: list})
222}
223
224// sendConfigure sizes the surface from committed state; zero means "fill",
225// the logical output size.
226func (l *layerSurface) sendConfigure() {
227	w, h := l.current.width, l.current.height
228	var ow, oh int
229	if l.output != nil {
230		ow, oh = l.output.place.Width, l.output.place.Height
231	}
232	if w == 0 {
233		w = uint32(ow)
234	}
235	if h == 0 {
236		h = uint32(oh)
237	}
238	l.shell.server.serial++
239	l.resource.SendConfigure(l.shell.server.serial, w, h)
240	l.serials = append(l.serials, l.shell.server.serial)
241	l.configured = true
242}
243
244func (l *layerSurface) commit(buffer bool) {
245	l.commitState(l.pending, buffer)
246}
247
248// commitState applies the state captured by this surface commit, not later
249// layer-shell requests still pending on the role object.
250func (l *layerSurface) commitState(p layerState, buffer bool) {
251	if l.surface.destroyed {
252		l.unmap()
253		l.captureDetach()
254		delete(l.shell.server.layers, l.id)
255		return
256	}
257	if l.closed {
258		return
259	}
260	if l.capture != nil {
261		p.keyboard = 0
262		if p.layer != ports.LayerTop && p.layer != ports.LayerOverlay {
263			// Only top and overlay layers may be attached: closing unmaps the
264			// surface (its LayerChanged is queued first), then it is detached,
265			// so core keeps it excluded until it is gone.
266			l.close()
267			l.captureDetach()
268			return
269		}
270	}
271	if (p.width == 0 && p.anchor&(ports.AnchorLeft|ports.AnchorRight) != ports.AnchorLeft|ports.AnchorRight) || (p.height == 0 && p.anchor&(ports.AnchorTop|ports.AnchorBottom) != ports.AnchorTop|ports.AnchorBottom) {
272		l.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSize), "zero size requires opposing anchors")
273		return
274	}
275	changed := p != l.current
276	sizeChanged := p.width != l.current.width || p.height != l.current.height
277	l.current = p
278	if !l.configured || sizeChanged {
279		l.sendConfigure()
280	}
281	if !buffer {
282		if l.mapped {
283			l.unmap()
284			l.configured = false
285			l.acked = false
286			l.serials = nil
287		}
288		return
289	}
290	if l.acked {
291		wasMapped := l.mapped
292		l.mapped = true
293		if changed || !wasMapped {
294			l.shell.server.layerChanged()
295		}
296	}
297}

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