feat(core): forward other touchpad gestures to the pointer-focus window
- commit
- 9447c77e22344f19ae02e0a20d43393ebd7b43b9
- author
- brice · · 2026-10-08 09:32 UTC
- committer
- brice · · 2026-10-08 09:59 UTC
- parent
- dc119ed
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +429 -9
M internal/core/client.go +1 -1
| @@ -114,7 +114,7 @@ func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error { | |||
| 114 | 114 | c.releaseSlots() | |
| 115 | 115 | if c.pointer == v.ID { | |
| 116 | 116 | c.pointer = 0 | |
| 117 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | |
| 117 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 118 | 118 | return err | |
| 119 | 119 | } | |
| 120 | 120 | } | |
| @@ -114,7 +114,7 @@ func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error { | |||||
| 114 | c.releaseSlots() | 114 | c.releaseSlots() | ||
| 115 | if c.pointer == v.ID { | 115 | if c.pointer == v.ID { | ||
| 116 | c.pointer = 0 | 116 | c.pointer = 0 | ||
| 117 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | 117 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { |
| 118 | return err | 118 | return err | ||
| 119 | } | 119 | } | ||
| 120 | } | 120 | } | ||
A internal/core/clientgesture.go +109 -0
| @@ -0,0 +1,109 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Touchpad gestures the compositor does not use go to the client under the | |
| 11 | + | // pointer (zwp_pointer_gestures_v1): swipes of any finger count but three | |
| 12 | + | // and four, every pinch and every hold. Like scrolling, they follow the | |
| 13 | + | // pointer focus. Three- and four-finger swipes stay with the compositor | |
| 14 | + | // (gesture.go). | |
| 15 | + | ||
| 16 | + | // clientGesture is the gesture being forwarded to a client. Input streams | |
| 17 | + | // one gesture at a time. A gesture that begins with no pointer focus, or | |
| 18 | + | // whose focus moved, still takes its stream (on stays set) so the rest of | |
| 19 | + | // it never reaches the compositor or another window. | |
| 20 | + | type clientGesture struct { | |
| 21 | + | on bool | |
| 22 | + | kind ports.GestureKind | |
| 23 | + | // id is the window that got the begin; 0 once cancelled or when none had | |
| 24 | + | // the pointer. | |
| 25 | + | id WindowID | |
| 26 | + | // at is the device time of the latest event, for a cancellation. | |
| 27 | + | at time.Duration | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // forwardsSwipe reports whether the compositor leaves a swipe of that many | |
| 31 | + | // fingers to the client. | |
| 32 | + | func forwardsSwipe(fingers int) bool { return fingers != 3 && fingers != 4 } | |
| 33 | + | ||
| 34 | + | // gestureBegin starts forwarding a gesture to the window with the pointer | |
| 35 | + | // focus. One still running (its end was lost) ends cancelled first. | |
| 36 | + | func (c *Core) gestureBegin(ctx context.Context, kind ports.GestureKind, fingers int, at time.Duration) error { | |
| 37 | + | if err := c.gestureAbort(ctx, at); err != nil { | |
| 38 | + | return err | |
| 39 | + | } | |
| 40 | + | c.gesture = clientGesture{on: true, kind: kind, at: at} | |
| 41 | + | if c.pointer == 0 || c.drag != nil { | |
| 42 | + | return nil | |
| 43 | + | } | |
| 44 | + | c.gesture.id = c.pointer | |
| 45 | + | return c.command(ctx, ports.GestureBeginTo{ID: c.pointer, Kind: kind, Fingers: fingers, Time: at}) | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // gestureUpdate moves the forwarded gesture of that kind. A window that | |
| 49 | + | // lost the pointer meanwhile gets its gesture cancelled. | |
| 50 | + | func (c *Core) gestureUpdate(ctx context.Context, u ports.GestureUpdateTo) error { | |
| 51 | + | g := &c.gesture | |
| 52 | + | if !g.on || g.kind != u.Kind { | |
| 53 | + | return nil | |
| 54 | + | } | |
| 55 | + | g.at = u.Time | |
| 56 | + | if g.id == 0 { | |
| 57 | + | return nil | |
| 58 | + | } | |
| 59 | + | if g.id != c.pointer { | |
| 60 | + | return c.gestureCancel(ctx) | |
| 61 | + | } | |
| 62 | + | u.ID = g.id | |
| 63 | + | return c.command(ctx, u) | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // gestureEnd ends the forwarded gesture of that kind. | |
| 67 | + | func (c *Core) gestureEnd(ctx context.Context, kind ports.GestureKind, cancelled bool, at time.Duration) error { | |
| 68 | + | g := c.gesture | |
| 69 | + | if !g.on || g.kind != kind { | |
| 70 | + | return nil | |
| 71 | + | } | |
| 72 | + | c.gesture = clientGesture{} | |
| 73 | + | if g.id == 0 { | |
| 74 | + | return nil | |
| 75 | + | } | |
| 76 | + | return c.command(ctx, ports.GestureEndTo{ID: g.id, Kind: kind, Cancelled: cancelled || g.id != c.pointer, Time: at}) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // gestureAbort ends whatever gesture is forwarded, cancelled. | |
| 80 | + | func (c *Core) gestureAbort(ctx context.Context, at time.Duration) error { | |
| 81 | + | if !c.gesture.on { | |
| 82 | + | return nil | |
| 83 | + | } | |
| 84 | + | return c.gestureEnd(ctx, c.gesture.kind, true, at) | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // gestureCancel ends the gesture of the window that lost the pointer, as | |
| 88 | + | // cancelled. The rest of the device's gesture is swallowed. | |
| 89 | + | func (c *Core) gestureCancel(ctx context.Context) error { | |
| 90 | + | g := &c.gesture | |
| 91 | + | if !g.on || g.id == 0 { | |
| 92 | + | return nil | |
| 93 | + | } | |
| 94 | + | id := g.id | |
| 95 | + | g.id = 0 | |
| 96 | + | return c.command(ctx, ports.GestureEndTo{ID: id, Kind: g.kind, Cancelled: true, Time: g.at}) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // pointerFocus sends a pointer focus change; the window leaving the | |
| 100 | + | // pointer first gets its gesture cancelled, so its client sees the end | |
| 101 | + | // before the leave. | |
| 102 | + | func (c *Core) pointerFocus(ctx context.Context, f ports.PointerFocus) error { | |
| 103 | + | if c.gesture.id != 0 && c.gesture.id != f.ID { | |
| 104 | + | if err := c.gestureCancel(ctx); err != nil { | |
| 105 | + | return err | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | return c.command(ctx, f) | |
| 109 | + | } | |
| @@ -0,0 +1,109 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Touchpad gestures the compositor does not use go to the client under the | |||
| 11 | + | // pointer (zwp_pointer_gestures_v1): swipes of any finger count but three | |||
| 12 | + | // and four, every pinch and every hold. Like scrolling, they follow the | |||
| 13 | + | // pointer focus. Three- and four-finger swipes stay with the compositor | |||
| 14 | + | // (gesture.go). | |||
| 15 | + | ||||
| 16 | + | // clientGesture is the gesture being forwarded to a client. Input streams | |||
| 17 | + | // one gesture at a time. A gesture that begins with no pointer focus, or | |||
| 18 | + | // whose focus moved, still takes its stream (on stays set) so the rest of | |||
| 19 | + | // it never reaches the compositor or another window. | |||
| 20 | + | type clientGesture struct { | |||
| 21 | + | on bool | |||
| 22 | + | kind ports.GestureKind | |||
| 23 | + | // id is the window that got the begin; 0 once cancelled or when none had | |||
| 24 | + | // the pointer. | |||
| 25 | + | id WindowID | |||
| 26 | + | // at is the device time of the latest event, for a cancellation. | |||
| 27 | + | at time.Duration | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // forwardsSwipe reports whether the compositor leaves a swipe of that many | |||
| 31 | + | // fingers to the client. | |||
| 32 | + | func forwardsSwipe(fingers int) bool { return fingers != 3 && fingers != 4 } | |||
| 33 | + | ||||
| 34 | + | // gestureBegin starts forwarding a gesture to the window with the pointer | |||
| 35 | + | // focus. One still running (its end was lost) ends cancelled first. | |||
| 36 | + | func (c *Core) gestureBegin(ctx context.Context, kind ports.GestureKind, fingers int, at time.Duration) error { | |||
| 37 | + | if err := c.gestureAbort(ctx, at); err != nil { | |||
| 38 | + | return err | |||
| 39 | + | } | |||
| 40 | + | c.gesture = clientGesture{on: true, kind: kind, at: at} | |||
| 41 | + | if c.pointer == 0 || c.drag != nil { | |||
| 42 | + | return nil | |||
| 43 | + | } | |||
| 44 | + | c.gesture.id = c.pointer | |||
| 45 | + | return c.command(ctx, ports.GestureBeginTo{ID: c.pointer, Kind: kind, Fingers: fingers, Time: at}) | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // gestureUpdate moves the forwarded gesture of that kind. A window that | |||
| 49 | + | // lost the pointer meanwhile gets its gesture cancelled. | |||
| 50 | + | func (c *Core) gestureUpdate(ctx context.Context, u ports.GestureUpdateTo) error { | |||
| 51 | + | g := &c.gesture | |||
| 52 | + | if !g.on || g.kind != u.Kind { | |||
| 53 | + | return nil | |||
| 54 | + | } | |||
| 55 | + | g.at = u.Time | |||
| 56 | + | if g.id == 0 { | |||
| 57 | + | return nil | |||
| 58 | + | } | |||
| 59 | + | if g.id != c.pointer { | |||
| 60 | + | return c.gestureCancel(ctx) | |||
| 61 | + | } | |||
| 62 | + | u.ID = g.id | |||
| 63 | + | return c.command(ctx, u) | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // gestureEnd ends the forwarded gesture of that kind. | |||
| 67 | + | func (c *Core) gestureEnd(ctx context.Context, kind ports.GestureKind, cancelled bool, at time.Duration) error { | |||
| 68 | + | g := c.gesture | |||
| 69 | + | if !g.on || g.kind != kind { | |||
| 70 | + | return nil | |||
| 71 | + | } | |||
| 72 | + | c.gesture = clientGesture{} | |||
| 73 | + | if g.id == 0 { | |||
| 74 | + | return nil | |||
| 75 | + | } | |||
| 76 | + | return c.command(ctx, ports.GestureEndTo{ID: g.id, Kind: kind, Cancelled: cancelled || g.id != c.pointer, Time: at}) | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // gestureAbort ends whatever gesture is forwarded, cancelled. | |||
| 80 | + | func (c *Core) gestureAbort(ctx context.Context, at time.Duration) error { | |||
| 81 | + | if !c.gesture.on { | |||
| 82 | + | return nil | |||
| 83 | + | } | |||
| 84 | + | return c.gestureEnd(ctx, c.gesture.kind, true, at) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // gestureCancel ends the gesture of the window that lost the pointer, as | |||
| 88 | + | // cancelled. The rest of the device's gesture is swallowed. | |||
| 89 | + | func (c *Core) gestureCancel(ctx context.Context) error { | |||
| 90 | + | g := &c.gesture | |||
| 91 | + | if !g.on || g.id == 0 { | |||
| 92 | + | return nil | |||
| 93 | + | } | |||
| 94 | + | id := g.id | |||
| 95 | + | g.id = 0 | |||
| 96 | + | return c.command(ctx, ports.GestureEndTo{ID: id, Kind: g.kind, Cancelled: true, Time: g.at}) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // pointerFocus sends a pointer focus change; the window leaving the | |||
| 100 | + | // pointer first gets its gesture cancelled, so its client sees the end | |||
| 101 | + | // before the leave. | |||
| 102 | + | func (c *Core) pointerFocus(ctx context.Context, f ports.PointerFocus) error { | |||
| 103 | + | if c.gesture.id != 0 && c.gesture.id != f.ID { | |||
| 104 | + | if err := c.gestureCancel(ctx); err != nil { | |||
| 105 | + | return err | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | return c.command(ctx, f) | |||
| 109 | + | } | |||
A internal/core/clientgesture_test.go +283 -0
| @@ -0,0 +1,283 @@ | |||
| 1 | + | package core_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "sync/atomic" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/neferwl/internal/adapters/config" | |
| 10 | + | "github.com/bnema/neferwl/internal/core" | |
| 11 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // gestureRig is a rig with window 1 mapped on the left output and the | |
| 16 | + | // pointer on it. The right output is empty: the pointer there has no focus. | |
| 17 | + | func gestureRig(t *testing.T) *multiRig { | |
| 18 | + | t.Helper() | |
| 19 | + | r := startMulti(t, nil, left, right) | |
| 20 | + | r.mapWindow(t, 1) | |
| 21 | + | r.input <- ports.PointerMotion{X: 50, Y: 50} | |
| 22 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) { | |
| 23 | + | t.Fatal(v) | |
| 24 | + | } | |
| 25 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) { | |
| 26 | + | t.Fatal(v) | |
| 27 | + | } | |
| 28 | + | return r | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // isPointerCommand reports whether a command is one the gesture tests follow: | |
| 32 | + | // the pointer's and the gestures'. | |
| 33 | + | func isPointerCommand(cmd ports.ClientCommand) bool { | |
| 34 | + | switch cmd.(type) { | |
| 35 | + | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo: | |
| 36 | + | return true | |
| 37 | + | } | |
| 38 | + | return false | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // pointerCommand receives the next pointer or gesture command, skipping | |
| 42 | + | // the configures and focus changes between them. | |
| 43 | + | func pointerCommand(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand { | |
| 44 | + | t.Helper() | |
| 45 | + | for { | |
| 46 | + | if v := command(t, ch); isPointerCommand(v) { | |
| 47 | + | return v | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // idle fails the test when core sent a command meanwhile. A marker input | |
| 53 | + | // goes first, so the check waits for core to have handled what came before. | |
| 54 | + | func (r *multiRig) idle(t *testing.T) { | |
| 55 | + | t.Helper() | |
| 56 | + | r.input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}} | |
| 57 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerAxisTo{ID: 1, Axis: ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}}) { | |
| 58 | + | t.Fatalf("unexpected command %#v", v) | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func TestPinchGoesToPointerFocus(t *testing.T) { | |
| 63 | + | r := gestureRig(t) | |
| 64 | + | r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second} | |
| 65 | + | r.input <- ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.25, Rotation: 5, Time: 2 * time.Second} | |
| 66 | + | r.input <- ports.PinchEnd{Time: 3 * time.Second} | |
| 67 | + | for _, want := range []ports.ClientCommand{ | |
| 68 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}, | |
| 69 | + | ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, DX: 1, DY: -2, Scale: 1.25, Rotation: 5, Time: 2 * time.Second}, | |
| 70 | + | ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Time: 3 * time.Second}, | |
| 71 | + | } { | |
| 72 | + | if v := pointerCommand(t, r.commands); v != want { | |
| 73 | + | t.Fatalf("got %#v, want %#v", v, want) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func TestHoldGoesToPointerFocus(t *testing.T) { | |
| 79 | + | r := gestureRig(t) | |
| 80 | + | r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second} | |
| 81 | + | r.input <- ports.HoldEnd{Cancelled: true, Time: 2 * time.Second} | |
| 82 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) { | |
| 83 | + | t.Fatal(v) | |
| 84 | + | } | |
| 85 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: 2 * time.Second}) { | |
| 86 | + | t.Fatal(v) | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | // Swipes of other finger counts than three and four go to the client; the | |
| 91 | + | // compositor keeps those. | |
| 92 | + | func TestSwipeFingerCounts(t *testing.T) { | |
| 93 | + | for _, fingers := range []int{2, 5} { | |
| 94 | + | r := gestureRig(t) | |
| 95 | + | r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second} | |
| 96 | + | r.input <- ports.SwipeUpdate{DX: 3, DY: 4, Time: 2 * time.Second} | |
| 97 | + | r.input <- ports.SwipeEnd{Time: 3 * time.Second} | |
| 98 | + | for _, want := range []ports.ClientCommand{ | |
| 99 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GestureSwipe, Fingers: fingers, Time: time.Second}, | |
| 100 | + | ports.GestureUpdateTo{ID: 1, Kind: ports.GestureSwipe, DX: 3, DY: 4, Time: 2 * time.Second}, | |
| 101 | + | ports.GestureEndTo{ID: 1, Kind: ports.GestureSwipe, Time: 3 * time.Second}, | |
| 102 | + | } { | |
| 103 | + | if v := pointerCommand(t, r.commands); v != want { | |
| 104 | + | t.Fatalf("%d fingers: got %#v, want %#v", fingers, v, want) | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | for _, fingers := range []int{3, 4} { | |
| 109 | + | r := gestureRig(t) | |
| 110 | + | r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second} | |
| 111 | + | r.input <- ports.SwipeUpdate{DX: 40, DY: 0, Time: 2 * time.Second} | |
| 112 | + | r.input <- ports.SwipeEnd{Time: 3 * time.Second} | |
| 113 | + | r.idle(t) | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // A gesture that began with no window under the pointer reaches nobody, | |
| 118 | + | // even when the pointer enters a window meanwhile. | |
| 119 | + | func TestGestureWithoutPointerFocus(t *testing.T) { | |
| 120 | + | r := gestureRig(t) | |
| 121 | + | r.input <- ports.PointerMotion{X: 300, Y: 50} | |
| 122 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) { | |
| 123 | + | t.Fatal(v) | |
| 124 | + | } | |
| 125 | + | r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second} | |
| 126 | + | r.input <- ports.PointerMotion{X: 50, Y: 50} | |
| 127 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) { | |
| 128 | + | t.Fatal(v) | |
| 129 | + | } | |
| 130 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) { | |
| 131 | + | t.Fatal(v) | |
| 132 | + | } | |
| 133 | + | r.input <- ports.PinchUpdate{Scale: 2, Time: 2 * time.Second} | |
| 134 | + | r.input <- ports.PinchEnd{Time: 3 * time.Second} | |
| 135 | + | r.idle(t) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // The window losing the pointer focus gets its gesture cancelled before the | |
| 139 | + | // leave, and nothing of the rest. | |
| 140 | + | func TestGestureCancelledOnFocusChange(t *testing.T) { | |
| 141 | + | r := gestureRig(t) | |
| 142 | + | r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second} | |
| 143 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}) { | |
| 144 | + | t.Fatal(v) | |
| 145 | + | } | |
| 146 | + | r.input <- ports.PinchUpdate{Scale: 1.1, Time: 2 * time.Second} | |
| 147 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, Scale: 1.1, Time: 2 * time.Second}) { | |
| 148 | + | t.Fatal(v) | |
| 149 | + | } | |
| 150 | + | r.input <- ports.PointerMotion{X: 300, Y: 50} | |
| 151 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) { | |
| 152 | + | t.Fatal(v) | |
| 153 | + | } | |
| 154 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) { | |
| 155 | + | t.Fatal(v) | |
| 156 | + | } | |
| 157 | + | r.input <- ports.PinchUpdate{Scale: 1.2, Time: 3 * time.Second} | |
| 158 | + | r.input <- ports.PinchEnd{Time: 4 * time.Second} | |
| 159 | + | r.input <- ports.PointerMotion{X: 50, Y: 50} | |
| 160 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) { | |
| 161 | + | t.Fatal(v) | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | // A window that goes away mid-gesture has its gesture cancelled with the | |
| 166 | + | // pointer focus. | |
| 167 | + | func TestGestureCancelledOnUnmap(t *testing.T) { | |
| 168 | + | r := gestureRig(t) | |
| 169 | + | r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second} | |
| 170 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) { | |
| 171 | + | t.Fatal(v) | |
| 172 | + | } | |
| 173 | + | r.client <- ports.WindowUnmapped{ID: 1} | |
| 174 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: time.Second}) { | |
| 175 | + | t.Fatal(v) | |
| 176 | + | } | |
| 177 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) { | |
| 178 | + | t.Fatal(v) | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // A gesture whose end was lost ends cancelled when the next one begins. | |
| 183 | + | func TestGestureBeginEndsPrevious(t *testing.T) { | |
| 184 | + | r := gestureRig(t) | |
| 185 | + | r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second} | |
| 186 | + | r.input <- ports.HoldBegin{Fingers: 3, Time: 2 * time.Second} | |
| 187 | + | for _, want := range []ports.ClientCommand{ | |
| 188 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}, | |
| 189 | + | ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}, | |
| 190 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: 2 * time.Second}, | |
| 191 | + | } { | |
| 192 | + | if v := pointerCommand(t, r.commands); v != want { | |
| 193 | + | t.Fatalf("got %#v, want %#v", v, want) | |
| 194 | + | } | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | // A protected session forwards no gesture to a window; one running when | |
| 199 | + | // the session locks ends with the pointer focus. | |
| 200 | + | func TestGesturesRefusedWhileLocked(t *testing.T) { | |
| 201 | + | var epoch atomic.Value | |
| 202 | + | epoch.Store(ports.SecurityState{}) | |
| 203 | + | gate := portsmocks.NewMockSessionSecurity(t) | |
| 204 | + | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | |
| 205 | + | r := &multiRig{ | |
| 206 | + | client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 32), | |
| 207 | + | output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4), | |
| 208 | + | commands: make(chan ports.ClientCommand, 256), scenes: make(chan []ports.Scene, 1), | |
| 209 | + | } | |
| 210 | + | cfg := config.Defaults() | |
| 211 | + | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Constraints: make(chan ports.PointerConstraint, 1)}, core.Options{Security: gate}) | |
| 212 | + | if err != nil { | |
| 213 | + | t.Fatal(err) | |
| 214 | + | } | |
| 215 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 216 | + | done := make(chan error, 1) | |
| 217 | + | go func() { done <- c.Run(ctx) }() | |
| 218 | + | t.Cleanup(func() { cancel(); <-done }) | |
| 219 | + | r.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 300, Height: 200}} | |
| 220 | + | scene(t, r.scenes) | |
| 221 | + | r.client <- ports.WindowMapped{ID: 1} | |
| 222 | + | scene(t, r.scenes) | |
| 223 | + | unlocked := ports.SecurityState{} | |
| 224 | + | r.input <- ports.SecurityInput{State: unlocked, Event: ports.PointerMotion{X: 50, Y: 50}} | |
| 225 | + | r.input <- ports.SecurityInput{State: unlocked, Event: ports.PinchBegin{Fingers: 2, Time: time.Second}} | |
| 226 | + | for _, want := range []ports.ClientCommand{ | |
| 227 | + | ports.PointerFocus{ID: 1, X: 50, Y: 50}, | |
| 228 | + | ports.PointerMotionTo{ID: 1, X: 50, Y: 50}, | |
| 229 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}, | |
| 230 | + | } { | |
| 231 | + | if v := unwrap(t, r.commands, unlocked); v != want { | |
| 232 | + | t.Fatalf("got %#v, want %#v", v, want) | |
| 233 | + | } | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // The lock clears the pointer focus, which cancels the gesture in wayland. | |
| 237 | + | locked := ports.SecurityState{Generation: 1, Protected: true} | |
| 238 | + | epoch.Store(locked) | |
| 239 | + | r.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 300, Height: 200}}} | |
| 240 | + | sceneMatch(t, r.scenes, func(s ports.Scene) bool { return s.Security == locked && len(s.Windows) == 1 }) | |
| 241 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.PointerMotion{X: 50, Y: 50}} | |
| 242 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.PinchBegin{Fingers: 2, Time: 2 * time.Second}} | |
| 243 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.PinchUpdate{Scale: 2, Time: 3 * time.Second}} | |
| 244 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.PinchEnd{Time: 4 * time.Second}} | |
| 245 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.HoldBegin{Fingers: 3, Time: 5 * time.Second}} | |
| 246 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.HoldEnd{Time: 6 * time.Second}} | |
| 247 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeBegin{Fingers: 2, Time: 7 * time.Second}} | |
| 248 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeEnd{Time: 8 * time.Second}} | |
| 249 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "marker", Keycode: 30, Pressed: true}} | |
| 250 | + | for { | |
| 251 | + | wrapped, ok := receive(t, r.commands).(ports.SecurityCommand) | |
| 252 | + | if !ok { | |
| 253 | + | t.Fatal("unwrapped command") | |
| 254 | + | } | |
| 255 | + | switch v := wrapped.Command.(type) { | |
| 256 | + | case ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo: | |
| 257 | + | if wrapped.State == locked { | |
| 258 | + | t.Fatalf("gesture forwarded while locked: %#v", v) | |
| 259 | + | } | |
| 260 | + | case ports.ForwardKey: | |
| 261 | + | if wrapped.State == locked { | |
| 262 | + | return | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | } | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | // unwrap returns the next pointer command of an envelope of the given epoch. | |
| 269 | + | func unwrap(t *testing.T, ch <-chan ports.ClientCommand, state ports.SecurityState) ports.ClientCommand { | |
| 270 | + | t.Helper() | |
| 271 | + | for { | |
| 272 | + | wrapped, ok := receive(t, ch).(ports.SecurityCommand) | |
| 273 | + | if !ok { | |
| 274 | + | t.Fatal("unwrapped command") | |
| 275 | + | } | |
| 276 | + | if wrapped.State != state { | |
| 277 | + | t.Fatalf("epoch %+v, want %+v", wrapped.State, state) | |
| 278 | + | } | |
| 279 | + | if isPointerCommand(wrapped.Command) { | |
| 280 | + | return wrapped.Command | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | } | |
| @@ -0,0 +1,283 @@ | |||||
| 1 | + | package core_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "sync/atomic" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/neferwl/internal/adapters/config" | |||
| 10 | + | "github.com/bnema/neferwl/internal/core" | |||
| 11 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // gestureRig is a rig with window 1 mapped on the left output and the | |||
| 16 | + | // pointer on it. The right output is empty: the pointer there has no focus. | |||
| 17 | + | func gestureRig(t *testing.T) *multiRig { | |||
| 18 | + | t.Helper() | |||
| 19 | + | r := startMulti(t, nil, left, right) | |||
| 20 | + | r.mapWindow(t, 1) | |||
| 21 | + | r.input <- ports.PointerMotion{X: 50, Y: 50} | |||
| 22 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) { | |||
| 23 | + | t.Fatal(v) | |||
| 24 | + | } | |||
| 25 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) { | |||
| 26 | + | t.Fatal(v) | |||
| 27 | + | } | |||
| 28 | + | return r | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // isPointerCommand reports whether a command is one the gesture tests follow: | |||
| 32 | + | // the pointer's and the gestures'. | |||
| 33 | + | func isPointerCommand(cmd ports.ClientCommand) bool { | |||
| 34 | + | switch cmd.(type) { | |||
| 35 | + | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo: | |||
| 36 | + | return true | |||
| 37 | + | } | |||
| 38 | + | return false | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // pointerCommand receives the next pointer or gesture command, skipping | |||
| 42 | + | // the configures and focus changes between them. | |||
| 43 | + | func pointerCommand(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand { | |||
| 44 | + | t.Helper() | |||
| 45 | + | for { | |||
| 46 | + | if v := command(t, ch); isPointerCommand(v) { | |||
| 47 | + | return v | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // idle fails the test when core sent a command meanwhile. A marker input | |||
| 53 | + | // goes first, so the check waits for core to have handled what came before. | |||
| 54 | + | func (r *multiRig) idle(t *testing.T) { | |||
| 55 | + | t.Helper() | |||
| 56 | + | r.input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}} | |||
| 57 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerAxisTo{ID: 1, Axis: ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}}) { | |||
| 58 | + | t.Fatalf("unexpected command %#v", v) | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func TestPinchGoesToPointerFocus(t *testing.T) { | |||
| 63 | + | r := gestureRig(t) | |||
| 64 | + | r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second} | |||
| 65 | + | r.input <- ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.25, Rotation: 5, Time: 2 * time.Second} | |||
| 66 | + | r.input <- ports.PinchEnd{Time: 3 * time.Second} | |||
| 67 | + | for _, want := range []ports.ClientCommand{ | |||
| 68 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}, | |||
| 69 | + | ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, DX: 1, DY: -2, Scale: 1.25, Rotation: 5, Time: 2 * time.Second}, | |||
| 70 | + | ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Time: 3 * time.Second}, | |||
| 71 | + | } { | |||
| 72 | + | if v := pointerCommand(t, r.commands); v != want { | |||
| 73 | + | t.Fatalf("got %#v, want %#v", v, want) | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func TestHoldGoesToPointerFocus(t *testing.T) { | |||
| 79 | + | r := gestureRig(t) | |||
| 80 | + | r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second} | |||
| 81 | + | r.input <- ports.HoldEnd{Cancelled: true, Time: 2 * time.Second} | |||
| 82 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) { | |||
| 83 | + | t.Fatal(v) | |||
| 84 | + | } | |||
| 85 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: 2 * time.Second}) { | |||
| 86 | + | t.Fatal(v) | |||
| 87 | + | } | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | // Swipes of other finger counts than three and four go to the client; the | |||
| 91 | + | // compositor keeps those. | |||
| 92 | + | func TestSwipeFingerCounts(t *testing.T) { | |||
| 93 | + | for _, fingers := range []int{2, 5} { | |||
| 94 | + | r := gestureRig(t) | |||
| 95 | + | r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second} | |||
| 96 | + | r.input <- ports.SwipeUpdate{DX: 3, DY: 4, Time: 2 * time.Second} | |||
| 97 | + | r.input <- ports.SwipeEnd{Time: 3 * time.Second} | |||
| 98 | + | for _, want := range []ports.ClientCommand{ | |||
| 99 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GestureSwipe, Fingers: fingers, Time: time.Second}, | |||
| 100 | + | ports.GestureUpdateTo{ID: 1, Kind: ports.GestureSwipe, DX: 3, DY: 4, Time: 2 * time.Second}, | |||
| 101 | + | ports.GestureEndTo{ID: 1, Kind: ports.GestureSwipe, Time: 3 * time.Second}, | |||
| 102 | + | } { | |||
| 103 | + | if v := pointerCommand(t, r.commands); v != want { | |||
| 104 | + | t.Fatalf("%d fingers: got %#v, want %#v", fingers, v, want) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | for _, fingers := range []int{3, 4} { | |||
| 109 | + | r := gestureRig(t) | |||
| 110 | + | r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second} | |||
| 111 | + | r.input <- ports.SwipeUpdate{DX: 40, DY: 0, Time: 2 * time.Second} | |||
| 112 | + | r.input <- ports.SwipeEnd{Time: 3 * time.Second} | |||
| 113 | + | r.idle(t) | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // A gesture that began with no window under the pointer reaches nobody, | |||
| 118 | + | // even when the pointer enters a window meanwhile. | |||
| 119 | + | func TestGestureWithoutPointerFocus(t *testing.T) { | |||
| 120 | + | r := gestureRig(t) | |||
| 121 | + | r.input <- ports.PointerMotion{X: 300, Y: 50} | |||
| 122 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) { | |||
| 123 | + | t.Fatal(v) | |||
| 124 | + | } | |||
| 125 | + | r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second} | |||
| 126 | + | r.input <- ports.PointerMotion{X: 50, Y: 50} | |||
| 127 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) { | |||
| 128 | + | t.Fatal(v) | |||
| 129 | + | } | |||
| 130 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) { | |||
| 131 | + | t.Fatal(v) | |||
| 132 | + | } | |||
| 133 | + | r.input <- ports.PinchUpdate{Scale: 2, Time: 2 * time.Second} | |||
| 134 | + | r.input <- ports.PinchEnd{Time: 3 * time.Second} | |||
| 135 | + | r.idle(t) | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // The window losing the pointer focus gets its gesture cancelled before the | |||
| 139 | + | // leave, and nothing of the rest. | |||
| 140 | + | func TestGestureCancelledOnFocusChange(t *testing.T) { | |||
| 141 | + | r := gestureRig(t) | |||
| 142 | + | r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second} | |||
| 143 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}) { | |||
| 144 | + | t.Fatal(v) | |||
| 145 | + | } | |||
| 146 | + | r.input <- ports.PinchUpdate{Scale: 1.1, Time: 2 * time.Second} | |||
| 147 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, Scale: 1.1, Time: 2 * time.Second}) { | |||
| 148 | + | t.Fatal(v) | |||
| 149 | + | } | |||
| 150 | + | r.input <- ports.PointerMotion{X: 300, Y: 50} | |||
| 151 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) { | |||
| 152 | + | t.Fatal(v) | |||
| 153 | + | } | |||
| 154 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) { | |||
| 155 | + | t.Fatal(v) | |||
| 156 | + | } | |||
| 157 | + | r.input <- ports.PinchUpdate{Scale: 1.2, Time: 3 * time.Second} | |||
| 158 | + | r.input <- ports.PinchEnd{Time: 4 * time.Second} | |||
| 159 | + | r.input <- ports.PointerMotion{X: 50, Y: 50} | |||
| 160 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) { | |||
| 161 | + | t.Fatal(v) | |||
| 162 | + | } | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | // A window that goes away mid-gesture has its gesture cancelled with the | |||
| 166 | + | // pointer focus. | |||
| 167 | + | func TestGestureCancelledOnUnmap(t *testing.T) { | |||
| 168 | + | r := gestureRig(t) | |||
| 169 | + | r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second} | |||
| 170 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) { | |||
| 171 | + | t.Fatal(v) | |||
| 172 | + | } | |||
| 173 | + | r.client <- ports.WindowUnmapped{ID: 1} | |||
| 174 | + | if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: time.Second}) { | |||
| 175 | + | t.Fatal(v) | |||
| 176 | + | } | |||
| 177 | + | if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) { | |||
| 178 | + | t.Fatal(v) | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // A gesture whose end was lost ends cancelled when the next one begins. | |||
| 183 | + | func TestGestureBeginEndsPrevious(t *testing.T) { | |||
| 184 | + | r := gestureRig(t) | |||
| 185 | + | r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second} | |||
| 186 | + | r.input <- ports.HoldBegin{Fingers: 3, Time: 2 * time.Second} | |||
| 187 | + | for _, want := range []ports.ClientCommand{ | |||
| 188 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}, | |||
| 189 | + | ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}, | |||
| 190 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: 2 * time.Second}, | |||
| 191 | + | } { | |||
| 192 | + | if v := pointerCommand(t, r.commands); v != want { | |||
| 193 | + | t.Fatalf("got %#v, want %#v", v, want) | |||
| 194 | + | } | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | // A protected session forwards no gesture to a window; one running when | |||
| 199 | + | // the session locks ends with the pointer focus. | |||
| 200 | + | func TestGesturesRefusedWhileLocked(t *testing.T) { | |||
| 201 | + | var epoch atomic.Value | |||
| 202 | + | epoch.Store(ports.SecurityState{}) | |||
| 203 | + | gate := portsmocks.NewMockSessionSecurity(t) | |||
| 204 | + | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | |||
| 205 | + | r := &multiRig{ | |||
| 206 | + | client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 32), | |||
| 207 | + | output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4), | |||
| 208 | + | commands: make(chan ports.ClientCommand, 256), scenes: make(chan []ports.Scene, 1), | |||
| 209 | + | } | |||
| 210 | + | cfg := config.Defaults() | |||
| 211 | + | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Constraints: make(chan ports.PointerConstraint, 1)}, core.Options{Security: gate}) | |||
| 212 | + | if err != nil { | |||
| 213 | + | t.Fatal(err) | |||
| 214 | + | } | |||
| 215 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 216 | + | done := make(chan error, 1) | |||
| 217 | + | go func() { done <- c.Run(ctx) }() | |||
| 218 | + | t.Cleanup(func() { cancel(); <-done }) | |||
| 219 | + | r.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 300, Height: 200}} | |||
| 220 | + | scene(t, r.scenes) | |||
| 221 | + | r.client <- ports.WindowMapped{ID: 1} | |||
| 222 | + | scene(t, r.scenes) | |||
| 223 | + | unlocked := ports.SecurityState{} | |||
| 224 | + | r.input <- ports.SecurityInput{State: unlocked, Event: ports.PointerMotion{X: 50, Y: 50}} | |||
| 225 | + | r.input <- ports.SecurityInput{State: unlocked, Event: ports.PinchBegin{Fingers: 2, Time: time.Second}} | |||
| 226 | + | for _, want := range []ports.ClientCommand{ | |||
| 227 | + | ports.PointerFocus{ID: 1, X: 50, Y: 50}, | |||
| 228 | + | ports.PointerMotionTo{ID: 1, X: 50, Y: 50}, | |||
| 229 | + | ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}, | |||
| 230 | + | } { | |||
| 231 | + | if v := unwrap(t, r.commands, unlocked); v != want { | |||
| 232 | + | t.Fatalf("got %#v, want %#v", v, want) | |||
| 233 | + | } | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // The lock clears the pointer focus, which cancels the gesture in wayland. | |||
| 237 | + | locked := ports.SecurityState{Generation: 1, Protected: true} | |||
| 238 | + | epoch.Store(locked) | |||
| 239 | + | r.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 300, Height: 200}}} | |||
| 240 | + | sceneMatch(t, r.scenes, func(s ports.Scene) bool { return s.Security == locked && len(s.Windows) == 1 }) | |||
| 241 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.PointerMotion{X: 50, Y: 50}} | |||
| 242 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.PinchBegin{Fingers: 2, Time: 2 * time.Second}} | |||
| 243 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.PinchUpdate{Scale: 2, Time: 3 * time.Second}} | |||
| 244 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.PinchEnd{Time: 4 * time.Second}} | |||
| 245 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.HoldBegin{Fingers: 3, Time: 5 * time.Second}} | |||
| 246 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.HoldEnd{Time: 6 * time.Second}} | |||
| 247 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeBegin{Fingers: 2, Time: 7 * time.Second}} | |||
| 248 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeEnd{Time: 8 * time.Second}} | |||
| 249 | + | r.input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "marker", Keycode: 30, Pressed: true}} | |||
| 250 | + | for { | |||
| 251 | + | wrapped, ok := receive(t, r.commands).(ports.SecurityCommand) | |||
| 252 | + | if !ok { | |||
| 253 | + | t.Fatal("unwrapped command") | |||
| 254 | + | } | |||
| 255 | + | switch v := wrapped.Command.(type) { | |||
| 256 | + | case ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo: | |||
| 257 | + | if wrapped.State == locked { | |||
| 258 | + | t.Fatalf("gesture forwarded while locked: %#v", v) | |||
| 259 | + | } | |||
| 260 | + | case ports.ForwardKey: | |||
| 261 | + | if wrapped.State == locked { | |||
| 262 | + | return | |||
| 263 | + | } | |||
| 264 | + | } | |||
| 265 | + | } | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | // unwrap returns the next pointer command of an envelope of the given epoch. | |||
| 269 | + | func unwrap(t *testing.T, ch <-chan ports.ClientCommand, state ports.SecurityState) ports.ClientCommand { | |||
| 270 | + | t.Helper() | |||
| 271 | + | for { | |||
| 272 | + | wrapped, ok := receive(t, ch).(ports.SecurityCommand) | |||
| 273 | + | if !ok { | |||
| 274 | + | t.Fatal("unwrapped command") | |||
| 275 | + | } | |||
| 276 | + | if wrapped.State != state { | |||
| 277 | + | t.Fatalf("epoch %+v, want %+v", wrapped.State, state) | |||
| 278 | + | } | |||
| 279 | + | if isPointerCommand(wrapped.Command) { | |||
| 280 | + | return wrapped.Command | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | } | |||
M internal/core/core.go +6 -3
| @@ -133,7 +133,10 @@ type Core struct { | |||
| 133 | 133 | // flip came in time to move a running slide; nil when not armed. It is | |
| 134 | 134 | // the channel of frameTimer, made once and reset per frame: an | |
| 135 | 135 | // animation frame allocates no timer. | |
| 136 | - | swipe *swipeGesture | |
| 136 | + | swipe *swipeGesture | |
| 137 | + | // gesture is the touchpad gesture forwarded to a client | |
| 138 | + | // (clientgesture.go). | |
| 139 | + | gesture clientGesture | |
| 137 | 140 | frameC <-chan time.Time | |
| 138 | 141 | frameTimer ports.Timer | |
| 139 | 142 | // shots is the snapshot of what the screens show before an action | |
| @@ -609,7 +612,7 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||
| 609 | 612 | if c.pointer != 0 && !c.visible(c.pointer) { | |
| 610 | 613 | c.pointer = 0 | |
| 611 | 614 | if c.grab == 0 { | |
| 612 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | |
| 615 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 613 | 616 | return err | |
| 614 | 617 | } | |
| 615 | 618 | } | |
| @@ -691,7 +694,7 @@ func (c *Core) rehit(ctx context.Context) error { | |||
| 691 | 694 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 692 | 695 | if id != c.pointer { | |
| 693 | 696 | c.pointer, c.pointerAt = id, [2]float64{x, y} | |
| 694 | - | return c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}) | |
| 697 | + | return c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}) | |
| 695 | 698 | } | |
| 696 | 699 | if id == 0 || c.pointerAt == [2]float64{x, y} { | |
| 697 | 700 | return nil | |
| @@ -133,7 +133,10 @@ type Core struct { | |||||
| 133 | // flip came in time to move a running slide; nil when not armed. It is | 133 | // flip came in time to move a running slide; nil when not armed. It is | ||
| 134 | // the channel of frameTimer, made once and reset per frame: an | 134 | // the channel of frameTimer, made once and reset per frame: an | ||
| 135 | // animation frame allocates no timer. | 135 | // animation frame allocates no timer. | ||
| 136 | - | swipe *swipeGesture | 136 | + | swipe *swipeGesture |
| 137 | + | // gesture is the touchpad gesture forwarded to a client | |||
| 138 | + | // (clientgesture.go). | |||
| 139 | + | gesture clientGesture | |||
| 137 | frameC <-chan time.Time | 140 | frameC <-chan time.Time | ||
| 138 | frameTimer ports.Timer | 141 | frameTimer ports.Timer | ||
| 139 | // shots is the snapshot of what the screens show before an action | 142 | // shots is the snapshot of what the screens show before an action | ||
| @@ -609,7 +612,7 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||||
| 609 | if c.pointer != 0 && !c.visible(c.pointer) { | 612 | if c.pointer != 0 && !c.visible(c.pointer) { | ||
| 610 | c.pointer = 0 | 613 | c.pointer = 0 | ||
| 611 | if c.grab == 0 { | 614 | if c.grab == 0 { | ||
| 612 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | 615 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { |
| 613 | return err | 616 | return err | ||
| 614 | } | 617 | } | ||
| 615 | } | 618 | } | ||
| @@ -691,7 +694,7 @@ func (c *Core) rehit(ctx context.Context) error { | |||||
| 691 | id, x, y := c.hit(c.cursorX, c.cursorY) | 694 | id, x, y := c.hit(c.cursorX, c.cursorY) | ||
| 692 | if id != c.pointer { | 695 | if id != c.pointer { | ||
| 693 | c.pointer, c.pointerAt = id, [2]float64{x, y} | 696 | c.pointer, c.pointerAt = id, [2]float64{x, y} | ||
| 694 | - | return c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}) | 697 | + | return c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}) |
| 695 | } | 698 | } | ||
| 696 | if id == 0 || c.pointerAt == [2]float64{x, y} { | 699 | if id == 0 || c.pointerAt == [2]float64{x, y} { | ||
| 697 | return nil | 700 | return nil | ||
M internal/core/drag.go +1 -1
| @@ -165,7 +165,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize | |||
| 165 | 165 | c.grab = 0 | |
| 166 | 166 | if c.pointer != 0 { | |
| 167 | 167 | c.pointer = 0 | |
| 168 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | |
| 168 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 169 | 169 | return err | |
| 170 | 170 | } | |
| 171 | 171 | } | |
| @@ -165,7 +165,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize | |||||
| 165 | c.grab = 0 | 165 | c.grab = 0 | ||
| 166 | if c.pointer != 0 { | 166 | if c.pointer != 0 { | ||
| 167 | c.pointer = 0 | 167 | c.pointer = 0 | ||
| 168 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | 168 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { |
| 169 | return err | 169 | return err | ||
| 170 | } | 170 | } | ||
| 171 | } | 171 | } | ||
M internal/core/input.go +23 -1
| @@ -26,18 +26,40 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 26 | 26 | case ports.PointerAxis: | |
| 27 | 27 | return c.pointerAxis(ctx, v) | |
| 28 | 28 | case ports.SwipeBegin: | |
| 29 | + | if forwardsSwipe(v.Fingers) { | |
| 30 | + | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | |
| 31 | + | } | |
| 32 | + | if err := c.gestureAbort(ctx, v.Time); err != nil { | |
| 33 | + | return err | |
| 34 | + | } | |
| 29 | 35 | if c.swipeBegin(v) { | |
| 30 | 36 | return c.slid(ctx, false) | |
| 31 | 37 | } | |
| 32 | 38 | return nil | |
| 33 | 39 | case ports.SwipeUpdate: | |
| 40 | + | if c.gesture.on { | |
| 41 | + | return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.DX, DY: v.DY, Time: v.Time}) | |
| 42 | + | } | |
| 34 | 43 | before := c.cur().mon.Current() | |
| 35 | 44 | if c.swipeUpdate(v) { | |
| 36 | 45 | return c.slid(ctx, c.cur().mon.Current() != before) | |
| 37 | 46 | } | |
| 38 | 47 | return nil | |
| 39 | 48 | case ports.SwipeEnd: | |
| 49 | + | if c.gesture.on && c.gesture.kind == ports.GestureSwipe { | |
| 50 | + | return c.gestureEnd(ctx, ports.GestureSwipe, v.Cancelled, v.Time) | |
| 51 | + | } | |
| 40 | 52 | return c.slid(ctx, c.swipeEnd(v)) | |
| 53 | + | case ports.PinchBegin: | |
| 54 | + | return c.gestureBegin(ctx, ports.GesturePinch, v.Fingers, v.Time) | |
| 55 | + | case ports.PinchUpdate: | |
| 56 | + | return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GesturePinch, DX: v.DX, DY: v.DY, Scale: v.Scale, Rotation: v.Rotation, Time: v.Time}) | |
| 57 | + | case ports.PinchEnd: | |
| 58 | + | return c.gestureEnd(ctx, ports.GesturePinch, v.Cancelled, v.Time) | |
| 59 | + | case ports.HoldBegin: | |
| 60 | + | return c.gestureBegin(ctx, ports.GestureHold, v.Fingers, v.Time) | |
| 61 | + | case ports.HoldEnd: | |
| 62 | + | return c.gestureEnd(ctx, ports.GestureHold, v.Cancelled, v.Time) | |
| 41 | 63 | case ports.KeyEvent: | |
| 42 | 64 | return c.keyEvent(ctx, v) | |
| 43 | 65 | } | |
| @@ -73,7 +95,7 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error { | |||
| 73 | 95 | // Layout changes are intentionally re-hit-tested only on motion. | |
| 74 | 96 | if id != c.pointer { | |
| 75 | 97 | c.pointer = id | |
| 76 | - | if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | |
| 98 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | |
| 77 | 99 | return err | |
| 78 | 100 | } | |
| 79 | 101 | } | |
| @@ -26,18 +26,40 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 26 | case ports.PointerAxis: | 26 | case ports.PointerAxis: | ||
| 27 | return c.pointerAxis(ctx, v) | 27 | return c.pointerAxis(ctx, v) | ||
| 28 | case ports.SwipeBegin: | 28 | case ports.SwipeBegin: | ||
| 29 | + | if forwardsSwipe(v.Fingers) { | |||
| 30 | + | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | |||
| 31 | + | } | |||
| 32 | + | if err := c.gestureAbort(ctx, v.Time); err != nil { | |||
| 33 | + | return err | |||
| 34 | + | } | |||
| 29 | if c.swipeBegin(v) { | 35 | if c.swipeBegin(v) { | ||
| 30 | return c.slid(ctx, false) | 36 | return c.slid(ctx, false) | ||
| 31 | } | 37 | } | ||
| 32 | return nil | 38 | return nil | ||
| 33 | case ports.SwipeUpdate: | 39 | case ports.SwipeUpdate: | ||
| 40 | + | if c.gesture.on { | |||
| 41 | + | return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.DX, DY: v.DY, Time: v.Time}) | |||
| 42 | + | } | |||
| 34 | before := c.cur().mon.Current() | 43 | before := c.cur().mon.Current() | ||
| 35 | if c.swipeUpdate(v) { | 44 | if c.swipeUpdate(v) { | ||
| 36 | return c.slid(ctx, c.cur().mon.Current() != before) | 45 | return c.slid(ctx, c.cur().mon.Current() != before) | ||
| 37 | } | 46 | } | ||
| 38 | return nil | 47 | return nil | ||
| 39 | case ports.SwipeEnd: | 48 | case ports.SwipeEnd: | ||
| 49 | + | if c.gesture.on && c.gesture.kind == ports.GestureSwipe { | |||
| 50 | + | return c.gestureEnd(ctx, ports.GestureSwipe, v.Cancelled, v.Time) | |||
| 51 | + | } | |||
| 40 | return c.slid(ctx, c.swipeEnd(v)) | 52 | return c.slid(ctx, c.swipeEnd(v)) | ||
| 53 | + | case ports.PinchBegin: | |||
| 54 | + | return c.gestureBegin(ctx, ports.GesturePinch, v.Fingers, v.Time) | |||
| 55 | + | case ports.PinchUpdate: | |||
| 56 | + | return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GesturePinch, DX: v.DX, DY: v.DY, Scale: v.Scale, Rotation: v.Rotation, Time: v.Time}) | |||
| 57 | + | case ports.PinchEnd: | |||
| 58 | + | return c.gestureEnd(ctx, ports.GesturePinch, v.Cancelled, v.Time) | |||
| 59 | + | case ports.HoldBegin: | |||
| 60 | + | return c.gestureBegin(ctx, ports.GestureHold, v.Fingers, v.Time) | |||
| 61 | + | case ports.HoldEnd: | |||
| 62 | + | return c.gestureEnd(ctx, ports.GestureHold, v.Cancelled, v.Time) | |||
| 41 | case ports.KeyEvent: | 63 | case ports.KeyEvent: | ||
| 42 | return c.keyEvent(ctx, v) | 64 | return c.keyEvent(ctx, v) | ||
| 43 | } | 65 | } | ||
| @@ -73,7 +95,7 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error { | |||||
| 73 | // Layout changes are intentionally re-hit-tested only on motion. | 95 | // Layout changes are intentionally re-hit-tested only on motion. | ||
| 74 | if id != c.pointer { | 96 | if id != c.pointer { | ||
| 75 | c.pointer = id | 97 | c.pointer = id | ||
| 76 | - | if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | 98 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { |
| 77 | return err | 99 | return err | ||
| 78 | } | 100 | } | ||
| 79 | } | 101 | } | ||
M internal/core/security.go +6 -3
| @@ -41,6 +41,9 @@ func (c *Core) syncSecurity() bool { | |||
| 41 | 41 | } | |
| 42 | 42 | c.dropSwipe() | |
| 43 | 43 | c.swipe = nil | |
| 44 | + | // The gesture ends with the pointer focus: wayland cancels it when the | |
| 45 | + | // protected epoch clears that focus. | |
| 46 | + | c.gesture = clientGesture{} | |
| 44 | 47 | c.stopAnimations() | |
| 45 | 48 | c.stopPulse() | |
| 46 | 49 | if state.Protected { | |
| @@ -64,7 +67,7 @@ func (c *Core) securityCheckpoint(ctx context.Context) error { | |||
| 64 | 67 | return nil | |
| 65 | 68 | } | |
| 66 | 69 | // Clear display focus even if the new epoch has no mapped lock surfaces. | |
| 67 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | |
| 70 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 68 | 71 | return err | |
| 69 | 72 | } | |
| 70 | 73 | if err := c.command(ctx, ports.FocusWindow{}); err != nil { | |
| @@ -204,7 +207,7 @@ func (c *Core) publishProtected(ctx context.Context) error { | |||
| 204 | 207 | if !c.validLockID(c.pointer) { | |
| 205 | 208 | c.pointer = 0 | |
| 206 | 209 | } | |
| 207 | - | if err := c.command(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil { | |
| 210 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil { | |
| 208 | 211 | return err | |
| 209 | 212 | } | |
| 210 | 213 | latest(c.ch.Scenes, scenes) | |
| @@ -259,7 +262,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 259 | 262 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | |
| 260 | 263 | if id != c.pointer { | |
| 261 | 264 | c.pointer = id | |
| 262 | - | if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | |
| 265 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | |
| 263 | 266 | return err | |
| 264 | 267 | } | |
| 265 | 268 | } | |
| @@ -41,6 +41,9 @@ func (c *Core) syncSecurity() bool { | |||||
| 41 | } | 41 | } | ||
| 42 | c.dropSwipe() | 42 | c.dropSwipe() | ||
| 43 | c.swipe = nil | 43 | c.swipe = nil | ||
| 44 | + | // The gesture ends with the pointer focus: wayland cancels it when the | |||
| 45 | + | // protected epoch clears that focus. | |||
| 46 | + | c.gesture = clientGesture{} | |||
| 44 | c.stopAnimations() | 47 | c.stopAnimations() | ||
| 45 | c.stopPulse() | 48 | c.stopPulse() | ||
| 46 | if state.Protected { | 49 | if state.Protected { | ||
| @@ -64,7 +67,7 @@ func (c *Core) securityCheckpoint(ctx context.Context) error { | |||||
| 64 | return nil | 67 | return nil | ||
| 65 | } | 68 | } | ||
| 66 | // Clear display focus even if the new epoch has no mapped lock surfaces. | 69 | // Clear display focus even if the new epoch has no mapped lock surfaces. | ||
| 67 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | 70 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { |
| 68 | return err | 71 | return err | ||
| 69 | } | 72 | } | ||
| 70 | if err := c.command(ctx, ports.FocusWindow{}); err != nil { | 73 | if err := c.command(ctx, ports.FocusWindow{}); err != nil { | ||
| @@ -204,7 +207,7 @@ func (c *Core) publishProtected(ctx context.Context) error { | |||||
| 204 | if !c.validLockID(c.pointer) { | 207 | if !c.validLockID(c.pointer) { | ||
| 205 | c.pointer = 0 | 208 | c.pointer = 0 | ||
| 206 | } | 209 | } | ||
| 207 | - | if err := c.command(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil { | 210 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil { |
| 208 | return err | 211 | return err | ||
| 209 | } | 212 | } | ||
| 210 | latest(c.ch.Scenes, scenes) | 213 | latest(c.ch.Scenes, scenes) | ||
| @@ -259,7 +262,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 259 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | 262 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | ||
| 260 | if id != c.pointer { | 263 | if id != c.pointer { | ||
| 261 | c.pointer = id | 264 | c.pointer = id | ||
| 262 | - | if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | 265 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { |
| 263 | return err | 266 | return err | ||
| 264 | } | 267 | } | ||
| 265 | } | 268 | } | ||