detached 56a0abd

Merge branch 'feat/x8-pointer' into feat/x8

commit
56a0abd9efaf7240e5466d189954d2be371be553
author
brice · · 2026-10-22 15:35 UTC
committer
brice · · 2026-10-22 17:54 UTC
merge of
b77ab13 + 864c005 · diff against the first parent
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/xwaylandkeyboardgrab"
@@ -34,6 +35,8 @@ type x11Rig struct {
3435 stop func()
3536 shell uint32
3637 comp uint32
38+ // surfaceProxy, when set, observes the next paired wl_surface.
39+ surfaceProxy func(id uint32) wlturbo.Proxy
3740 }
3841
3942 func newX11Rig(t *testing.T) *x11Rig {
@@ -87,7 +90,11 @@ func (r *x11Rig) pair(win uint32, serial uint64) uint32 {
8790 r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32))
8891 surf, xs := r.xc.AllocateID(), r.xc.AllocateID()
8992 requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf)
90- registerProtocol(r.t, r.xc, surf)
93+ if r.surfaceProxy != nil {
94+ registerWireProxy(r.xc, r.surfaceProxy(surf))
95+ } else {
96+ registerProtocol(r.t, r.xc, surf)
97+ }
9198 requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf)
9299 registerProtocol(r.t, r.xc, xs)
93100 requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32))
@@ -414,3 +421,194 @@ func TestXwaylandKeyboardGrabThreeStates(t *testing.T) {
414421 t.Fatal(v)
415422 }
416423 }
424+
425+// x11Pointer gives the Xwayland client a wl_pointer and a relative pointer,
426+// both with event logs.
427+func (r *x11Rig) x11Pointer() (pointer uint32, ptrEvents, relEvents chan []float64) {
428+ r.t.Helper()
429+ seat := bindProtocol(r.t, r.xc, "wl_seat")
430+ registerProtocol(r.t, r.xc, seat)
431+ pointer = r.xc.AllocateID()
432+ ptrEvents = logProxy(r.t, r.xc, pointer)
433+ requestProtocol(r.t, r.xc, seat, wayland.SeatRequestGetPointer, pointer)
434+ mgr := bindProtocol(r.t, r.xc, "zwp_relative_pointer_manager_v1")
435+ rel := r.xc.AllocateID()
436+ relEvents = logProxy(r.t, r.xc, rel)
437+ requestProtocol(r.t, r.xc, mgr, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer)
438+ return pointer, ptrEvents, relEvents
439+}
440+
441+// A lock on an X11 surface activates with pointer and keyboard focus, sends
442+// relative motion to the Xwayland client and deactivates when a native
443+// window takes the focus; it comes back with the X11 focus.
444+func TestX11LockFollowsFocusAndRelativeMotion(t *testing.T) {
445+ r := newX11Rig(t)
446+ win := uint32(x11Win)
447+ r.x11Window(win)
448+ surf := r.pair(win, x11Serial)
449+ m := awaitEvent[ports.WindowMapped](t, r.events)
450+ pointer, _, relEvents := r.x11Pointer()
451+ constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1")
452+ lock := r.xc.AllocateID()
453+ lockEvents := logProxy(t, r.xc, lock)
454+ requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
455+ if err := r.xc.Roundtrip(); err != nil {
456+ t.Fatal(err)
457+ }
458+ if len(lockEvents) > 0 {
459+ t.Fatal("locked without focus")
460+ }
461+
462+ r.commands <- ports.PointerFocus{ID: m.ID}
463+ r.commands <- ports.FocusWindow{ID: m.ID}
464+ if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) {
465+ t.Fatalf("lock event %v", ev)
466+ }
467+ if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock || got.Rect != (ports.Rect{}) {
468+ t.Fatalf("constrained %+v", got)
469+ }
470+
471+ r.commands <- ports.PointerMotionTo{ID: m.ID, X: 5, Y: 5, DX: 2.5, DY: -1, UnaccelDX: 4, UnaccelDY: -2}
472+ if ev := next(t, r.xc, relEvents); len(ev) != 5 || ev[1] != 2.5 || ev[2] != -1 || ev[3] != 4 || ev[4] != -2 {
473+ t.Fatalf("relative motion %v", ev)
474+ }
475+
476+ // A native window takes the focus: the X11 lock ends and the XWM drops
477+ // the X focus.
478+ nc := protocolClient(t, r.s, r.dir)
479+ nm, _, _ := surfaceMapper(t, nc, r.events)()
480+ skip := len(r.peer.requests())
481+ r.commands <- ports.FocusWindow{ID: nm.ID}
482+ if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) {
483+ t.Fatalf("unlock event %v", ev)
484+ }
485+ if got := constrained(t, r.events); got.ID != 0 {
486+ t.Fatalf("constrained %+v after native focus", got)
487+ }
488+ if f, _ := r.peer.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) == win {
489+ t.Fatalf("X focus still on %#x", win)
490+ }
491+
492+ r.commands <- ports.FocusWindow{ID: m.ID}
493+ if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) {
494+ t.Fatalf("relock event %v", ev)
495+ }
496+}
497+
498+// A confine on an X11 surface reaches core with its region as given: an X11
499+// window has no geometry offset.
500+func TestX11ConfineRegionHasNoOffset(t *testing.T) {
501+ r := newX11Rig(t)
502+ win := uint32(x11Win)
503+ r.x11Window(win)
504+ surf := r.pair(win, x11Serial)
505+ m := awaitEvent[ports.WindowMapped](t, r.events)
506+ pointer, _, _ := r.x11Pointer()
507+ region := r.xc.AllocateID()
508+ registerProtocol(t, r.xc, region)
509+ requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateRegion, region)
510+ requestProtocol(t, r.xc, region, wayland.RegionRequestAdd, int32(5), int32(6), int32(20), int32(30))
511+ constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1")
512+ confine := r.xc.AllocateID()
513+ confineEvents := logProxy(t, r.xc, confine)
514+ requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
515+ if err := r.xc.Roundtrip(); err != nil {
516+ t.Fatal(err)
517+ }
518+ r.state(func() {
519+ if g := r.s.windows[m.ID].geometry(); g != (ports.Rect{}) {
520+ t.Errorf("X11 geometry %+v, want zero", g)
521+ }
522+ })
523+
524+ r.commands <- ports.PointerFocus{ID: m.ID, X: 10, Y: 10}
525+ r.commands <- ports.FocusWindow{ID: m.ID}
526+ r.commands <- ports.PointerMotionTo{ID: m.ID, X: 10, Y: 10}
527+ if ev := next(t, r.xc, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) {
528+ t.Fatalf("confine event %v", ev)
529+ }
530+ want := ports.PointerConstrained{ID: m.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 5, Y: 6, W: 20, H: 30}}}
531+ if got := constrained(t, r.events); got != want {
532+ t.Fatalf("constrained %+v, want %+v", got, want)
533+ }
534+}
535+
536+// An X11 window is on one output only, even straddling two: a single
537+// wl_surface.enter, and a leave only when its output changes.
538+func TestX11WindowEntersOneOutput(t *testing.T) {
539+ r := newX11Rig(t)
540+ r.commands <- ports.SetOutputs{Outputs: ports.Layout{
541+ {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1},
542+ {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1},
543+ }, Focused: "A"}
544+ r.state(func() {}) // the layout is applied
545+ if err := r.xc.Roundtrip(); err != nil {
546+ t.Fatal(err)
547+ }
548+ outputs := 0
549+ for _, g := range r.xc.Registry().GetGlobals() {
550+ if g.Interface != "wl_output" {
551+ continue
552+ }
553+ id, err := bindWireID(r.xc, g.Name, g.Interface, 1)
554+ if err != nil {
555+ t.Fatal(err)
556+ }
557+ registerProtocol(t, r.xc, id)
558+ outputs++
559+ }
560+ if outputs != 2 {
561+ t.Fatalf("%d wl_output globals, want 2", outputs)
562+ }
563+
564+ win := uint32(x11Win)
565+ r.x11Window(win)
566+ events := make(chan []float64, 64)
567+ r.surfaceProxy = func(id uint32) wlturbo.Proxy {
568+ p := &eventLog{events: events}
569+ p.SetID(id)
570+ return p
571+ }
572+ r.pair(win, x11Serial)
573+ m := awaitEvent[ports.WindowMapped](t, r.events)
574+
575+ // Straddling both outputs, configured on A.
576+ r.commands <- ports.PlaceWindow{ID: m.ID, X: 1900, Y: 0}
577+ r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "A"}
578+ if err := r.xc.Roundtrip(); err != nil {
579+ t.Fatal(err)
580+ }
581+ r.state(func() {})
582+ if err := r.xc.Roundtrip(); err != nil {
583+ t.Fatal(err)
584+ }
585+ count := func() (enters, leaves int) {
586+ for len(events) > 0 {
587+ switch ev := <-events; ev[0] {
588+ case float64(wayland.SurfaceEventEnter):
589+ enters++
590+ case float64(wayland.SurfaceEventLeave):
591+ leaves++
592+ }
593+ }
594+ return
595+ }
596+ if e, l := count(); e != 1 || l != 0 {
597+ t.Fatalf("straddling window: %d enters, %d leaves, want 1, 0", e, l)
598+ }
599+ r.state(func() {
600+ if on := r.s.windows[m.ID].surf().on; on == nil || on.name() != "A" {
601+ t.Errorf("surface on %v, want A", on)
602+ }
603+ })
604+
605+ // Moving to B leaves A and enters B once.
606+ r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "B"}
607+ r.state(func() {})
608+ if err := r.xc.Roundtrip(); err != nil {
609+ t.Fatal(err)
610+ }
611+ if e, l := count(); e != 1 || l != 1 {
612+ t.Fatalf("move to B: %d enters, %d leaves, want 1, 1", e, l)
613+ }
614+}
@@ -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/xwaylandkeyboardgrab"15 "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab"
@@ -34,6 +35,8 @@ type x11Rig struct {
34 stop func()35 stop func()
35 shell uint3236 shell uint32
36 comp uint3237 comp uint32
38+ // surfaceProxy, when set, observes the next paired wl_surface.
39+ surfaceProxy func(id uint32) wlturbo.Proxy
37 }40 }
38 41
39 func newX11Rig(t *testing.T) *x11Rig {42 func newX11Rig(t *testing.T) *x11Rig {
@@ -87,7 +90,11 @@ func (r *x11Rig) pair(win uint32, serial uint64) uint32 {
87 r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32))90 r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32))
88 surf, xs := r.xc.AllocateID(), r.xc.AllocateID()91 surf, xs := r.xc.AllocateID(), r.xc.AllocateID()
89 requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf)92 requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf)
90- registerProtocol(r.t, r.xc, surf)93+ if r.surfaceProxy != nil {
94+ registerWireProxy(r.xc, r.surfaceProxy(surf))
95+ } else {
96+ registerProtocol(r.t, r.xc, surf)
97+ }
91 requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf)98 requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf)
92 registerProtocol(r.t, r.xc, xs)99 registerProtocol(r.t, r.xc, xs)
93 requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32))100 requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32))
@@ -414,3 +421,194 @@ func TestXwaylandKeyboardGrabThreeStates(t *testing.T) {
414 t.Fatal(v)421 t.Fatal(v)
415 }422 }
416 }423 }
424+
425+// x11Pointer gives the Xwayland client a wl_pointer and a relative pointer,
426+// both with event logs.
427+func (r *x11Rig) x11Pointer() (pointer uint32, ptrEvents, relEvents chan []float64) {
428+ r.t.Helper()
429+ seat := bindProtocol(r.t, r.xc, "wl_seat")
430+ registerProtocol(r.t, r.xc, seat)
431+ pointer = r.xc.AllocateID()
432+ ptrEvents = logProxy(r.t, r.xc, pointer)
433+ requestProtocol(r.t, r.xc, seat, wayland.SeatRequestGetPointer, pointer)
434+ mgr := bindProtocol(r.t, r.xc, "zwp_relative_pointer_manager_v1")
435+ rel := r.xc.AllocateID()
436+ relEvents = logProxy(r.t, r.xc, rel)
437+ requestProtocol(r.t, r.xc, mgr, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer)
438+ return pointer, ptrEvents, relEvents
439+}
440+
441+// A lock on an X11 surface activates with pointer and keyboard focus, sends
442+// relative motion to the Xwayland client and deactivates when a native
443+// window takes the focus; it comes back with the X11 focus.
444+func TestX11LockFollowsFocusAndRelativeMotion(t *testing.T) {
445+ r := newX11Rig(t)
446+ win := uint32(x11Win)
447+ r.x11Window(win)
448+ surf := r.pair(win, x11Serial)
449+ m := awaitEvent[ports.WindowMapped](t, r.events)
450+ pointer, _, relEvents := r.x11Pointer()
451+ constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1")
452+ lock := r.xc.AllocateID()
453+ lockEvents := logProxy(t, r.xc, lock)
454+ requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
455+ if err := r.xc.Roundtrip(); err != nil {
456+ t.Fatal(err)
457+ }
458+ if len(lockEvents) > 0 {
459+ t.Fatal("locked without focus")
460+ }
461+
462+ r.commands <- ports.PointerFocus{ID: m.ID}
463+ r.commands <- ports.FocusWindow{ID: m.ID}
464+ if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) {
465+ t.Fatalf("lock event %v", ev)
466+ }
467+ if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock || got.Rect != (ports.Rect{}) {
468+ t.Fatalf("constrained %+v", got)
469+ }
470+
471+ r.commands <- ports.PointerMotionTo{ID: m.ID, X: 5, Y: 5, DX: 2.5, DY: -1, UnaccelDX: 4, UnaccelDY: -2}
472+ if ev := next(t, r.xc, relEvents); len(ev) != 5 || ev[1] != 2.5 || ev[2] != -1 || ev[3] != 4 || ev[4] != -2 {
473+ t.Fatalf("relative motion %v", ev)
474+ }
475+
476+ // A native window takes the focus: the X11 lock ends and the XWM drops
477+ // the X focus.
478+ nc := protocolClient(t, r.s, r.dir)
479+ nm, _, _ := surfaceMapper(t, nc, r.events)()
480+ skip := len(r.peer.requests())
481+ r.commands <- ports.FocusWindow{ID: nm.ID}
482+ if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) {
483+ t.Fatalf("unlock event %v", ev)
484+ }
485+ if got := constrained(t, r.events); got.ID != 0 {
486+ t.Fatalf("constrained %+v after native focus", got)
487+ }
488+ if f, _ := r.peer.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) == win {
489+ t.Fatalf("X focus still on %#x", win)
490+ }
491+
492+ r.commands <- ports.FocusWindow{ID: m.ID}
493+ if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) {
494+ t.Fatalf("relock event %v", ev)
495+ }
496+}
497+
498+// A confine on an X11 surface reaches core with its region as given: an X11
499+// window has no geometry offset.
500+func TestX11ConfineRegionHasNoOffset(t *testing.T) {
501+ r := newX11Rig(t)
502+ win := uint32(x11Win)
503+ r.x11Window(win)
504+ surf := r.pair(win, x11Serial)
505+ m := awaitEvent[ports.WindowMapped](t, r.events)
506+ pointer, _, _ := r.x11Pointer()
507+ region := r.xc.AllocateID()
508+ registerProtocol(t, r.xc, region)
509+ requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateRegion, region)
510+ requestProtocol(t, r.xc, region, wayland.RegionRequestAdd, int32(5), int32(6), int32(20), int32(30))
511+ constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1")
512+ confine := r.xc.AllocateID()
513+ confineEvents := logProxy(t, r.xc, confine)
514+ requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
515+ if err := r.xc.Roundtrip(); err != nil {
516+ t.Fatal(err)
517+ }
518+ r.state(func() {
519+ if g := r.s.windows[m.ID].geometry(); g != (ports.Rect{}) {
520+ t.Errorf("X11 geometry %+v, want zero", g)
521+ }
522+ })
523+
524+ r.commands <- ports.PointerFocus{ID: m.ID, X: 10, Y: 10}
525+ r.commands <- ports.FocusWindow{ID: m.ID}
526+ r.commands <- ports.PointerMotionTo{ID: m.ID, X: 10, Y: 10}
527+ if ev := next(t, r.xc, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) {
528+ t.Fatalf("confine event %v", ev)
529+ }
530+ want := ports.PointerConstrained{ID: m.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 5, Y: 6, W: 20, H: 30}}}
531+ if got := constrained(t, r.events); got != want {
532+ t.Fatalf("constrained %+v, want %+v", got, want)
533+ }
534+}
535+
536+// An X11 window is on one output only, even straddling two: a single
537+// wl_surface.enter, and a leave only when its output changes.
538+func TestX11WindowEntersOneOutput(t *testing.T) {
539+ r := newX11Rig(t)
540+ r.commands <- ports.SetOutputs{Outputs: ports.Layout{
541+ {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1},
542+ {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1},
543+ }, Focused: "A"}
544+ r.state(func() {}) // the layout is applied
545+ if err := r.xc.Roundtrip(); err != nil {
546+ t.Fatal(err)
547+ }
548+ outputs := 0
549+ for _, g := range r.xc.Registry().GetGlobals() {
550+ if g.Interface != "wl_output" {
551+ continue
552+ }
553+ id, err := bindWireID(r.xc, g.Name, g.Interface, 1)
554+ if err != nil {
555+ t.Fatal(err)
556+ }
557+ registerProtocol(t, r.xc, id)
558+ outputs++
559+ }
560+ if outputs != 2 {
561+ t.Fatalf("%d wl_output globals, want 2", outputs)
562+ }
563+
564+ win := uint32(x11Win)
565+ r.x11Window(win)
566+ events := make(chan []float64, 64)
567+ r.surfaceProxy = func(id uint32) wlturbo.Proxy {
568+ p := &eventLog{events: events}
569+ p.SetID(id)
570+ return p
571+ }
572+ r.pair(win, x11Serial)
573+ m := awaitEvent[ports.WindowMapped](t, r.events)
574+
575+ // Straddling both outputs, configured on A.
576+ r.commands <- ports.PlaceWindow{ID: m.ID, X: 1900, Y: 0}
577+ r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "A"}
578+ if err := r.xc.Roundtrip(); err != nil {
579+ t.Fatal(err)
580+ }
581+ r.state(func() {})
582+ if err := r.xc.Roundtrip(); err != nil {
583+ t.Fatal(err)
584+ }
585+ count := func() (enters, leaves int) {
586+ for len(events) > 0 {
587+ switch ev := <-events; ev[0] {
588+ case float64(wayland.SurfaceEventEnter):
589+ enters++
590+ case float64(wayland.SurfaceEventLeave):
591+ leaves++
592+ }
593+ }
594+ return
595+ }
596+ if e, l := count(); e != 1 || l != 0 {
597+ t.Fatalf("straddling window: %d enters, %d leaves, want 1, 0", e, l)
598+ }
599+ r.state(func() {
600+ if on := r.s.windows[m.ID].surf().on; on == nil || on.name() != "A" {
601+ t.Errorf("surface on %v, want A", on)
602+ }
603+ })
604+
605+ // Moving to B leaves A and enters B once.
606+ r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "B"}
607+ r.state(func() {})
608+ if err := r.xc.Roundtrip(); err != nil {
609+ t.Fatal(err)
610+ }
611+ if e, l := count(); e != 1 || l != 1 {
612+ t.Fatalf("move to B: %d enters, %d leaves, want 1, 1", e, l)
613+ }
614+}

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 26

Tags 10

↑↓ moveenter openesc close