test(wayland): cover X11 pointer lock, confine, relative motion and single-output enter
- commit
- 864c00579152f5c5e1f3eb66a495f1d729bc706d
- author
- brice · · 2026-10-22 15:33 UTC
- committer
- brice · · 2026-10-22 17:54 UTC
- parent
- c5b7312
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +199 -1
M internal/adapters/wayland/x11window_test.go +199 -1
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "time" | |
| 10 | 10 | ||
| 11 | 11 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |
| 12 | + | "github.com/bnema/go-wayland-bindings/server/relativepointer" | |
| 12 | 13 | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 13 | 14 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 14 | 15 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| @@ -33,6 +34,8 @@ type x11Rig struct { | |||
| 33 | 34 | stop func() | |
| 34 | 35 | shell uint32 | |
| 35 | 36 | comp uint32 | |
| 37 | + | // surfaceProxy, when set, observes the next paired wl_surface. | |
| 38 | + | surfaceProxy func(id uint32) wlturbo.Proxy | |
| 36 | 39 | } | |
| 37 | 40 | ||
| 38 | 41 | func newX11Rig(t *testing.T) *x11Rig { | |
| @@ -86,7 +89,11 @@ func (r *x11Rig) pair(win uint32, serial uint64) uint32 { | |||
| 86 | 89 | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | |
| 87 | 90 | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |
| 88 | 91 | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 89 | - | registerProtocol(r.t, r.xc, surf) | |
| 92 | + | if r.surfaceProxy != nil { | |
| 93 | + | registerWireProxy(r.xc, r.surfaceProxy(surf)) | |
| 94 | + | } else { | |
| 95 | + | registerProtocol(r.t, r.xc, surf) | |
| 96 | + | } | |
| 90 | 97 | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 91 | 98 | registerProtocol(r.t, r.xc, xs) | |
| 92 | 99 | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | |
| @@ -351,3 +358,194 @@ func TestX11WindowPointerLock(t *testing.T) { | |||
| 351 | 358 | t.Fatalf("constrained %+v", got) | |
| 352 | 359 | } | |
| 353 | 360 | } | |
| 361 | + | ||
| 362 | + | // x11Pointer gives the Xwayland client a wl_pointer and a relative pointer, | |
| 363 | + | // both with event logs. | |
| 364 | + | func (r *x11Rig) x11Pointer() (pointer uint32, ptrEvents, relEvents chan []float64) { | |
| 365 | + | r.t.Helper() | |
| 366 | + | seat := bindProtocol(r.t, r.xc, "wl_seat") | |
| 367 | + | registerProtocol(r.t, r.xc, seat) | |
| 368 | + | pointer = r.xc.AllocateID() | |
| 369 | + | ptrEvents = logProxy(r.t, r.xc, pointer) | |
| 370 | + | requestProtocol(r.t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |
| 371 | + | mgr := bindProtocol(r.t, r.xc, "zwp_relative_pointer_manager_v1") | |
| 372 | + | rel := r.xc.AllocateID() | |
| 373 | + | relEvents = logProxy(r.t, r.xc, rel) | |
| 374 | + | requestProtocol(r.t, r.xc, mgr, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer) | |
| 375 | + | return pointer, ptrEvents, relEvents | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | // A lock on an X11 surface activates with pointer and keyboard focus, sends | |
| 379 | + | // relative motion to the Xwayland client and deactivates when a native | |
| 380 | + | // window takes the focus; it comes back with the X11 focus. | |
| 381 | + | func TestX11LockFollowsFocusAndRelativeMotion(t *testing.T) { | |
| 382 | + | r := newX11Rig(t) | |
| 383 | + | win := uint32(x11Win) | |
| 384 | + | r.x11Window(win) | |
| 385 | + | surf := r.pair(win, x11Serial) | |
| 386 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 387 | + | pointer, _, relEvents := r.x11Pointer() | |
| 388 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |
| 389 | + | lock := r.xc.AllocateID() | |
| 390 | + | lockEvents := logProxy(t, r.xc, lock) | |
| 391 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 392 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 393 | + | t.Fatal(err) | |
| 394 | + | } | |
| 395 | + | if len(lockEvents) > 0 { | |
| 396 | + | t.Fatal("locked without focus") | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |
| 400 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 401 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |
| 402 | + | t.Fatalf("lock event %v", ev) | |
| 403 | + | } | |
| 404 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock || got.Rect != (ports.Rect{}) { | |
| 405 | + | t.Fatalf("constrained %+v", got) | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | r.commands <- ports.PointerMotionTo{ID: m.ID, X: 5, Y: 5, DX: 2.5, DY: -1, UnaccelDX: 4, UnaccelDY: -2} | |
| 409 | + | if ev := next(t, r.xc, relEvents); len(ev) != 5 || ev[1] != 2.5 || ev[2] != -1 || ev[3] != 4 || ev[4] != -2 { | |
| 410 | + | t.Fatalf("relative motion %v", ev) | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | // A native window takes the focus: the X11 lock ends and the XWM drops | |
| 414 | + | // the X focus. | |
| 415 | + | nc := protocolClient(t, r.s, r.dir) | |
| 416 | + | nm, _, _ := surfaceMapper(t, nc, r.events)() | |
| 417 | + | skip := len(r.peer.requests()) | |
| 418 | + | r.commands <- ports.FocusWindow{ID: nm.ID} | |
| 419 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) { | |
| 420 | + | t.Fatalf("unlock event %v", ev) | |
| 421 | + | } | |
| 422 | + | if got := constrained(t, r.events); got.ID != 0 { | |
| 423 | + | t.Fatalf("constrained %+v after native focus", got) | |
| 424 | + | } | |
| 425 | + | if f, _ := r.peer.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) == win { | |
| 426 | + | t.Fatalf("X focus still on %#x", win) | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 430 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |
| 431 | + | t.Fatalf("relock event %v", ev) | |
| 432 | + | } | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | // A confine on an X11 surface reaches core with its region as given: an X11 | |
| 436 | + | // window has no geometry offset. | |
| 437 | + | func TestX11ConfineRegionHasNoOffset(t *testing.T) { | |
| 438 | + | r := newX11Rig(t) | |
| 439 | + | win := uint32(x11Win) | |
| 440 | + | r.x11Window(win) | |
| 441 | + | surf := r.pair(win, x11Serial) | |
| 442 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 443 | + | pointer, _, _ := r.x11Pointer() | |
| 444 | + | region := r.xc.AllocateID() | |
| 445 | + | registerProtocol(t, r.xc, region) | |
| 446 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateRegion, region) | |
| 447 | + | requestProtocol(t, r.xc, region, wayland.RegionRequestAdd, int32(5), int32(6), int32(20), int32(30)) | |
| 448 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |
| 449 | + | confine := r.xc.AllocateID() | |
| 450 | + | confineEvents := logProxy(t, r.xc, confine) | |
| 451 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 452 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 453 | + | t.Fatal(err) | |
| 454 | + | } | |
| 455 | + | r.state(func() { | |
| 456 | + | if g := r.s.windows[m.ID].geometry(); g != (ports.Rect{}) { | |
| 457 | + | t.Errorf("X11 geometry %+v, want zero", g) | |
| 458 | + | } | |
| 459 | + | }) | |
| 460 | + | ||
| 461 | + | r.commands <- ports.PointerFocus{ID: m.ID, X: 10, Y: 10} | |
| 462 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 463 | + | r.commands <- ports.PointerMotionTo{ID: m.ID, X: 10, Y: 10} | |
| 464 | + | if ev := next(t, r.xc, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) { | |
| 465 | + | t.Fatalf("confine event %v", ev) | |
| 466 | + | } | |
| 467 | + | want := ports.PointerConstrained{ID: m.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 5, Y: 6, W: 20, H: 30}}} | |
| 468 | + | if got := constrained(t, r.events); got != want { | |
| 469 | + | t.Fatalf("constrained %+v, want %+v", got, want) | |
| 470 | + | } | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | // An X11 window is on one output only, even straddling two: a single | |
| 474 | + | // wl_surface.enter, and a leave only when its output changes. | |
| 475 | + | func TestX11WindowEntersOneOutput(t *testing.T) { | |
| 476 | + | r := newX11Rig(t) | |
| 477 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |
| 478 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |
| 479 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |
| 480 | + | }, Focused: "A"} | |
| 481 | + | r.state(func() {}) // the layout is applied | |
| 482 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 483 | + | t.Fatal(err) | |
| 484 | + | } | |
| 485 | + | outputs := 0 | |
| 486 | + | for _, g := range r.xc.Registry().GetGlobals() { | |
| 487 | + | if g.Interface != "wl_output" { | |
| 488 | + | continue | |
| 489 | + | } | |
| 490 | + | id, err := bindWireID(r.xc, g.Name, g.Interface, 1) | |
| 491 | + | if err != nil { | |
| 492 | + | t.Fatal(err) | |
| 493 | + | } | |
| 494 | + | registerProtocol(t, r.xc, id) | |
| 495 | + | outputs++ | |
| 496 | + | } | |
| 497 | + | if outputs != 2 { | |
| 498 | + | t.Fatalf("%d wl_output globals, want 2", outputs) | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | win := uint32(x11Win) | |
| 502 | + | r.x11Window(win) | |
| 503 | + | events := make(chan []float64, 64) | |
| 504 | + | r.surfaceProxy = func(id uint32) wlturbo.Proxy { | |
| 505 | + | p := &eventLog{events: events} | |
| 506 | + | p.SetID(id) | |
| 507 | + | return p | |
| 508 | + | } | |
| 509 | + | r.pair(win, x11Serial) | |
| 510 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 511 | + | ||
| 512 | + | // Straddling both outputs, configured on A. | |
| 513 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 1900, Y: 0} | |
| 514 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "A"} | |
| 515 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 516 | + | t.Fatal(err) | |
| 517 | + | } | |
| 518 | + | r.state(func() {}) | |
| 519 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 520 | + | t.Fatal(err) | |
| 521 | + | } | |
| 522 | + | count := func() (enters, leaves int) { | |
| 523 | + | for len(events) > 0 { | |
| 524 | + | switch ev := <-events; ev[0] { | |
| 525 | + | case float64(wayland.SurfaceEventEnter): | |
| 526 | + | enters++ | |
| 527 | + | case float64(wayland.SurfaceEventLeave): | |
| 528 | + | leaves++ | |
| 529 | + | } | |
| 530 | + | } | |
| 531 | + | return | |
| 532 | + | } | |
| 533 | + | if e, l := count(); e != 1 || l != 0 { | |
| 534 | + | t.Fatalf("straddling window: %d enters, %d leaves, want 1, 0", e, l) | |
| 535 | + | } | |
| 536 | + | r.state(func() { | |
| 537 | + | if on := r.s.windows[m.ID].surf().on; on == nil || on.name() != "A" { | |
| 538 | + | t.Errorf("surface on %v, want A", on) | |
| 539 | + | } | |
| 540 | + | }) | |
| 541 | + | ||
| 542 | + | // Moving to B leaves A and enters B once. | |
| 543 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "B"} | |
| 544 | + | r.state(func() {}) | |
| 545 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 546 | + | t.Fatal(err) | |
| 547 | + | } | |
| 548 | + | if e, l := count(); e != 1 || l != 1 { | |
| 549 | + | t.Fatalf("move to B: %d enters, %d leaves, want 1, 1", e, l) | |
| 550 | + | } | |
| 551 | + | } | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | "time" | 9 | "time" | ||
| 10 | 10 | ||||
| 11 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | 11 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | ||
| 12 | + | "github.com/bnema/go-wayland-bindings/server/relativepointer" | |||
| 12 | "github.com/bnema/go-wayland-bindings/server/wayland" | 13 | "github.com/bnema/go-wayland-bindings/server/wayland" | ||
| 13 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | 14 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | ||
| 14 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | 15 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | ||
| @@ -33,6 +34,8 @@ type x11Rig struct { | |||||
| 33 | stop func() | 34 | stop func() | ||
| 34 | shell uint32 | 35 | shell uint32 | ||
| 35 | comp uint32 | 36 | comp uint32 | ||
| 37 | + | // surfaceProxy, when set, observes the next paired wl_surface. | |||
| 38 | + | surfaceProxy func(id uint32) wlturbo.Proxy | |||
| 36 | } | 39 | } | ||
| 37 | 40 | ||||
| 38 | func newX11Rig(t *testing.T) *x11Rig { | 41 | func newX11Rig(t *testing.T) *x11Rig { | ||
| @@ -86,7 +89,11 @@ func (r *x11Rig) pair(win uint32, serial uint64) uint32 { | |||||
| 86 | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | 89 | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | ||
| 87 | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | 90 | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | ||
| 88 | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | 91 | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | ||
| 89 | - | registerProtocol(r.t, r.xc, surf) | 92 | + | if r.surfaceProxy != nil { |
| 93 | + | registerWireProxy(r.xc, r.surfaceProxy(surf)) | |||
| 94 | + | } else { | |||
| 95 | + | registerProtocol(r.t, r.xc, surf) | |||
| 96 | + | } | |||
| 90 | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | 97 | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | ||
| 91 | registerProtocol(r.t, r.xc, xs) | 98 | registerProtocol(r.t, r.xc, xs) | ||
| 92 | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | 99 | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | ||
| @@ -351,3 +358,194 @@ func TestX11WindowPointerLock(t *testing.T) { | |||||
| 351 | t.Fatalf("constrained %+v", got) | 358 | t.Fatalf("constrained %+v", got) | ||
| 352 | } | 359 | } | ||
| 353 | } | 360 | } | ||
| 361 | + | ||||
| 362 | + | // x11Pointer gives the Xwayland client a wl_pointer and a relative pointer, | |||
| 363 | + | // both with event logs. | |||
| 364 | + | func (r *x11Rig) x11Pointer() (pointer uint32, ptrEvents, relEvents chan []float64) { | |||
| 365 | + | r.t.Helper() | |||
| 366 | + | seat := bindProtocol(r.t, r.xc, "wl_seat") | |||
| 367 | + | registerProtocol(r.t, r.xc, seat) | |||
| 368 | + | pointer = r.xc.AllocateID() | |||
| 369 | + | ptrEvents = logProxy(r.t, r.xc, pointer) | |||
| 370 | + | requestProtocol(r.t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 371 | + | mgr := bindProtocol(r.t, r.xc, "zwp_relative_pointer_manager_v1") | |||
| 372 | + | rel := r.xc.AllocateID() | |||
| 373 | + | relEvents = logProxy(r.t, r.xc, rel) | |||
| 374 | + | requestProtocol(r.t, r.xc, mgr, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer) | |||
| 375 | + | return pointer, ptrEvents, relEvents | |||
| 376 | + | } | |||
| 377 | + | ||||
| 378 | + | // A lock on an X11 surface activates with pointer and keyboard focus, sends | |||
| 379 | + | // relative motion to the Xwayland client and deactivates when a native | |||
| 380 | + | // window takes the focus; it comes back with the X11 focus. | |||
| 381 | + | func TestX11LockFollowsFocusAndRelativeMotion(t *testing.T) { | |||
| 382 | + | r := newX11Rig(t) | |||
| 383 | + | win := uint32(x11Win) | |||
| 384 | + | r.x11Window(win) | |||
| 385 | + | surf := r.pair(win, x11Serial) | |||
| 386 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 387 | + | pointer, _, relEvents := r.x11Pointer() | |||
| 388 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |||
| 389 | + | lock := r.xc.AllocateID() | |||
| 390 | + | lockEvents := logProxy(t, r.xc, lock) | |||
| 391 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 392 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 393 | + | t.Fatal(err) | |||
| 394 | + | } | |||
| 395 | + | if len(lockEvents) > 0 { | |||
| 396 | + | t.Fatal("locked without focus") | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |||
| 400 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 401 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |||
| 402 | + | t.Fatalf("lock event %v", ev) | |||
| 403 | + | } | |||
| 404 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock || got.Rect != (ports.Rect{}) { | |||
| 405 | + | t.Fatalf("constrained %+v", got) | |||
| 406 | + | } | |||
| 407 | + | ||||
| 408 | + | r.commands <- ports.PointerMotionTo{ID: m.ID, X: 5, Y: 5, DX: 2.5, DY: -1, UnaccelDX: 4, UnaccelDY: -2} | |||
| 409 | + | if ev := next(t, r.xc, relEvents); len(ev) != 5 || ev[1] != 2.5 || ev[2] != -1 || ev[3] != 4 || ev[4] != -2 { | |||
| 410 | + | t.Fatalf("relative motion %v", ev) | |||
| 411 | + | } | |||
| 412 | + | ||||
| 413 | + | // A native window takes the focus: the X11 lock ends and the XWM drops | |||
| 414 | + | // the X focus. | |||
| 415 | + | nc := protocolClient(t, r.s, r.dir) | |||
| 416 | + | nm, _, _ := surfaceMapper(t, nc, r.events)() | |||
| 417 | + | skip := len(r.peer.requests()) | |||
| 418 | + | r.commands <- ports.FocusWindow{ID: nm.ID} | |||
| 419 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) { | |||
| 420 | + | t.Fatalf("unlock event %v", ev) | |||
| 421 | + | } | |||
| 422 | + | if got := constrained(t, r.events); got.ID != 0 { | |||
| 423 | + | t.Fatalf("constrained %+v after native focus", got) | |||
| 424 | + | } | |||
| 425 | + | if f, _ := r.peer.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) == win { | |||
| 426 | + | t.Fatalf("X focus still on %#x", win) | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 430 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |||
| 431 | + | t.Fatalf("relock event %v", ev) | |||
| 432 | + | } | |||
| 433 | + | } | |||
| 434 | + | ||||
| 435 | + | // A confine on an X11 surface reaches core with its region as given: an X11 | |||
| 436 | + | // window has no geometry offset. | |||
| 437 | + | func TestX11ConfineRegionHasNoOffset(t *testing.T) { | |||
| 438 | + | r := newX11Rig(t) | |||
| 439 | + | win := uint32(x11Win) | |||
| 440 | + | r.x11Window(win) | |||
| 441 | + | surf := r.pair(win, x11Serial) | |||
| 442 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 443 | + | pointer, _, _ := r.x11Pointer() | |||
| 444 | + | region := r.xc.AllocateID() | |||
| 445 | + | registerProtocol(t, r.xc, region) | |||
| 446 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateRegion, region) | |||
| 447 | + | requestProtocol(t, r.xc, region, wayland.RegionRequestAdd, int32(5), int32(6), int32(20), int32(30)) | |||
| 448 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |||
| 449 | + | confine := r.xc.AllocateID() | |||
| 450 | + | confineEvents := logProxy(t, r.xc, confine) | |||
| 451 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 452 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 453 | + | t.Fatal(err) | |||
| 454 | + | } | |||
| 455 | + | r.state(func() { | |||
| 456 | + | if g := r.s.windows[m.ID].geometry(); g != (ports.Rect{}) { | |||
| 457 | + | t.Errorf("X11 geometry %+v, want zero", g) | |||
| 458 | + | } | |||
| 459 | + | }) | |||
| 460 | + | ||||
| 461 | + | r.commands <- ports.PointerFocus{ID: m.ID, X: 10, Y: 10} | |||
| 462 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 463 | + | r.commands <- ports.PointerMotionTo{ID: m.ID, X: 10, Y: 10} | |||
| 464 | + | if ev := next(t, r.xc, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) { | |||
| 465 | + | t.Fatalf("confine event %v", ev) | |||
| 466 | + | } | |||
| 467 | + | want := ports.PointerConstrained{ID: m.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 5, Y: 6, W: 20, H: 30}}} | |||
| 468 | + | if got := constrained(t, r.events); got != want { | |||
| 469 | + | t.Fatalf("constrained %+v, want %+v", got, want) | |||
| 470 | + | } | |||
| 471 | + | } | |||
| 472 | + | ||||
| 473 | + | // An X11 window is on one output only, even straddling two: a single | |||
| 474 | + | // wl_surface.enter, and a leave only when its output changes. | |||
| 475 | + | func TestX11WindowEntersOneOutput(t *testing.T) { | |||
| 476 | + | r := newX11Rig(t) | |||
| 477 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |||
| 478 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 479 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 480 | + | }, Focused: "A"} | |||
| 481 | + | r.state(func() {}) // the layout is applied | |||
| 482 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 483 | + | t.Fatal(err) | |||
| 484 | + | } | |||
| 485 | + | outputs := 0 | |||
| 486 | + | for _, g := range r.xc.Registry().GetGlobals() { | |||
| 487 | + | if g.Interface != "wl_output" { | |||
| 488 | + | continue | |||
| 489 | + | } | |||
| 490 | + | id, err := bindWireID(r.xc, g.Name, g.Interface, 1) | |||
| 491 | + | if err != nil { | |||
| 492 | + | t.Fatal(err) | |||
| 493 | + | } | |||
| 494 | + | registerProtocol(t, r.xc, id) | |||
| 495 | + | outputs++ | |||
| 496 | + | } | |||
| 497 | + | if outputs != 2 { | |||
| 498 | + | t.Fatalf("%d wl_output globals, want 2", outputs) | |||
| 499 | + | } | |||
| 500 | + | ||||
| 501 | + | win := uint32(x11Win) | |||
| 502 | + | r.x11Window(win) | |||
| 503 | + | events := make(chan []float64, 64) | |||
| 504 | + | r.surfaceProxy = func(id uint32) wlturbo.Proxy { | |||
| 505 | + | p := &eventLog{events: events} | |||
| 506 | + | p.SetID(id) | |||
| 507 | + | return p | |||
| 508 | + | } | |||
| 509 | + | r.pair(win, x11Serial) | |||
| 510 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 511 | + | ||||
| 512 | + | // Straddling both outputs, configured on A. | |||
| 513 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 1900, Y: 0} | |||
| 514 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "A"} | |||
| 515 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 516 | + | t.Fatal(err) | |||
| 517 | + | } | |||
| 518 | + | r.state(func() {}) | |||
| 519 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 520 | + | t.Fatal(err) | |||
| 521 | + | } | |||
| 522 | + | count := func() (enters, leaves int) { | |||
| 523 | + | for len(events) > 0 { | |||
| 524 | + | switch ev := <-events; ev[0] { | |||
| 525 | + | case float64(wayland.SurfaceEventEnter): | |||
| 526 | + | enters++ | |||
| 527 | + | case float64(wayland.SurfaceEventLeave): | |||
| 528 | + | leaves++ | |||
| 529 | + | } | |||
| 530 | + | } | |||
| 531 | + | return | |||
| 532 | + | } | |||
| 533 | + | if e, l := count(); e != 1 || l != 0 { | |||
| 534 | + | t.Fatalf("straddling window: %d enters, %d leaves, want 1, 0", e, l) | |||
| 535 | + | } | |||
| 536 | + | r.state(func() { | |||
| 537 | + | if on := r.s.windows[m.ID].surf().on; on == nil || on.name() != "A" { | |||
| 538 | + | t.Errorf("surface on %v, want A", on) | |||
| 539 | + | } | |||
| 540 | + | }) | |||
| 541 | + | ||||
| 542 | + | // Moving to B leaves A and enters B once. | |||
| 543 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "B"} | |||
| 544 | + | r.state(func() {}) | |||
| 545 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 546 | + | t.Fatal(err) | |||
| 547 | + | } | |||
| 548 | + | if e, l := count(); e != 1 || l != 1 { | |||
| 549 | + | t.Fatalf("move to B: %d enters, %d leaves, want 1, 1", e, l) | |||
| 550 | + | } | |||
| 551 | + | } | |||