fix(wayland): deactivate constraints outside committed input
- commit
- adaacbc4461c12d356e9732b8aeab2d38f26021e
- author
- brice · · 2026-09-27 14:02 UTC
- parent
- 45fc191
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +176 -35
M internal/adapters/wayland/constraints.go +25 -34
| @@ -205,6 +205,26 @@ func (s *Server) dropConstraint(c *constraint) { | |||
| 205 | 205 | s.updateConstraint() | |
| 206 | 206 | } | |
| 207 | 207 | ||
| 208 | + | // deactivateConstraint ends the active constraint; a oneshot cannot reactivate. | |
| 209 | + | func (s *Server) deactivateConstraint() { | |
| 210 | + | old := s.constraint | |
| 211 | + | if old == nil { | |
| 212 | + | return | |
| 213 | + | } | |
| 214 | + | old.active = false | |
| 215 | + | if !old.persistent { | |
| 216 | + | old.defunct = true | |
| 217 | + | } | |
| 218 | + | if old.lock != nil && old.lock.Resource.Alive() { | |
| 219 | + | old.lock.SendUnlocked() | |
| 220 | + | } | |
| 221 | + | if old.confine != nil && old.confine.Resource.Alive() { | |
| 222 | + | old.confine.SendUnconfined() | |
| 223 | + | } | |
| 224 | + | s.constraint = nil | |
| 225 | + | s.emit(ports.PointerConstrained{}) | |
| 226 | + | } | |
| 227 | + | ||
| 208 | 228 | // updateConstraint activates the constraint of the window holding both | |
| 209 | 229 | // pointer and keyboard focus, and deactivates any other. | |
| 210 | 230 | func (s *Server) updateConstraint() { | |
| @@ -217,23 +237,11 @@ func (s *Server) updateConstraint() { | |||
| 217 | 237 | if want == s.constraint { | |
| 218 | 238 | return | |
| 219 | 239 | } | |
| 220 | - | if old := s.constraint; old != nil { | |
| 221 | - | old.active = false | |
| 222 | - | if !old.persistent { | |
| 223 | - | old.defunct = true | |
| 224 | - | } | |
| 225 | - | if old.lock != nil && old.lock.Resource.Alive() { | |
| 226 | - | old.lock.SendUnlocked() | |
| 227 | - | } | |
| 228 | - | if old.confine != nil && old.confine.Resource.Alive() { | |
| 229 | - | old.confine.SendUnconfined() | |
| 230 | - | } | |
| 231 | - | } | |
| 232 | - | s.constraint = want | |
| 240 | + | s.deactivateConstraint() | |
| 233 | 241 | if want == nil { | |
| 234 | - | s.emit(ports.PointerConstrained{}) | |
| 235 | 242 | return | |
| 236 | 243 | } | |
| 244 | + | s.constraint = want | |
| 237 | 245 | want.active = true | |
| 238 | 246 | if want.lock != nil { | |
| 239 | 247 | want.lock.SendLocked() | |
| @@ -258,8 +266,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 258 | 266 | r.X -= x.geometry.X | |
| 259 | 267 | r.Y -= x.geometry.Y | |
| 260 | 268 | } | |
| 261 | - | // The input region also bounds confinement. A box is sufficient for core; | |
| 262 | - | // contains uses the exact region to decide activation. | |
| 269 | + | // Core supports one confinement rect, so this bounds the intersection; | |
| 270 | + | // contains uses the exact input region to decide activation. | |
| 263 | 271 | if mode == ports.ConstraintConfine { | |
| 264 | 272 | if all, rects := c.surface.effectiveInput(); !all { | |
| 265 | 273 | var box ports.Rect | |
| @@ -277,24 +285,7 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 277 | 285 | box = ports.Rect{X: x0, Y: y0, W: max(box.X+box.W, input.X+input.W) - x0, H: max(box.Y+box.H, input.Y+input.H) - y0} | |
| 278 | 286 | } | |
| 279 | 287 | } | |
| 280 | - | // Core treats an empty rect as the entire window. Keep the | |
| 281 | - | // original constraint (or the input box for an unbounded one) | |
| 282 | - | // when disjoint, rather than widening it to the whole window. | |
| 283 | - | if box.W > 0 { | |
| 284 | - | r = box | |
| 285 | - | } else if r.W <= 0 || r.H <= 0 { | |
| 286 | - | for _, input := range rects { | |
| 287 | - | if input.W <= 0 || input.H <= 0 { | |
| 288 | - | continue | |
| 289 | - | } | |
| 290 | - | if r.W <= 0 || r.H <= 0 { | |
| 291 | - | r = input | |
| 292 | - | } else { | |
| 293 | - | x0, y0 := min(r.X, input.X), min(r.Y, input.Y) | |
| 294 | - | r = ports.Rect{X: x0, Y: y0, W: max(r.X+r.W, input.X+input.W) - x0, H: max(r.Y+r.H, input.Y+input.H) - y0} | |
| 295 | - | } | |
| 296 | - | } | |
| 297 | - | } | |
| 288 | + | r = box | |
| 298 | 289 | } | |
| 299 | 290 | } | |
| 300 | 291 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| @@ -205,6 +205,26 @@ func (s *Server) dropConstraint(c *constraint) { | |||||
| 205 | s.updateConstraint() | 205 | s.updateConstraint() | ||
| 206 | } | 206 | } | ||
| 207 | 207 | ||||
| 208 | + | // deactivateConstraint ends the active constraint; a oneshot cannot reactivate. | |||
| 209 | + | func (s *Server) deactivateConstraint() { | |||
| 210 | + | old := s.constraint | |||
| 211 | + | if old == nil { | |||
| 212 | + | return | |||
| 213 | + | } | |||
| 214 | + | old.active = false | |||
| 215 | + | if !old.persistent { | |||
| 216 | + | old.defunct = true | |||
| 217 | + | } | |||
| 218 | + | if old.lock != nil && old.lock.Resource.Alive() { | |||
| 219 | + | old.lock.SendUnlocked() | |||
| 220 | + | } | |||
| 221 | + | if old.confine != nil && old.confine.Resource.Alive() { | |||
| 222 | + | old.confine.SendUnconfined() | |||
| 223 | + | } | |||
| 224 | + | s.constraint = nil | |||
| 225 | + | s.emit(ports.PointerConstrained{}) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 208 | // updateConstraint activates the constraint of the window holding both | 228 | // updateConstraint activates the constraint of the window holding both | ||
| 209 | // pointer and keyboard focus, and deactivates any other. | 229 | // pointer and keyboard focus, and deactivates any other. | ||
| 210 | func (s *Server) updateConstraint() { | 230 | func (s *Server) updateConstraint() { | ||
| @@ -217,23 +237,11 @@ func (s *Server) updateConstraint() { | |||||
| 217 | if want == s.constraint { | 237 | if want == s.constraint { | ||
| 218 | return | 238 | return | ||
| 219 | } | 239 | } | ||
| 220 | - | if old := s.constraint; old != nil { | 240 | + | s.deactivateConstraint() |
| 221 | - | old.active = false | |||
| 222 | - | if !old.persistent { | |||
| 223 | - | old.defunct = true | |||
| 224 | - | } | |||
| 225 | - | if old.lock != nil && old.lock.Resource.Alive() { | |||
| 226 | - | old.lock.SendUnlocked() | |||
| 227 | - | } | |||
| 228 | - | if old.confine != nil && old.confine.Resource.Alive() { | |||
| 229 | - | old.confine.SendUnconfined() | |||
| 230 | - | } | |||
| 231 | - | } | |||
| 232 | - | s.constraint = want | |||
| 233 | if want == nil { | 241 | if want == nil { | ||
| 234 | - | s.emit(ports.PointerConstrained{}) | |||
| 235 | return | 242 | return | ||
| 236 | } | 243 | } | ||
| 244 | + | s.constraint = want | |||
| 237 | want.active = true | 245 | want.active = true | ||
| 238 | if want.lock != nil { | 246 | if want.lock != nil { | ||
| 239 | want.lock.SendLocked() | 247 | want.lock.SendLocked() | ||
| @@ -258,8 +266,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 258 | r.X -= x.geometry.X | 266 | r.X -= x.geometry.X | ||
| 259 | r.Y -= x.geometry.Y | 267 | r.Y -= x.geometry.Y | ||
| 260 | } | 268 | } | ||
| 261 | - | // The input region also bounds confinement. A box is sufficient for core; | 269 | + | // Core supports one confinement rect, so this bounds the intersection; |
| 262 | - | // contains uses the exact region to decide activation. | 270 | + | // contains uses the exact input region to decide activation. |
| 263 | if mode == ports.ConstraintConfine { | 271 | if mode == ports.ConstraintConfine { | ||
| 264 | if all, rects := c.surface.effectiveInput(); !all { | 272 | if all, rects := c.surface.effectiveInput(); !all { | ||
| 265 | var box ports.Rect | 273 | var box ports.Rect | ||
| @@ -277,24 +285,7 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 277 | box = ports.Rect{X: x0, Y: y0, W: max(box.X+box.W, input.X+input.W) - x0, H: max(box.Y+box.H, input.Y+input.H) - y0} | 285 | box = ports.Rect{X: x0, Y: y0, W: max(box.X+box.W, input.X+input.W) - x0, H: max(box.Y+box.H, input.Y+input.H) - y0} | ||
| 278 | } | 286 | } | ||
| 279 | } | 287 | } | ||
| 280 | - | // Core treats an empty rect as the entire window. Keep the | 288 | + | r = box |
| 281 | - | // original constraint (or the input box for an unbounded one) | |||
| 282 | - | // when disjoint, rather than widening it to the whole window. | |||
| 283 | - | if box.W > 0 { | |||
| 284 | - | r = box | |||
| 285 | - | } else if r.W <= 0 || r.H <= 0 { | |||
| 286 | - | for _, input := range rects { | |||
| 287 | - | if input.W <= 0 || input.H <= 0 { | |||
| 288 | - | continue | |||
| 289 | - | } | |||
| 290 | - | if r.W <= 0 || r.H <= 0 { | |||
| 291 | - | r = input | |||
| 292 | - | } else { | |||
| 293 | - | x0, y0 := min(r.X, input.X), min(r.Y, input.Y) | |||
| 294 | - | r = ports.Rect{X: x0, Y: y0, W: max(r.X+r.W, input.X+input.W) - x0, H: max(r.Y+r.H, input.Y+input.H) - y0} | |||
| 295 | - | } | |||
| 296 | - | } | |||
| 297 | - | } | |||
| 298 | } | 289 | } | ||
| 299 | } | 290 | } | ||
| 300 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | 291 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | ||
M internal/adapters/wayland/input_region.go +6 -1
| @@ -96,7 +96,12 @@ func (s *surface) emitInput() { | |||
| 96 | 96 | s.server.emit(ports.InputRegionChanged{ID: id, All: all, Rects: rects}) | |
| 97 | 97 | if c := s.server.constraints[root]; c != nil { | |
| 98 | 98 | if c.active { | |
| 99 | - | s.server.emitConstraint(c) | |
| 99 | + | w := s.server.windows[id] | |
| 100 | + | if w == nil || !c.contains(w) { | |
| 101 | + | s.server.deactivateConstraint() | |
| 102 | + | } else { | |
| 103 | + | s.server.emitConstraint(c) | |
| 104 | + | } | |
| 100 | 105 | } else { | |
| 101 | 106 | s.server.updateConstraint() | |
| 102 | 107 | } | |
| @@ -96,7 +96,12 @@ func (s *surface) emitInput() { | |||||
| 96 | s.server.emit(ports.InputRegionChanged{ID: id, All: all, Rects: rects}) | 96 | s.server.emit(ports.InputRegionChanged{ID: id, All: all, Rects: rects}) | ||
| 97 | if c := s.server.constraints[root]; c != nil { | 97 | if c := s.server.constraints[root]; c != nil { | ||
| 98 | if c.active { | 98 | if c.active { | ||
| 99 | - | s.server.emitConstraint(c) | 99 | + | w := s.server.windows[id] |
| 100 | + | if w == nil || !c.contains(w) { | |||
| 101 | + | s.server.deactivateConstraint() | |||
| 102 | + | } else { | |||
| 103 | + | s.server.emitConstraint(c) | |||
| 104 | + | } | |||
| 100 | } else { | 105 | } else { | ||
| 101 | s.server.updateConstraint() | 106 | s.server.updateConstraint() | ||
| 102 | } | 107 | } | ||
M internal/adapters/wayland/input_region_test.go +109 -0
| @@ -1,10 +1,12 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "testing" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/fifo" | |
| 8 | 10 | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |
| 9 | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 10 | 12 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| @@ -214,3 +216,110 @@ func TestInputRegionResentAfterRemap(t *testing.T) { | |||
| 214 | 216 | t.Fatalf("remap input: %+v", got) | |
| 215 | 217 | } | |
| 216 | 218 | } | |
| 219 | + | ||
| 220 | + | func TestRegionOverlappingAddsSubtractUnion(t *testing.T) { | |
| 221 | + | g := ®ion{} | |
| 222 | + | g.Add(nil, 0, 0, 6, 6) | |
| 223 | + | g.Add(nil, 2, 2, 6, 6) | |
| 224 | + | g.Subtract(nil, 3, 3, 2, 2) | |
| 225 | + | for _, r := range g.rects { | |
| 226 | + | if intersectRect(r, ports.Rect{X: 3, Y: 3, W: 2, H: 2}).W != 0 { | |
| 227 | + | t.Fatalf("subtracted overlap remains: %+v", g.rects) | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | if g.boxRect() != (ports.Rect{W: 8, H: 8}) { | |
| 231 | + | t.Fatalf("outer region changed: %+v", g.rects) | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | func TestInputRegionDeactivatesConstraint(t *testing.T) { | |
| 236 | + | for _, kind := range []string{"confine", "lock"} { | |
| 237 | + | for _, persistent := range []bool{false, true} { | |
| 238 | + | t.Run(fmt.Sprintf("%s/persistent=%t", kind, persistent), func(t *testing.T) { | |
| 239 | + | s, events, commands, dir := lifecycleServer(t) | |
| 240 | + | c := protocolClient(t, s, dir) | |
| 241 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 242 | + | registerProtocol(t, c, seat) | |
| 243 | + | pointer := c.AllocateID() | |
| 244 | + | registerProtocol(t, c, pointer) | |
| 245 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 246 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 247 | + | manager := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |
| 248 | + | id := c.AllocateID() | |
| 249 | + | notices := logProxy(t, c, id) | |
| 250 | + | lifetime := uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot) | |
| 251 | + | if persistent { | |
| 252 | + | lifetime = uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent) | |
| 253 | + | } | |
| 254 | + | if kind == "lock" { | |
| 255 | + | requestProtocol(t, c, manager, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, id, surf, pointer, uint32(0), lifetime) | |
| 256 | + | } else { | |
| 257 | + | requestProtocol(t, c, manager, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, id, surf, pointer, uint32(0), lifetime) | |
| 258 | + | } | |
| 259 | + | commands <- ports.PointerFocus{ID: w.ID} | |
| 260 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 261 | + | if ev := next(t, c, notices); ev[0] != 0 { | |
| 262 | + | t.Fatalf("activation: %v", ev) | |
| 263 | + | } | |
| 264 | + | if got := constrained(t, events); got.ID != w.ID { | |
| 265 | + | t.Fatalf("constraint: %+v", got) | |
| 266 | + | } | |
| 267 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 268 | + | region := c.AllocateID() | |
| 269 | + | registerProtocol(t, c, region) | |
| 270 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |
| 271 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |
| 272 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 273 | + | if ev := next(t, c, notices); ev[0] != 1 { | |
| 274 | + | t.Fatalf("deactivation: %v", ev) | |
| 275 | + | } | |
| 276 | + | if got := constrained(t, events); got.ID != 0 { | |
| 277 | + | t.Fatalf("constraint retained: %+v", got) | |
| 278 | + | } | |
| 279 | + | if err := c.Roundtrip(); err != nil { | |
| 280 | + | t.Fatal(err) | |
| 281 | + | } | |
| 282 | + | select { | |
| 283 | + | case ev := <-notices: | |
| 284 | + | t.Fatalf("unexpected reactivation: %v", ev) | |
| 285 | + | default: | |
| 286 | + | } | |
| 287 | + | }) | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | func TestFifoQueuedInputRegionsApplyInOrder(t *testing.T) { | |
| 293 | + | s, events, _, dir := lifecycleServer(t) | |
| 294 | + | c := protocolClient(t, s, dir) | |
| 295 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 296 | + | manager := bindProtocol(t, c, "wp_fifo_manager_v1") | |
| 297 | + | f := c.AllocateID() | |
| 298 | + | registerProtocol(t, c, f) | |
| 299 | + | requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf) | |
| 300 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 301 | + | region := c.AllocateID() | |
| 302 | + | registerProtocol(t, c, region) | |
| 303 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |
| 304 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |
| 305 | + | // First commit sets a barrier; the next two wait behind it. | |
| 306 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier) | |
| 307 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 308 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |
| 309 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |
| 310 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 311 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |
| 312 | + | empty := c.AllocateID() | |
| 313 | + | registerProtocol(t, c, empty) | |
| 314 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, empty) | |
| 315 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, empty) | |
| 316 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 317 | + | if err := c.Roundtrip(); err != nil { | |
| 318 | + | t.Fatal(err) | |
| 319 | + | } | |
| 320 | + | first := inputChanged(t, events) | |
| 321 | + | second := inputChanged(t, events) | |
| 322 | + | if first.ID != w.ID || len(first.Rects) != 1 || first.Rects[0] != (ports.Rect{W: 1, H: 1}) || second.ID != w.ID || second.All || len(second.Rects) != 0 { | |
| 323 | + | t.Fatalf("queued input order: first %+v, second %+v", first, second) | |
| 324 | + | } | |
| 325 | + | } | |
| @@ -1,10 +1,12 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 4 | "testing" | 5 | "testing" | ||
| 5 | "time" | 6 | "time" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/fifo" | |||
| 8 | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | 10 | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | ||
| 9 | "github.com/bnema/purego-libwayland/protocol/wayland" | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 10 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | 12 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | ||
| @@ -214,3 +216,110 @@ func TestInputRegionResentAfterRemap(t *testing.T) { | |||||
| 214 | t.Fatalf("remap input: %+v", got) | 216 | t.Fatalf("remap input: %+v", got) | ||
| 215 | } | 217 | } | ||
| 216 | } | 218 | } | ||
| 219 | + | ||||
| 220 | + | func TestRegionOverlappingAddsSubtractUnion(t *testing.T) { | |||
| 221 | + | g := ®ion{} | |||
| 222 | + | g.Add(nil, 0, 0, 6, 6) | |||
| 223 | + | g.Add(nil, 2, 2, 6, 6) | |||
| 224 | + | g.Subtract(nil, 3, 3, 2, 2) | |||
| 225 | + | for _, r := range g.rects { | |||
| 226 | + | if intersectRect(r, ports.Rect{X: 3, Y: 3, W: 2, H: 2}).W != 0 { | |||
| 227 | + | t.Fatalf("subtracted overlap remains: %+v", g.rects) | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 230 | + | if g.boxRect() != (ports.Rect{W: 8, H: 8}) { | |||
| 231 | + | t.Fatalf("outer region changed: %+v", g.rects) | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | func TestInputRegionDeactivatesConstraint(t *testing.T) { | |||
| 236 | + | for _, kind := range []string{"confine", "lock"} { | |||
| 237 | + | for _, persistent := range []bool{false, true} { | |||
| 238 | + | t.Run(fmt.Sprintf("%s/persistent=%t", kind, persistent), func(t *testing.T) { | |||
| 239 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 240 | + | c := protocolClient(t, s, dir) | |||
| 241 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 242 | + | registerProtocol(t, c, seat) | |||
| 243 | + | pointer := c.AllocateID() | |||
| 244 | + | registerProtocol(t, c, pointer) | |||
| 245 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 246 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 247 | + | manager := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |||
| 248 | + | id := c.AllocateID() | |||
| 249 | + | notices := logProxy(t, c, id) | |||
| 250 | + | lifetime := uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot) | |||
| 251 | + | if persistent { | |||
| 252 | + | lifetime = uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent) | |||
| 253 | + | } | |||
| 254 | + | if kind == "lock" { | |||
| 255 | + | requestProtocol(t, c, manager, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, id, surf, pointer, uint32(0), lifetime) | |||
| 256 | + | } else { | |||
| 257 | + | requestProtocol(t, c, manager, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, id, surf, pointer, uint32(0), lifetime) | |||
| 258 | + | } | |||
| 259 | + | commands <- ports.PointerFocus{ID: w.ID} | |||
| 260 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 261 | + | if ev := next(t, c, notices); ev[0] != 0 { | |||
| 262 | + | t.Fatalf("activation: %v", ev) | |||
| 263 | + | } | |||
| 264 | + | if got := constrained(t, events); got.ID != w.ID { | |||
| 265 | + | t.Fatalf("constraint: %+v", got) | |||
| 266 | + | } | |||
| 267 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 268 | + | region := c.AllocateID() | |||
| 269 | + | registerProtocol(t, c, region) | |||
| 270 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |||
| 271 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |||
| 272 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 273 | + | if ev := next(t, c, notices); ev[0] != 1 { | |||
| 274 | + | t.Fatalf("deactivation: %v", ev) | |||
| 275 | + | } | |||
| 276 | + | if got := constrained(t, events); got.ID != 0 { | |||
| 277 | + | t.Fatalf("constraint retained: %+v", got) | |||
| 278 | + | } | |||
| 279 | + | if err := c.Roundtrip(); err != nil { | |||
| 280 | + | t.Fatal(err) | |||
| 281 | + | } | |||
| 282 | + | select { | |||
| 283 | + | case ev := <-notices: | |||
| 284 | + | t.Fatalf("unexpected reactivation: %v", ev) | |||
| 285 | + | default: | |||
| 286 | + | } | |||
| 287 | + | }) | |||
| 288 | + | } | |||
| 289 | + | } | |||
| 290 | + | } | |||
| 291 | + | ||||
| 292 | + | func TestFifoQueuedInputRegionsApplyInOrder(t *testing.T) { | |||
| 293 | + | s, events, _, dir := lifecycleServer(t) | |||
| 294 | + | c := protocolClient(t, s, dir) | |||
| 295 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 296 | + | manager := bindProtocol(t, c, "wp_fifo_manager_v1") | |||
| 297 | + | f := c.AllocateID() | |||
| 298 | + | registerProtocol(t, c, f) | |||
| 299 | + | requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf) | |||
| 300 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 301 | + | region := c.AllocateID() | |||
| 302 | + | registerProtocol(t, c, region) | |||
| 303 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |||
| 304 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |||
| 305 | + | // First commit sets a barrier; the next two wait behind it. | |||
| 306 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier) | |||
| 307 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 308 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |||
| 309 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |||
| 310 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 311 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |||
| 312 | + | empty := c.AllocateID() | |||
| 313 | + | registerProtocol(t, c, empty) | |||
| 314 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, empty) | |||
| 315 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, empty) | |||
| 316 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 317 | + | if err := c.Roundtrip(); err != nil { | |||
| 318 | + | t.Fatal(err) | |||
| 319 | + | } | |||
| 320 | + | first := inputChanged(t, events) | |||
| 321 | + | second := inputChanged(t, events) | |||
| 322 | + | if first.ID != w.ID || len(first.Rects) != 1 || first.Rects[0] != (ports.Rect{W: 1, H: 1}) || second.ID != w.ID || second.All || len(second.Rects) != 0 { | |||
| 323 | + | t.Fatalf("queued input order: first %+v, second %+v", first, second) | |||
| 324 | + | } | |||
| 325 | + | } | |||
M internal/core/layer_input_test.go +36 -0
| @@ -357,3 +357,39 @@ func TestRemovedLayerInputRegionCleared(t *testing.T) { | |||
| 357 | 357 | t.Fatalf("window region removed with layer: %+v", v) | |
| 358 | 358 | } | |
| 359 | 359 | } | |
| 360 | + | ||
| 361 | + | func TestPopupInputRegionPassesThroughToWindow(t *testing.T) { | |
| 362 | + | cfg := config.Defaults() | |
| 363 | + | cfg.Border.Width = 0 | |
| 364 | + | client := make(chan ports.ClientEvent, 16) | |
| 365 | + | input := make(chan ports.InputEvent, 16) | |
| 366 | + | output := make(chan ports.OutputEvent, 16) | |
| 367 | + | commands := make(chan ports.ClientCommand, 256) | |
| 368 | + | scenes := make(chan []ports.Scene, 1) | |
| 369 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 370 | + | if err != nil { | |
| 371 | + | t.Fatal(err) | |
| 372 | + | } | |
| 373 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 374 | + | defer cancel() | |
| 375 | + | go c.Run(ctx) | |
| 376 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 377 | + | scene(t, scenes) | |
| 378 | + | client <- ports.WindowMapped{ID: 1} | |
| 379 | + | scene(t, scenes) | |
| 380 | + | client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 10, Y: 10, W: 10, H: 10}}} | |
| 381 | + | next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 }) | |
| 382 | + | client <- ports.PopupMapped{ID: 9} | |
| 383 | + | sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 }) | |
| 384 | + | client <- ports.InputRegionChanged{ID: 9, Rects: []ports.Rect{{X: 10, Y: 0, W: 20, H: 20}}} | |
| 385 | + | scene(t, scenes) | |
| 386 | + | popup := sc.Windows[1].Rect | |
| 387 | + | input <- ports.PointerMotion{X: float64(popup.X + 2), Y: float64(popup.Y + 2)} | |
| 388 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 1 { | |
| 389 | + | t.Fatalf("popup margin motion: %+v, popup %+v", v, popup) | |
| 390 | + | } | |
| 391 | + | input <- ports.PointerMotion{X: float64(popup.X + 12), Y: float64(popup.Y + 2)} | |
| 392 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 9 { | |
| 393 | + | t.Fatalf("popup interior motion: %+v", v) | |
| 394 | + | } | |
| 395 | + | } | |
| @@ -357,3 +357,39 @@ func TestRemovedLayerInputRegionCleared(t *testing.T) { | |||||
| 357 | t.Fatalf("window region removed with layer: %+v", v) | 357 | t.Fatalf("window region removed with layer: %+v", v) | ||
| 358 | } | 358 | } | ||
| 359 | } | 359 | } | ||
| 360 | + | ||||
| 361 | + | func TestPopupInputRegionPassesThroughToWindow(t *testing.T) { | |||
| 362 | + | cfg := config.Defaults() | |||
| 363 | + | cfg.Border.Width = 0 | |||
| 364 | + | client := make(chan ports.ClientEvent, 16) | |||
| 365 | + | input := make(chan ports.InputEvent, 16) | |||
| 366 | + | output := make(chan ports.OutputEvent, 16) | |||
| 367 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 368 | + | scenes := make(chan []ports.Scene, 1) | |||
| 369 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |||
| 370 | + | if err != nil { | |||
| 371 | + | t.Fatal(err) | |||
| 372 | + | } | |||
| 373 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 374 | + | defer cancel() | |||
| 375 | + | go c.Run(ctx) | |||
| 376 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 377 | + | scene(t, scenes) | |||
| 378 | + | client <- ports.WindowMapped{ID: 1} | |||
| 379 | + | scene(t, scenes) | |||
| 380 | + | client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 10, Y: 10, W: 10, H: 10}}} | |||
| 381 | + | next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 }) | |||
| 382 | + | client <- ports.PopupMapped{ID: 9} | |||
| 383 | + | sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 }) | |||
| 384 | + | client <- ports.InputRegionChanged{ID: 9, Rects: []ports.Rect{{X: 10, Y: 0, W: 20, H: 20}}} | |||
| 385 | + | scene(t, scenes) | |||
| 386 | + | popup := sc.Windows[1].Rect | |||
| 387 | + | input <- ports.PointerMotion{X: float64(popup.X + 2), Y: float64(popup.Y + 2)} | |||
| 388 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 1 { | |||
| 389 | + | t.Fatalf("popup margin motion: %+v, popup %+v", v, popup) | |||
| 390 | + | } | |||
| 391 | + | input <- ports.PointerMotion{X: float64(popup.X + 12), Y: float64(popup.Y + 2)} | |||
| 392 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 9 { | |||
| 393 | + | t.Fatalf("popup interior motion: %+v", v) | |||
| 394 | + | } | |||
| 395 | + | } | |||