detached 864c005

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 (
99 "time"
1010
1111 "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
12+ "github.com/bnema/go-wayland-bindings/server/relativepointer"
1213 "github.com/bnema/go-wayland-bindings/server/wayland"
1314 "github.com/bnema/go-wayland-bindings/server/xdgshell"
1415 "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
@@ -33,6 +34,8 @@ type x11Rig struct {
3334 stop func()
3435 shell uint32
3536 comp uint32
37+ // surfaceProxy, when set, observes the next paired wl_surface.
38+ surfaceProxy func(id uint32) wlturbo.Proxy
3639 }
3740
3841 func newX11Rig(t *testing.T) *x11Rig {
@@ -86,7 +89,11 @@ func (r *x11Rig) pair(win uint32, serial uint64) uint32 {
8689 r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32))
8790 surf, xs := r.xc.AllocateID(), r.xc.AllocateID()
8891 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+ }
9097 requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf)
9198 registerProtocol(r.t, r.xc, xs)
9299 requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32))
@@ -351,3 +358,194 @@ func TestX11WindowPointerLock(t *testing.T) {
351358 t.Fatalf("constrained %+v", got)
352359 }
353360 }
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 uint3235 shell uint32
35 comp uint3236 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close