v0.3.0 6800c6a

layer.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"errors"
7	"fmt"
8	"math"
9	"slices"
10	"sort"
11	"strings"
12
13	"github.com/bnema/wlturbo"
14	"github.com/bnema/wlturbo/protocol/core"
15	"github.com/bnema/wlturbo/wl"
16
17	"github.com/bnema/nefergui/internal/platform/wayland/layershell"
18)
19
20// zwlr_layer_shell_v1 layers. The values are the wire values.
21const (
22	LayerBackground uint32 = 0
23	LayerBottom     uint32 = 1
24	LayerTop        uint32 = 2
25	LayerOverlay    uint32 = 3
26)
27
28// zwlr_layer_surface_v1 anchor bits. They may be OR-ed together.
29const (
30	AnchorTop    uint32 = 1
31	AnchorBottom uint32 = 2
32	AnchorLeft   uint32 = 4
33	AnchorRight  uint32 = 8
34
35	anchorAll = AnchorTop | AnchorBottom | AnchorLeft | AnchorRight
36)
37
38// zwlr_layer_surface_v1 keyboard interactivity modes.
39const (
40	KeyboardNone      uint32 = 0
41	KeyboardExclusive uint32 = 1
42	// KeyboardOnDemand needs zwlr_layer_shell_v1 version 4.
43	KeyboardOnDemand uint32 = 2
44)
45
46const (
47	layerShellSupported = 4
48	outputNameVersion   = 4 // wl_output.name was added in version 4
49	// MaxLayerDimension bounds a compositor-assigned layer surface extent.
50	MaxLayerDimension = 16384
51	// DefaultLayerNamespace is used when LayerOptions.Namespace is empty. The
52	// namespace is an opaque, generic role hint; nothing privileged depends on it.
53	DefaultLayerNamespace = "nefergui"
54)
55
56// Rect is an integer rectangle in surface-local logical coordinates.
57type Rect struct{ X, Y, W, H int32 }
58
59// LayerOptions selects wlr-layer-shell instead of xdg-toplevel.
60type LayerOptions struct {
61	// Output is the exact wl_output name (for example "DP-1"). Empty lets the
62	// compositor choose its preferred output. A named output that is absent
63	// fails the connection; a named output removed later closes the window.
64	Output string
65	// Namespace is the layer surface namespace; empty selects DefaultLayerNamespace.
66	Namespace string
67	// Anchor is a bit set of Anchor* values.
68	Anchor uint32
69	// Layer is one of the Layer* values.
70	Layer uint32
71	// Keyboard is one of the Keyboard* values.
72	Keyboard uint32
73	// ExclusiveZone follows zwlr_layer_surface_v1.set_exclusive_zone
74	// (positive reserves, 0 is neutral, -1 ignores other zones).
75	ExclusiveZone int32
76	// Margin is ordered top, right, bottom, left, like the protocol request.
77	Margin [4]int32
78}
79
80// SurfaceOptions configures the role and input region of a new Window. The zero
81// value selects the default xdg-toplevel with full-surface input.
82type SurfaceOptions struct {
83	// Layer, when non-nil, creates a layer surface and binds no xdg shell.
84	Layer *LayerOptions
85	// Lock selects a private output lifetime on this Connection's sole lock.
86	// It is incompatible with Layer, transparency and custom input regions.
87	Lock *LockOptions
88	// InputRects is the initial input region in surface-local logical pixels.
89	// nil means the whole surface (the protocol default); a non-nil empty slice
90	// means no input (pointer and touch pass through).
91	InputRects []Rect
92}
93
94// Validate reports the first invalid option.
95func (o SurfaceOptions) Validate() error {
96	if o.Lock != nil && (o.Layer != nil || o.InputRects != nil) {
97		return errors.New("lock role cannot have layer or custom input")
98	}
99	if o.Layer != nil {
100		if err := o.Layer.Validate(); err != nil {
101			return err
102		}
103	}
104	return validateRects(o.InputRects)
105}
106
107// Validate reports the first invalid layer option.
108func (o LayerOptions) Validate() error {
109	switch {
110	case o.Layer > LayerOverlay:
111		return fmt.Errorf("invalid layer %d", o.Layer)
112	case o.Anchor&^anchorAll != 0:
113		return fmt.Errorf("invalid anchor bits %#x", o.Anchor)
114	case o.ExclusiveZone < -1:
115		return fmt.Errorf("invalid exclusive zone %d", o.ExclusiveZone)
116	case o.Keyboard > KeyboardOnDemand:
117		return fmt.Errorf("invalid keyboard interactivity %d", o.Keyboard)
118	case strings.ContainsRune(o.Namespace, 0):
119		return errors.New("layer namespace contains NUL")
120	case strings.ContainsRune(o.Output, 0):
121		return errors.New("layer output contains NUL")
122	}
123	return nil
124}
125
126func validateRects(rects []Rect) error {
127	for i, r := range rects {
128		if r.W <= 0 || r.H <= 0 {
129			return fmt.Errorf("input rect %d has non-positive size %dx%d", i, r.W, r.H)
130		}
131		if int64(r.X)+int64(r.W) > math.MaxInt32 || int64(r.Y)+int64(r.H) > math.MaxInt32 {
132			return fmt.Errorf("input rect %d overflows", i)
133		}
134	}
135	return nil
136}
137
138// layerConfigureSize validates a configure extent. Zero leaves that axis unchanged.
139func layerConfigureSize(width, height uint32) (int32, int32, error) {
140	if width > MaxLayerDimension || height > MaxLayerDimension {
141		return 0, 0, fmt.Errorf("layer configure size %dx%d exceeds %d", width, height, MaxLayerDimension)
142	}
143	return int32(width), int32(height), nil
144}
145
146// SetTitle sets the toplevel title. Layer surfaces have no title, so it is a no-op.
147func (w *Window) SetTitle(title string) error {
148	if w == nil || w.Toplevel == nil {
149		return nil
150	}
151	return w.Toplevel.SetTitle(title)
152}
153
154// IsLayer reports whether the window is a layer surface.
155func (w *Window) IsLayer() bool { return w != nil && w.LayerSurface != nil }
156
157// StageInputRects validates rects and stages them as the pending input region
158// without committing; the next commit (normally Present) applies them. nil
159// restores the full surface and a non-nil empty slice makes the surface pass
160// all input through. A request identical to the current one sends nothing and
161// allocates nothing. Call from the session owner loop only.
162func (w *Window) StageInputRects(rects []Rect) error {
163	_, err := w.stageInputRects(rects)
164	return err
165}
166
167// SetInputRects stages rects like StageInputRects and, only if they changed and
168// the window is configured, commits immediately. Before the first configure
169// the initial commit carries the region.
170func (w *Window) SetInputRects(rects []Rect) error {
171	changed, err := w.stageInputRects(rects)
172	if err != nil || !changed || !w.Configured {
173		return err
174	}
175	return w.Surface.Commit()
176}
177
178func (w *Window) stageInputRects(rects []Rect) (changed bool, err error) {
179	if w == nil || w.Surface == nil || w.Compositor == nil {
180		return false, errors.New("window has no surface")
181	}
182	if err = validateRects(rects); err != nil {
183		return false, err
184	}
185	if (rects != nil) == w.inputCustom && slices.Equal(rects, w.inputRects) {
186		return false, nil
187	}
188	if err = w.stageInputRegion(rects); err != nil {
189		return false, err
190	}
191	w.inputCustom = rects != nil
192	w.inputRects = append(w.inputRects[:0], rects...)
193	return true, nil
194}
195
196func (w *Window) stageInputRegion(rects []Rect) error {
197	if rects == nil {
198		return w.Surface.SetInputRegion(nil)
199	}
200	region, err := w.Compositor.CreateRegion()
201	if err != nil {
202		return err
203	}
204	for _, r := range rects {
205		if err = region.Add(r.X, r.Y, r.W, r.H); err != nil {
206			break
207		}
208	}
209	if err == nil {
210		err = w.Surface.SetInputRegion(region)
211	}
212	_ = region.Destroy()
213	return err
214}
215
216// applyLayerSize applies a validated layer configure; zero axes are unchanged.
217func (w *Window) applyLayerSize(width, height int32) {
218	if width > 0 && w.Width != width {
219		w.Width, w.Dirty = width, true
220	}
221	if height > 0 && w.Height != height {
222		w.Height, w.Dirty = height, true
223	}
224}
225
226// bindLayerShell binds zwlr_layer_shell_v1 (up to version 4).
227func (w *Window) bindLayerShell(bind func(string, uint32, uint32, wl.Proxy) error) error {
228	ls := layershell.NewLayerShell(w.Display.Context())
229	if err := bind(layershell.LayerShellInterface, layerShellSupported, 1, ls); err != nil {
230		w.Display.Context().Unregister(ls) // never bound: no destructor to send
231		return err
232	}
233	w.LayerShell = ls
234	return nil
235}
236
237type outputCandidate struct {
238	out    *core.Output
239	global uint32
240	name   string
241}
242
243// releaseOutputs releases every candidate except keep.
244func releaseOutputs(cands []*outputCandidate, keep *outputCandidate) {
245	for _, c := range cands {
246		if c != keep {
247			_ = c.out.Release()
248		}
249	}
250}
251
252// selectOutput binds every wl_output that can report a name and returns the
253// global name of the exact match. Unmatched outputs are released.
254func (w *Window) selectOutput(name string) (*core.Output, uint32, error) {
255	if w.connection != nil {
256		for _, out := range w.connection.outputs {
257			if out.Name == name {
258				return out.proxy, out.Global, nil
259			}
260		}
261		return nil, 0, &CapabilityError{Name: core.OutputInterface, Cause: fmt.Errorf("output %q not found in applied inventory", name)}
262	}
263	reg := w.Display.Registry()
264	var globals []wl.Global
265	legacy := 0 // outputs older than v4 cannot report a name
266	for _, g := range reg.GetGlobals() {
267		if g.Interface != core.OutputInterface {
268			continue
269		}
270		if g.Version >= outputNameVersion {
271			globals = append(globals, g)
272		} else {
273			legacy++
274		}
275	}
276	sort.Slice(globals, func(i, j int) bool { return globals[i].Name < globals[j].Name })
277	cands := make([]*outputCandidate, 0, len(globals))
278	ctx := w.Display.Context()
279	for _, g := range globals {
280		c := &outputCandidate{out: core.NewOutput(ctx), global: g.Name}
281		c.out.OnName(func(n string) { c.name = n })
282		if err := reg.Bind(g.Name, core.OutputInterface, outputNameVersion, c.out); err != nil {
283			ctx.Unregister(c.out)
284			releaseOutputs(cands, nil)
285			return nil, 0, &CapabilityError{Name: core.OutputInterface, Cause: err}
286		}
287		cands = append(cands, c)
288	}
289	if len(cands) > 0 {
290		if err := w.Display.Roundtrip(); err != nil {
291			releaseOutputs(cands, nil)
292			return nil, 0, err
293		}
294	}
295	var chosen *outputCandidate
296	for _, c := range cands {
297		if c.name == name {
298			chosen = c
299			break
300		}
301	}
302	releaseOutputs(cands, chosen)
303	if chosen == nil {
304		cause := fmt.Errorf("output %q not found", name)
305		if legacy > 0 {
306			cause = fmt.Errorf("output %q not found; %d wl_output global(s) older than version %d cannot report names", name, legacy, outputNameVersion)
307		}
308		return nil, 0, &CapabilityError{Name: core.OutputInterface, Cause: cause}
309	}
310	return chosen.out, chosen.global, nil
311}
312
313// createLayerSurface creates the role object and stages all double-buffered
314// state. The caller performs the initial empty commit.
315func (w *Window) createLayerSurface(o LayerOptions) error {
316	if o.Keyboard == KeyboardOnDemand && w.LayerShell.Version() < 4 {
317		return &CapabilityError{Name: layershell.LayerShellInterface, Cause: fmt.Errorf("on-demand keyboard requires version 4, compositor negotiated %d", w.LayerShell.Version())}
318	}
319	var out *core.Output
320	if o.Output != "" {
321		var global uint32
322		var err error
323		if out, global, err = w.selectOutput(o.Output); err != nil {
324			return err
325		}
326		w.layerOutputGlobal, w.layerOutputSelected = global, true
327		w.Display.Registry().AddGlobalRemoveHandler(outputRemoved{w})
328	}
329	ns := o.Namespace
330	if ns == "" {
331		ns = DefaultLayerNamespace
332	}
333	ls, err := w.LayerShell.GetLayerSurface(w.Surface, out, o.Layer, ns)
334	if out != nil && w.connection == nil {
335		// The compositor keeps its own reference to the output. A shared
336		// connection owns the proxy in its output inventory instead.
337		_ = out.Release()
338	}
339	if err != nil {
340		return err
341	}
342	w.LayerSurface = ls
343	ls.OnClosed(func() { w.postClosed() })
344	ls.OnConfigure(func(serial, width, height uint32) {
345		cw, ch, e := layerConfigureSize(width, height)
346		if e != nil {
347			w.feedbackFailure(e)
348			return
349		}
350		if w.readerStarted.Load() {
351			w.post(Event{Kind: ConfigureLayer, Width: cw, Height: ch, Serial: serial})
352			return
353		}
354		w.applyLayerSize(cw, ch)
355		if e = ls.AckConfigure(serial); e != nil {
356			w.feedbackFailure(e)
357			return
358		}
359		w.Configured, w.FrameReady = true, true
360	})
361	// Opposite anchors let the compositor assign that axis (size 0).
362	reqW, reqH := uint32(w.Width), uint32(w.Height)
363	if o.Anchor&(AnchorLeft|AnchorRight) == AnchorLeft|AnchorRight {
364		reqW = 0
365	}
366	if o.Anchor&(AnchorTop|AnchorBottom) == AnchorTop|AnchorBottom {
367		reqH = 0
368	}
369	if err = ls.SetSize(reqW, reqH); err != nil {
370		return err
371	}
372	if err = ls.SetAnchor(o.Anchor); err != nil {
373		return err
374	}
375	if err = ls.SetExclusiveZone(o.ExclusiveZone); err != nil {
376		return err
377	}
378	if err = ls.SetMargin(o.Margin[0], o.Margin[1], o.Margin[2], o.Margin[3]); err != nil {
379		return err
380	}
381	return ls.SetKeyboardInteractivity(o.Keyboard)
382}
383
384// outputRemoved closes the window when its selected output disappears.
385type outputRemoved struct{ w *Window }
386
387func (h outputRemoved) HandleRegistryGlobalRemove(ev wlturbo.RegistryGlobalRemoveEvent) {
388	if h.w.layerOutputSelected && ev.Name == h.w.layerOutputGlobal {
389		h.w.postClosed()
390	}
391}
392
393func (w *Window) postClosed() {
394	if w.readerStarted.Load() {
395		w.post(Event{Kind: CloseEvent})
396		return
397	}
398	w.Closed = true
399}
400
401// closeLayer destroys the layer role and its factory; the wl_surface goes last.
402func (w *Window) closeLayer() {
403	if w.LayerSurface != nil {
404		_ = w.LayerSurface.Destroy()
405		w.LayerSurface = nil
406	}
407	if w.LayerShell != nil {
408		// zwlr_layer_shell_v1.destroy needs version 3; otherwise forget it locally.
409		if w.LayerShell.Destroy() != nil {
410			if c := w.LayerShell.Context(); c != nil {
411				c.Unregister(w.LayerShell)
412			}
413		}
414		w.LayerShell = nil
415	}
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 1

Tags 12

↑↓ moveenter openesc close