detached 17fe726

fix(wayland): let a surface take a new layer surface after the old one is destroyed

swayosd hides its OSD by destroying the zwlr_layer_surface_v1 and reuses the
same wl_surface for the next one. GetLayerSurface rejected any surface that
had ever held the layer role, posting already_constructed and killing the
client, so volume and brightness keys worked only once per session.
commit
17fe726aa4dec075fb79851da3bfa0c0c5dadec4
author
bnema · · 2026-10-08 19:14 UTC
parent
65119ef
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +24 -1
M internal/adapters/wayland/layer.go +3 -1
@@ -47,7 +47,9 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
4747 return
4848 }
4949 state := h.server.surfaces[w.Resource]
50- if state.kind != roleNone || (state.next.attached && state.next.buffer != nil) || state.current != nil {
50+ // A surface keeps its layer role once its layer surface is destroyed and
51+ // may take a new one; only a live role object blocks it.
52+ if state.kind != roleNone && state.kind != roleLayer || state.layer != nil || (state.next.attached && state.next.buffer != nil) || state.current != nil {
5153 r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")
5254 return
5355 }
@@ -47,7 +47,9 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
47 return47 return
48 }48 }
49 state := h.server.surfaces[w.Resource]49 state := h.server.surfaces[w.Resource]
50- if state.kind != roleNone || (state.next.attached && state.next.buffer != nil) || state.current != nil {50+ // A surface keeps its layer role once its layer surface is destroyed and
51+ // may take a new one; only a live role object blocks it.
52+ if state.kind != roleNone && state.kind != roleLayer || state.layer != nil || (state.next.attached && state.next.buffer != nil) || state.current != nil {
51 r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")53 r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")
52 return54 return
53 }55 }
M internal/adapters/wayland/layer_test.go +21 -0
@@ -179,6 +179,27 @@ func TestLayerAlreadyConstructed(t *testing.T) {
179179 }
180180 }
181181
182+// A wl_surface keeps its layer role after its layer surface is destroyed,
183+// so the client may give it a new one (swayosd does this on every OSD).
184+func TestLayerRecreateOnSameSurface(t *testing.T) {
185+ s, _, _, dir := lifecycleServer(t)
186+ c := protocolClient(t, s, dir)
187+ shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
188+ surf, layer := layerProtocol(t, c)
189+ requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(10), uint32(10))
190+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
191+ requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
192+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
193+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
194+ again := c.AllocateID()
195+ requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, again, surf, uint32(0), uint32(ports.LayerOverlay), "test")
196+ requestProtocol(t, c, again, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(10), uint32(10))
197+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
198+ if err := c.Roundtrip(); err != nil {
199+ t.Fatalf("recreating the layer surface failed: %v", err)
200+ }
201+}
202+
182203 type doneProxy struct {
183204 wlturbo.BaseProxy
184205 done int
@@ -179,6 +179,27 @@ func TestLayerAlreadyConstructed(t *testing.T) {
179 }179 }
180 }180 }
181 181
182+// A wl_surface keeps its layer role after its layer surface is destroyed,
183+// so the client may give it a new one (swayosd does this on every OSD).
184+func TestLayerRecreateOnSameSurface(t *testing.T) {
185+ s, _, _, dir := lifecycleServer(t)
186+ c := protocolClient(t, s, dir)
187+ shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
188+ surf, layer := layerProtocol(t, c)
189+ requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(10), uint32(10))
190+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
191+ requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
192+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
193+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
194+ again := c.AllocateID()
195+ requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, again, surf, uint32(0), uint32(ports.LayerOverlay), "test")
196+ requestProtocol(t, c, again, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(10), uint32(10))
197+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
198+ if err := c.Roundtrip(); err != nil {
199+ t.Fatalf("recreating the layer surface failed: %v", err)
200+ }
201+}
202+
182 type doneProxy struct {203 type doneProxy struct {
183 wlturbo.BaseProxy204 wlturbo.BaseProxy
184 done int205 done int

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 23

Tags 10

↑↓ moveenter openesc close