main 7c2aaeb

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

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