v0.2.0 e178e0b

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

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