v0.4.0 2de4e1d

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

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