fix(wayland): forward X11 pointer constraints and modal changes, remap on surface replacement, clear dangling parents
- commit
- ace7dd1493faa536f734ad928164f978e9b46094
- author
- brice · · 2026-10-22 13:42 UTC
- committer
- brice · · 2026-10-22 17:54 UTC
- parent
- 24f47eb
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +46 -7
M internal/adapters/wayland/constraints.go +6 -5
| @@ -298,8 +298,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 298 | 298 | if s.protected() { | |
| 299 | 299 | return | |
| 300 | 300 | } | |
| 301 | - | x := c.surface.xdg | |
| 302 | - | if x == nil || x.window == nil { | |
| 301 | + | w := c.surface.toplevelWindow() | |
| 302 | + | if w == nil { | |
| 303 | 303 | return | |
| 304 | 304 | } | |
| 305 | 305 | mode := ports.ConstraintConfine | |
| @@ -308,8 +308,9 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 308 | 308 | } | |
| 309 | 309 | r := c.region | |
| 310 | 310 | if r.W > 0 && r.H > 0 { | |
| 311 | - | r.X -= x.geometry.X | |
| 312 | - | r.Y -= x.geometry.Y | |
| 311 | + | g := w.geometry() | |
| 312 | + | r.X -= g.X | |
| 313 | + | r.Y -= g.Y | |
| 313 | 314 | } | |
| 314 | 315 | // Core supports one confinement rect, so this bounds the intersection; | |
| 315 | 316 | // contains uses the exact input region to decide activation. | |
| @@ -333,7 +334,7 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 333 | 334 | r = box | |
| 334 | 335 | } | |
| 335 | 336 | } | |
| 336 | - | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 337 | + | s.emit(ports.PointerConstrained{ID: w.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 337 | 338 | } | |
| 338 | 339 | ||
| 339 | 340 | // contains reports whether the pointer is in the region: a constraint | |
| @@ -298,8 +298,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 298 | if s.protected() { | 298 | if s.protected() { | ||
| 299 | return | 299 | return | ||
| 300 | } | 300 | } | ||
| 301 | - | x := c.surface.xdg | 301 | + | w := c.surface.toplevelWindow() |
| 302 | - | if x == nil || x.window == nil { | 302 | + | if w == nil { |
| 303 | return | 303 | return | ||
| 304 | } | 304 | } | ||
| 305 | mode := ports.ConstraintConfine | 305 | mode := ports.ConstraintConfine | ||
| @@ -308,8 +308,9 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 308 | } | 308 | } | ||
| 309 | r := c.region | 309 | r := c.region | ||
| 310 | if r.W > 0 && r.H > 0 { | 310 | if r.W > 0 && r.H > 0 { | ||
| 311 | - | r.X -= x.geometry.X | 311 | + | g := w.geometry() |
| 312 | - | r.Y -= x.geometry.Y | 312 | + | r.X -= g.X |
| 313 | + | r.Y -= g.Y | |||
| 313 | } | 314 | } | ||
| 314 | // Core supports one confinement rect, so this bounds the intersection; | 315 | // Core supports one confinement rect, so this bounds the intersection; | ||
| 315 | // contains uses the exact input region to decide activation. | 316 | // contains uses the exact input region to decide activation. | ||
| @@ -333,7 +334,7 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 333 | r = box | 334 | r = box | ||
| 334 | } | 335 | } | ||
| 335 | } | 336 | } | ||
| 336 | - | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | 337 | + | s.emit(ports.PointerConstrained{ID: w.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) |
| 337 | } | 338 | } | ||
| 338 | 339 | ||||
| 339 | // contains reports whether the pointer is in the region: a constraint | 340 | // contains reports whether the pointer is in the region: a constraint | ||
M internal/adapters/wayland/dialog.go +2 -2
| @@ -76,7 +76,7 @@ func (w *window) setModal(modal bool) { | |||
| 76 | 76 | return | |
| 77 | 77 | } | |
| 78 | 78 | w.modal = modal | |
| 79 | - | if w.mapped && w.xdg != nil { | |
| 80 | - | w.xdg.server.emit(ports.WindowModal{ID: w.id, Modal: modal}) | |
| 79 | + | if w.mapped { | |
| 80 | + | w.srv().emit(ports.WindowModal{ID: w.id, Modal: modal}) | |
| 81 | 81 | } | |
| 82 | 82 | } | |
| @@ -76,7 +76,7 @@ func (w *window) setModal(modal bool) { | |||||
| 76 | return | 76 | return | ||
| 77 | } | 77 | } | ||
| 78 | w.modal = modal | 78 | w.modal = modal | ||
| 79 | - | if w.mapped && w.xdg != nil { | 79 | + | if w.mapped { |
| 80 | - | w.xdg.server.emit(ports.WindowModal{ID: w.id, Modal: modal}) | 80 | + | w.srv().emit(ports.WindowModal{ID: w.id, Modal: modal}) |
| 81 | } | 81 | } | ||
| 82 | } | 82 | } | ||
M internal/adapters/wayland/x11window.go +9 -0
| @@ -175,6 +175,10 @@ func (h hostImpl) Associate(slot xwm.Slot, surfaceID uint64) { | |||
| 175 | 175 | return | |
| 176 | 176 | } | |
| 177 | 177 | x := w.x11 | |
| 178 | + | if x.surface != nil && x.surface != surf { | |
| 179 | + | // A new surface replaces the shown one: core sees a remap. | |
| 180 | + | w.unmap() | |
| 181 | + | } | |
| 178 | 182 | x.drop() | |
| 179 | 183 | x.surface, surf.x11win = surf, w | |
| 180 | 184 | surf.role = func(buffer bool) { | |
| @@ -204,6 +208,11 @@ func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) { | |||
| 204 | 208 | w.x11.drop() | |
| 205 | 209 | w.x11.ready = false | |
| 206 | 210 | if destroyed { | |
| 211 | + | for _, c := range h.s.windows { | |
| 212 | + | if c.parent == w { | |
| 213 | + | c.parent = nil | |
| 214 | + | } | |
| 215 | + | } | |
| 207 | 216 | delete(h.s.windows, w.id) | |
| 208 | 217 | delete(h.s.x11Slots, slot) | |
| 209 | 218 | h.s.foreignToplevelGone(w) | |
| @@ -175,6 +175,10 @@ func (h hostImpl) Associate(slot xwm.Slot, surfaceID uint64) { | |||||
| 175 | return | 175 | return | ||
| 176 | } | 176 | } | ||
| 177 | x := w.x11 | 177 | x := w.x11 | ||
| 178 | + | if x.surface != nil && x.surface != surf { | |||
| 179 | + | // A new surface replaces the shown one: core sees a remap. | |||
| 180 | + | w.unmap() | |||
| 181 | + | } | |||
| 178 | x.drop() | 182 | x.drop() | ||
| 179 | x.surface, surf.x11win = surf, w | 183 | x.surface, surf.x11win = surf, w | ||
| 180 | surf.role = func(buffer bool) { | 184 | surf.role = func(buffer bool) { | ||
| @@ -204,6 +208,11 @@ func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) { | |||||
| 204 | w.x11.drop() | 208 | w.x11.drop() | ||
| 205 | w.x11.ready = false | 209 | w.x11.ready = false | ||
| 206 | if destroyed { | 210 | if destroyed { | ||
| 211 | + | for _, c := range h.s.windows { | |||
| 212 | + | if c.parent == w { | |||
| 213 | + | c.parent = nil | |||
| 214 | + | } | |||
| 215 | + | } | |||
| 207 | delete(h.s.windows, w.id) | 216 | delete(h.s.windows, w.id) | ||
| 208 | delete(h.s.x11Slots, slot) | 217 | delete(h.s.x11Slots, slot) | ||
| 209 | h.s.foreignToplevelGone(w) | 218 | h.s.foreignToplevelGone(w) | ||
M internal/adapters/wayland/x11window_test.go +29 -0
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "testing" | |
| 9 | 9 | "time" | |
| 10 | 10 | ||
| 11 | + | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |
| 11 | 12 | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 12 | 13 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 13 | 14 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| @@ -322,3 +323,31 @@ func TestAttachXWMBootAbortsWithContext(t *testing.T) { | |||
| 322 | 323 | assertFDClosed(t, fds[0]) | |
| 323 | 324 | protocolClient(t, s, dir) // still serving | |
| 324 | 325 | } | |
| 326 | + | ||
| 327 | + | // Xwayland locks the pointer for games: the constraint reaches core for the | |
| 328 | + | // X11 window like for an xdg toplevel. | |
| 329 | + | func TestX11WindowPointerLock(t *testing.T) { | |
| 330 | + | r := newX11Rig(t) | |
| 331 | + | win := uint32(x11Win) | |
| 332 | + | r.x11Window(win) | |
| 333 | + | surf := r.pair(win, x11Serial) | |
| 334 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 335 | + | ||
| 336 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |
| 337 | + | registerProtocol(t, r.xc, seat) | |
| 338 | + | pointer := r.xc.AllocateID() | |
| 339 | + | requestProtocol(t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |
| 340 | + | registerProtocol(t, r.xc, pointer) | |
| 341 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |
| 342 | + | lock := r.xc.AllocateID() | |
| 343 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 344 | + | registerProtocol(t, r.xc, lock) | |
| 345 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 346 | + | t.Fatal(err) | |
| 347 | + | } | |
| 348 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |
| 349 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 350 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock { | |
| 351 | + | t.Fatalf("constrained %+v", got) | |
| 352 | + | } | |
| 353 | + | } | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | "testing" | 8 | "testing" | ||
| 9 | "time" | 9 | "time" | ||
| 10 | 10 | ||||
| 11 | + | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |||
| 11 | "github.com/bnema/go-wayland-bindings/server/wayland" | 12 | "github.com/bnema/go-wayland-bindings/server/wayland" | ||
| 12 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | 13 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | ||
| 13 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | 14 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | ||
| @@ -322,3 +323,31 @@ func TestAttachXWMBootAbortsWithContext(t *testing.T) { | |||||
| 322 | assertFDClosed(t, fds[0]) | 323 | assertFDClosed(t, fds[0]) | ||
| 323 | protocolClient(t, s, dir) // still serving | 324 | protocolClient(t, s, dir) // still serving | ||
| 324 | } | 325 | } | ||
| 326 | + | ||||
| 327 | + | // Xwayland locks the pointer for games: the constraint reaches core for the | |||
| 328 | + | // X11 window like for an xdg toplevel. | |||
| 329 | + | func TestX11WindowPointerLock(t *testing.T) { | |||
| 330 | + | r := newX11Rig(t) | |||
| 331 | + | win := uint32(x11Win) | |||
| 332 | + | r.x11Window(win) | |||
| 333 | + | surf := r.pair(win, x11Serial) | |||
| 334 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 335 | + | ||||
| 336 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |||
| 337 | + | registerProtocol(t, r.xc, seat) | |||
| 338 | + | pointer := r.xc.AllocateID() | |||
| 339 | + | requestProtocol(t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 340 | + | registerProtocol(t, r.xc, pointer) | |||
| 341 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |||
| 342 | + | lock := r.xc.AllocateID() | |||
| 343 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 344 | + | registerProtocol(t, r.xc, lock) | |||
| 345 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 346 | + | t.Fatal(err) | |||
| 347 | + | } | |||
| 348 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |||
| 349 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 350 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock { | |||
| 351 | + | t.Fatalf("constrained %+v", got) | |||
| 352 | + | } | |||
| 353 | + | } | |||