fix(wayland): track popup role objects and reset buffers on role destroy
- commit
- 8fd5205ff6419e58d423d7323ed2f57d30483f01
- author
- brice · · 2026-09-23 12:33 UTC
- parent
- 78a0c3a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +141 -8
M internal/adapters/wayland/protocol_test.go +124 -4
| @@ -2,6 +2,7 @@ package wayland | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "github.com/bnema/nefertty/internal/ports" | |
| 5 | 6 | "path/filepath" | |
| 6 | 7 | "testing" | |
| 7 | 8 | "time" | |
| @@ -103,10 +104,6 @@ func TestXDGProtocolErrors(t *testing.T) { | |||
| 103 | 104 | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |
| 104 | 105 | switch scenario { | |
| 105 | 106 | case "bogus ack": | |
| 106 | - | requestProtocol(t, c, surface, wayland.SurfaceRequestCommit) | |
| 107 | - | if err := c.Roundtrip(); err != nil { | |
| 108 | - | t.Fatal(err) | |
| 109 | - | } | |
| 110 | 107 | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0)) | |
| 111 | 108 | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial)) | |
| 112 | 109 | case "buffer before ack": | |
| @@ -138,3 +135,126 @@ func TestXDGProtocolErrors(t *testing.T) { | |||
| 138 | 135 | }) | |
| 139 | 136 | } | |
| 140 | 137 | } | |
| 138 | + | ||
| 139 | + | func TestPopupBlocksSecondRole(t *testing.T) { | |
| 140 | + | s, _, _, dir := lifecycleServer(t) | |
| 141 | + | c := protocolClient(t, s, dir) | |
| 142 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 143 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |
| 144 | + | surf, xdg := c.AllocateID(), c.AllocateID() | |
| 145 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 146 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 147 | + | registerProtocol(t, c, xdg) | |
| 148 | + | registerProtocol(t, c, surf) | |
| 149 | + | positioner := c.AllocateID() | |
| 150 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner) | |
| 151 | + | if err := c.Roundtrip(); err != nil { | |
| 152 | + | t.Fatal(err) | |
| 153 | + | } | |
| 154 | + | popup := c.AllocateID() | |
| 155 | + | registerProtocol(t, c, popup) | |
| 156 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner) | |
| 157 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, c.AllocateID()) | |
| 158 | + | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorAlreadyConstructed)) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | type configureProxy struct { | |
| 162 | + | wlturbo.BaseProxy | |
| 163 | + | serial chan uint32 | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | func (p *configureProxy) Dispatch(e *wlturbo.Event) { | |
| 167 | + | if e.Opcode == uint16(xdgshell.SurfaceEventConfigure) { | |
| 168 | + | p.serial <- e.Uint32() | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) { | |
| 173 | + | s, events, _, dir := lifecycleServer(t) | |
| 174 | + | c := protocolClient(t, s, dir) | |
| 175 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 176 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |
| 177 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 178 | + | surf, xdg := c.AllocateID(), c.AllocateID() | |
| 179 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 180 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 181 | + | serials := make(chan uint32, 4) | |
| 182 | + | proxy := &configureProxy{serial: serials} | |
| 183 | + | proxy.SetID(xdg) | |
| 184 | + | c.Context().Register(proxy) | |
| 185 | + | fd, err := unix.MemfdCreate("recreated-buffer", 0) | |
| 186 | + | if err != nil { | |
| 187 | + | t.Fatal(err) | |
| 188 | + | } | |
| 189 | + | defer unix.Close(fd) | |
| 190 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |
| 191 | + | t.Fatal(err) | |
| 192 | + | } | |
| 193 | + | registerProtocol(t, c, shm) | |
| 194 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |
| 195 | + | registerProtocol(t, c, pool) | |
| 196 | + | registerProtocol(t, c, buffer) | |
| 197 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil { | |
| 198 | + | t.Fatal(err) | |
| 199 | + | } | |
| 200 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |
| 201 | + | top := c.AllocateID() | |
| 202 | + | registerProtocol(t, c, top) | |
| 203 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |
| 204 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 205 | + | if err := c.Roundtrip(); err != nil { | |
| 206 | + | t.Fatal(err) | |
| 207 | + | } | |
| 208 | + | select { | |
| 209 | + | case serial := <-serials: | |
| 210 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial) | |
| 211 | + | case <-time.After(2 * time.Second): | |
| 212 | + | t.Fatal("initial configure timeout") | |
| 213 | + | } | |
| 214 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |
| 215 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 216 | + | if err := c.Roundtrip(); err != nil { | |
| 217 | + | t.Fatal(err) | |
| 218 | + | } | |
| 219 | + | mapped(t, events, 2*time.Second) | |
| 220 | + | requestProtocol(t, c, top, xdgshell.ToplevelRequestDestroy) | |
| 221 | + | if err := c.Roundtrip(); err != nil { | |
| 222 | + | t.Fatal(err) | |
| 223 | + | } | |
| 224 | + | select { | |
| 225 | + | case ev := <-events: | |
| 226 | + | if _, ok := ev.(ports.WindowUnmapped); !ok { | |
| 227 | + | t.Fatalf("expected unmap, got %T", ev) | |
| 228 | + | } | |
| 229 | + | case <-time.After(2 * time.Second): | |
| 230 | + | t.Fatal("unmap timeout") | |
| 231 | + | } | |
| 232 | + | top = c.AllocateID() | |
| 233 | + | registerProtocol(t, c, top) | |
| 234 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |
| 235 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 236 | + | if err := c.Roundtrip(); err != nil { | |
| 237 | + | t.Fatal(err) | |
| 238 | + | } | |
| 239 | + | select { | |
| 240 | + | case serial := <-serials: | |
| 241 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial) | |
| 242 | + | case <-time.After(2 * time.Second): | |
| 243 | + | t.Fatal("recreated configure timeout") | |
| 244 | + | } | |
| 245 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 246 | + | if err := c.Roundtrip(); err != nil { | |
| 247 | + | t.Fatal(err) | |
| 248 | + | } | |
| 249 | + | select { | |
| 250 | + | case ev := <-events: | |
| 251 | + | t.Fatalf("mapped without new buffer: %T", ev) | |
| 252 | + | case <-time.After(300 * time.Millisecond): | |
| 253 | + | } | |
| 254 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |
| 255 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 256 | + | if err := c.Roundtrip(); err != nil { | |
| 257 | + | t.Fatal(err) | |
| 258 | + | } | |
| 259 | + | mapped(t, events, 2*time.Second) | |
| 260 | + | } | |
| @@ -2,6 +2,7 @@ package wayland | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 5 | "path/filepath" | 6 | "path/filepath" | ||
| 6 | "testing" | 7 | "testing" | ||
| 7 | "time" | 8 | "time" | ||
| @@ -103,10 +104,6 @@ func TestXDGProtocolErrors(t *testing.T) { | |||||
| 103 | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | 104 | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | ||
| 104 | switch scenario { | 105 | switch scenario { | ||
| 105 | case "bogus ack": | 106 | case "bogus ack": | ||
| 106 | - | requestProtocol(t, c, surface, wayland.SurfaceRequestCommit) | |||
| 107 | - | if err := c.Roundtrip(); err != nil { | |||
| 108 | - | t.Fatal(err) | |||
| 109 | - | } | |||
| 110 | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0)) | 107 | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0)) | ||
| 111 | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial)) | 108 | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial)) | ||
| 112 | case "buffer before ack": | 109 | case "buffer before ack": | ||
| @@ -138,3 +135,126 @@ func TestXDGProtocolErrors(t *testing.T) { | |||||
| 138 | }) | 135 | }) | ||
| 139 | } | 136 | } | ||
| 140 | } | 137 | } | ||
| 138 | + | ||||
| 139 | + | func TestPopupBlocksSecondRole(t *testing.T) { | |||
| 140 | + | s, _, _, dir := lifecycleServer(t) | |||
| 141 | + | c := protocolClient(t, s, dir) | |||
| 142 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 143 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 144 | + | surf, xdg := c.AllocateID(), c.AllocateID() | |||
| 145 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 146 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 147 | + | registerProtocol(t, c, xdg) | |||
| 148 | + | registerProtocol(t, c, surf) | |||
| 149 | + | positioner := c.AllocateID() | |||
| 150 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner) | |||
| 151 | + | if err := c.Roundtrip(); err != nil { | |||
| 152 | + | t.Fatal(err) | |||
| 153 | + | } | |||
| 154 | + | popup := c.AllocateID() | |||
| 155 | + | registerProtocol(t, c, popup) | |||
| 156 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner) | |||
| 157 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, c.AllocateID()) | |||
| 158 | + | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorAlreadyConstructed)) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | type configureProxy struct { | |||
| 162 | + | wlturbo.BaseProxy | |||
| 163 | + | serial chan uint32 | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | func (p *configureProxy) Dispatch(e *wlturbo.Event) { | |||
| 167 | + | if e.Opcode == uint16(xdgshell.SurfaceEventConfigure) { | |||
| 168 | + | p.serial <- e.Uint32() | |||
| 169 | + | } | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) { | |||
| 173 | + | s, events, _, dir := lifecycleServer(t) | |||
| 174 | + | c := protocolClient(t, s, dir) | |||
| 175 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 176 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 177 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 178 | + | surf, xdg := c.AllocateID(), c.AllocateID() | |||
| 179 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 180 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 181 | + | serials := make(chan uint32, 4) | |||
| 182 | + | proxy := &configureProxy{serial: serials} | |||
| 183 | + | proxy.SetID(xdg) | |||
| 184 | + | c.Context().Register(proxy) | |||
| 185 | + | fd, err := unix.MemfdCreate("recreated-buffer", 0) | |||
| 186 | + | if err != nil { | |||
| 187 | + | t.Fatal(err) | |||
| 188 | + | } | |||
| 189 | + | defer unix.Close(fd) | |||
| 190 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |||
| 191 | + | t.Fatal(err) | |||
| 192 | + | } | |||
| 193 | + | registerProtocol(t, c, shm) | |||
| 194 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |||
| 195 | + | registerProtocol(t, c, pool) | |||
| 196 | + | registerProtocol(t, c, buffer) | |||
| 197 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil { | |||
| 198 | + | t.Fatal(err) | |||
| 199 | + | } | |||
| 200 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |||
| 201 | + | top := c.AllocateID() | |||
| 202 | + | registerProtocol(t, c, top) | |||
| 203 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |||
| 204 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 205 | + | if err := c.Roundtrip(); err != nil { | |||
| 206 | + | t.Fatal(err) | |||
| 207 | + | } | |||
| 208 | + | select { | |||
| 209 | + | case serial := <-serials: | |||
| 210 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial) | |||
| 211 | + | case <-time.After(2 * time.Second): | |||
| 212 | + | t.Fatal("initial configure timeout") | |||
| 213 | + | } | |||
| 214 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |||
| 215 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 216 | + | if err := c.Roundtrip(); err != nil { | |||
| 217 | + | t.Fatal(err) | |||
| 218 | + | } | |||
| 219 | + | mapped(t, events, 2*time.Second) | |||
| 220 | + | requestProtocol(t, c, top, xdgshell.ToplevelRequestDestroy) | |||
| 221 | + | if err := c.Roundtrip(); err != nil { | |||
| 222 | + | t.Fatal(err) | |||
| 223 | + | } | |||
| 224 | + | select { | |||
| 225 | + | case ev := <-events: | |||
| 226 | + | if _, ok := ev.(ports.WindowUnmapped); !ok { | |||
| 227 | + | t.Fatalf("expected unmap, got %T", ev) | |||
| 228 | + | } | |||
| 229 | + | case <-time.After(2 * time.Second): | |||
| 230 | + | t.Fatal("unmap timeout") | |||
| 231 | + | } | |||
| 232 | + | top = c.AllocateID() | |||
| 233 | + | registerProtocol(t, c, top) | |||
| 234 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |||
| 235 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 236 | + | if err := c.Roundtrip(); err != nil { | |||
| 237 | + | t.Fatal(err) | |||
| 238 | + | } | |||
| 239 | + | select { | |||
| 240 | + | case serial := <-serials: | |||
| 241 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial) | |||
| 242 | + | case <-time.After(2 * time.Second): | |||
| 243 | + | t.Fatal("recreated configure timeout") | |||
| 244 | + | } | |||
| 245 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 246 | + | if err := c.Roundtrip(); err != nil { | |||
| 247 | + | t.Fatal(err) | |||
| 248 | + | } | |||
| 249 | + | select { | |||
| 250 | + | case ev := <-events: | |||
| 251 | + | t.Fatalf("mapped without new buffer: %T", ev) | |||
| 252 | + | case <-time.After(300 * time.Millisecond): | |||
| 253 | + | } | |||
| 254 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |||
| 255 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 256 | + | if err := c.Roundtrip(); err != nil { | |||
| 257 | + | t.Fatal(err) | |||
| 258 | + | } | |||
| 259 | + | mapped(t, events, 2*time.Second) | |||
| 260 | + | } | |||
M internal/adapters/wayland/server.go +4 -0
| @@ -42,6 +42,10 @@ type Server struct { | |||
| 42 | 42 | nextWindow ports.WindowID | |
| 43 | 43 | focused ports.WindowID | |
| 44 | 44 | // eventMu protects only the notification queue, not display-owned window state. | |
| 45 | + | // The event queue is unbounded by design: it grows only if core stops draining | |
| 46 | + | // Events, which happens only at shutdown (core owns the receiving end). A | |
| 47 | + | // stalled core is a bug surfaced by ctx cancellation, not a reason to block | |
| 48 | + | // the display goroutine. | |
| 45 | 49 | eventMu sync.Mutex | |
| 46 | 50 | events []ports.ClientEvent | |
| 47 | 51 | eventReady chan struct{} | |
| @@ -42,6 +42,10 @@ type Server struct { | |||||
| 42 | nextWindow ports.WindowID | 42 | nextWindow ports.WindowID | ||
| 43 | focused ports.WindowID | 43 | focused ports.WindowID | ||
| 44 | // eventMu protects only the notification queue, not display-owned window state. | 44 | // eventMu protects only the notification queue, not display-owned window state. | ||
| 45 | + | // The event queue is unbounded by design: it grows only if core stops draining | |||
| 46 | + | // Events, which happens only at shutdown (core owns the receiving end). A | |||
| 47 | + | // stalled core is a bug surfaced by ctx cancellation, not a reason to block | |||
| 48 | + | // the display goroutine. | |||
| 45 | eventMu sync.Mutex | 49 | eventMu sync.Mutex | ||
| 46 | events []ports.ClientEvent | 50 | events []ports.ClientEvent | ||
| 47 | eventReady chan struct{} | 51 | eventReady chan struct{} | ||
M internal/adapters/wayland/xdg.go +13 -4
| @@ -61,12 +61,13 @@ type xdgSurface struct { | |||
| 61 | 61 | configured bool | |
| 62 | 62 | acked bool | |
| 63 | 63 | window *window | |
| 64 | + | popup *xdgshell.Popup | |
| 64 | 65 | serials []uint32 | |
| 65 | 66 | } | |
| 66 | 67 | ||
| 67 | 68 | func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | |
| 68 | - | if x.window != nil { | |
| 69 | - | r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "toplevel still exists") | |
| 69 | + | if x.window != nil || x.popup != nil { | |
| 70 | + | r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "role object still exists") | |
| 70 | 71 | } | |
| 71 | 72 | } | |
| 72 | 73 | ||
| @@ -89,8 +90,8 @@ func (w *window) unmap() { | |||
| 89 | 90 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |
| 90 | 91 | } | |
| 91 | 92 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |
| 92 | - | if x.window != nil { | |
| 93 | - | r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "toplevel already constructed") | |
| 93 | + | if x.window != nil || x.popup != nil { | |
| 94 | + | r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed") | |
| 94 | 95 | return | |
| 95 | 96 | } | |
| 96 | 97 | w := &window{id: x.server.nextWindow, xdg: x} | |
| @@ -123,6 +124,8 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||
| 123 | 124 | } | |
| 124 | 125 | t.OnDestroy = func() { | |
| 125 | 126 | w.unmap() | |
| 127 | + | x.surface.current, x.surface.pending = nil, nil | |
| 128 | + | x.surface.attached = false | |
| 126 | 129 | delete(x.server.windows, w.id) | |
| 127 | 130 | x.window = nil | |
| 128 | 131 | x.surface.role = nil | |
| @@ -132,7 +135,13 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||
| 132 | 135 | } | |
| 133 | 136 | } | |
| 134 | 137 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | |
| 138 | + | if x.window != nil || x.popup != nil { | |
| 139 | + | r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed") | |
| 140 | + | return | |
| 141 | + | } | |
| 135 | 142 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | |
| 143 | + | x.popup = p | |
| 144 | + | p.OnDestroy = func() { x.popup = nil } | |
| 136 | 145 | p.SendPopupDone() | |
| 137 | 146 | } | |
| 138 | 147 | } | |
| @@ -61,12 +61,13 @@ type xdgSurface struct { | |||||
| 61 | configured bool | 61 | configured bool | ||
| 62 | acked bool | 62 | acked bool | ||
| 63 | window *window | 63 | window *window | ||
| 64 | + | popup *xdgshell.Popup | |||
| 64 | serials []uint32 | 65 | serials []uint32 | ||
| 65 | } | 66 | } | ||
| 66 | 67 | ||||
| 67 | func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | 68 | func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | ||
| 68 | - | if x.window != nil { | 69 | + | if x.window != nil || x.popup != nil { |
| 69 | - | r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "toplevel still exists") | 70 | + | r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "role object still exists") |
| 70 | } | 71 | } | ||
| 71 | } | 72 | } | ||
| 72 | 73 | ||||
| @@ -89,8 +90,8 @@ func (w *window) unmap() { | |||||
| 89 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 90 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | ||
| 90 | } | 91 | } | ||
| 91 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 92 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| 92 | - | if x.window != nil { | 93 | + | if x.window != nil || x.popup != nil { |
| 93 | - | r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "toplevel already constructed") | 94 | + | r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed") |
| 94 | return | 95 | return | ||
| 95 | } | 96 | } | ||
| 96 | w := &window{id: x.server.nextWindow, xdg: x} | 97 | w := &window{id: x.server.nextWindow, xdg: x} | ||
| @@ -123,6 +124,8 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||||
| 123 | } | 124 | } | ||
| 124 | t.OnDestroy = func() { | 125 | t.OnDestroy = func() { | ||
| 125 | w.unmap() | 126 | w.unmap() | ||
| 127 | + | x.surface.current, x.surface.pending = nil, nil | |||
| 128 | + | x.surface.attached = false | |||
| 126 | delete(x.server.windows, w.id) | 129 | delete(x.server.windows, w.id) | ||
| 127 | x.window = nil | 130 | x.window = nil | ||
| 128 | x.surface.role = nil | 131 | x.surface.role = nil | ||
| @@ -132,7 +135,13 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||||
| 132 | } | 135 | } | ||
| 133 | } | 136 | } | ||
| 134 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | 137 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | ||
| 138 | + | if x.window != nil || x.popup != nil { | |||
| 139 | + | r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed") | |||
| 140 | + | return | |||
| 141 | + | } | |||
| 135 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | 142 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | ||
| 143 | + | x.popup = p | |||
| 144 | + | p.OnDestroy = func() { x.popup = nil } | |||
| 136 | p.SendPopupDone() | 145 | p.SendPopupDone() | ||
| 137 | } | 146 | } | ||
| 138 | } | 147 | } | ||