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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 47 | 47 | return | |
| 48 | 48 | } | |
| 49 | 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 | 53 | r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed") | |
| 52 | 54 | return | |
| 53 | 55 | } | |
| @@ -47,7 +47,9 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32 | |||||
| 47 | return | 47 | 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 | return | 54 | return | ||
| 53 | } | 55 | } | ||
M internal/adapters/wayland/layer_test.go +21 -0
| @@ -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 | 203 | type doneProxy struct { | |
| 183 | 204 | wlturbo.BaseProxy | |
| 184 | 205 | 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.BaseProxy | 204 | wlturbo.BaseProxy | ||
| 184 | done int | 205 | done int | ||