v0.1.0 f503a24

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

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