detached 143cbf8

feat: add layer, toplevel and lock surfaces with dmabuf feedback

commit
143cbf8e899679af5e4b389b9cae2df7c58b3944
author
brice · · 2026-10-01 16:32 UTC
parent
cd00fc0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +1051 -0
M conn.go +11 -0
@@ -76,6 +76,13 @@ type Conn struct {
7676 outputs []*outputEntry // sorted by Global
7777 view []Output // reused by Outputs
7878
79+ g globals // lazily bound protocol globals, owner only
80+ surfaces map[SurfaceID]*Surface
81+ nextSurf SurfaceID
82+ nextLock uint32
83+ curLock *Lock // the live session lock, if any
84+ tableBuf []byte
85+
7986 ready bool // Connect finished; announcements go to the Handler
8087 dispatching bool
8188 closed bool
@@ -137,6 +144,8 @@ func connectConn(ctx context.Context, sock net.Conn) (res *Conn, err error) {
137144 efd: -1,
138145 epfd: -1,
139146 watched: make(map[int32]uint64),
147+
148+ surfaces: make(map[SurfaceID]*Surface),
140149 }
141150 watch := context.AfterFunc(ctx, func() { _ = display.Close() })
142151 defer func() {
@@ -385,6 +394,8 @@ func (c *Conn) apply(ev *event, h Handler) {
385394 h.Error(err)
386395 }
387396 }
397+ default:
398+ c.applySurface(ev, h)
388399 }
389400 }
390401
@@ -76,6 +76,13 @@ type Conn struct {
76 outputs []*outputEntry // sorted by Global76 outputs []*outputEntry // sorted by Global
77 view []Output // reused by Outputs77 view []Output // reused by Outputs
78 78
79+ g globals // lazily bound protocol globals, owner only
80+ surfaces map[SurfaceID]*Surface
81+ nextSurf SurfaceID
82+ nextLock uint32
83+ curLock *Lock // the live session lock, if any
84+ tableBuf []byte
85+
79 ready bool // Connect finished; announcements go to the Handler86 ready bool // Connect finished; announcements go to the Handler
80 dispatching bool87 dispatching bool
81 closed bool88 closed bool
@@ -137,6 +144,8 @@ func connectConn(ctx context.Context, sock net.Conn) (res *Conn, err error) {
137 efd: -1,144 efd: -1,
138 epfd: -1,145 epfd: -1,
139 watched: make(map[int32]uint64),146 watched: make(map[int32]uint64),
147+
148+ surfaces: make(map[SurfaceID]*Surface),
140 }149 }
141 watch := context.AfterFunc(ctx, func() { _ = display.Close() })150 watch := context.AfterFunc(ctx, func() { _ = display.Close() })
142 defer func() {151 defer func() {
@@ -385,6 +394,8 @@ func (c *Conn) apply(ev *event, h Handler) {
385 h.Error(err)394 h.Error(err)
386 }395 }
387 }396 }
397+ default:
398+ c.applySurface(ev, h)
388 }399 }
389 }400 }
390 401
A globals.go +133 -0
@@ -0,0 +1,133 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8+ "github.com/bnema/go-wayland-bindings/client/fractionalscale"
9+ "github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
10+ "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
11+ "github.com/bnema/go-wayland-bindings/client/viewporter"
12+ "github.com/bnema/go-wayland-bindings/client/wayland"
13+ "github.com/bnema/go-wayland-bindings/client/wlrlayershell"
14+ "github.com/bnema/go-wayland-bindings/client/xdgshell"
15+ "github.com/bnema/wlturbo/wl"
16+)
17+
18+// CapabilityError reports a missing or too old required protocol global.
19+type CapabilityError struct {
20+ Name string
21+ Cause error
22+}
23+
24+func (e *CapabilityError) Error() string {
25+ return fmt.Sprintf("neferclient: capability %s: %v", e.Name, e.Cause)
26+}
27+func (e *CapabilityError) Unwrap() error { return e.Cause }
28+
29+// globals holds the protocol globals bound on first use. Owner goroutine only.
30+type globals struct {
31+ compositor *wayland.Compositor
32+ dmabuf *linuxdmabuf.LinuxDmabuf
33+ syncobj *linuxdrmsyncobj.WpLinuxDrmSyncobjManager
34+ xdg *xdgshell.XdgWmBase
35+ layer *wlrlayershell.LayerShell
36+ lockMgr *extsessionlock.ExtSessionLockManager
37+ viewporter *viewporter.WpViewporter
38+ fscale *fractionalscale.WpFractionalScaleManager
39+
40+ layerVersion uint32
41+ triedViewport bool
42+ triedFScale bool
43+}
44+
45+// require binds iface at min(announced, supported) and fails with a
46+// *CapabilityError when it is absent or older than minimum.
47+func (c *Conn) require[P wl.Proxy](iface string, supported, minimum uint32, newProxy func(*wl.Context) P) (P, uint32, error) {
48+ p, v, err := c.Bind(iface, supported, newProxy)
49+ if err != nil {
50+ var zero P
51+ return zero, 0, &CapabilityError{Name: iface, Cause: err}
52+ }
53+ if v < minimum {
54+ _ = c.wlctx // the proxy stays bound; it is unusable below minimum
55+ var zero P
56+ return zero, 0, &CapabilityError{Name: iface, Cause: fmt.Errorf("requires version %d, negotiated %d", minimum, v)}
57+ }
58+ return p, v, nil
59+}
60+
61+// ensureCore binds wl_compositor, linux-dmabuf (v4) and drm-syncobj, and the
62+// optional viewporter and fractional-scale managers.
63+func (c *Conn) ensureCore() error {
64+ g := &c.g
65+ var err error
66+ if g.compositor == nil {
67+ if g.compositor, _, err = c.require(wayland.CompositorInterface, 6, 4, wayland.NewCompositor); err != nil {
68+ return err
69+ }
70+ }
71+ if g.dmabuf == nil {
72+ if g.dmabuf, _, err = c.require(linuxdmabuf.LinuxDmabufInterface, 4, 4, linuxdmabuf.NewLinuxDmabuf); err != nil {
73+ return err
74+ }
75+ }
76+ if g.syncobj == nil {
77+ if g.syncobj, _, err = c.require(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, linuxdrmsyncobj.NewWpLinuxDrmSyncobjManager); err != nil {
78+ return err
79+ }
80+ }
81+ if !g.triedViewport {
82+ g.triedViewport = true
83+ v, _, e := c.Bind(viewporter.WpViewporterInterface, 1, viewporter.NewWpViewporter)
84+ if e != nil && !errors.Is(e, ErrGlobalNotFound) {
85+ return e
86+ }
87+ g.viewporter = v
88+ }
89+ if !g.triedFScale {
90+ g.triedFScale = true
91+ v, _, e := c.Bind(fractionalscale.WpFractionalScaleManagerInterface, 1, fractionalscale.NewWpFractionalScaleManager)
92+ if e != nil && !errors.Is(e, ErrGlobalNotFound) {
93+ return e
94+ }
95+ g.fscale = v
96+ }
97+ return nil
98+}
99+
100+func (c *Conn) ensureXdg() error {
101+ if c.g.xdg != nil {
102+ return nil
103+ }
104+ q := c.q
105+ x, _, err := c.require(xdgshell.XdgWmBaseInterface, 6, 1, func(ctx *wl.Context) *xdgshell.XdgWmBase {
106+ x := xdgshell.NewXdgWmBase(ctx)
107+ x.OnPing(func(serial uint32) {
108+ e := event{kind: evPing, serial: serial}
109+ q.post(&e)
110+ })
111+ return x
112+ })
113+ c.g.xdg = x
114+ return err
115+}
116+
117+func (c *Conn) ensureLayerShell() error {
118+ if c.g.layer != nil {
119+ return nil
120+ }
121+ l, v, err := c.require(wlrlayershell.LayerShellInterface, 4, 1, wlrlayershell.NewLayerShell)
122+ c.g.layer, c.g.layerVersion = l, v
123+ return err
124+}
125+
126+func (c *Conn) ensureLockManager() error {
127+ if c.g.lockMgr != nil {
128+ return nil
129+ }
130+ m, _, err := c.require(extsessionlock.ExtSessionLockManagerInterface, 1, 1, extsessionlock.NewExtSessionLockManager)
131+ c.g.lockMgr = m
132+ return err
133+}
@@ -0,0 +1,133 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8+ "github.com/bnema/go-wayland-bindings/client/fractionalscale"
9+ "github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
10+ "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
11+ "github.com/bnema/go-wayland-bindings/client/viewporter"
12+ "github.com/bnema/go-wayland-bindings/client/wayland"
13+ "github.com/bnema/go-wayland-bindings/client/wlrlayershell"
14+ "github.com/bnema/go-wayland-bindings/client/xdgshell"
15+ "github.com/bnema/wlturbo/wl"
16+)
17+
18+// CapabilityError reports a missing or too old required protocol global.
19+type CapabilityError struct {
20+ Name string
21+ Cause error
22+}
23+
24+func (e *CapabilityError) Error() string {
25+ return fmt.Sprintf("neferclient: capability %s: %v", e.Name, e.Cause)
26+}
27+func (e *CapabilityError) Unwrap() error { return e.Cause }
28+
29+// globals holds the protocol globals bound on first use. Owner goroutine only.
30+type globals struct {
31+ compositor *wayland.Compositor
32+ dmabuf *linuxdmabuf.LinuxDmabuf
33+ syncobj *linuxdrmsyncobj.WpLinuxDrmSyncobjManager
34+ xdg *xdgshell.XdgWmBase
35+ layer *wlrlayershell.LayerShell
36+ lockMgr *extsessionlock.ExtSessionLockManager
37+ viewporter *viewporter.WpViewporter
38+ fscale *fractionalscale.WpFractionalScaleManager
39+
40+ layerVersion uint32
41+ triedViewport bool
42+ triedFScale bool
43+}
44+
45+// require binds iface at min(announced, supported) and fails with a
46+// *CapabilityError when it is absent or older than minimum.
47+func (c *Conn) require[P wl.Proxy](iface string, supported, minimum uint32, newProxy func(*wl.Context) P) (P, uint32, error) {
48+ p, v, err := c.Bind(iface, supported, newProxy)
49+ if err != nil {
50+ var zero P
51+ return zero, 0, &CapabilityError{Name: iface, Cause: err}
52+ }
53+ if v < minimum {
54+ _ = c.wlctx // the proxy stays bound; it is unusable below minimum
55+ var zero P
56+ return zero, 0, &CapabilityError{Name: iface, Cause: fmt.Errorf("requires version %d, negotiated %d", minimum, v)}
57+ }
58+ return p, v, nil
59+}
60+
61+// ensureCore binds wl_compositor, linux-dmabuf (v4) and drm-syncobj, and the
62+// optional viewporter and fractional-scale managers.
63+func (c *Conn) ensureCore() error {
64+ g := &c.g
65+ var err error
66+ if g.compositor == nil {
67+ if g.compositor, _, err = c.require(wayland.CompositorInterface, 6, 4, wayland.NewCompositor); err != nil {
68+ return err
69+ }
70+ }
71+ if g.dmabuf == nil {
72+ if g.dmabuf, _, err = c.require(linuxdmabuf.LinuxDmabufInterface, 4, 4, linuxdmabuf.NewLinuxDmabuf); err != nil {
73+ return err
74+ }
75+ }
76+ if g.syncobj == nil {
77+ if g.syncobj, _, err = c.require(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, linuxdrmsyncobj.NewWpLinuxDrmSyncobjManager); err != nil {
78+ return err
79+ }
80+ }
81+ if !g.triedViewport {
82+ g.triedViewport = true
83+ v, _, e := c.Bind(viewporter.WpViewporterInterface, 1, viewporter.NewWpViewporter)
84+ if e != nil && !errors.Is(e, ErrGlobalNotFound) {
85+ return e
86+ }
87+ g.viewporter = v
88+ }
89+ if !g.triedFScale {
90+ g.triedFScale = true
91+ v, _, e := c.Bind(fractionalscale.WpFractionalScaleManagerInterface, 1, fractionalscale.NewWpFractionalScaleManager)
92+ if e != nil && !errors.Is(e, ErrGlobalNotFound) {
93+ return e
94+ }
95+ g.fscale = v
96+ }
97+ return nil
98+}
99+
100+func (c *Conn) ensureXdg() error {
101+ if c.g.xdg != nil {
102+ return nil
103+ }
104+ q := c.q
105+ x, _, err := c.require(xdgshell.XdgWmBaseInterface, 6, 1, func(ctx *wl.Context) *xdgshell.XdgWmBase {
106+ x := xdgshell.NewXdgWmBase(ctx)
107+ x.OnPing(func(serial uint32) {
108+ e := event{kind: evPing, serial: serial}
109+ q.post(&e)
110+ })
111+ return x
112+ })
113+ c.g.xdg = x
114+ return err
115+}
116+
117+func (c *Conn) ensureLayerShell() error {
118+ if c.g.layer != nil {
119+ return nil
120+ }
121+ l, v, err := c.require(wlrlayershell.LayerShellInterface, 4, 1, wlrlayershell.NewLayerShell)
122+ c.g.layer, c.g.layerVersion = l, v
123+ return err
124+}
125+
126+func (c *Conn) ensureLockManager() error {
127+ if c.g.lockMgr != nil {
128+ return nil
129+ }
130+ m, _, err := c.require(extsessionlock.ExtSessionLockManagerInterface, 1, 1, extsessionlock.NewExtSessionLockManager)
131+ c.g.lockMgr = m
132+ return err
133+}
A lock.go +147 -0
@@ -0,0 +1,147 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8+ "github.com/bnema/wlturbo/wl"
9+)
10+
11+// Lock is an ext-session-lock-v1 session lock. It reports [Handler.Locked]
12+// once every output is protected and [Handler.LockFinished] if the compositor
13+// refuses or ends it. Closing the connection or destroying surfaces never
14+// unlocks; only [Lock.Unlock] after Locked does. Owner goroutine only.
15+type Lock struct {
16+ c *Conn
17+ gen uint32
18+ obj *extsessionlock.ExtSessionLock
19+ locked bool
20+ finished bool
21+ done bool
22+}
23+
24+// Lock requests a session lock. At most one lock is live per connection.
25+// The compositor answers through Handler.Locked or Handler.LockFinished.
26+func (c *Conn) Lock() (*Lock, error) {
27+ if c.closed {
28+ return nil, ErrClosed
29+ }
30+ if c.curLock != nil {
31+ return nil, errors.New("neferclient: session lock already acquired")
32+ }
33+ if err := c.ensureCore(); err != nil {
34+ return nil, err
35+ }
36+ if err := c.ensureLockManager(); err != nil {
37+ return nil, err
38+ }
39+ c.pause() // locked/finished may follow the request immediately
40+ defer c.resume()
41+ c.nextLock++
42+ l := &Lock{c: c, gen: c.nextLock}
43+ q, gen := c.q, l.gen
44+ obj, err := c.g.lockMgr.Lock()
45+ if err != nil {
46+ return nil, err
47+ }
48+ obj.OnLocked(func() {
49+ e := event{kind: evLocked, id: gen}
50+ q.post(&e)
51+ })
52+ obj.OnFinished(func() {
53+ e := event{kind: evLockFinished, id: gen}
54+ q.post(&e)
55+ })
56+ l.obj = obj
57+ c.curLock = l
58+ return l, nil
59+}
60+
61+// Locked reports whether the compositor confirmed the lock (applied by
62+// Dispatch) and it has not finished.
63+func (l *Lock) Locked() bool { return l.locked && !l.finished && !l.done }
64+
65+// NewSurface creates the lock surface covering the output with the given
66+// registry name (see [Output.Global]). It becomes presentable after the first
67+// Handler.Configure; a lock surface is always opaque and takes all input.
68+func (l *Lock) NewSurface(output uint32) (*Surface, error) {
69+ c := l.c
70+ if c.closed {
71+ return nil, ErrClosed
72+ }
73+ if l.done || l.finished {
74+ return nil, errors.New("neferclient: lock ended")
75+ }
76+ e := c.entry(output)
77+ if e == nil || !e.done {
78+ return nil, fmt.Errorf("neferclient: lock output %d unavailable", output)
79+ }
80+ c.pause()
81+ defer c.resume()
82+ s, err := c.newSurface(roleLock, 0, 0)
83+ if err != nil {
84+ return nil, err
85+ }
86+ ok := false
87+ defer func() {
88+ if !ok {
89+ s.destroy()
90+ }
91+ }()
92+ if s.lockSurf, err = l.obj.GetLockSurface(s.surf, e.proxy); err != nil {
93+ return nil, err
94+ }
95+ s.lock = l
96+ q, sid := c.q, uint32(s.id)
97+ s.lockSurf.OnConfigure(func(serial, w, h uint32) {
98+ ev := event{kind: evLockConfigure, id: sid, serial: serial, a: int32(w), b: int32(h)}
99+ q.post(&ev)
100+ })
101+ // ext-session-lock forbids the initial empty commit: the configure comes
102+ // with get_lock_surface and the first commit carries a buffer.
103+ if err = s.finishSurface(); err != nil {
104+ return nil, err
105+ }
106+ ok = true
107+ return s, nil
108+}
109+
110+// Unlock sends unlock_and_destroy and roundtrips so the compositor processed
111+// it before the caller closes anything. It is only valid after Handler.Locked.
112+// The lock surfaces must be closed afterwards (or before: both are legal).
113+func (l *Lock) Unlock() error {
114+ if l.done {
115+ return errors.New("neferclient: lock already destroyed")
116+ }
117+ if !l.Locked() {
118+ return errors.New("neferclient: unlock before the locked event")
119+ }
120+ if err := l.obj.UnlockAndDestroy(); err != nil {
121+ return err
122+ }
123+ l.done = true
124+ l.c.curLock = nil
125+ return l.c.Roundtrip()
126+}
127+
128+// Close destroys a lock that never reached Locked (refused or abandoned). It
129+// is an error after Locked: a locked session is only ended by Unlock, and
130+// leaving the connection closed keeps the session locked.
131+func (l *Lock) Close() error {
132+ if l.done {
133+ return nil
134+ }
135+ if l.locked && !l.finished {
136+ return errors.New("neferclient: lock is locked: use Unlock")
137+ }
138+ l.done = true
139+ l.c.curLock = nil
140+ var err error
141+ if l.obj != nil {
142+ err = l.obj.Destroy()
143+ }
144+ return err
145+}
146+
147+var _ wl.Proxy = (*frameCallback)(nil)
@@ -0,0 +1,147 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8+ "github.com/bnema/wlturbo/wl"
9+)
10+
11+// Lock is an ext-session-lock-v1 session lock. It reports [Handler.Locked]
12+// once every output is protected and [Handler.LockFinished] if the compositor
13+// refuses or ends it. Closing the connection or destroying surfaces never
14+// unlocks; only [Lock.Unlock] after Locked does. Owner goroutine only.
15+type Lock struct {
16+ c *Conn
17+ gen uint32
18+ obj *extsessionlock.ExtSessionLock
19+ locked bool
20+ finished bool
21+ done bool
22+}
23+
24+// Lock requests a session lock. At most one lock is live per connection.
25+// The compositor answers through Handler.Locked or Handler.LockFinished.
26+func (c *Conn) Lock() (*Lock, error) {
27+ if c.closed {
28+ return nil, ErrClosed
29+ }
30+ if c.curLock != nil {
31+ return nil, errors.New("neferclient: session lock already acquired")
32+ }
33+ if err := c.ensureCore(); err != nil {
34+ return nil, err
35+ }
36+ if err := c.ensureLockManager(); err != nil {
37+ return nil, err
38+ }
39+ c.pause() // locked/finished may follow the request immediately
40+ defer c.resume()
41+ c.nextLock++
42+ l := &Lock{c: c, gen: c.nextLock}
43+ q, gen := c.q, l.gen
44+ obj, err := c.g.lockMgr.Lock()
45+ if err != nil {
46+ return nil, err
47+ }
48+ obj.OnLocked(func() {
49+ e := event{kind: evLocked, id: gen}
50+ q.post(&e)
51+ })
52+ obj.OnFinished(func() {
53+ e := event{kind: evLockFinished, id: gen}
54+ q.post(&e)
55+ })
56+ l.obj = obj
57+ c.curLock = l
58+ return l, nil
59+}
60+
61+// Locked reports whether the compositor confirmed the lock (applied by
62+// Dispatch) and it has not finished.
63+func (l *Lock) Locked() bool { return l.locked && !l.finished && !l.done }
64+
65+// NewSurface creates the lock surface covering the output with the given
66+// registry name (see [Output.Global]). It becomes presentable after the first
67+// Handler.Configure; a lock surface is always opaque and takes all input.
68+func (l *Lock) NewSurface(output uint32) (*Surface, error) {
69+ c := l.c
70+ if c.closed {
71+ return nil, ErrClosed
72+ }
73+ if l.done || l.finished {
74+ return nil, errors.New("neferclient: lock ended")
75+ }
76+ e := c.entry(output)
77+ if e == nil || !e.done {
78+ return nil, fmt.Errorf("neferclient: lock output %d unavailable", output)
79+ }
80+ c.pause()
81+ defer c.resume()
82+ s, err := c.newSurface(roleLock, 0, 0)
83+ if err != nil {
84+ return nil, err
85+ }
86+ ok := false
87+ defer func() {
88+ if !ok {
89+ s.destroy()
90+ }
91+ }()
92+ if s.lockSurf, err = l.obj.GetLockSurface(s.surf, e.proxy); err != nil {
93+ return nil, err
94+ }
95+ s.lock = l
96+ q, sid := c.q, uint32(s.id)
97+ s.lockSurf.OnConfigure(func(serial, w, h uint32) {
98+ ev := event{kind: evLockConfigure, id: sid, serial: serial, a: int32(w), b: int32(h)}
99+ q.post(&ev)
100+ })
101+ // ext-session-lock forbids the initial empty commit: the configure comes
102+ // with get_lock_surface and the first commit carries a buffer.
103+ if err = s.finishSurface(); err != nil {
104+ return nil, err
105+ }
106+ ok = true
107+ return s, nil
108+}
109+
110+// Unlock sends unlock_and_destroy and roundtrips so the compositor processed
111+// it before the caller closes anything. It is only valid after Handler.Locked.
112+// The lock surfaces must be closed afterwards (or before: both are legal).
113+func (l *Lock) Unlock() error {
114+ if l.done {
115+ return errors.New("neferclient: lock already destroyed")
116+ }
117+ if !l.Locked() {
118+ return errors.New("neferclient: unlock before the locked event")
119+ }
120+ if err := l.obj.UnlockAndDestroy(); err != nil {
121+ return err
122+ }
123+ l.done = true
124+ l.c.curLock = nil
125+ return l.c.Roundtrip()
126+}
127+
128+// Close destroys a lock that never reached Locked (refused or abandoned). It
129+// is an error after Locked: a locked session is only ended by Unlock, and
130+// leaving the connection closed keeps the session locked.
131+func (l *Lock) Close() error {
132+ if l.done {
133+ return nil
134+ }
135+ if l.locked && !l.finished {
136+ return errors.New("neferclient: lock is locked: use Unlock")
137+ }
138+ l.done = true
139+ l.c.curLock = nil
140+ var err error
141+ if l.obj != nil {
142+ err = l.obj.Destroy()
143+ }
144+ return err
145+}
146+
147+var _ wl.Proxy = (*frameCallback)(nil)
A surface.go +760 -0
@@ -0,0 +1,760 @@
1+package neferclient
2+
3+import (
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.
24+type LayerLevel uint8
25+
26+const (
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.
34+type Anchor uint8
35+
36+const (
37+ AnchorTop Anchor = 1 << iota
38+ AnchorBottom
39+ AnchorLeft
40+ AnchorRight
41+)
42+
43+const 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.
47+type KeyboardMode uint8
48+
49+const (
50+ KeyboardNone KeyboardMode = iota
51+ KeyboardExclusive
52+ KeyboardOnDemand
53+)
54+
55+// maxLayerDimension bounds a compositor-assigned layer surface extent.
56+const maxLayerDimension = 16384
57+
58+// DefaultLayerNamespace is used when [LayerConfig.Namespace] is empty.
59+const 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.
64+type 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+
80+type surfaceRole uint8
81+
82+const (
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.
90+type 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
117+ opaqueSet bool
118+ opaque *wayland.Region
119+ opaqueW, opaqueH int32
120+
121+ cb *frameCallback
122+
123+ inputRects []Rect
124+ inputCustom bool
125+
126+ buffers map[uint64]*wayland.Buffer
127+ timelines map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline
128+
129+ fb Feedback
130+ table []Format // current format table
131+ pending Feedback // round in progress; Formats reused across rounds
132+ fbDone bool
133+}
134+
135+// frameCallback is a reusable wl_callback: the library keeps a single frame
136+// callback pending per surface, so one proxy object serves all of them.
137+type frameCallback struct {
138+ wl.BaseProxy
139+ q *queue
140+ id SurfaceID
141+}
142+
143+func (f *frameCallback) EventSignature(op uint16) (string, bool) {
144+ if op == 0 {
145+ return "uint,", true
146+ }
147+ return "", false
148+}
149+
150+func (f *frameCallback) Dispatch(ev *wl.Event) {
151+ if ev.Opcode != 0 {
152+ return
153+ }
154+ _ = ev.Uint32()
155+ f.Context().Unregister(f)
156+ e := event{kind: evFrame, id: uint32(f.id)}
157+ f.q.post(&e)
158+}
159+
160+// ID returns the surface's identifier used in Handler callbacks.
161+func (s *Surface) ID() SurfaceID { return s.id }
162+
163+// Size returns the current logical size and scale (1 until announced). It is
164+// zero until the first configure.
165+func (s *Surface) Size() (w, h int32, scale float64) { return s.width, s.height, s.scale }
166+
167+// Feedback returns the latest complete dmabuf feedback, or nil before the
168+// first Handler.FeedbackDone. The value and its slice stay valid until the
169+// next FeedbackDone.
170+func (s *Surface) Feedback() *Feedback {
171+ if !s.fbDone {
172+ return nil
173+ }
174+ return &s.fb
175+}
176+
177+// PhysicalSize returns the buffer size in pixels for the current logical size
178+// and scale.
179+func (s *Surface) PhysicalSize() (int32, int32, error) {
180+ if s.scale <= 0 || s.width <= 0 || s.height <= 0 {
181+ return 0, 0, errors.New("neferclient: invalid surface geometry")
182+ }
183+ if s.scale != math.Trunc(s.scale) && s.viewport == nil {
184+ return 0, 0, &CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %g requires viewporter", s.scale)}
185+ }
186+ // Ignore sub-nanopixel floating noise at exact integer boundaries.
187+ x, y := math.Ceil(float64(s.width)*s.scale-1e-9), math.Ceil(float64(s.height)*s.scale-1e-9)
188+ if x > math.MaxInt32 || y > math.MaxInt32 {
189+ return 0, 0, errors.New("neferclient: buffer dimensions overflow")
190+ }
191+ return int32(x), int32(y), nil
192+}
193+
194+func validateLayer(o *LayerConfig) error {
195+ switch {
196+ case o.Level < LayerBackground || o.Level > LayerOverlay:
197+ return fmt.Errorf("neferclient: invalid layer level %d", o.Level)
198+ case o.Anchors&^anchorAll != 0:
199+ return fmt.Errorf("neferclient: invalid layer anchors %#x", uint8(o.Anchors))
200+ case o.Keyboard > KeyboardOnDemand:
201+ return fmt.Errorf("neferclient: invalid keyboard mode %d", o.Keyboard)
202+ case o.ExclusiveZone < -1:
203+ return fmt.Errorf("neferclient: invalid exclusive zone %d", o.ExclusiveZone)
204+ case strings.ContainsRune(o.Namespace, 0), strings.ContainsRune(o.Output, 0):
205+ return errors.New("neferclient: layer namespace or output contains NUL")
206+ case o.Width < 0 || o.Height < 0:
207+ return errors.New("neferclient: negative layer size")
208+ }
209+ if o.Anchors&(AnchorLeft|AnchorRight) != AnchorLeft|AnchorRight && o.Width == 0 ||
210+ o.Anchors&(AnchorTop|AnchorBottom) != AnchorTop|AnchorBottom && o.Height == 0 {
211+ return errors.New("neferclient: layer size required on axes not anchored to both edges")
212+ }
213+ return validateRects(o.InputRects)
214+}
215+
216+func validateRects(rects []Rect) error {
217+ for i, r := range rects {
218+ if r.Width <= 0 || r.Height <= 0 {
219+ return fmt.Errorf("neferclient: input rect %d has non-positive size %dx%d", i, r.Width, r.Height)
220+ }
221+ if int64(r.X)+int64(r.Width) > math.MaxInt32 || int64(r.Y)+int64(r.Height) > math.MaxInt32 {
222+ return fmt.Errorf("neferclient: input rect %d overflows", i)
223+ }
224+ }
225+ return nil
226+}
227+
228+// newSurface creates the wl_surface and the add-on objects shared by every
229+// role. The reader must be paused: handlers are installed after the requests.
230+func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) {
231+ if c.closed {
232+ return nil, ErrClosed
233+ }
234+ if err := c.ensureCore(); err != nil {
235+ return nil, err
236+ }
237+ g := &c.g
238+ c.nextSurf++
239+ s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1,
240+ buffers: map[uint64]*wayland.Buffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}}
241+ ok := false
242+ defer func() {
243+ if !ok {
244+ s.destroy()
245+ }
246+ }()
247+ var err error
248+ if s.surf, err = g.compositor.CreateSurface(); err != nil {
249+ return nil, err
250+ }
251+ q, sid := c.q, uint32(s.id)
252+ s.surf.OnPreferredBufferScale(func(f int32) {
253+ e := event{kind: evBufferScale, id: sid, a: f}
254+ q.post(&e)
255+ })
256+ if g.viewporter != nil {
257+ if s.viewport, err = g.viewporter.GetViewport(s.surf); err != nil {
258+ return nil, err
259+ }
260+ }
261+ if g.fscale != nil {
262+ if s.frac, err = g.fscale.GetFractionalScale(s.surf); err != nil {
263+ return nil, err
264+ }
265+ s.frac.OnPreferredScale(func(n uint32) {
266+ e := event{kind: evPreferredScale, id: sid, a: int32(n)}
267+ q.post(&e)
268+ })
269+ }
270+ // The role objects come next (callers); sync surface and feedback after.
271+ s.cb = &frameCallback{q: q, id: s.id}
272+ s.cb.SetContext(c.wlctx)
273+ c.surfaces[s.id] = s
274+ ok = true
275+ return s, nil
276+}
277+
278+// finishSurface creates the explicit-sync and feedback objects once the role
279+// exists.
280+func (s *Surface) finishSurface() error {
281+ c := s.c
282+ var err error
283+ if s.sync, err = c.g.syncobj.GetSurface(s.surf); err != nil {
284+ return err
285+ }
286+ if s.feedback, err = c.g.dmabuf.GetDefaultFeedback(); err != nil {
287+ return err
288+ }
289+ s.watchFeedback()
290+ return nil
291+}
292+
293+func (s *Surface) watchFeedback() {
294+ q, sid := s.c.q, uint32(s.id)
295+ s.feedback.OnFormatTable(func(fd *wl.OwnedFD, size uint32) {
296+ n, err := fd.Take()
297+ if err != nil {
298+ e := event{kind: evBadEvent, id: sid, a: badFeedbackFD}
299+ q.post(&e)
300+ return
301+ }
302+ e := event{kind: evFeedbackTable, id: sid, fd: int32(n), flags: size}
303+ q.post(&e)
304+ })
305+ s.feedback.OnMainDevice(func(dev []byte) {
306+ if len(dev) != 8 {
307+ e := event{kind: evBadEvent, id: sid, a: badMainDevice}
308+ q.post(&e)
309+ return
310+ }
311+ e := event{kind: evFeedbackMainDevice, id: sid, dev: binary.NativeEndian.Uint64(dev)}
312+ q.post(&e)
313+ })
314+ s.feedback.OnTrancheFormats(func(data []byte) {
315+ if len(data)%2 != 0 {
316+ e := event{kind: evBadEvent, id: sid, a: badTranche}
317+ q.post(&e)
318+ return
319+ }
320+ for len(data) > 0 { // chunks of up to maxNameLen/2 indices
321+ n := min(len(data), maxNameLen)
322+ e := event{kind: evFeedbackFormats, id: sid}
323+ e.setBytes(data[:n])
324+ q.post(&e)
325+ data = data[n:]
326+ }
327+ })
328+ s.feedback.OnDone(func() {
329+ e := event{kind: evFeedbackDone, id: sid}
330+ q.post(&e)
331+ })
332+}
333+
334+// Codes of evBadEvent.
335+const (
336+ badFeedbackFD int32 = iota + 1
337+ badMainDevice
338+ badTranche
339+ badConfigure
340+)
341+
342+var badEventErrors = [...]string{
343+ badFeedbackFD: "neferclient: dmabuf format table descriptor unavailable",
344+ badMainDevice: "neferclient: invalid dmabuf main_device length",
345+ badTranche: "neferclient: odd dmabuf tranche format list",
346+ badConfigure: "neferclient: layer configure size out of range",
347+}
348+
349+// NewLayerSurface creates a wlr-layer-shell surface. It becomes presentable
350+// after the first Handler.Configure.
351+func (c *Conn) NewLayerSurface(cfg LayerConfig) (*Surface, error) {
352+ if c.closed {
353+ return nil, ErrClosed
354+ }
355+ if err := validateLayer(&cfg); err != nil {
356+ return nil, err
357+ }
358+ if err := c.ensureLayerShell(); err != nil {
359+ return nil, err
360+ }
361+ if cfg.Keyboard == KeyboardOnDemand && c.g.layerVersion < 4 {
362+ return nil, &CapabilityError{Name: wlrlayershell.LayerShellInterface, Cause: fmt.Errorf("on-demand keyboard requires version 4, negotiated %d", c.g.layerVersion)}
363+ }
364+ var out *wayland.Output
365+ if cfg.Output != "" {
366+ for _, o := range c.outputs {
367+ if o.done && o.pub.Name == cfg.Output {
368+ out = o.proxy
369+ break
370+ }
371+ }
372+ if out == nil {
373+ return nil, &CapabilityError{Name: wayland.OutputInterface, Cause: fmt.Errorf("output %q not found", cfg.Output)}
374+ }
375+ }
376+ c.pause()
377+ defer c.resume()
378+ s, err := c.newSurface(roleLayer, cfg.Width, cfg.Height)
379+ if err != nil {
380+ return nil, err
381+ }
382+ ok := false
383+ defer func() {
384+ if !ok {
385+ s.destroy()
386+ }
387+ }()
388+ ns := cfg.Namespace
389+ if ns == "" {
390+ ns = DefaultLayerNamespace
391+ }
392+ level := [...]uint32{LayerBackground: wlrlayershell.LAYER_BACKGROUND, LayerBottom: wlrlayershell.LAYER_BOTTOM,
393+ LayerTop: wlrlayershell.LAYER_TOP, LayerOverlay: wlrlayershell.LAYER_OVERLAY}[cfg.Level]
394+ if s.layer, err = c.g.layer.GetLayerSurface(s.surf, out, level, ns); err != nil {
395+ return nil, err
396+ }
397+ q, sid := c.q, uint32(s.id)
398+ s.layer.OnClosed(func() {
399+ e := event{kind: evClosed, id: sid}
400+ q.post(&e)
401+ })
402+ s.layer.OnConfigure(func(serial, w, h uint32) {
403+ if w > maxLayerDimension || h > maxLayerDimension {
404+ e := event{kind: evBadEvent, id: sid, a: badConfigure}
405+ q.post(&e)
406+ return
407+ }
408+ e := event{kind: evLayerConfigure, id: sid, serial: serial, a: int32(w), b: int32(h)}
409+ q.post(&e)
410+ })
411+ // Opposite anchors let the compositor assign that axis (size 0).
412+ reqW, reqH := uint32(cfg.Width), uint32(cfg.Height)
413+ if cfg.Anchors&(AnchorLeft|AnchorRight) == AnchorLeft|AnchorRight {
414+ reqW = 0
415+ }
416+ if cfg.Anchors&(AnchorTop|AnchorBottom) == AnchorTop|AnchorBottom {
417+ reqH = 0
418+ }
419+ var anchor uint32
420+ for _, m := range [...][2]uint32{{uint32(AnchorTop), wlrlayershell.ANCHOR_TOP}, {uint32(AnchorBottom), wlrlayershell.ANCHOR_BOTTOM},
421+ {uint32(AnchorLeft), wlrlayershell.ANCHOR_LEFT}, {uint32(AnchorRight), wlrlayershell.ANCHOR_RIGHT}} {
422+ if uint32(cfg.Anchors)&m[0] != 0 {
423+ anchor |= m[1]
424+ }
425+ }
426+ for _, e := range []error{
427+ s.layer.SetSize(reqW, reqH),
428+ s.layer.SetAnchor(anchor),
429+ s.layer.SetExclusiveZone(cfg.ExclusiveZone),
430+ s.layer.SetMargin(cfg.Margin[0], cfg.Margin[1], cfg.Margin[2], cfg.Margin[3]),
431+ s.layer.SetKeyboardInteractivity(uint32(cfg.Keyboard)),
432+ } {
433+ if e != nil {
434+ return nil, e
435+ }
436+ }
437+ if cfg.InputRects != nil {
438+ if _, err = s.stageInputRects(cfg.InputRects); err != nil {
439+ return nil, err
440+ }
441+ }
442+ if err = s.finishSurface(); err != nil {
443+ return nil, err
444+ }
445+ if err = s.surf.Commit(); err != nil { // initial empty commit asks for configure
446+ return nil, err
447+ }
448+ ok = true
449+ return s, nil
450+}
451+
452+// NewToplevel creates an xdg-toplevel window with the given title and initial
453+// logical size. It becomes presentable after the first Handler.Configure.
454+func (c *Conn) NewToplevel(title string, w, h int32) (*Surface, error) {
455+ if c.closed {
456+ return nil, ErrClosed
457+ }
458+ if w <= 0 || h <= 0 {
459+ return nil, fmt.Errorf("neferclient: invalid logical size %dx%d", w, h)
460+ }
461+ if err := c.ensureXdg(); err != nil {
462+ return nil, err
463+ }
464+ c.pause()
465+ defer c.resume()
466+ s, err := c.newSurface(roleToplevel, w, h)
467+ if err != nil {
468+ return nil, err
469+ }
470+ ok := false
471+ defer func() {
472+ if !ok {
473+ s.destroy()
474+ }
475+ }()
476+ if s.xdg, err = c.g.xdg.GetXdgSurface(s.surf); err != nil {
477+ return nil, err
478+ }
479+ if s.toplevel, err = s.xdg.GetToplevel(); err != nil {
480+ return nil, err
481+ }
482+ q, sid := c.q, uint32(s.id)
483+ s.toplevel.OnClose(func() {
484+ e := event{kind: evClosed, id: sid}
485+ q.post(&e)
486+ })
487+ s.toplevel.OnConfigure(func(w, h int32, _ []byte) {
488+ e := event{kind: evXdgToplevel, id: sid, a: w, b: h}
489+ q.post(&e)
490+ })
491+ s.xdg.OnConfigure(func(serial uint32) {
492+ e := event{kind: evXdgConfigure, id: sid, serial: serial}
493+ q.post(&e)
494+ })
495+ if err = s.toplevel.SetTitle(title); err != nil {
496+ return nil, err
497+ }
498+ if err = s.finishSurface(); err != nil {
499+ return nil, err
500+ }
501+ if err = s.surf.Commit(); err != nil {
502+ return nil, err
503+ }
504+ ok = true
505+ return s, nil
506+}
507+
508+// applySurface handles every surface, lock and xdg_wm_base event on the owner.
509+func (c *Conn) applySurface(ev *event, h Handler) {
510+ defer ev.closeFD()
511+ switch ev.kind {
512+ case evPing:
513+ if c.g.xdg != nil {
514+ if err := c.g.xdg.Pong(ev.serial); err != nil {
515+ h.Error(err)
516+ }
517+ }
518+ return
519+ case evLocked, evLockFinished:
520+ l := c.curLock
521+ if l == nil || l.gen != ev.id {
522+ return
523+ }
524+ if ev.kind == evLocked {
525+ l.locked = true
526+ h.Locked()
527+ } else {
528+ l.finished = true
529+ h.LockFinished()
530+ }
531+ return
532+ }
533+ s := c.surfaces[SurfaceID(ev.id)]
534+ if s == nil || s.closed {
535+ return // closed while the event was queued
536+ }
537+ id := s.id
538+ switch ev.kind {
539+ case evBadEvent:
540+ h.Error(errors.New(badEventErrors[ev.a]))
541+ case evClosed:
542+ h.Closed(id)
543+ case evXdgToplevel:
544+ s.pendW, s.pendH = ev.a, ev.b
545+ case evXdgConfigure:
546+ if s.pendW > 0 && s.pendH > 0 {
547+ s.width, s.height = s.pendW, s.pendH
548+ }
549+ s.configure(s.xdg.AckConfigure(ev.serial), h)
550+ case evLayerConfigure:
551+ if ev.a > 0 {
552+ s.width = ev.a
553+ }
554+ if ev.b > 0 {
555+ s.height = ev.b
556+ }
557+ s.configure(s.layer.AckConfigure(ev.serial), h)
558+ case evLockConfigure:
559+ s.width, s.height = ev.a, ev.b
560+ s.configure(s.lockSurf.AckConfigure(ev.serial), h)
561+ case evFrame:
562+ s.frameReady = true
563+ h.Frame(id)
564+ case evPreferredScale:
565+ if ev.a <= 0 {
566+ h.Error(errors.New("neferclient: invalid fractional scale zero"))
567+ return
568+ }
569+ s.setScale(float64(ev.a)/120, h)
570+ case evBufferScale:
571+ if s.frac == nil && ev.a > 0 {
572+ s.setScale(float64(ev.a), h)
573+ }
574+ case evFeedbackTable:
575+ if err := s.readTable(int(ev.fd), ev.flags); err != nil {
576+ h.Error(err)
577+ }
578+ case evFeedbackMainDevice:
579+ s.pending.MainDevice = ev.dev
580+ case evFeedbackFormats:
581+ b := ev.nameBytes()
582+ for i := 0; i+1 < len(b); i += 2 {
583+ idx := int(binary.NativeEndian.Uint16(b[i:]))
584+ if idx >= len(s.table) {
585+ h.Error(fmt.Errorf("neferclient: dmabuf format table index %d out of range", idx))
586+ continue
587+ }
588+ s.pending.Formats = append(s.pending.Formats, s.table[idx])
589+ }
590+ case evFeedbackDone:
591+ s.fb, s.pending = s.pending, Feedback{Formats: s.fb.Formats[:0]}
592+ s.fbDone = true
593+ h.FeedbackDone(id)
594+ }
595+}
596+
597+func (s *Surface) configure(ackErr error, h Handler) {
598+ if ackErr != nil {
599+ h.Error(ackErr)
600+ return
601+ }
602+ if !s.configured {
603+ s.configured, s.frameReady = true, true
604+ }
605+ h.Configure(s.id, s.width, s.height)
606+}
607+
608+func (s *Surface) setScale(v float64, h Handler) {
609+ if v == s.scale {
610+ return
611+ }
612+ s.scale = v
613+ h.Scale(s.id, v)
614+}
615+
616+// readTable snapshots the format table with pread (no mapping of
617+// compositor-owned memory) and always consumes fd.
618+func (s *Surface) readTable(fd int, size uint32) error {
619+ defer unix.Close(fd)
620+ if size%16 != 0 || size > 16<<20 {
621+ return fmt.Errorf("neferclient: invalid dmabuf format table size %d", size)
622+ }
623+ c := s.c
624+ c.tableBuf = slices.Grow(c.tableBuf[:0], int(size))[:size]
625+ b := c.tableBuf
626+ for off := 0; off < len(b); {
627+ n, err := unix.Pread(fd, b[off:], int64(off))
628+ if err == unix.EINTR {
629+ continue
630+ }
631+ if err != nil {
632+ return fmt.Errorf("neferclient: read dmabuf format table: %w", err)
633+ }
634+ if n == 0 {
635+ return errors.New("neferclient: truncated dmabuf format table")
636+ }
637+ off += n
638+ }
639+ s.table = s.table[:0]
640+ for i := 0; i < len(b); i += 16 {
641+ s.table = append(s.table, Format{binary.NativeEndian.Uint32(b[i:]), binary.NativeEndian.Uint64(b[i+8:])})
642+ }
643+ return nil
644+}
645+
646+// SetInputRegion sets the input region (logical pixels) and commits when it
647+// changed. nil restores the whole surface; a non-nil empty slice passes all
648+// input through. Before the first configure the next commit applies it.
649+func (s *Surface) SetInputRegion(rects []Rect) error {
650+ if s.closed {
651+ return ErrClosed
652+ }
653+ changed, err := s.stageInputRects(rects)
654+ if err != nil || !changed || !s.configured {
655+ return err
656+ }
657+ return s.surf.Commit()
658+}
659+
660+func (s *Surface) stageInputRects(rects []Rect) (bool, error) {
661+ if err := validateRects(rects); err != nil {
662+ return false, err
663+ }
664+ if (rects != nil) == s.inputCustom && slices.Equal(rects, s.inputRects) {
665+ return false, nil
666+ }
667+ if rects == nil {
668+ if err := s.surf.SetInputRegion(nil); err != nil {
669+ return false, err
670+ }
671+ } else {
672+ region, err := s.c.g.compositor.CreateRegion()
673+ if err != nil {
674+ return false, err
675+ }
676+ for _, r := range rects {
677+ if err = region.Add(r.X, r.Y, r.Width, r.Height); err != nil {
678+ break
679+ }
680+ }
681+ if err == nil {
682+ err = s.surf.SetInputRegion(region)
683+ }
684+ _ = region.Destroy()
685+ if err != nil {
686+ return false, err
687+ }
688+ }
689+ s.inputCustom = rects != nil
690+ s.inputRects = append(s.inputRects[:0], rects...)
691+ return true, nil
692+}
693+
694+// Close destroys the surface, its buffers, timelines and role objects,
695+// children before parents. It is safe to call more than once. The caller must
696+// stop using buffers it imported first (the compositor keeps them until it
697+// processes the destruction).
698+func (s *Surface) Close() error {
699+ if s == nil || s.closed {
700+ return nil
701+ }
702+ if s.c.closed {
703+ s.closed = true
704+ return nil
705+ }
706+ return s.destroy()
707+}
708+
709+func (s *Surface) destroy() error {
710+ s.closed = true
711+ delete(s.c.surfaces, s.id)
712+ var errs []error
713+ add := func(err error) {
714+ if err != nil {
715+ errs = append(errs, err)
716+ }
717+ }
718+ for id, b := range s.buffers {
719+ add(b.Destroy())
720+ delete(s.buffers, id)
721+ }
722+ for id, t := range s.timelines {
723+ add(t.Destroy())
724+ delete(s.timelines, id)
725+ }
726+ if s.opaque != nil {
727+ add(s.opaque.Destroy())
728+ }
729+ if s.feedback != nil {
730+ add(s.feedback.Destroy())
731+ }
732+ if s.sync != nil {
733+ add(s.sync.Destroy())
734+ }
735+ if s.lockSurf != nil {
736+ add(s.lockSurf.Destroy())
737+ }
738+ if s.layer != nil {
739+ add(s.layer.Destroy())
740+ }
741+ if s.toplevel != nil {
742+ add(s.toplevel.Destroy())
743+ }
744+ if s.xdg != nil {
745+ add(s.xdg.Destroy())
746+ }
747+ if s.frac != nil {
748+ add(s.frac.Destroy())
749+ }
750+ if s.viewport != nil {
751+ add(s.viewport.Destroy())
752+ }
753+ if s.surf != nil {
754+ add(s.surf.Destroy())
755+ }
756+ if s.cb != nil {
757+ s.c.wlctx.Unregister(s.cb)
758+ }
759+ return errors.Join(errs...)
760+}
@@ -0,0 +1,760 @@
1+package neferclient
2+
3+import (
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.
24+type LayerLevel uint8
25+
26+const (
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.
34+type Anchor uint8
35+
36+const (
37+ AnchorTop Anchor = 1 << iota
38+ AnchorBottom
39+ AnchorLeft
40+ AnchorRight
41+)
42+
43+const 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.
47+type KeyboardMode uint8
48+
49+const (
50+ KeyboardNone KeyboardMode = iota
51+ KeyboardExclusive
52+ KeyboardOnDemand
53+)
54+
55+// maxLayerDimension bounds a compositor-assigned layer surface extent.
56+const maxLayerDimension = 16384
57+
58+// DefaultLayerNamespace is used when [LayerConfig.Namespace] is empty.
59+const 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.
64+type 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+
80+type surfaceRole uint8
81+
82+const (
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.
90+type 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
117+ opaqueSet bool
118+ opaque *wayland.Region
119+ opaqueW, opaqueH int32
120+
121+ cb *frameCallback
122+
123+ inputRects []Rect
124+ inputCustom bool
125+
126+ buffers map[uint64]*wayland.Buffer
127+ timelines map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline
128+
129+ fb Feedback
130+ table []Format // current format table
131+ pending Feedback // round in progress; Formats reused across rounds
132+ fbDone bool
133+}
134+
135+// frameCallback is a reusable wl_callback: the library keeps a single frame
136+// callback pending per surface, so one proxy object serves all of them.
137+type frameCallback struct {
138+ wl.BaseProxy
139+ q *queue
140+ id SurfaceID
141+}
142+
143+func (f *frameCallback) EventSignature(op uint16) (string, bool) {
144+ if op == 0 {
145+ return "uint,", true
146+ }
147+ return "", false
148+}
149+
150+func (f *frameCallback) Dispatch(ev *wl.Event) {
151+ if ev.Opcode != 0 {
152+ return
153+ }
154+ _ = ev.Uint32()
155+ f.Context().Unregister(f)
156+ e := event{kind: evFrame, id: uint32(f.id)}
157+ f.q.post(&e)
158+}
159+
160+// ID returns the surface's identifier used in Handler callbacks.
161+func (s *Surface) ID() SurfaceID { return s.id }
162+
163+// Size returns the current logical size and scale (1 until announced). It is
164+// zero until the first configure.
165+func (s *Surface) Size() (w, h int32, scale float64) { return s.width, s.height, s.scale }
166+
167+// Feedback returns the latest complete dmabuf feedback, or nil before the
168+// first Handler.FeedbackDone. The value and its slice stay valid until the
169+// next FeedbackDone.
170+func (s *Surface) Feedback() *Feedback {
171+ if !s.fbDone {
172+ return nil
173+ }
174+ return &s.fb
175+}
176+
177+// PhysicalSize returns the buffer size in pixels for the current logical size
178+// and scale.
179+func (s *Surface) PhysicalSize() (int32, int32, error) {
180+ if s.scale <= 0 || s.width <= 0 || s.height <= 0 {
181+ return 0, 0, errors.New("neferclient: invalid surface geometry")
182+ }
183+ if s.scale != math.Trunc(s.scale) && s.viewport == nil {
184+ return 0, 0, &CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %g requires viewporter", s.scale)}
185+ }
186+ // Ignore sub-nanopixel floating noise at exact integer boundaries.
187+ x, y := math.Ceil(float64(s.width)*s.scale-1e-9), math.Ceil(float64(s.height)*s.scale-1e-9)
188+ if x > math.MaxInt32 || y > math.MaxInt32 {
189+ return 0, 0, errors.New("neferclient: buffer dimensions overflow")
190+ }
191+ return int32(x), int32(y), nil
192+}
193+
194+func validateLayer(o *LayerConfig) error {
195+ switch {
196+ case o.Level < LayerBackground || o.Level > LayerOverlay:
197+ return fmt.Errorf("neferclient: invalid layer level %d", o.Level)
198+ case o.Anchors&^anchorAll != 0:
199+ return fmt.Errorf("neferclient: invalid layer anchors %#x", uint8(o.Anchors))
200+ case o.Keyboard > KeyboardOnDemand:
201+ return fmt.Errorf("neferclient: invalid keyboard mode %d", o.Keyboard)
202+ case o.ExclusiveZone < -1:
203+ return fmt.Errorf("neferclient: invalid exclusive zone %d", o.ExclusiveZone)
204+ case strings.ContainsRune(o.Namespace, 0), strings.ContainsRune(o.Output, 0):
205+ return errors.New("neferclient: layer namespace or output contains NUL")
206+ case o.Width < 0 || o.Height < 0:
207+ return errors.New("neferclient: negative layer size")
208+ }
209+ if o.Anchors&(AnchorLeft|AnchorRight) != AnchorLeft|AnchorRight && o.Width == 0 ||
210+ o.Anchors&(AnchorTop|AnchorBottom) != AnchorTop|AnchorBottom && o.Height == 0 {
211+ return errors.New("neferclient: layer size required on axes not anchored to both edges")
212+ }
213+ return validateRects(o.InputRects)
214+}
215+
216+func validateRects(rects []Rect) error {
217+ for i, r := range rects {
218+ if r.Width <= 0 || r.Height <= 0 {
219+ return fmt.Errorf("neferclient: input rect %d has non-positive size %dx%d", i, r.Width, r.Height)
220+ }
221+ if int64(r.X)+int64(r.Width) > math.MaxInt32 || int64(r.Y)+int64(r.Height) > math.MaxInt32 {
222+ return fmt.Errorf("neferclient: input rect %d overflows", i)
223+ }
224+ }
225+ return nil
226+}
227+
228+// newSurface creates the wl_surface and the add-on objects shared by every
229+// role. The reader must be paused: handlers are installed after the requests.
230+func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) {
231+ if c.closed {
232+ return nil, ErrClosed
233+ }
234+ if err := c.ensureCore(); err != nil {
235+ return nil, err
236+ }
237+ g := &c.g
238+ c.nextSurf++
239+ s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1,
240+ buffers: map[uint64]*wayland.Buffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}}
241+ ok := false
242+ defer func() {
243+ if !ok {
244+ s.destroy()
245+ }
246+ }()
247+ var err error
248+ if s.surf, err = g.compositor.CreateSurface(); err != nil {
249+ return nil, err
250+ }
251+ q, sid := c.q, uint32(s.id)
252+ s.surf.OnPreferredBufferScale(func(f int32) {
253+ e := event{kind: evBufferScale, id: sid, a: f}
254+ q.post(&e)
255+ })
256+ if g.viewporter != nil {
257+ if s.viewport, err = g.viewporter.GetViewport(s.surf); err != nil {
258+ return nil, err
259+ }
260+ }
261+ if g.fscale != nil {
262+ if s.frac, err = g.fscale.GetFractionalScale(s.surf); err != nil {
263+ return nil, err
264+ }
265+ s.frac.OnPreferredScale(func(n uint32) {
266+ e := event{kind: evPreferredScale, id: sid, a: int32(n)}
267+ q.post(&e)
268+ })
269+ }
270+ // The role objects come next (callers); sync surface and feedback after.
271+ s.cb = &frameCallback{q: q, id: s.id}
272+ s.cb.SetContext(c.wlctx)
273+ c.surfaces[s.id] = s
274+ ok = true
275+ return s, nil
276+}
277+
278+// finishSurface creates the explicit-sync and feedback objects once the role
279+// exists.
280+func (s *Surface) finishSurface() error {
281+ c := s.c
282+ var err error
283+ if s.sync, err = c.g.syncobj.GetSurface(s.surf); err != nil {
284+ return err
285+ }
286+ if s.feedback, err = c.g.dmabuf.GetDefaultFeedback(); err != nil {
287+ return err
288+ }
289+ s.watchFeedback()
290+ return nil
291+}
292+
293+func (s *Surface) watchFeedback() {
294+ q, sid := s.c.q, uint32(s.id)
295+ s.feedback.OnFormatTable(func(fd *wl.OwnedFD, size uint32) {
296+ n, err := fd.Take()
297+ if err != nil {
298+ e := event{kind: evBadEvent, id: sid, a: badFeedbackFD}
299+ q.post(&e)
300+ return
301+ }
302+ e := event{kind: evFeedbackTable, id: sid, fd: int32(n), flags: size}
303+ q.post(&e)
304+ })
305+ s.feedback.OnMainDevice(func(dev []byte) {
306+ if len(dev) != 8 {
307+ e := event{kind: evBadEvent, id: sid, a: badMainDevice}
308+ q.post(&e)
309+ return
310+ }
311+ e := event{kind: evFeedbackMainDevice, id: sid, dev: binary.NativeEndian.Uint64(dev)}
312+ q.post(&e)
313+ })
314+ s.feedback.OnTrancheFormats(func(data []byte) {
315+ if len(data)%2 != 0 {
316+ e := event{kind: evBadEvent, id: sid, a: badTranche}
317+ q.post(&e)
318+ return
319+ }
320+ for len(data) > 0 { // chunks of up to maxNameLen/2 indices
321+ n := min(len(data), maxNameLen)
322+ e := event{kind: evFeedbackFormats, id: sid}
323+ e.setBytes(data[:n])
324+ q.post(&e)
325+ data = data[n:]
326+ }
327+ })
328+ s.feedback.OnDone(func() {
329+ e := event{kind: evFeedbackDone, id: sid}
330+ q.post(&e)
331+ })
332+}
333+
334+// Codes of evBadEvent.
335+const (
336+ badFeedbackFD int32 = iota + 1
337+ badMainDevice
338+ badTranche
339+ badConfigure
340+)
341+
342+var badEventErrors = [...]string{
343+ badFeedbackFD: "neferclient: dmabuf format table descriptor unavailable",
344+ badMainDevice: "neferclient: invalid dmabuf main_device length",
345+ badTranche: "neferclient: odd dmabuf tranche format list",
346+ badConfigure: "neferclient: layer configure size out of range",
347+}
348+
349+// NewLayerSurface creates a wlr-layer-shell surface. It becomes presentable
350+// after the first Handler.Configure.
351+func (c *Conn) NewLayerSurface(cfg LayerConfig) (*Surface, error) {
352+ if c.closed {
353+ return nil, ErrClosed
354+ }
355+ if err := validateLayer(&cfg); err != nil {
356+ return nil, err
357+ }
358+ if err := c.ensureLayerShell(); err != nil {
359+ return nil, err
360+ }
361+ if cfg.Keyboard == KeyboardOnDemand && c.g.layerVersion < 4 {
362+ return nil, &CapabilityError{Name: wlrlayershell.LayerShellInterface, Cause: fmt.Errorf("on-demand keyboard requires version 4, negotiated %d", c.g.layerVersion)}
363+ }
364+ var out *wayland.Output
365+ if cfg.Output != "" {
366+ for _, o := range c.outputs {
367+ if o.done && o.pub.Name == cfg.Output {
368+ out = o.proxy
369+ break
370+ }
371+ }
372+ if out == nil {
373+ return nil, &CapabilityError{Name: wayland.OutputInterface, Cause: fmt.Errorf("output %q not found", cfg.Output)}
374+ }
375+ }
376+ c.pause()
377+ defer c.resume()
378+ s, err := c.newSurface(roleLayer, cfg.Width, cfg.Height)
379+ if err != nil {
380+ return nil, err
381+ }
382+ ok := false
383+ defer func() {
384+ if !ok {
385+ s.destroy()
386+ }
387+ }()
388+ ns := cfg.Namespace
389+ if ns == "" {
390+ ns = DefaultLayerNamespace
391+ }
392+ level := [...]uint32{LayerBackground: wlrlayershell.LAYER_BACKGROUND, LayerBottom: wlrlayershell.LAYER_BOTTOM,
393+ LayerTop: wlrlayershell.LAYER_TOP, LayerOverlay: wlrlayershell.LAYER_OVERLAY}[cfg.Level]
394+ if s.layer, err = c.g.layer.GetLayerSurface(s.surf, out, level, ns); err != nil {
395+ return nil, err
396+ }
397+ q, sid := c.q, uint32(s.id)
398+ s.layer.OnClosed(func() {
399+ e := event{kind: evClosed, id: sid}
400+ q.post(&e)
401+ })
402+ s.layer.OnConfigure(func(serial, w, h uint32) {
403+ if w > maxLayerDimension || h > maxLayerDimension {
404+ e := event{kind: evBadEvent, id: sid, a: badConfigure}
405+ q.post(&e)
406+ return
407+ }
408+ e := event{kind: evLayerConfigure, id: sid, serial: serial, a: int32(w), b: int32(h)}
409+ q.post(&e)
410+ })
411+ // Opposite anchors let the compositor assign that axis (size 0).
412+ reqW, reqH := uint32(cfg.Width), uint32(cfg.Height)
413+ if cfg.Anchors&(AnchorLeft|AnchorRight) == AnchorLeft|AnchorRight {
414+ reqW = 0
415+ }
416+ if cfg.Anchors&(AnchorTop|AnchorBottom) == AnchorTop|AnchorBottom {
417+ reqH = 0
418+ }
419+ var anchor uint32
420+ for _, m := range [...][2]uint32{{uint32(AnchorTop), wlrlayershell.ANCHOR_TOP}, {uint32(AnchorBottom), wlrlayershell.ANCHOR_BOTTOM},
421+ {uint32(AnchorLeft), wlrlayershell.ANCHOR_LEFT}, {uint32(AnchorRight), wlrlayershell.ANCHOR_RIGHT}} {
422+ if uint32(cfg.Anchors)&m[0] != 0 {
423+ anchor |= m[1]
424+ }
425+ }
426+ for _, e := range []error{
427+ s.layer.SetSize(reqW, reqH),
428+ s.layer.SetAnchor(anchor),
429+ s.layer.SetExclusiveZone(cfg.ExclusiveZone),
430+ s.layer.SetMargin(cfg.Margin[0], cfg.Margin[1], cfg.Margin[2], cfg.Margin[3]),
431+ s.layer.SetKeyboardInteractivity(uint32(cfg.Keyboard)),
432+ } {
433+ if e != nil {
434+ return nil, e
435+ }
436+ }
437+ if cfg.InputRects != nil {
438+ if _, err = s.stageInputRects(cfg.InputRects); err != nil {
439+ return nil, err
440+ }
441+ }
442+ if err = s.finishSurface(); err != nil {
443+ return nil, err
444+ }
445+ if err = s.surf.Commit(); err != nil { // initial empty commit asks for configure
446+ return nil, err
447+ }
448+ ok = true
449+ return s, nil
450+}
451+
452+// NewToplevel creates an xdg-toplevel window with the given title and initial
453+// logical size. It becomes presentable after the first Handler.Configure.
454+func (c *Conn) NewToplevel(title string, w, h int32) (*Surface, error) {
455+ if c.closed {
456+ return nil, ErrClosed
457+ }
458+ if w <= 0 || h <= 0 {
459+ return nil, fmt.Errorf("neferclient: invalid logical size %dx%d", w, h)
460+ }
461+ if err := c.ensureXdg(); err != nil {
462+ return nil, err
463+ }
464+ c.pause()
465+ defer c.resume()
466+ s, err := c.newSurface(roleToplevel, w, h)
467+ if err != nil {
468+ return nil, err
469+ }
470+ ok := false
471+ defer func() {
472+ if !ok {
473+ s.destroy()
474+ }
475+ }()
476+ if s.xdg, err = c.g.xdg.GetXdgSurface(s.surf); err != nil {
477+ return nil, err
478+ }
479+ if s.toplevel, err = s.xdg.GetToplevel(); err != nil {
480+ return nil, err
481+ }
482+ q, sid := c.q, uint32(s.id)
483+ s.toplevel.OnClose(func() {
484+ e := event{kind: evClosed, id: sid}
485+ q.post(&e)
486+ })
487+ s.toplevel.OnConfigure(func(w, h int32, _ []byte) {
488+ e := event{kind: evXdgToplevel, id: sid, a: w, b: h}
489+ q.post(&e)
490+ })
491+ s.xdg.OnConfigure(func(serial uint32) {
492+ e := event{kind: evXdgConfigure, id: sid, serial: serial}
493+ q.post(&e)
494+ })
495+ if err = s.toplevel.SetTitle(title); err != nil {
496+ return nil, err
497+ }
498+ if err = s.finishSurface(); err != nil {
499+ return nil, err
500+ }
501+ if err = s.surf.Commit(); err != nil {
502+ return nil, err
503+ }
504+ ok = true
505+ return s, nil
506+}
507+
508+// applySurface handles every surface, lock and xdg_wm_base event on the owner.
509+func (c *Conn) applySurface(ev *event, h Handler) {
510+ defer ev.closeFD()
511+ switch ev.kind {
512+ case evPing:
513+ if c.g.xdg != nil {
514+ if err := c.g.xdg.Pong(ev.serial); err != nil {
515+ h.Error(err)
516+ }
517+ }
518+ return
519+ case evLocked, evLockFinished:
520+ l := c.curLock
521+ if l == nil || l.gen != ev.id {
522+ return
523+ }
524+ if ev.kind == evLocked {
525+ l.locked = true
526+ h.Locked()
527+ } else {
528+ l.finished = true
529+ h.LockFinished()
530+ }
531+ return
532+ }
533+ s := c.surfaces[SurfaceID(ev.id)]
534+ if s == nil || s.closed {
535+ return // closed while the event was queued
536+ }
537+ id := s.id
538+ switch ev.kind {
539+ case evBadEvent:
540+ h.Error(errors.New(badEventErrors[ev.a]))
541+ case evClosed:
542+ h.Closed(id)
543+ case evXdgToplevel:
544+ s.pendW, s.pendH = ev.a, ev.b
545+ case evXdgConfigure:
546+ if s.pendW > 0 && s.pendH > 0 {
547+ s.width, s.height = s.pendW, s.pendH
548+ }
549+ s.configure(s.xdg.AckConfigure(ev.serial), h)
550+ case evLayerConfigure:
551+ if ev.a > 0 {
552+ s.width = ev.a
553+ }
554+ if ev.b > 0 {
555+ s.height = ev.b
556+ }
557+ s.configure(s.layer.AckConfigure(ev.serial), h)
558+ case evLockConfigure:
559+ s.width, s.height = ev.a, ev.b
560+ s.configure(s.lockSurf.AckConfigure(ev.serial), h)
561+ case evFrame:
562+ s.frameReady = true
563+ h.Frame(id)
564+ case evPreferredScale:
565+ if ev.a <= 0 {
566+ h.Error(errors.New("neferclient: invalid fractional scale zero"))
567+ return
568+ }
569+ s.setScale(float64(ev.a)/120, h)
570+ case evBufferScale:
571+ if s.frac == nil && ev.a > 0 {
572+ s.setScale(float64(ev.a), h)
573+ }
574+ case evFeedbackTable:
575+ if err := s.readTable(int(ev.fd), ev.flags); err != nil {
576+ h.Error(err)
577+ }
578+ case evFeedbackMainDevice:
579+ s.pending.MainDevice = ev.dev
580+ case evFeedbackFormats:
581+ b := ev.nameBytes()
582+ for i := 0; i+1 < len(b); i += 2 {
583+ idx := int(binary.NativeEndian.Uint16(b[i:]))
584+ if idx >= len(s.table) {
585+ h.Error(fmt.Errorf("neferclient: dmabuf format table index %d out of range", idx))
586+ continue
587+ }
588+ s.pending.Formats = append(s.pending.Formats, s.table[idx])
589+ }
590+ case evFeedbackDone:
591+ s.fb, s.pending = s.pending, Feedback{Formats: s.fb.Formats[:0]}
592+ s.fbDone = true
593+ h.FeedbackDone(id)
594+ }
595+}
596+
597+func (s *Surface) configure(ackErr error, h Handler) {
598+ if ackErr != nil {
599+ h.Error(ackErr)
600+ return
601+ }
602+ if !s.configured {
603+ s.configured, s.frameReady = true, true
604+ }
605+ h.Configure(s.id, s.width, s.height)
606+}
607+
608+func (s *Surface) setScale(v float64, h Handler) {
609+ if v == s.scale {
610+ return
611+ }
612+ s.scale = v
613+ h.Scale(s.id, v)
614+}
615+
616+// readTable snapshots the format table with pread (no mapping of
617+// compositor-owned memory) and always consumes fd.
618+func (s *Surface) readTable(fd int, size uint32) error {
619+ defer unix.Close(fd)
620+ if size%16 != 0 || size > 16<<20 {
621+ return fmt.Errorf("neferclient: invalid dmabuf format table size %d", size)
622+ }
623+ c := s.c
624+ c.tableBuf = slices.Grow(c.tableBuf[:0], int(size))[:size]
625+ b := c.tableBuf
626+ for off := 0; off < len(b); {
627+ n, err := unix.Pread(fd, b[off:], int64(off))
628+ if err == unix.EINTR {
629+ continue
630+ }
631+ if err != nil {
632+ return fmt.Errorf("neferclient: read dmabuf format table: %w", err)
633+ }
634+ if n == 0 {
635+ return errors.New("neferclient: truncated dmabuf format table")
636+ }
637+ off += n
638+ }
639+ s.table = s.table[:0]
640+ for i := 0; i < len(b); i += 16 {
641+ s.table = append(s.table, Format{binary.NativeEndian.Uint32(b[i:]), binary.NativeEndian.Uint64(b[i+8:])})
642+ }
643+ return nil
644+}
645+
646+// SetInputRegion sets the input region (logical pixels) and commits when it
647+// changed. nil restores the whole surface; a non-nil empty slice passes all
648+// input through. Before the first configure the next commit applies it.
649+func (s *Surface) SetInputRegion(rects []Rect) error {
650+ if s.closed {
651+ return ErrClosed
652+ }
653+ changed, err := s.stageInputRects(rects)
654+ if err != nil || !changed || !s.configured {
655+ return err
656+ }
657+ return s.surf.Commit()
658+}
659+
660+func (s *Surface) stageInputRects(rects []Rect) (bool, error) {
661+ if err := validateRects(rects); err != nil {
662+ return false, err
663+ }
664+ if (rects != nil) == s.inputCustom && slices.Equal(rects, s.inputRects) {
665+ return false, nil
666+ }
667+ if rects == nil {
668+ if err := s.surf.SetInputRegion(nil); err != nil {
669+ return false, err
670+ }
671+ } else {
672+ region, err := s.c.g.compositor.CreateRegion()
673+ if err != nil {
674+ return false, err
675+ }
676+ for _, r := range rects {
677+ if err = region.Add(r.X, r.Y, r.Width, r.Height); err != nil {
678+ break
679+ }
680+ }
681+ if err == nil {
682+ err = s.surf.SetInputRegion(region)
683+ }
684+ _ = region.Destroy()
685+ if err != nil {
686+ return false, err
687+ }
688+ }
689+ s.inputCustom = rects != nil
690+ s.inputRects = append(s.inputRects[:0], rects...)
691+ return true, nil
692+}
693+
694+// Close destroys the surface, its buffers, timelines and role objects,
695+// children before parents. It is safe to call more than once. The caller must
696+// stop using buffers it imported first (the compositor keeps them until it
697+// processes the destruction).
698+func (s *Surface) Close() error {
699+ if s == nil || s.closed {
700+ return nil
701+ }
702+ if s.c.closed {
703+ s.closed = true
704+ return nil
705+ }
706+ return s.destroy()
707+}
708+
709+func (s *Surface) destroy() error {
710+ s.closed = true
711+ delete(s.c.surfaces, s.id)
712+ var errs []error
713+ add := func(err error) {
714+ if err != nil {
715+ errs = append(errs, err)
716+ }
717+ }
718+ for id, b := range s.buffers {
719+ add(b.Destroy())
720+ delete(s.buffers, id)
721+ }
722+ for id, t := range s.timelines {
723+ add(t.Destroy())
724+ delete(s.timelines, id)
725+ }
726+ if s.opaque != nil {
727+ add(s.opaque.Destroy())
728+ }
729+ if s.feedback != nil {
730+ add(s.feedback.Destroy())
731+ }
732+ if s.sync != nil {
733+ add(s.sync.Destroy())
734+ }
735+ if s.lockSurf != nil {
736+ add(s.lockSurf.Destroy())
737+ }
738+ if s.layer != nil {
739+ add(s.layer.Destroy())
740+ }
741+ if s.toplevel != nil {
742+ add(s.toplevel.Destroy())
743+ }
744+ if s.xdg != nil {
745+ add(s.xdg.Destroy())
746+ }
747+ if s.frac != nil {
748+ add(s.frac.Destroy())
749+ }
750+ if s.viewport != nil {
751+ add(s.viewport.Destroy())
752+ }
753+ if s.surf != nil {
754+ add(s.surf.Destroy())
755+ }
756+ if s.cb != nil {
757+ s.c.wlctx.Unregister(s.cb)
758+ }
759+ return errors.Join(errs...)
760+}

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