v0.3.0 2df0761

surface.go RawHistoryFolder

1package neferclient
2
3import (
4	"encoding/binary"
5	"errors"
6	"fmt"
7	"math"
8	"slices"
9	"strings"
10
11	"github.com/bnema/go-wayland-bindings/client/extsessionlock"
12	"github.com/bnema/go-wayland-bindings/client/fractionalscale"
13	"github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
14	"github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
15	"github.com/bnema/go-wayland-bindings/client/viewporter"
16	"github.com/bnema/go-wayland-bindings/client/wayland"
17	"github.com/bnema/go-wayland-bindings/client/wlrlayershell"
18	"github.com/bnema/go-wayland-bindings/client/xdgshell"
19	"github.com/bnema/wlturbo/wl"
20	"golang.org/x/sys/unix"
21)
22
23// LayerLevel is the wlr-layer-shell stacking layer. The zero value is invalid.
24type LayerLevel uint8
25
26const (
27	LayerBackground LayerLevel = iota + 1
28	LayerBottom
29	LayerTop
30	LayerOverlay
31)
32
33// Anchor is a bit set of output edges a layer surface attaches to.
34type Anchor uint8
35
36const (
37	AnchorTop Anchor = 1 << iota
38	AnchorBottom
39	AnchorLeft
40	AnchorRight
41)
42
43const anchorAll = AnchorTop | AnchorBottom | AnchorLeft | AnchorRight
44
45// KeyboardMode is the layer surface keyboard interactivity. The zero value is
46// KeyboardNone. KeyboardOnDemand needs zwlr_layer_shell_v1 version 4.
47type KeyboardMode uint8
48
49const (
50	KeyboardNone KeyboardMode = iota
51	KeyboardExclusive
52	KeyboardOnDemand
53)
54
55// maxLayerDimension bounds a compositor-assigned layer surface extent.
56const maxLayerDimension = 16384
57
58// DefaultLayerNamespace is used when [LayerConfig.Namespace] is empty.
59const DefaultLayerNamespace = "neferclient"
60
61// LayerConfig requests a wlr-layer-shell surface. Width and Height give the
62// initial logical size; an axis anchored to both opposite edges is sized by
63// the compositor and reported through Handler.Configure.
64type LayerConfig struct {
65	Output        string // wl_output name; empty lets the compositor choose
66	Namespace     string // defaults to [DefaultLayerNamespace]
67	Level         LayerLevel
68	Anchors       Anchor
69	Keyboard      KeyboardMode
70	ExclusiveZone int32    // >0 reserves, 0 neutral, -1 ignores other zones
71	Margin        [4]int32 // top, right, bottom, left
72	// InputRects, when non-nil, restricts pointer input to these rectangles
73	// (logical pixels). An empty non-nil slice makes the surface click-through.
74	InputRects []Rect
75	// Width and Height are the requested logical size. They are required on
76	// every axis that is not anchored to both opposite edges.
77	Width, Height int32
78}
79
80type surfaceRole uint8
81
82const (
83	roleToplevel surfaceRole = iota + 1
84	roleLayer
85	roleLock
86)
87
88// Surface is one wl_surface with its role and the DMA-BUF presentation
89// objects. Every method belongs to the owner goroutine.
90type Surface struct {
91	c    *Conn
92	id   SurfaceID
93	role surfaceRole
94
95	surf     *wayland.Surface
96	viewport *viewporter.WpViewport
97	frac     *fractionalscale.WpFractionalScale
98	sync     *linuxdrmsyncobj.WpLinuxDrmSyncobjSurface
99	feedback *linuxdmabuf.LinuxDmabufFeedback
100	xdg      *xdgshell.XdgSurface
101	toplevel *xdgshell.XdgToplevel
102	layer    *wlrlayershell.LayerSurface
103	lockSurf *extsessionlock.ExtSessionLockSurface
104	lock     *Lock
105
106	width, height int32
107	scale         float64
108	pendW, pendH  int32 // toplevel size announced before the xdg configure
109	configured    bool
110	frameReady    bool
111	closed        bool
112
113	// destination/opaque state last sent; they are double-buffered surface
114	// state that persists across commits, so unchanged values are not resent.
115	sentDestW, sentDestH int32
116	sentBufScale         int32  // starts at 1, the protocol default
117	presented            bool   // a Present committed: the surface is mapped
118	output               uint32 // lock surfaces: the covered wl_output global
119	opaqueSet            bool
120	opaqueW, opaqueH     int32
121
122	cb  *frameCallback
123	req surfaceRequests
124
125	inputRects  []Rect
126	inputCustom bool
127
128	buffers   map[uint64]importedBuffer
129	timelines map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline
130
131	fb      Feedback
132	table   []Format // current format table
133	pending Feedback // round in progress; Formats reused across rounds
134	fbDone  bool
135}
136
137// frameCallback is a reusable wl_callback: the library keeps a single frame
138// callback pending per surface, so one proxy object serves all of them.
139type frameCallback struct {
140	wl.BaseProxy
141	q  *queue
142	id SurfaceID
143}
144
145func (f *frameCallback) EventSignature(op uint16) (string, bool) {
146	if op == 0 {
147		return "uint,", true
148	}
149	return "", false
150}
151
152func (f *frameCallback) Dispatch(ev *wl.Event) {
153	if ev.Opcode != 0 {
154		return
155	}
156	_ = ev.Uint32()
157	f.Context().Unregister(f)
158	e := event{kind: evFrame, id: uint32(f.id)}
159	f.q.post(&e)
160}
161
162// ID returns the surface's identifier used in Handler callbacks.
163func (s *Surface) ID() SurfaceID { return s.id }
164
165// WLSurface returns the wl_surface proxy, for requests of protocol extensions
166// bound with [Conn.Bind] that take a surface argument, or nil after
167// [Surface.Close]. The Surface owns it: use it only as a request argument; do
168// not call its methods or add handlers. Owner goroutine only.
169//
170// Handlers of objects bound with [Conn.Bind] run on the reader goroutine. To
171// wait for their answer, keep calling [Conn.Dispatch] on [Conn.Wake]: the
172// reader stops reading while the event queue is full.
173func (s *Surface) WLSurface() *wayland.Surface {
174	if s.closed {
175		return nil
176	}
177	return s.surf
178}
179
180// Size returns the current logical size and scale (1 until announced). Before
181// the first configure the size is the requested one (zero for lock surfaces).
182func (s *Surface) Size() (w, h int32, scale float64) { return s.width, s.height, s.scale }
183
184// Feedback returns the latest complete dmabuf feedback, or nil before the
185// first Handler.FeedbackDone. The value and its slice stay valid until the
186// next FeedbackDone; use [Feedback.Clone] to keep it, and [Feedback.Equal] to
187// tell a changed preference from a repeated one.
188func (s *Surface) Feedback() *Feedback {
189	if !s.fbDone {
190		return nil
191	}
192	return &s.fb
193}
194
195// PhysicalSize returns the buffer size in pixels for the current logical size
196// and scale.
197func (s *Surface) PhysicalSize() (int32, int32, error) {
198	if s.scale <= 0 || s.width <= 0 || s.height <= 0 {
199		return 0, 0, errors.New("neferclient: invalid surface geometry")
200	}
201	if s.scale != math.Trunc(s.scale) && s.viewport == nil {
202		return 0, 0, &CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %g requires viewporter", s.scale)}
203	}
204	// Ignore sub-nanopixel floating noise at exact integer boundaries.
205	x, y := math.Ceil(float64(s.width)*s.scale-1e-9), math.Ceil(float64(s.height)*s.scale-1e-9)
206	if x > math.MaxInt32 || y > math.MaxInt32 {
207		return 0, 0, errors.New("neferclient: buffer dimensions overflow")
208	}
209	return int32(x), int32(y), nil
210}
211
212func validateLayer(o *LayerConfig) error {
213	switch {
214	case o.Level < LayerBackground || o.Level > LayerOverlay:
215		return fmt.Errorf("neferclient: invalid layer level %d", o.Level)
216	case o.Anchors&^anchorAll != 0:
217		return fmt.Errorf("neferclient: invalid layer anchors %#x", uint8(o.Anchors))
218	case o.Keyboard > KeyboardOnDemand:
219		return fmt.Errorf("neferclient: invalid keyboard mode %d", o.Keyboard)
220	case o.ExclusiveZone < -1:
221		return fmt.Errorf("neferclient: invalid exclusive zone %d", o.ExclusiveZone)
222	case strings.ContainsRune(o.Namespace, 0), strings.ContainsRune(o.Output, 0):
223		return errors.New("neferclient: layer namespace or output contains NUL")
224	case o.Width < 0 || o.Height < 0:
225		return errors.New("neferclient: negative layer size")
226	}
227	if o.Anchors&(AnchorLeft|AnchorRight) != AnchorLeft|AnchorRight && o.Width == 0 ||
228		o.Anchors&(AnchorTop|AnchorBottom) != AnchorTop|AnchorBottom && o.Height == 0 {
229		return errors.New("neferclient: layer size required on axes not anchored to both edges")
230	}
231	return validateRects(o.InputRects)
232}
233
234func validateRects(rects []Rect) error {
235	for i, r := range rects {
236		if r.Width <= 0 || r.Height <= 0 {
237			return fmt.Errorf("neferclient: input rect %d has non-positive size %dx%d", i, r.Width, r.Height)
238		}
239		if int64(r.X)+int64(r.Width) > math.MaxInt32 || int64(r.Y)+int64(r.Height) > math.MaxInt32 {
240			return fmt.Errorf("neferclient: input rect %d overflows", i)
241		}
242	}
243	return nil
244}
245
246// newSurface creates the wl_surface and the add-on objects shared by every
247// role. The reader must be paused: handlers are installed after the requests.
248func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) {
249	if c.closed {
250		return nil, ErrClosed
251	}
252	if err := c.ensureCore(); err != nil {
253		return nil, err
254	}
255	c.ensureSeat() // optional: input flows to the Handler once the seat is bound
256	g := &c.g
257	c.nextSurf++
258	s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, sentBufScale: 1,
259		buffers: map[uint64]importedBuffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}}
260	ok := false
261	defer func() {
262		if !ok {
263			s.destroy()
264		}
265	}()
266	var err error
267	if s.surf, err = g.compositor.CreateSurface(); err != nil {
268		return nil, err
269	}
270	q, sid := c.q, uint32(s.id)
271	s.surf.OnPreferredBufferScale(func(f int32) {
272		e := event{kind: evBufferScale, id: sid, a: f}
273		q.post(&e)
274	})
275	if g.viewporter != nil {
276		if s.viewport, err = g.viewporter.GetViewport(s.surf); err != nil {
277			return nil, err
278		}
279	}
280	if g.fscale != nil {
281		if s.frac, err = g.fscale.GetFractionalScale(s.surf); err != nil {
282			return nil, err
283		}
284		s.frac.OnPreferredScale(func(n uint32) {
285			e := event{kind: evPreferredScale, id: sid, a: int32(n)}
286			q.post(&e)
287		})
288	}
289	// The role objects come next (callers); sync surface and feedback after.
290	s.cb = &frameCallback{q: q, id: s.id}
291	s.cb.SetContext(c.wlctx)
292	s.req = wireRequests{s: s}
293	c.surfaces[s.id] = s
294	ok = true
295	return s, nil
296}
297
298// finishSurface creates the explicit-sync and feedback objects once the role
299// exists.
300func (s *Surface) finishSurface() error {
301	c := s.c
302	var err error
303	if s.sync, err = c.g.syncobj.GetSurface(s.surf); err != nil {
304		return err
305	}
306	if s.feedback, err = c.g.dmabuf.GetDefaultFeedback(); err != nil {
307		return err
308	}
309	s.watchFeedback()
310	return nil
311}
312
313func (s *Surface) watchFeedback() {
314	q, sid := s.c.q, uint32(s.id)
315	s.feedback.OnFormatTable(func(fd *wl.OwnedFD, size uint32) {
316		n, err := fd.Take()
317		if err != nil {
318			e := event{kind: evBadEvent, id: sid, a: badFeedbackFD}
319			q.post(&e)
320			return
321		}
322		e := event{kind: evFeedbackTable, id: sid, fd: int32(n), flags: size}
323		q.post(&e)
324	})
325	s.feedback.OnMainDevice(func(dev []byte) {
326		if len(dev) != 8 {
327			e := event{kind: evBadEvent, id: sid, a: badMainDevice}
328			q.post(&e)
329			return
330		}
331		e := event{kind: evFeedbackMainDevice, id: sid, dev: binary.NativeEndian.Uint64(dev)}
332		q.post(&e)
333	})
334	s.feedback.OnTrancheFormats(func(data []byte) {
335		if len(data)%2 != 0 {
336			e := event{kind: evBadEvent, id: sid, a: badTranche}
337			q.post(&e)
338			return
339		}
340		for len(data) > 0 { // chunks of up to maxNameLen/2 indices
341			n := min(len(data), maxNameLen)
342			e := event{kind: evFeedbackFormats, id: sid}
343			e.setBytes(data[:n])
344			q.post(&e)
345			data = data[n:]
346		}
347	})
348	s.feedback.OnDone(func() {
349		e := event{kind: evFeedbackDone, id: sid}
350		q.post(&e)
351	})
352}
353
354// Codes of evBadEvent.
355const (
356	badFeedbackFD int32 = iota + 1
357	badMainDevice
358	badTranche
359	badConfigure
360)
361
362var badEventErrors = [...]string{
363	badFeedbackFD: "neferclient: dmabuf format table descriptor unavailable",
364	badMainDevice: "neferclient: invalid dmabuf main_device length",
365	badTranche:    "neferclient: odd dmabuf tranche format list",
366	badConfigure:  "neferclient: configure size out of range",
367}
368
369// configureEvent builds a layer or lock configure event, or a badConfigure
370// event when the compositor announced an out-of-range size.
371func configureEvent(kind eventKind, sid, serial, w, h uint32) event {
372	if w > maxLayerDimension || h > maxLayerDimension {
373		return event{kind: evBadEvent, id: sid, a: badConfigure}
374	}
375	return event{kind: kind, id: sid, serial: serial, a: int32(w), b: int32(h)}
376}
377
378// NewLayerSurface creates a wlr-layer-shell surface. It becomes presentable
379// after the first Handler.Configure.
380func (c *Conn) NewLayerSurface(cfg LayerConfig) (*Surface, error) {
381	if c.closed {
382		return nil, ErrClosed
383	}
384	if err := validateLayer(&cfg); err != nil {
385		return nil, err
386	}
387	if err := c.ensureLayerShell(); err != nil {
388		return nil, err
389	}
390	if cfg.Keyboard == KeyboardOnDemand && c.g.layerVersion < 4 {
391		return nil, &CapabilityError{Name: wlrlayershell.LayerShellInterface, Cause: fmt.Errorf("on-demand keyboard requires version 4, negotiated %d", c.g.layerVersion)}
392	}
393	var out *wayland.Output
394	if cfg.Output != "" {
395		for _, o := range c.outputs {
396			if o.done && o.pub.Name == cfg.Output {
397				out = o.proxy
398				break
399			}
400		}
401		if out == nil {
402			return nil, &CapabilityError{Name: wayland.OutputInterface, Cause: fmt.Errorf("output %q not found", cfg.Output)}
403		}
404	}
405	c.pause()
406	defer c.resume()
407	s, err := c.newSurface(roleLayer, cfg.Width, cfg.Height)
408	if err != nil {
409		return nil, err
410	}
411	ok := false
412	defer func() {
413		if !ok {
414			s.destroy()
415		}
416	}()
417	ns := cfg.Namespace
418	if ns == "" {
419		ns = DefaultLayerNamespace
420	}
421	level := [...]uint32{LayerBackground: wlrlayershell.LAYER_BACKGROUND, LayerBottom: wlrlayershell.LAYER_BOTTOM,
422		LayerTop: wlrlayershell.LAYER_TOP, LayerOverlay: wlrlayershell.LAYER_OVERLAY}[cfg.Level]
423	if s.layer, err = c.g.layer.GetLayerSurface(s.surf, out, level, ns); err != nil {
424		return nil, err
425	}
426	q, sid := c.q, uint32(s.id)
427	s.layer.OnClosed(func() {
428		e := event{kind: evClosed, id: sid}
429		q.post(&e)
430	})
431	s.layer.OnConfigure(func(serial, w, h uint32) {
432		e := configureEvent(evLayerConfigure, sid, serial, w, h)
433		q.post(&e)
434	})
435	// Opposite anchors let the compositor assign that axis (size 0).
436	reqW, reqH := uint32(cfg.Width), uint32(cfg.Height)
437	if cfg.Anchors&(AnchorLeft|AnchorRight) == AnchorLeft|AnchorRight {
438		reqW = 0
439	}
440	if cfg.Anchors&(AnchorTop|AnchorBottom) == AnchorTop|AnchorBottom {
441		reqH = 0
442	}
443	var anchor uint32
444	for _, m := range [...][2]uint32{{uint32(AnchorTop), wlrlayershell.ANCHOR_TOP}, {uint32(AnchorBottom), wlrlayershell.ANCHOR_BOTTOM},
445		{uint32(AnchorLeft), wlrlayershell.ANCHOR_LEFT}, {uint32(AnchorRight), wlrlayershell.ANCHOR_RIGHT}} {
446		if uint32(cfg.Anchors)&m[0] != 0 {
447			anchor |= m[1]
448		}
449	}
450	for _, e := range []error{
451		s.layer.SetSize(reqW, reqH),
452		s.layer.SetAnchor(anchor),
453		s.layer.SetExclusiveZone(cfg.ExclusiveZone),
454		s.layer.SetMargin(cfg.Margin[0], cfg.Margin[1], cfg.Margin[2], cfg.Margin[3]),
455		s.layer.SetKeyboardInteractivity(uint32(cfg.Keyboard)),
456	} {
457		if e != nil {
458			return nil, e
459		}
460	}
461	if cfg.InputRects != nil {
462		if _, err = s.stageInputRects(cfg.InputRects); err != nil {
463			return nil, err
464		}
465	}
466	if err = s.finishSurface(); err != nil {
467		return nil, err
468	}
469	if err = s.surf.Commit(); err != nil { // initial empty commit asks for configure
470		return nil, err
471	}
472	ok = true
473	return s, nil
474}
475
476// NewToplevel creates an xdg-toplevel window with the given title and initial
477// logical size. It becomes presentable after the first Handler.Configure.
478func (c *Conn) NewToplevel(title string, w, h int32) (*Surface, error) {
479	if c.closed {
480		return nil, ErrClosed
481	}
482	if w <= 0 || h <= 0 {
483		return nil, fmt.Errorf("neferclient: invalid logical size %dx%d", w, h)
484	}
485	if err := c.ensureXdg(); err != nil {
486		return nil, err
487	}
488	c.pause()
489	defer c.resume()
490	s, err := c.newSurface(roleToplevel, w, h)
491	if err != nil {
492		return nil, err
493	}
494	ok := false
495	defer func() {
496		if !ok {
497			s.destroy()
498		}
499	}()
500	if s.xdg, err = c.g.xdg.GetXdgSurface(s.surf); err != nil {
501		return nil, err
502	}
503	if s.toplevel, err = s.xdg.GetToplevel(); err != nil {
504		return nil, err
505	}
506	q, sid := c.q, uint32(s.id)
507	s.toplevel.OnClose(func() {
508		e := event{kind: evClosed, id: sid}
509		q.post(&e)
510	})
511	s.toplevel.OnConfigure(func(w, h int32, _ []byte) {
512		e := event{kind: evXdgToplevel, id: sid, a: w, b: h}
513		q.post(&e)
514	})
515	s.xdg.OnConfigure(func(serial uint32) {
516		e := event{kind: evXdgConfigure, id: sid, serial: serial}
517		q.post(&e)
518	})
519	if err = s.toplevel.SetTitle(title); err != nil {
520		return nil, err
521	}
522	if err = s.finishSurface(); err != nil {
523		return nil, err
524	}
525	if err = s.surf.Commit(); err != nil {
526		return nil, err
527	}
528	ok = true
529	return s, nil
530}
531
532// applySurface handles every surface, lock and xdg_wm_base event on the owner.
533func (c *Conn) applySurface(ev *event, h Handler) {
534	defer ev.closeFD()
535	switch ev.kind {
536	case evPing:
537		if c.g.xdg != nil {
538			if err := c.g.xdg.Pong(ev.serial); err != nil {
539				h.Error(err)
540			}
541		}
542		return
543	case evLocked, evLockFinished:
544		l := c.curLock
545		if l == nil || l.gen != ev.id {
546			return
547		}
548		if ev.kind == evLocked {
549			l.locked = true
550			h.Locked()
551		} else {
552			l.finished = true
553			h.LockFinished()
554		}
555		return
556	}
557	s := c.surfaces[SurfaceID(ev.id)]
558	if s == nil || s.closed {
559		return // closed while the event was queued
560	}
561	id := s.id
562	switch ev.kind {
563	case evBadEvent:
564		h.Error(errors.New(badEventErrors[ev.a]))
565	case evClosed:
566		h.Closed(id)
567	case evXdgToplevel:
568		s.pendW, s.pendH = ev.a, ev.b
569	case evXdgConfigure:
570		if s.pendW > 0 && s.pendH > 0 {
571			s.width, s.height = s.pendW, s.pendH
572		}
573		s.configure(s.xdg.AckConfigure(ev.serial), h)
574	case evLayerConfigure:
575		if ev.a > 0 {
576			s.width = ev.a
577		}
578		if ev.b > 0 {
579			s.height = ev.b
580		}
581		s.configure(s.layer.AckConfigure(ev.serial), h)
582	case evLockConfigure:
583		s.width, s.height = ev.a, ev.b
584		s.configure(s.lockSurf.AckConfigure(ev.serial), h)
585	case evFrame:
586		s.frameReady = true
587		h.Frame(id)
588	case evPreferredScale:
589		if ev.a <= 0 {
590			h.Error(errors.New("neferclient: invalid fractional scale zero"))
591			return
592		}
593		s.setScale(float64(ev.a)/120, h)
594	case evBufferScale:
595		if s.frac == nil && ev.a > 0 {
596			s.setScale(float64(ev.a), h)
597		}
598	case evFeedbackTable:
599		fd := int(ev.fd)
600		ev.fd = -1 // readTable consumes fd; the deferred closeFD must not touch the number again
601		if err := s.readTable(fd, ev.flags); err != nil {
602			h.Error(err)
603		}
604	case evFeedbackMainDevice:
605		s.pending.MainDevice = ev.dev
606	case evFeedbackFormats:
607		b := ev.nameBytes()
608		for i := 0; i+1 < len(b); i += 2 {
609			idx := int(binary.NativeEndian.Uint16(b[i:]))
610			if idx >= len(s.table) {
611				h.Error(fmt.Errorf("neferclient: dmabuf format table index %d out of range", idx))
612				continue
613			}
614			s.pending.Formats = append(s.pending.Formats, s.table[idx])
615		}
616	case evFeedbackDone:
617		s.fb, s.pending = s.pending, Feedback{Formats: s.fb.Formats[:0]}
618		s.fbDone = true
619		h.FeedbackDone(id)
620	}
621}
622
623func (s *Surface) configure(ackErr error, h Handler) {
624	if ackErr != nil {
625		h.Error(ackErr)
626		return
627	}
628	if !s.configured {
629		s.configured, s.frameReady = true, true
630	}
631	h.Configure(s.id, s.width, s.height)
632}
633
634func (s *Surface) setScale(v float64, h Handler) {
635	if v == s.scale {
636		return
637	}
638	s.scale = v
639	h.Scale(s.id, v)
640}
641
642// readTable snapshots the format table with pread (no mapping of
643// compositor-owned memory) and always consumes fd.
644func (s *Surface) readTable(fd int, size uint32) error {
645	defer unix.Close(fd)
646	if size%16 != 0 || size > 16<<20 {
647		return fmt.Errorf("neferclient: invalid dmabuf format table size %d", size)
648	}
649	c := s.c
650	c.tableBuf = slices.Grow(c.tableBuf[:0], int(size))[:size]
651	b := c.tableBuf
652	for off := 0; off < len(b); {
653		n, err := unix.Pread(fd, b[off:], int64(off))
654		if err == unix.EINTR {
655			continue
656		}
657		if err != nil {
658			return fmt.Errorf("neferclient: read dmabuf format table: %w", err)
659		}
660		if n == 0 {
661			return errors.New("neferclient: truncated dmabuf format table")
662		}
663		off += n
664	}
665	s.table = s.table[:0]
666	for i := 0; i < len(b); i += 16 {
667		s.table = append(s.table, Format{binary.NativeEndian.Uint32(b[i:]), binary.NativeEndian.Uint64(b[i+8:])})
668	}
669	return nil
670}
671
672// SetInputRegion sets the input region (logical pixels). nil restores the
673// whole surface; a non-nil empty slice passes all input through. The region is
674// double-buffered state: it is committed immediately only once the surface
675// presented a buffer; before that the next Present applies it (committing
676// earlier would map a surface without a buffer, which lock surfaces forbid and
677// layer surfaces answer with an error). Lock surfaces always take all input
678// and refuse it.
679func (s *Surface) SetInputRegion(rects []Rect) error {
680	if s.closed {
681		return ErrClosed
682	}
683	if s.role == roleLock {
684		return errors.New("neferclient: lock surfaces take all input")
685	}
686	changed, err := s.stageInputRects(rects)
687	if err != nil || !changed || !s.presented {
688		return err
689	}
690	return s.surf.Commit()
691}
692
693func (s *Surface) stageInputRects(rects []Rect) (bool, error) {
694	if err := validateRects(rects); err != nil {
695		return false, err
696	}
697	if (rects != nil) == s.inputCustom && slices.Equal(rects, s.inputRects) {
698		return false, nil
699	}
700	if rects == nil {
701		if err := s.surf.SetInputRegion(nil); err != nil {
702			return false, err
703		}
704	} else {
705		region, err := s.c.g.compositor.CreateRegion()
706		if err != nil {
707			return false, err
708		}
709		for _, r := range rects {
710			if err = region.Add(r.X, r.Y, r.Width, r.Height); err != nil {
711				break
712			}
713		}
714		if err == nil {
715			err = s.surf.SetInputRegion(region)
716		}
717		_ = region.Destroy()
718		if err != nil {
719			return false, err
720		}
721	}
722	s.inputCustom = rects != nil
723	s.inputRects = append(s.inputRects[:0], rects...)
724	return true, nil
725}
726
727// Close destroys the surface, its buffers, timelines and role objects,
728// children before parents. It is safe to call more than once. The caller must
729// stop using buffers it imported first (the compositor keeps them until it
730// processes the destruction).
731func (s *Surface) Close() error {
732	if s == nil || s.closed {
733		return nil
734	}
735	if s.c.closed {
736		s.closed = true
737		return nil
738	}
739	return s.destroy()
740}
741
742func (s *Surface) destroy() error {
743	s.closed = true
744	delete(s.c.surfaces, s.id)
745	s.c.seatSurfaceGone(s)
746	var errs []error
747	add := func(err error) {
748		if err != nil {
749			errs = append(errs, err)
750		}
751	}
752	for id, b := range s.buffers {
753		add(b.buf.Destroy())
754		delete(s.buffers, id)
755	}
756	for id, t := range s.timelines {
757		add(t.Destroy())
758		delete(s.timelines, id)
759	}
760	if s.feedback != nil {
761		add(s.feedback.Destroy())
762	}
763	if s.sync != nil {
764		add(s.sync.Destroy())
765	}
766	if s.lockSurf != nil {
767		add(s.lockSurf.Destroy())
768	}
769	if s.layer != nil {
770		add(s.layer.Destroy())
771	}
772	if s.toplevel != nil {
773		add(s.toplevel.Destroy())
774	}
775	if s.xdg != nil {
776		add(s.xdg.Destroy())
777	}
778	if s.frac != nil {
779		add(s.frac.Destroy())
780	}
781	if s.viewport != nil {
782		add(s.viewport.Destroy())
783	}
784	if s.surf != nil {
785		add(s.surf.Destroy())
786	}
787	if s.cb != nil {
788		// A pending frame callback is owned by the compositor: abandon it so
789		// its late done and delete_id are discarded. An error only means no
790		// callback was pending.
791		_ = s.c.wlctx.Abandon(s.cb)
792	}
793	return errors.Join(errs...)
794}

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 4

↑↓ moveenter openesc close