detached 1806e6d

Merge pull request 'feat: forward touchpad gestures to clients (zwp_pointer_gestures_v1)' (#23) from feat/pointer-gestures into main

Reviewed-on: https://git.bnema.dev/bnema/neferwl/pulls/23
commit
1806e6d3161cb63f14e5e923c9dd02eb1ef962e2
author
bnema · · 2026-10-08 12:52 UTC
merge of
ac31712 + 86835f1 · diff against the first parent
signature
unsigned
18 files · +1173 -71
M docs/config.md +2 -0
@@ -186,6 +186,8 @@ Over the shown stash a sideways swipe slides it with the fingers, with a stop on
186186
187187 Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia.
188188
189+Three- and four-finger swipes belong to the compositor. Every other touchpad gesture goes to the window under the pointer through `zwp_pointer_gestures_v1` (version 3): swipes of other finger counts, pinch (zoom and rotation) and hold. A gesture stays with the window that had the pointer when it began; it ends as cancelled if the pointer moves to another window or the window closes. Nothing is sent to windows while the session is locked.
190+
189191 ## HDR
190192
191193 `output.<name>.hdr = on` sends HDR10 (BT.2020, PQ) to displays whose EDID and connector support it. The desktop stays SDR content, shown at `sdr-brightness` nits.
@@ -186,6 +186,8 @@ Over the shown stash a sideways swipe slides it with the fingers, with a stop on
186 186
187 Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia.187 Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia.
188 188
189+Three- and four-finger swipes belong to the compositor. Every other touchpad gesture goes to the window under the pointer through `zwp_pointer_gestures_v1` (version 3): swipes of other finger counts, pinch (zoom and rotation) and hold. A gesture stays with the window that had the pointer when it began; it ends as cancelled if the pointer moves to another window or the window closes. Nothing is sent to windows while the session is locked.
190+
189 ## HDR191 ## HDR
190 192
191 `output.<name>.hdr = on` sends HDR10 (BT.2020, PQ) to displays whose EDID and connector support it. The desktop stays SDR content, shown at `sdr-brightness` nits.193 `output.<name>.hdr = on` sends HDR10 (BT.2020, PQ) to displays whose EDID and connector support it. The desktop stays SDR content, shown at `sdr-brightness` nits.
M internal/adapters/libinput/libinput.go +96 -27
@@ -33,6 +33,11 @@ const (
3333 evSwipeBegin = 800
3434 evSwipeUpdate = 801
3535 evSwipeEnd = 802
36+ evPinchBegin = 803
37+ evPinchUpdate = 804
38+ evPinchEnd = 805
39+ evHoldBegin = 806
40+ evHoldEnd = 807
3641 )
3742
3843 var (
@@ -75,7 +80,11 @@ var (
7580 gestureCanceled func(gev uintptr) int32
7681 gestureDX func(gev uintptr) float64
7782 gestureDY func(gev uintptr) float64
83+ gestureAccelDX func(gev uintptr) float64
84+ gestureAccelDY func(gev uintptr) float64
7885 gestureUsec func(gev uintptr) uint64
86+ gestureScale func(gev uintptr) float64
87+ gestureAngle func(gev uintptr) float64
7988 deviceRef func(dev uintptr) uintptr
8089 deviceUnref func(dev uintptr) uintptr
8190 hasNatural func(dev uintptr) int32
@@ -148,7 +157,11 @@ func load() error {
148157 reg(&gestureCanceled, "event_gesture_get_cancelled")
149158 reg(&gestureDX, "event_gesture_get_dx_unaccelerated")
150159 reg(&gestureDY, "event_gesture_get_dy_unaccelerated")
160+ reg(&gestureAccelDX, "event_gesture_get_dx")
161+ reg(&gestureAccelDY, "event_gesture_get_dy")
151162 reg(&gestureUsec, "event_gesture_get_time_usec")
163+ reg(&gestureScale, "event_gesture_get_scale")
164+ reg(&gestureAngle, "event_gesture_get_angle_delta")
152165 reg(&deviceRef, "device_ref")
153166 reg(&deviceUnref, "device_unref")
154167 reg(&hasNatural, "device_config_scroll_has_natural_scroll")
@@ -325,14 +338,14 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
325338 }
326339
327340 // inputState is the device state Run owns: the device config, the devices
328-// it configures (referenced until removed) and the swipe in progress on
329-// each touchpad. dev applies the config to libinput; it is a zero-size
341+// it configures (referenced until removed) and the gesture in progress on
342+// the touchpads. dev applies the config to libinput; it is a zero-size
330343 // type, so storing it in the interface allocates nothing.
331344 type inputState struct {
332- cfg ports.InputDevicesConfig
333- dev deviceConfig
334- devices map[uintptr]bool
335- swipes swipes
345+ cfg ports.InputDevicesConfig
346+ dev deviceConfig
347+ devices map[uintptr]bool
348+ gestures gestures
336349 }
337350
338351 // deviceConfig is libinput's device configuration side: what a device is
@@ -478,33 +491,74 @@ func (s *inputState) release() {
478491 clear(s.devices)
479492 }
480493
481-// swipes streams one three- or four-finger swipe at a time to core, which
482-// follows
483-// the fingers. owner is the touchpad of the swipe in progress (0: none);
484-// another touchpad's swipe is ignored until it ends, so core never sees
485-// two streams interleaved.
486-type swipes struct{ owner uintptr }
494+// gestures streams one touchpad gesture at a time to core: a swipe of any
495+// finger count, a pinch or a hold. Core follows the fingers of a three- or
496+// four-finger swipe and hands the other gestures to the window under the
497+// pointer. owner is the touchpad of the gesture in progress (0: none) and
498+// kind its kind; another touchpad's gesture, or another kind's event, is
499+// ignored until it ends, so core never sees two streams interleaved.
500+type gestures struct {
501+ owner uintptr
502+ kind ports.GestureKind
503+}
487504
488-func (s *swipes) begin(dev uintptr, fingers int, at time.Duration) ports.InputEvent {
489- if (fingers != 3 && fingers != 4) || s.owner != 0 {
505+func (g *gestures) begin(dev uintptr, kind ports.GestureKind, fingers int, at time.Duration) ports.InputEvent {
506+ if dev == 0 || fingers <= 0 || g.owner != 0 {
490507 return nil
491508 }
492- s.owner = dev
509+ g.owner, g.kind = dev, kind
510+ switch kind {
511+ case ports.GesturePinch:
512+ return ports.PinchBegin{Fingers: fingers, Time: at}
513+ case ports.GestureHold:
514+ return ports.HoldBegin{Fingers: fingers, Time: at}
515+ }
493516 return ports.SwipeBegin{Fingers: fingers, Time: at}
494517 }
495518
496-func (s *swipes) update(dev uintptr, dx, dy float64, at time.Duration) ports.InputEvent {
497- if dev == 0 || s.owner != dev {
519+// running reports whether dev has a gesture of the given kind in progress.
520+func (g *gestures) running(dev uintptr, kind ports.GestureKind) bool {
521+ return dev != 0 && g.owner == dev && g.kind == kind
522+}
523+
524+func (g *gestures) swipeUpdate(dev uintptr, dx, dy, accelDX, accelDY float64, at time.Duration) ports.InputEvent {
525+ if !g.running(dev, ports.GestureSwipe) {
526+ return nil
527+ }
528+ return ports.SwipeUpdate{DX: dx, DY: dy, AccelDX: accelDX, AccelDY: accelDY, Time: at}
529+}
530+
531+func (g *gestures) pinchUpdate(dev uintptr, dx, dy, scale, rotation float64, at time.Duration) ports.InputEvent {
532+ if !g.running(dev, ports.GesturePinch) {
498533 return nil
499534 }
500- return ports.SwipeUpdate{DX: dx, DY: dy, Time: at}
535+ return ports.PinchUpdate{DX: dx, DY: dy, Scale: scale, Rotation: rotation, Time: at}
501536 }
502537
503-func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputEvent {
504- if dev == 0 || s.owner != dev {
538+// end finishes the gesture of the given kind on dev.
539+func (g *gestures) end(dev uintptr, kind ports.GestureKind, cancelled bool, at time.Duration) ports.InputEvent {
540+ if !g.running(dev, kind) {
505541 return nil
506542 }
507- s.owner = 0
543+ return g.finish(cancelled, at)
544+}
545+
546+// abort cancels whatever gesture dev has in progress (the device went).
547+func (g *gestures) abort(dev uintptr) ports.InputEvent {
548+ if dev == 0 || g.owner != dev {
549+ return nil
550+ }
551+ return g.finish(true, 0)
552+}
553+
554+func (g *gestures) finish(cancelled bool, at time.Duration) ports.InputEvent {
555+ g.owner = 0
556+ switch g.kind {
557+ case ports.GesturePinch:
558+ return ports.PinchEnd{Cancelled: cancelled, Time: at}
559+ case ports.GestureHold:
560+ return ports.HoldEnd{Cancelled: cancelled, Time: at}
561+ }
508562 return ports.SwipeEnd{Cancelled: cancelled, Time: at}
509563 }
510564
@@ -552,17 +606,32 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
552606 delete(in.devices, dev)
553607 deviceUnref(dev)
554608 }
555- // Core waits for the end of a swipe the device began.
556- return in.swipes.end(dev, true, 0), nil
557- case evSwipeBegin:
609+ // Core waits for the end of a gesture the device began.
610+ return in.gestures.abort(dev), nil
611+ case evSwipeBegin, evPinchBegin, evHoldBegin:
558612 ge := gestureEvent(ev)
559- return in.swipes.begin(eventDevice(ev), int(gestureFingers(ge)), usec(gestureUsec(ge))), nil
613+ kind := ports.GestureSwipe
614+ if eventType(ev) == evPinchBegin {
615+ kind = ports.GesturePinch
616+ } else if eventType(ev) == evHoldBegin {
617+ kind = ports.GestureHold
618+ }
619+ return in.gestures.begin(eventDevice(ev), kind, int(gestureFingers(ge)), usec(gestureUsec(ge))), nil
560620 case evSwipeUpdate:
561621 ge := gestureEvent(ev)
562- return in.swipes.update(eventDevice(ev), gestureDX(ge), gestureDY(ge), usec(gestureUsec(ge))), nil
622+ return in.gestures.swipeUpdate(eventDevice(ev), gestureDX(ge), gestureDY(ge), gestureAccelDX(ge), gestureAccelDY(ge), usec(gestureUsec(ge))), nil
623+ case evPinchUpdate:
624+ ge := gestureEvent(ev)
625+ return in.gestures.pinchUpdate(eventDevice(ev), gestureAccelDX(ge), gestureAccelDY(ge), gestureScale(ge), gestureAngle(ge), usec(gestureUsec(ge))), nil
563626 case evSwipeEnd:
564627 ge := gestureEvent(ev)
565- return in.swipes.end(eventDevice(ev), gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
628+ return in.gestures.end(eventDevice(ev), ports.GestureSwipe, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
629+ case evPinchEnd:
630+ ge := gestureEvent(ev)
631+ return in.gestures.end(eventDevice(ev), ports.GesturePinch, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
632+ case evHoldEnd:
633+ ge := gestureEvent(ev)
634+ return in.gestures.end(eventDevice(ev), ports.GestureHold, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
566635 case evKeyboardKey:
567636 k := keyboardEvent(ev)
568637 code, pressed := keyboardKey(k), keyboardState(k) == 1
@@ -33,6 +33,11 @@ const (
33 evSwipeBegin = 80033 evSwipeBegin = 800
34 evSwipeUpdate = 80134 evSwipeUpdate = 801
35 evSwipeEnd = 80235 evSwipeEnd = 802
36+ evPinchBegin = 803
37+ evPinchUpdate = 804
38+ evPinchEnd = 805
39+ evHoldBegin = 806
40+ evHoldEnd = 807
36 )41 )
37 42
38 var (43 var (
@@ -75,7 +80,11 @@ var (
75 gestureCanceled func(gev uintptr) int3280 gestureCanceled func(gev uintptr) int32
76 gestureDX func(gev uintptr) float6481 gestureDX func(gev uintptr) float64
77 gestureDY func(gev uintptr) float6482 gestureDY func(gev uintptr) float64
83+ gestureAccelDX func(gev uintptr) float64
84+ gestureAccelDY func(gev uintptr) float64
78 gestureUsec func(gev uintptr) uint6485 gestureUsec func(gev uintptr) uint64
86+ gestureScale func(gev uintptr) float64
87+ gestureAngle func(gev uintptr) float64
79 deviceRef func(dev uintptr) uintptr88 deviceRef func(dev uintptr) uintptr
80 deviceUnref func(dev uintptr) uintptr89 deviceUnref func(dev uintptr) uintptr
81 hasNatural func(dev uintptr) int3290 hasNatural func(dev uintptr) int32
@@ -148,7 +157,11 @@ func load() error {
148 reg(&gestureCanceled, "event_gesture_get_cancelled")157 reg(&gestureCanceled, "event_gesture_get_cancelled")
149 reg(&gestureDX, "event_gesture_get_dx_unaccelerated")158 reg(&gestureDX, "event_gesture_get_dx_unaccelerated")
150 reg(&gestureDY, "event_gesture_get_dy_unaccelerated")159 reg(&gestureDY, "event_gesture_get_dy_unaccelerated")
160+ reg(&gestureAccelDX, "event_gesture_get_dx")
161+ reg(&gestureAccelDY, "event_gesture_get_dy")
151 reg(&gestureUsec, "event_gesture_get_time_usec")162 reg(&gestureUsec, "event_gesture_get_time_usec")
163+ reg(&gestureScale, "event_gesture_get_scale")
164+ reg(&gestureAngle, "event_gesture_get_angle_delta")
152 reg(&deviceRef, "device_ref")165 reg(&deviceRef, "device_ref")
153 reg(&deviceUnref, "device_unref")166 reg(&deviceUnref, "device_unref")
154 reg(&hasNatural, "device_config_scroll_has_natural_scroll")167 reg(&hasNatural, "device_config_scroll_has_natural_scroll")
@@ -325,14 +338,14 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
325 }338 }
326 339
327 // inputState is the device state Run owns: the device config, the devices340 // inputState is the device state Run owns: the device config, the devices
328-// it configures (referenced until removed) and the swipe in progress on341+// it configures (referenced until removed) and the gesture in progress on
329-// each touchpad. dev applies the config to libinput; it is a zero-size342+// the touchpads. dev applies the config to libinput; it is a zero-size
330 // type, so storing it in the interface allocates nothing.343 // type, so storing it in the interface allocates nothing.
331 type inputState struct {344 type inputState struct {
332- cfg ports.InputDevicesConfig345+ cfg ports.InputDevicesConfig
333- dev deviceConfig346+ dev deviceConfig
334- devices map[uintptr]bool347+ devices map[uintptr]bool
335- swipes swipes348+ gestures gestures
336 }349 }
337 350
338 // deviceConfig is libinput's device configuration side: what a device is351 // deviceConfig is libinput's device configuration side: what a device is
@@ -478,33 +491,74 @@ func (s *inputState) release() {
478 clear(s.devices)491 clear(s.devices)
479 }492 }
480 493
481-// swipes streams one three- or four-finger swipe at a time to core, which494+// gestures streams one touchpad gesture at a time to core: a swipe of any
482-// follows495+// finger count, a pinch or a hold. Core follows the fingers of a three- or
483-// the fingers. owner is the touchpad of the swipe in progress (0: none);496+// four-finger swipe and hands the other gestures to the window under the
484-// another touchpad's swipe is ignored until it ends, so core never sees497+// pointer. owner is the touchpad of the gesture in progress (0: none) and
485-// two streams interleaved.498+// kind its kind; another touchpad's gesture, or another kind's event, is
486-type swipes struct{ owner uintptr }499+// ignored until it ends, so core never sees two streams interleaved.
500+type gestures struct {
501+ owner uintptr
502+ kind ports.GestureKind
503+}
487 504
488-func (s *swipes) begin(dev uintptr, fingers int, at time.Duration) ports.InputEvent {505+func (g *gestures) begin(dev uintptr, kind ports.GestureKind, fingers int, at time.Duration) ports.InputEvent {
489- if (fingers != 3 && fingers != 4) || s.owner != 0 {506+ if dev == 0 || fingers <= 0 || g.owner != 0 {
490 return nil507 return nil
491 }508 }
492- s.owner = dev509+ g.owner, g.kind = dev, kind
510+ switch kind {
511+ case ports.GesturePinch:
512+ return ports.PinchBegin{Fingers: fingers, Time: at}
513+ case ports.GestureHold:
514+ return ports.HoldBegin{Fingers: fingers, Time: at}
515+ }
493 return ports.SwipeBegin{Fingers: fingers, Time: at}516 return ports.SwipeBegin{Fingers: fingers, Time: at}
494 }517 }
495 518
496-func (s *swipes) update(dev uintptr, dx, dy float64, at time.Duration) ports.InputEvent {519+// running reports whether dev has a gesture of the given kind in progress.
497- if dev == 0 || s.owner != dev {520+func (g *gestures) running(dev uintptr, kind ports.GestureKind) bool {
521+ return dev != 0 && g.owner == dev && g.kind == kind
522+}
523+
524+func (g *gestures) swipeUpdate(dev uintptr, dx, dy, accelDX, accelDY float64, at time.Duration) ports.InputEvent {
525+ if !g.running(dev, ports.GestureSwipe) {
526+ return nil
527+ }
528+ return ports.SwipeUpdate{DX: dx, DY: dy, AccelDX: accelDX, AccelDY: accelDY, Time: at}
529+}
530+
531+func (g *gestures) pinchUpdate(dev uintptr, dx, dy, scale, rotation float64, at time.Duration) ports.InputEvent {
532+ if !g.running(dev, ports.GesturePinch) {
498 return nil533 return nil
499 }534 }
500- return ports.SwipeUpdate{DX: dx, DY: dy, Time: at}535+ return ports.PinchUpdate{DX: dx, DY: dy, Scale: scale, Rotation: rotation, Time: at}
501 }536 }
502 537
503-func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputEvent {538+// end finishes the gesture of the given kind on dev.
504- if dev == 0 || s.owner != dev {539+func (g *gestures) end(dev uintptr, kind ports.GestureKind, cancelled bool, at time.Duration) ports.InputEvent {
540+ if !g.running(dev, kind) {
505 return nil541 return nil
506 }542 }
507- s.owner = 0543+ return g.finish(cancelled, at)
544+}
545+
546+// abort cancels whatever gesture dev has in progress (the device went).
547+func (g *gestures) abort(dev uintptr) ports.InputEvent {
548+ if dev == 0 || g.owner != dev {
549+ return nil
550+ }
551+ return g.finish(true, 0)
552+}
553+
554+func (g *gestures) finish(cancelled bool, at time.Duration) ports.InputEvent {
555+ g.owner = 0
556+ switch g.kind {
557+ case ports.GesturePinch:
558+ return ports.PinchEnd{Cancelled: cancelled, Time: at}
559+ case ports.GestureHold:
560+ return ports.HoldEnd{Cancelled: cancelled, Time: at}
561+ }
508 return ports.SwipeEnd{Cancelled: cancelled, Time: at}562 return ports.SwipeEnd{Cancelled: cancelled, Time: at}
509 }563 }
510 564
@@ -552,17 +606,32 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
552 delete(in.devices, dev)606 delete(in.devices, dev)
553 deviceUnref(dev)607 deviceUnref(dev)
554 }608 }
555- // Core waits for the end of a swipe the device began.609+ // Core waits for the end of a gesture the device began.
556- return in.swipes.end(dev, true, 0), nil610+ return in.gestures.abort(dev), nil
557- case evSwipeBegin:611+ case evSwipeBegin, evPinchBegin, evHoldBegin:
558 ge := gestureEvent(ev)612 ge := gestureEvent(ev)
559- return in.swipes.begin(eventDevice(ev), int(gestureFingers(ge)), usec(gestureUsec(ge))), nil613+ kind := ports.GestureSwipe
614+ if eventType(ev) == evPinchBegin {
615+ kind = ports.GesturePinch
616+ } else if eventType(ev) == evHoldBegin {
617+ kind = ports.GestureHold
618+ }
619+ return in.gestures.begin(eventDevice(ev), kind, int(gestureFingers(ge)), usec(gestureUsec(ge))), nil
560 case evSwipeUpdate:620 case evSwipeUpdate:
561 ge := gestureEvent(ev)621 ge := gestureEvent(ev)
562- return in.swipes.update(eventDevice(ev), gestureDX(ge), gestureDY(ge), usec(gestureUsec(ge))), nil622+ return in.gestures.swipeUpdate(eventDevice(ev), gestureDX(ge), gestureDY(ge), gestureAccelDX(ge), gestureAccelDY(ge), usec(gestureUsec(ge))), nil
623+ case evPinchUpdate:
624+ ge := gestureEvent(ev)
625+ return in.gestures.pinchUpdate(eventDevice(ev), gestureAccelDX(ge), gestureAccelDY(ge), gestureScale(ge), gestureAngle(ge), usec(gestureUsec(ge))), nil
563 case evSwipeEnd:626 case evSwipeEnd:
564 ge := gestureEvent(ev)627 ge := gestureEvent(ev)
565- return in.swipes.end(eventDevice(ev), gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil628+ return in.gestures.end(eventDevice(ev), ports.GestureSwipe, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
629+ case evPinchEnd:
630+ ge := gestureEvent(ev)
631+ return in.gestures.end(eventDevice(ev), ports.GesturePinch, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
632+ case evHoldEnd:
633+ ge := gestureEvent(ev)
634+ return in.gestures.end(eventDevice(ev), ports.GestureHold, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
566 case evKeyboardKey:635 case evKeyboardKey:
567 k := keyboardEvent(ev)636 k := keyboardEvent(ev)
568 code, pressed := keyboardKey(k), keyboardState(k) == 1637 code, pressed := keyboardKey(k), keyboardState(k) == 1
M internal/adapters/libinput/libinput_test.go +91 -25
@@ -42,61 +42,126 @@ func TestHotkey(t *testing.T) {
4242 }
4343 }
4444
45-func TestSwipesIgnoreSecondTouchpad(t *testing.T) {
46- var s swipes
45+func TestGesturesIgnoreSecondTouchpad(t *testing.T) {
46+ var g gestures
4747 const pad, other = 1, 2
48- s.begin(pad, 3, time.Second)
49- if ev := s.begin(other, 3, time.Second); ev != nil {
48+ g.begin(pad, ports.GestureSwipe, 3, time.Second)
49+ if ev := g.begin(other, ports.GestureSwipe, 3, time.Second); ev != nil {
5050 t.Fatalf("second touchpad began: %v", ev)
5151 }
52- if ev := s.update(other, 1, 1, time.Second); ev != nil {
52+ if ev := g.swipeUpdate(other, 1, 1, 1, 1, time.Second); ev != nil {
5353 t.Fatalf("second touchpad moved: %v", ev)
5454 }
55- if ev := s.end(other, false, time.Second); ev != nil {
55+ if ev := g.end(other, ports.GestureSwipe, false, time.Second); ev != nil {
5656 t.Fatalf("second touchpad ended the swipe: %v", ev)
5757 }
58- if ev := s.update(pad, 1, 1, 2*time.Second); ev == nil {
58+ if ev := g.swipeUpdate(pad, 1, 1, 1, 1, 2*time.Second); ev == nil {
5959 t.Fatal("first touchpad lost its swipe")
6060 }
61- s.end(pad, false, 3*time.Second)
62- if ev := s.begin(other, 3, 4*time.Second); ev == nil {
61+ g.end(pad, ports.GestureSwipe, false, 3*time.Second)
62+ if ev := g.begin(other, ports.GestureSwipe, 3, 4*time.Second); ev == nil {
6363 t.Fatal("second touchpad cannot swipe after the first ended")
6464 }
6565 }
6666
67-func TestSwipesStreamThreeFingers(t *testing.T) {
68- var s swipes
67+func TestGesturesStreamSwipe(t *testing.T) {
68+ var g gestures
6969 const pad, other = 1, 2
70- if ev := s.begin(pad, 2, time.Second); ev != nil {
71- t.Fatalf("two fingers: %v", ev)
70+ if ev := g.begin(pad, ports.GestureSwipe, 0, time.Second); ev != nil {
71+ t.Fatalf("no fingers: %v", ev)
7272 }
73- if ev := s.update(pad, 1, 1, time.Second); ev != nil {
73+ if ev := g.swipeUpdate(pad, 1, 1, 1, 1, time.Second); ev != nil {
7474 t.Fatalf("update without a swipe: %v", ev)
7575 }
76- if ev := s.begin(pad, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
76+ if ev := g.begin(pad, ports.GestureSwipe, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
7777 t.Fatalf("begin: %v", ev)
7878 }
79- if ev := s.update(pad, -3, 2, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, Time: 2 * time.Second}) {
79+ if ev := g.swipeUpdate(pad, -3, 2, -6, 4, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, AccelDX: -6, AccelDY: 4, Time: 2 * time.Second}) {
8080 t.Fatalf("update: %v", ev)
8181 }
82- if ev := s.end(other, false, 0); ev != nil {
82+ if ev := g.end(other, ports.GestureSwipe, false, 0); ev != nil {
8383 t.Fatalf("end on another device: %v", ev)
8484 }
85- if ev := s.end(pad, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
85+ if ev := g.end(pad, ports.GestureSwipe, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
8686 t.Fatalf("end: %v", ev)
8787 }
88- if ev := s.end(pad, false, 0); ev != nil {
88+ if ev := g.end(pad, ports.GestureSwipe, false, 0); ev != nil {
8989 t.Fatalf("second end: %v", ev)
9090 }
9191 }
9292
93-func TestSwipesStreamFourFingers(t *testing.T) {
94- var s swipes
95- if ev := s.begin(1, 4, time.Second); ev != (ports.SwipeBegin{Fingers: 4, Time: time.Second}) {
96- t.Fatalf("four fingers: %v", ev)
93+// Swipes of every finger count stream; core picks which ones it keeps.
94+func TestGesturesStreamAnyFingerCount(t *testing.T) {
95+ for _, fingers := range []int{2, 3, 4, 5} {
96+ var g gestures
97+ if ev := g.begin(1, ports.GestureSwipe, fingers, time.Second); ev != (ports.SwipeBegin{Fingers: fingers, Time: time.Second}) {
98+ t.Fatalf("%d fingers: %v", fingers, ev)
99+ }
100+ if ev := g.begin(1, ports.GestureSwipe, 5, time.Second); ev != nil {
101+ t.Fatalf("a second swipe began during a swipe: %v", ev)
102+ }
103+ }
104+}
105+
106+func TestGesturesStreamPinch(t *testing.T) {
107+ var g gestures
108+ const pad = 1
109+ if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != (ports.PinchBegin{Fingers: 2, Time: time.Second}) {
110+ t.Fatalf("begin: %v", ev)
111+ }
112+ if ev := g.swipeUpdate(pad, 1, 1, 1, 1, time.Second); ev != nil {
113+ t.Fatalf("swipe update during a pinch: %v", ev)
97114 }
98- if ev := s.begin(1, 5, time.Second); ev != nil {
99- t.Fatalf("five fingers during a swipe: %v", ev)
115+ if ev := g.end(pad, ports.GestureHold, false, time.Second); ev != nil {
116+ t.Fatalf("hold end during a pinch: %v", ev)
117+ }
118+ want := ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.5, Rotation: -10, Time: 2 * time.Second}
119+ if ev := g.pinchUpdate(pad, 1, -2, 1.5, -10, 2*time.Second); ev != want {
120+ t.Fatalf("update: %v", ev)
121+ }
122+ if ev := g.end(pad, ports.GesturePinch, false, 3*time.Second); ev != (ports.PinchEnd{Time: 3 * time.Second}) {
123+ t.Fatalf("end: %v", ev)
124+ }
125+ if ev := g.pinchUpdate(pad, 1, 1, 1, 0, 4*time.Second); ev != nil {
126+ t.Fatalf("update after the end: %v", ev)
127+ }
128+}
129+
130+func TestGesturesStreamHold(t *testing.T) {
131+ var g gestures
132+ const pad = 1
133+ if ev := g.begin(pad, ports.GestureHold, 3, time.Second); ev != (ports.HoldBegin{Fingers: 3, Time: time.Second}) {
134+ t.Fatalf("begin: %v", ev)
135+ }
136+ if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != nil {
137+ t.Fatalf("pinch began during a hold: %v", ev)
138+ }
139+ if ev := g.end(pad, ports.GestureHold, true, 2*time.Second); ev != (ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}) {
140+ t.Fatalf("end: %v", ev)
141+ }
142+}
143+
144+// A device that goes during a gesture ends it cancelled, whatever its kind.
145+func TestGesturesAbortOnRemoval(t *testing.T) {
146+ for kind, want := range map[ports.GestureKind]ports.InputEvent{
147+ ports.GestureSwipe: ports.SwipeEnd{Cancelled: true},
148+ ports.GesturePinch: ports.PinchEnd{Cancelled: true},
149+ ports.GestureHold: ports.HoldEnd{Cancelled: true},
150+ } {
151+ var g gestures
152+ g.begin(1, kind, 3, time.Second)
153+ if ev := g.abort(2); ev != nil {
154+ t.Fatalf("kind %d: another device aborted: %v", kind, ev)
155+ }
156+ if ev := g.abort(1); ev != want {
157+ t.Fatalf("kind %d: abort %v, want %v", kind, ev, want)
158+ }
159+ if ev := g.abort(1); ev != nil {
160+ t.Fatalf("kind %d: second abort %v", kind, ev)
161+ }
162+ if ev := g.begin(1, kind, 3, time.Second); ev == nil {
163+ t.Fatalf("kind %d: no gesture after the abort", kind)
164+ }
100165 }
101166 }
102167
@@ -262,6 +327,7 @@ func TestSecurityInputAllKindsProductionEpoch(t *testing.T) {
262327 ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},
263328 ports.PointerButton{Button: 272}, ports.PointerAxis{},
264329 ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},
330+ ports.PinchBegin{}, ports.PinchUpdate{}, ports.PinchEnd{}, ports.HoldBegin{}, ports.HoldEnd{},
265331 }
266332 security.EXPECT().Snapshot().Return(old).Times(len(events))
267333 security.EXPECT().Snapshot().Return(newState).Once()
@@ -42,61 +42,126 @@ func TestHotkey(t *testing.T) {
42 }42 }
43 }43 }
44 44
45-func TestSwipesIgnoreSecondTouchpad(t *testing.T) {45+func TestGesturesIgnoreSecondTouchpad(t *testing.T) {
46- var s swipes46+ var g gestures
47 const pad, other = 1, 247 const pad, other = 1, 2
48- s.begin(pad, 3, time.Second)48+ g.begin(pad, ports.GestureSwipe, 3, time.Second)
49- if ev := s.begin(other, 3, time.Second); ev != nil {49+ if ev := g.begin(other, ports.GestureSwipe, 3, time.Second); ev != nil {
50 t.Fatalf("second touchpad began: %v", ev)50 t.Fatalf("second touchpad began: %v", ev)
51 }51 }
52- if ev := s.update(other, 1, 1, time.Second); ev != nil {52+ if ev := g.swipeUpdate(other, 1, 1, 1, 1, time.Second); ev != nil {
53 t.Fatalf("second touchpad moved: %v", ev)53 t.Fatalf("second touchpad moved: %v", ev)
54 }54 }
55- if ev := s.end(other, false, time.Second); ev != nil {55+ if ev := g.end(other, ports.GestureSwipe, false, time.Second); ev != nil {
56 t.Fatalf("second touchpad ended the swipe: %v", ev)56 t.Fatalf("second touchpad ended the swipe: %v", ev)
57 }57 }
58- if ev := s.update(pad, 1, 1, 2*time.Second); ev == nil {58+ if ev := g.swipeUpdate(pad, 1, 1, 1, 1, 2*time.Second); ev == nil {
59 t.Fatal("first touchpad lost its swipe")59 t.Fatal("first touchpad lost its swipe")
60 }60 }
61- s.end(pad, false, 3*time.Second)61+ g.end(pad, ports.GestureSwipe, false, 3*time.Second)
62- if ev := s.begin(other, 3, 4*time.Second); ev == nil {62+ if ev := g.begin(other, ports.GestureSwipe, 3, 4*time.Second); ev == nil {
63 t.Fatal("second touchpad cannot swipe after the first ended")63 t.Fatal("second touchpad cannot swipe after the first ended")
64 }64 }
65 }65 }
66 66
67-func TestSwipesStreamThreeFingers(t *testing.T) {67+func TestGesturesStreamSwipe(t *testing.T) {
68- var s swipes68+ var g gestures
69 const pad, other = 1, 269 const pad, other = 1, 2
70- if ev := s.begin(pad, 2, time.Second); ev != nil {70+ if ev := g.begin(pad, ports.GestureSwipe, 0, time.Second); ev != nil {
71- t.Fatalf("two fingers: %v", ev)71+ t.Fatalf("no fingers: %v", ev)
72 }72 }
73- if ev := s.update(pad, 1, 1, time.Second); ev != nil {73+ if ev := g.swipeUpdate(pad, 1, 1, 1, 1, time.Second); ev != nil {
74 t.Fatalf("update without a swipe: %v", ev)74 t.Fatalf("update without a swipe: %v", ev)
75 }75 }
76- if ev := s.begin(pad, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {76+ if ev := g.begin(pad, ports.GestureSwipe, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
77 t.Fatalf("begin: %v", ev)77 t.Fatalf("begin: %v", ev)
78 }78 }
79- if ev := s.update(pad, -3, 2, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, Time: 2 * time.Second}) {79+ if ev := g.swipeUpdate(pad, -3, 2, -6, 4, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, AccelDX: -6, AccelDY: 4, Time: 2 * time.Second}) {
80 t.Fatalf("update: %v", ev)80 t.Fatalf("update: %v", ev)
81 }81 }
82- if ev := s.end(other, false, 0); ev != nil {82+ if ev := g.end(other, ports.GestureSwipe, false, 0); ev != nil {
83 t.Fatalf("end on another device: %v", ev)83 t.Fatalf("end on another device: %v", ev)
84 }84 }
85- if ev := s.end(pad, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {85+ if ev := g.end(pad, ports.GestureSwipe, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
86 t.Fatalf("end: %v", ev)86 t.Fatalf("end: %v", ev)
87 }87 }
88- if ev := s.end(pad, false, 0); ev != nil {88+ if ev := g.end(pad, ports.GestureSwipe, false, 0); ev != nil {
89 t.Fatalf("second end: %v", ev)89 t.Fatalf("second end: %v", ev)
90 }90 }
91 }91 }
92 92
93-func TestSwipesStreamFourFingers(t *testing.T) {93+// Swipes of every finger count stream; core picks which ones it keeps.
94- var s swipes94+func TestGesturesStreamAnyFingerCount(t *testing.T) {
95- if ev := s.begin(1, 4, time.Second); ev != (ports.SwipeBegin{Fingers: 4, Time: time.Second}) {95+ for _, fingers := range []int{2, 3, 4, 5} {
96- t.Fatalf("four fingers: %v", ev)96+ var g gestures
97+ if ev := g.begin(1, ports.GestureSwipe, fingers, time.Second); ev != (ports.SwipeBegin{Fingers: fingers, Time: time.Second}) {
98+ t.Fatalf("%d fingers: %v", fingers, ev)
99+ }
100+ if ev := g.begin(1, ports.GestureSwipe, 5, time.Second); ev != nil {
101+ t.Fatalf("a second swipe began during a swipe: %v", ev)
102+ }
103+ }
104+}
105+
106+func TestGesturesStreamPinch(t *testing.T) {
107+ var g gestures
108+ const pad = 1
109+ if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != (ports.PinchBegin{Fingers: 2, Time: time.Second}) {
110+ t.Fatalf("begin: %v", ev)
111+ }
112+ if ev := g.swipeUpdate(pad, 1, 1, 1, 1, time.Second); ev != nil {
113+ t.Fatalf("swipe update during a pinch: %v", ev)
97 }114 }
98- if ev := s.begin(1, 5, time.Second); ev != nil {115+ if ev := g.end(pad, ports.GestureHold, false, time.Second); ev != nil {
99- t.Fatalf("five fingers during a swipe: %v", ev)116+ t.Fatalf("hold end during a pinch: %v", ev)
117+ }
118+ want := ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.5, Rotation: -10, Time: 2 * time.Second}
119+ if ev := g.pinchUpdate(pad, 1, -2, 1.5, -10, 2*time.Second); ev != want {
120+ t.Fatalf("update: %v", ev)
121+ }
122+ if ev := g.end(pad, ports.GesturePinch, false, 3*time.Second); ev != (ports.PinchEnd{Time: 3 * time.Second}) {
123+ t.Fatalf("end: %v", ev)
124+ }
125+ if ev := g.pinchUpdate(pad, 1, 1, 1, 0, 4*time.Second); ev != nil {
126+ t.Fatalf("update after the end: %v", ev)
127+ }
128+}
129+
130+func TestGesturesStreamHold(t *testing.T) {
131+ var g gestures
132+ const pad = 1
133+ if ev := g.begin(pad, ports.GestureHold, 3, time.Second); ev != (ports.HoldBegin{Fingers: 3, Time: time.Second}) {
134+ t.Fatalf("begin: %v", ev)
135+ }
136+ if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != nil {
137+ t.Fatalf("pinch began during a hold: %v", ev)
138+ }
139+ if ev := g.end(pad, ports.GestureHold, true, 2*time.Second); ev != (ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}) {
140+ t.Fatalf("end: %v", ev)
141+ }
142+}
143+
144+// A device that goes during a gesture ends it cancelled, whatever its kind.
145+func TestGesturesAbortOnRemoval(t *testing.T) {
146+ for kind, want := range map[ports.GestureKind]ports.InputEvent{
147+ ports.GestureSwipe: ports.SwipeEnd{Cancelled: true},
148+ ports.GesturePinch: ports.PinchEnd{Cancelled: true},
149+ ports.GestureHold: ports.HoldEnd{Cancelled: true},
150+ } {
151+ var g gestures
152+ g.begin(1, kind, 3, time.Second)
153+ if ev := g.abort(2); ev != nil {
154+ t.Fatalf("kind %d: another device aborted: %v", kind, ev)
155+ }
156+ if ev := g.abort(1); ev != want {
157+ t.Fatalf("kind %d: abort %v, want %v", kind, ev, want)
158+ }
159+ if ev := g.abort(1); ev != nil {
160+ t.Fatalf("kind %d: second abort %v", kind, ev)
161+ }
162+ if ev := g.begin(1, kind, 3, time.Second); ev == nil {
163+ t.Fatalf("kind %d: no gesture after the abort", kind)
164+ }
100 }165 }
101 }166 }
102 167
@@ -262,6 +327,7 @@ func TestSecurityInputAllKindsProductionEpoch(t *testing.T) {
262 ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},327 ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},
263 ports.PointerButton{Button: 272}, ports.PointerAxis{},328 ports.PointerButton{Button: 272}, ports.PointerAxis{},
264 ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},329 ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},
330+ ports.PinchBegin{}, ports.PinchUpdate{}, ports.PinchEnd{}, ports.HoldBegin{}, ports.HoldEnd{},
265 }331 }
266 security.EXPECT().Snapshot().Return(old).Times(len(events))332 security.EXPECT().Snapshot().Return(old).Times(len(events))
267 security.EXPECT().Snapshot().Return(newState).Once()333 security.EXPECT().Snapshot().Return(newState).Once()
A internal/adapters/wayland/gestures.go +220 -0
@@ -0,0 +1,220 @@
1+package wayland
2+
3+import (
4+ "time"
5+
6+ "github.com/bnema/go-wayland-bindings/server/pointergestures"
7+ "github.com/bnema/go-wayland-bindings/server/wayland"
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/server"
10+)
11+
12+// zwp_pointer_gestures_v1: touchpad swipes (other than the compositor's own
13+// three and four fingers), pinches and holds go to the client under the
14+// pointer. A gesture belongs to the window that had the pointer when it
15+// began; it ends cancelled when the pointer leaves that window.
16+
17+// gestureObject is one swipe, pinch or hold object of a client. Exactly one
18+// of the three is set.
19+type gestureObject struct {
20+ swipe *pointergestures.ZwpPointerGestureSwipeV1
21+ pinch *pointergestures.ZwpPointerGesturePinchV1
22+ hold *pointergestures.ZwpPointerGestureHoldV1
23+}
24+
25+func (g *gestureObject) alive() bool {
26+ switch {
27+ case g.swipe != nil:
28+ return g.swipe.Resource.Alive()
29+ case g.pinch != nil:
30+ return g.pinch.Resource.Alive()
31+ case g.hold != nil:
32+ return g.hold.Resource.Alive()
33+ }
34+ return false
35+}
36+
37+func (g *gestureObject) kind() ports.GestureKind {
38+ switch {
39+ case g.pinch != nil:
40+ return ports.GesturePinch
41+ case g.hold != nil:
42+ return ports.GestureHold
43+ }
44+ return ports.GestureSwipe
45+}
46+
47+func (g *gestureObject) begin(serial uint32, at uint32, surf *wayland.Surface, fingers uint32) {
48+ switch {
49+ case g.swipe != nil:
50+ g.swipe.SendBegin(serial, at, surf, fingers)
51+ case g.pinch != nil:
52+ g.pinch.SendBegin(serial, at, surf, fingers)
53+ case g.hold != nil:
54+ g.hold.SendBegin(serial, at, surf, fingers)
55+ }
56+}
57+
58+func (g *gestureObject) update(u ports.GestureUpdateTo) {
59+ at := wireMsec(u.Time)
60+ dx, dy := server.FixedFromFloat(u.DX), server.FixedFromFloat(u.DY)
61+ switch {
62+ case g.swipe != nil:
63+ g.swipe.SendUpdate(at, dx, dy)
64+ case g.pinch != nil:
65+ g.pinch.SendUpdate(at, dx, dy, server.FixedFromFloat(u.Scale), server.FixedFromFloat(u.Rotation))
66+ }
67+}
68+
69+func (g *gestureObject) end(serial uint32, at uint32, cancelled int32) {
70+ switch {
71+ case g.swipe != nil:
72+ g.swipe.SendEnd(serial, at, cancelled)
73+ case g.pinch != nil:
74+ g.pinch.SendEnd(serial, at, cancelled)
75+ case g.hold != nil:
76+ g.hold.SendEnd(serial, at, cancelled)
77+ }
78+}
79+
80+// activeGesture is the gesture sent to a window: the objects that got its
81+// begin are the ones that get the rest, even when a client makes more
82+// meanwhile.
83+type activeGesture struct {
84+ id ports.WindowID
85+ kind ports.GestureKind
86+ objs []*gestureObject
87+ // at is the latest device time sent, for a cancellation.
88+ at time.Duration
89+}
90+
91+func registerPointerGestures(d *server.Display, s *Server) error {
92+ return pointergestures.NewZwpPointerGesturesV1Global(d, 3, func(c server.Client, v, id uint32) {
93+ _, _ = pointergestures.NewZwpPointerGesturesV1(c, int32(v), id, gestureManager{s})
94+ })
95+}
96+
97+// gestureManager is zwp_pointer_gestures_v1. Releasing it leaves the
98+// gesture objects it made alive, as the protocol says.
99+type gestureManager struct{ server *Server }
100+
101+func (gestureManager) Release(*pointergestures.ZwpPointerGesturesV1) {}
102+
103+// gestureDestroy handles the destroy request of the three gesture objects;
104+// the resource's OnDestroy forgets the object.
105+type gestureDestroy struct{}
106+
107+func (gestureDestroy) Destroy(*pointergestures.ZwpPointerGestureSwipeV1) {}
108+
109+type pinchDestroy struct{}
110+
111+func (pinchDestroy) Destroy(*pointergestures.ZwpPointerGesturePinchV1) {}
112+
113+type holdDestroy struct{}
114+
115+func (holdDestroy) Destroy(*pointergestures.ZwpPointerGestureHoldV1) {}
116+
117+func (m gestureManager) GetSwipeGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
118+ if res, err := pointergestures.NewZwpPointerGestureSwipeV1(r.Client(), r.Version(), id, gestureDestroy{}); err == nil {
119+ m.server.addGesture(r.Client(), &gestureObject{swipe: res}, res.Resource)
120+ }
121+}
122+
123+func (m gestureManager) GetPinchGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
124+ if res, err := pointergestures.NewZwpPointerGesturePinchV1(r.Client(), r.Version(), id, pinchDestroy{}); err == nil {
125+ m.server.addGesture(r.Client(), &gestureObject{pinch: res}, res.Resource)
126+ }
127+}
128+
129+func (m gestureManager) GetHoldGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
130+ if res, err := pointergestures.NewZwpPointerGestureHoldV1(r.Client(), r.Version(), id, holdDestroy{}); err == nil {
131+ m.server.addGesture(r.Client(), &gestureObject{hold: res}, res.Resource)
132+ }
133+}
134+
135+// addGesture lists a client's gesture object until its resource dies.
136+func (s *Server) addGesture(c server.Client, o *gestureObject, res *server.Resource) {
137+ s.gestures[c] = append(s.gestures[c], o)
138+ res.OnDestroy = func() {
139+ if list := removeItem(s.gestures[c], o); len(list) > 0 {
140+ s.gestures[c] = list
141+ } else {
142+ delete(s.gestures, c)
143+ }
144+ }
145+}
146+
147+// beginGesture sends the begin of a gesture to the objects of the kind that
148+// the client of the pointer-focus window made. A gesture still running
149+// ends cancelled first. Nothing is recorded when the client has no object.
150+func (s *Server) beginGesture(c ports.GestureBeginTo) {
151+ s.cancelGesture()
152+ if c.ID != s.seat.pointerFocus || c.Fingers <= 0 {
153+ return
154+ }
155+ surf, _, _, _ := s.pointerSurface(c.ID, 0, 0)
156+ if surf == nil {
157+ return
158+ }
159+ var objs []*gestureObject
160+ for _, o := range s.gestures[surf.Client()] {
161+ if o.kind() == c.Kind && o.alive() {
162+ objs = append(objs, o)
163+ }
164+ }
165+ if len(objs) == 0 {
166+ return
167+ }
168+ s.seat.gesture = &activeGesture{id: c.ID, kind: c.Kind, objs: objs, at: c.Time}
169+ s.serial++
170+ for _, o := range objs {
171+ o.begin(s.serial, wireMsec(c.Time), surf, uint32(c.Fingers))
172+ }
173+}
174+
175+// updateGesture moves the running gesture of the window.
176+func (s *Server) updateGesture(c ports.GestureUpdateTo) {
177+ g := s.seat.gesture
178+ if g == nil || g.id != c.ID || g.kind != c.Kind {
179+ return
180+ }
181+ g.at = c.Time
182+ for _, o := range g.objs {
183+ if o.alive() {
184+ o.update(c)
185+ }
186+ }
187+}
188+
189+// endGesture ends the running gesture of the window.
190+func (s *Server) endGesture(c ports.GestureEndTo) {
191+ g := s.seat.gesture
192+ if g == nil || g.id != c.ID || g.kind != c.Kind {
193+ return
194+ }
195+ g.at = c.Time
196+ s.finishGesture(c.Cancelled)
197+}
198+
199+// cancelGesture ends the running gesture, if any, as cancelled: the
200+// pointer left its window or the window went.
201+func (s *Server) cancelGesture() {
202+ if s.seat.gesture != nil {
203+ s.finishGesture(true)
204+ }
205+}
206+
207+func (s *Server) finishGesture(cancelled bool) {
208+ g := s.seat.gesture
209+ s.seat.gesture = nil
210+ state := int32(0)
211+ if cancelled {
212+ state = 1
213+ }
214+ s.serial++
215+ for _, o := range g.objs {
216+ if o.alive() {
217+ o.end(s.serial, wireMsec(g.at), state)
218+ }
219+ }
220+}
@@ -0,0 +1,220 @@
1+package wayland
2+
3+import (
4+ "time"
5+
6+ "github.com/bnema/go-wayland-bindings/server/pointergestures"
7+ "github.com/bnema/go-wayland-bindings/server/wayland"
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/server"
10+)
11+
12+// zwp_pointer_gestures_v1: touchpad swipes (other than the compositor's own
13+// three and four fingers), pinches and holds go to the client under the
14+// pointer. A gesture belongs to the window that had the pointer when it
15+// began; it ends cancelled when the pointer leaves that window.
16+
17+// gestureObject is one swipe, pinch or hold object of a client. Exactly one
18+// of the three is set.
19+type gestureObject struct {
20+ swipe *pointergestures.ZwpPointerGestureSwipeV1
21+ pinch *pointergestures.ZwpPointerGesturePinchV1
22+ hold *pointergestures.ZwpPointerGestureHoldV1
23+}
24+
25+func (g *gestureObject) alive() bool {
26+ switch {
27+ case g.swipe != nil:
28+ return g.swipe.Resource.Alive()
29+ case g.pinch != nil:
30+ return g.pinch.Resource.Alive()
31+ case g.hold != nil:
32+ return g.hold.Resource.Alive()
33+ }
34+ return false
35+}
36+
37+func (g *gestureObject) kind() ports.GestureKind {
38+ switch {
39+ case g.pinch != nil:
40+ return ports.GesturePinch
41+ case g.hold != nil:
42+ return ports.GestureHold
43+ }
44+ return ports.GestureSwipe
45+}
46+
47+func (g *gestureObject) begin(serial uint32, at uint32, surf *wayland.Surface, fingers uint32) {
48+ switch {
49+ case g.swipe != nil:
50+ g.swipe.SendBegin(serial, at, surf, fingers)
51+ case g.pinch != nil:
52+ g.pinch.SendBegin(serial, at, surf, fingers)
53+ case g.hold != nil:
54+ g.hold.SendBegin(serial, at, surf, fingers)
55+ }
56+}
57+
58+func (g *gestureObject) update(u ports.GestureUpdateTo) {
59+ at := wireMsec(u.Time)
60+ dx, dy := server.FixedFromFloat(u.DX), server.FixedFromFloat(u.DY)
61+ switch {
62+ case g.swipe != nil:
63+ g.swipe.SendUpdate(at, dx, dy)
64+ case g.pinch != nil:
65+ g.pinch.SendUpdate(at, dx, dy, server.FixedFromFloat(u.Scale), server.FixedFromFloat(u.Rotation))
66+ }
67+}
68+
69+func (g *gestureObject) end(serial uint32, at uint32, cancelled int32) {
70+ switch {
71+ case g.swipe != nil:
72+ g.swipe.SendEnd(serial, at, cancelled)
73+ case g.pinch != nil:
74+ g.pinch.SendEnd(serial, at, cancelled)
75+ case g.hold != nil:
76+ g.hold.SendEnd(serial, at, cancelled)
77+ }
78+}
79+
80+// activeGesture is the gesture sent to a window: the objects that got its
81+// begin are the ones that get the rest, even when a client makes more
82+// meanwhile.
83+type activeGesture struct {
84+ id ports.WindowID
85+ kind ports.GestureKind
86+ objs []*gestureObject
87+ // at is the latest device time sent, for a cancellation.
88+ at time.Duration
89+}
90+
91+func registerPointerGestures(d *server.Display, s *Server) error {
92+ return pointergestures.NewZwpPointerGesturesV1Global(d, 3, func(c server.Client, v, id uint32) {
93+ _, _ = pointergestures.NewZwpPointerGesturesV1(c, int32(v), id, gestureManager{s})
94+ })
95+}
96+
97+// gestureManager is zwp_pointer_gestures_v1. Releasing it leaves the
98+// gesture objects it made alive, as the protocol says.
99+type gestureManager struct{ server *Server }
100+
101+func (gestureManager) Release(*pointergestures.ZwpPointerGesturesV1) {}
102+
103+// gestureDestroy handles the destroy request of the three gesture objects;
104+// the resource's OnDestroy forgets the object.
105+type gestureDestroy struct{}
106+
107+func (gestureDestroy) Destroy(*pointergestures.ZwpPointerGestureSwipeV1) {}
108+
109+type pinchDestroy struct{}
110+
111+func (pinchDestroy) Destroy(*pointergestures.ZwpPointerGesturePinchV1) {}
112+
113+type holdDestroy struct{}
114+
115+func (holdDestroy) Destroy(*pointergestures.ZwpPointerGestureHoldV1) {}
116+
117+func (m gestureManager) GetSwipeGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
118+ if res, err := pointergestures.NewZwpPointerGestureSwipeV1(r.Client(), r.Version(), id, gestureDestroy{}); err == nil {
119+ m.server.addGesture(r.Client(), &gestureObject{swipe: res}, res.Resource)
120+ }
121+}
122+
123+func (m gestureManager) GetPinchGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
124+ if res, err := pointergestures.NewZwpPointerGesturePinchV1(r.Client(), r.Version(), id, pinchDestroy{}); err == nil {
125+ m.server.addGesture(r.Client(), &gestureObject{pinch: res}, res.Resource)
126+ }
127+}
128+
129+func (m gestureManager) GetHoldGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
130+ if res, err := pointergestures.NewZwpPointerGestureHoldV1(r.Client(), r.Version(), id, holdDestroy{}); err == nil {
131+ m.server.addGesture(r.Client(), &gestureObject{hold: res}, res.Resource)
132+ }
133+}
134+
135+// addGesture lists a client's gesture object until its resource dies.
136+func (s *Server) addGesture(c server.Client, o *gestureObject, res *server.Resource) {
137+ s.gestures[c] = append(s.gestures[c], o)
138+ res.OnDestroy = func() {
139+ if list := removeItem(s.gestures[c], o); len(list) > 0 {
140+ s.gestures[c] = list
141+ } else {
142+ delete(s.gestures, c)
143+ }
144+ }
145+}
146+
147+// beginGesture sends the begin of a gesture to the objects of the kind that
148+// the client of the pointer-focus window made. A gesture still running
149+// ends cancelled first. Nothing is recorded when the client has no object.
150+func (s *Server) beginGesture(c ports.GestureBeginTo) {
151+ s.cancelGesture()
152+ if c.ID != s.seat.pointerFocus || c.Fingers <= 0 {
153+ return
154+ }
155+ surf, _, _, _ := s.pointerSurface(c.ID, 0, 0)
156+ if surf == nil {
157+ return
158+ }
159+ var objs []*gestureObject
160+ for _, o := range s.gestures[surf.Client()] {
161+ if o.kind() == c.Kind && o.alive() {
162+ objs = append(objs, o)
163+ }
164+ }
165+ if len(objs) == 0 {
166+ return
167+ }
168+ s.seat.gesture = &activeGesture{id: c.ID, kind: c.Kind, objs: objs, at: c.Time}
169+ s.serial++
170+ for _, o := range objs {
171+ o.begin(s.serial, wireMsec(c.Time), surf, uint32(c.Fingers))
172+ }
173+}
174+
175+// updateGesture moves the running gesture of the window.
176+func (s *Server) updateGesture(c ports.GestureUpdateTo) {
177+ g := s.seat.gesture
178+ if g == nil || g.id != c.ID || g.kind != c.Kind {
179+ return
180+ }
181+ g.at = c.Time
182+ for _, o := range g.objs {
183+ if o.alive() {
184+ o.update(c)
185+ }
186+ }
187+}
188+
189+// endGesture ends the running gesture of the window.
190+func (s *Server) endGesture(c ports.GestureEndTo) {
191+ g := s.seat.gesture
192+ if g == nil || g.id != c.ID || g.kind != c.Kind {
193+ return
194+ }
195+ g.at = c.Time
196+ s.finishGesture(c.Cancelled)
197+}
198+
199+// cancelGesture ends the running gesture, if any, as cancelled: the
200+// pointer left its window or the window went.
201+func (s *Server) cancelGesture() {
202+ if s.seat.gesture != nil {
203+ s.finishGesture(true)
204+ }
205+}
206+
207+func (s *Server) finishGesture(cancelled bool) {
208+ g := s.seat.gesture
209+ s.seat.gesture = nil
210+ state := int32(0)
211+ if cancelled {
212+ state = 1
213+ }
214+ s.serial++
215+ for _, o := range g.objs {
216+ if o.alive() {
217+ o.end(s.serial, wireMsec(g.at), state)
218+ }
219+ }
220+}
A internal/adapters/wayland/gestures_test.go +180 -0
@@ -0,0 +1,180 @@
1+package wayland
2+
3+import (
4+ "slices"
5+ "testing"
6+
7+ "github.com/bnema/go-wayland-bindings/server/pointergestures"
8+ "github.com/bnema/go-wayland-bindings/server/wayland"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/bnema/wlturbo"
11+)
12+
13+// gestureLog records the events of a zwp_pointer_gesture_*_v1 object as
14+// floats: the opcode, then its arguments (fixed values as they are).
15+type gestureLog struct {
16+ wlturbo.BaseProxy
17+ kind ports.GestureKind
18+ events chan []float64
19+}
20+
21+func (p *gestureLog) Dispatch(e *wlturbo.Event) {
22+ ev := []float64{float64(e.Opcode)}
23+ // Hold has no update: its end is opcode 1.
24+ op := e.Opcode
25+ if p.kind == ports.GestureHold && op == 1 {
26+ op = uint16(pointergestures.ZwpPointerGestureSwipeV1EventEnd)
27+ }
28+ switch op {
29+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventBegin):
30+ // serial, time, surface, fingers
31+ for range 4 {
32+ ev = append(ev, float64(e.Uint32()))
33+ }
34+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventUpdate):
35+ // time, then dx and dy (and scale and rotation of a pinch)
36+ fixed := 2
37+ if p.kind == ports.GesturePinch {
38+ fixed = 4
39+ }
40+ ev = append(ev, float64(e.Uint32()))
41+ for range fixed {
42+ ev = append(ev, e.Fixed().Float64())
43+ }
44+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventEnd):
45+ ev = append(ev, float64(e.Uint32()), float64(e.Uint32()), float64(e.Int32()))
46+ }
47+ p.events <- ev
48+}
49+
50+func gestureObjects(t *testing.T, c *wlturbo.Display, manager uint32) (swipe, pinch, hold chan []float64) {
51+ t.Helper()
52+ seat := bindProtocol(t, c, "wl_seat")
53+ registerProtocol(t, c, seat)
54+ pointer := c.AllocateID()
55+ registerProtocol(t, c, pointer)
56+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
57+ var logs [3]chan []float64
58+ for i, op := range []uint32{pointergestures.ZwpPointerGesturesV1RequestGetSwipeGesture, pointergestures.ZwpPointerGesturesV1RequestGetPinchGesture, pointergestures.ZwpPointerGesturesV1RequestGetHoldGesture} {
59+ id := c.AllocateID()
60+ p := &gestureLog{kind: ports.GestureKind(i), events: make(chan []float64, 16)}
61+ p.SetID(id)
62+ registerWireProxy(c, p)
63+ requestProtocol(t, c, manager, op, id, pointer)
64+ logs[i] = p.events
65+ }
66+ return logs[0], logs[1], logs[2]
67+}
68+
69+// quiet fails when the log got an event after a roundtrip.
70+func quiet(t *testing.T, c *wlturbo.Display, events chan []float64) {
71+ t.Helper()
72+ if err := c.Roundtrip(); err != nil {
73+ t.Fatal(err)
74+ }
75+ select {
76+ case ev := <-events:
77+ t.Fatalf("unexpected event %v", ev)
78+ default:
79+ }
80+}
81+
82+func TestPointerGestures(t *testing.T) {
83+ s, events, commands, dir := lifecycleServer(t)
84+ c := protocolClient(t, s, dir)
85+ manager := bindVersion(t, c, "zwp_pointer_gestures_v1", 3)
86+ swipe, pinch, hold := gestureObjects(t, c, manager)
87+ w, surf, _ := surfaceMapper(t, c, events)()
88+ // Another client with gestures never has the pointer.
89+ other := protocolClient(t, s, dir)
90+ oSwipe, oPinch, oHold := gestureObjects(t, other, bindVersion(t, other, "zwp_pointer_gestures_v1", 3))
91+
92+ // No gesture reaches a client before it has the pointer.
93+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2}
94+ commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 10}
95+ quiet(t, c, pinch)
96+
97+ // A pinch: begin carries the serial, time, surface and fingers; updates
98+ // the centre, scale and rotation; the end a later serial.
99+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2, Time: 1000 * 1e6}
100+ begin := next(t, c, pinch)
101+ if begin[0] != 0 || begin[2] != 1000 || begin[3] != float64(surf) || begin[4] != 2 {
102+ t.Fatalf("pinch begin %v", begin)
103+ }
104+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, DX: 1.5, DY: -2, Scale: 1.25, Rotation: 10, Time: 1008 * 1e6}
105+ if ev := next(t, c, pinch); !slices.Equal(ev, []float64{1, 1008, 1.5, -2, 1.25, 10}) {
106+ t.Fatalf("pinch update %v", ev)
107+ }
108+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch, Time: 1016 * 1e6}
109+ end := next(t, c, pinch)
110+ if end[0] != 2 || end[1] <= begin[1] || end[2] != 1016 || end[3] != 0 {
111+ t.Fatalf("pinch end %v (begin %v)", end, begin)
112+ }
113+
114+ // A swipe and a hold, the hold ended cancelled.
115+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureSwipe, Fingers: 5, Time: 2000 * 1e6}
116+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GestureSwipe, DX: 3, DY: 4, Time: 2008 * 1e6}
117+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GestureSwipe, Time: 2016 * 1e6}
118+ if ev := next(t, c, swipe); ev[0] != 0 || ev[4] != 5 {
119+ t.Fatalf("swipe begin %v", ev)
120+ }
121+ if ev := next(t, c, swipe); !slices.Equal(ev, []float64{1, 2008, 3, 4}) {
122+ t.Fatalf("swipe update %v", ev)
123+ }
124+ if ev := next(t, c, swipe); ev[0] != 2 || ev[3] != 0 {
125+ t.Fatalf("swipe end %v", ev)
126+ }
127+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureHold, Fingers: 3, Time: 3000 * 1e6}
128+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GestureHold, Cancelled: true, Time: 3008 * 1e6}
129+ if ev := next(t, c, hold); ev[0] != 0 || ev[4] != 3 {
130+ t.Fatalf("hold begin %v", ev)
131+ }
132+ if ev := next(t, c, hold); ev[0] != 1 || ev[3] != 1 {
133+ t.Fatalf("hold end %v", ev)
134+ }
135+
136+ // A gesture of one kind does not reach the objects of the others, and an
137+ // update of a gesture that is not running is dropped.
138+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, Scale: 2}
139+ quiet(t, c, pinch)
140+ quiet(t, c, swipe)
141+ quiet(t, c, hold)
142+
143+ // The pointer leaving the window cancels the gesture.
144+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2, Time: 4000 * 1e6}
145+ next(t, c, pinch)
146+ commands <- ports.PointerFocus{}
147+ if ev := next(t, c, pinch); ev[0] != 2 || ev[3] != 1 {
148+ t.Fatalf("pinch not cancelled on leave: %v", ev)
149+ }
150+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch, Time: 4008 * 1e6}
151+ quiet(t, c, pinch)
152+
153+ // Releasing the manager keeps its objects.
154+ commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 10}
155+ requestProtocol(t, c, manager, pointergestures.ZwpPointerGesturesV1RequestRelease)
156+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureHold, Fingers: 4, Time: 5000 * 1e6}
157+ if ev := next(t, c, hold); ev[0] != 0 || ev[4] != 4 {
158+ t.Fatalf("hold begin after release %v", ev)
159+ }
160+
161+ // The client without the pointer saw nothing.
162+ quiet(t, other, oSwipe)
163+ quiet(t, other, oPinch)
164+ quiet(t, other, oHold)
165+}
166+
167+// A window with the pointer but no gesture object gets nothing, and a
168+// gesture on a window that does not have the pointer is refused.
169+func TestPointerGesturesWithoutObjects(t *testing.T) {
170+ s, events, commands, dir := lifecycleServer(t)
171+ c := protocolClient(t, s, dir)
172+ w, _, _ := surfaceMapper(t, c, events)()
173+ commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1}
174+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2}
175+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, Scale: 2}
176+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch}
177+ if err := c.Roundtrip(); err != nil {
178+ t.Fatal(err)
179+ }
180+}
@@ -0,0 +1,180 @@
1+package wayland
2+
3+import (
4+ "slices"
5+ "testing"
6+
7+ "github.com/bnema/go-wayland-bindings/server/pointergestures"
8+ "github.com/bnema/go-wayland-bindings/server/wayland"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/bnema/wlturbo"
11+)
12+
13+// gestureLog records the events of a zwp_pointer_gesture_*_v1 object as
14+// floats: the opcode, then its arguments (fixed values as they are).
15+type gestureLog struct {
16+ wlturbo.BaseProxy
17+ kind ports.GestureKind
18+ events chan []float64
19+}
20+
21+func (p *gestureLog) Dispatch(e *wlturbo.Event) {
22+ ev := []float64{float64(e.Opcode)}
23+ // Hold has no update: its end is opcode 1.
24+ op := e.Opcode
25+ if p.kind == ports.GestureHold && op == 1 {
26+ op = uint16(pointergestures.ZwpPointerGestureSwipeV1EventEnd)
27+ }
28+ switch op {
29+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventBegin):
30+ // serial, time, surface, fingers
31+ for range 4 {
32+ ev = append(ev, float64(e.Uint32()))
33+ }
34+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventUpdate):
35+ // time, then dx and dy (and scale and rotation of a pinch)
36+ fixed := 2
37+ if p.kind == ports.GesturePinch {
38+ fixed = 4
39+ }
40+ ev = append(ev, float64(e.Uint32()))
41+ for range fixed {
42+ ev = append(ev, e.Fixed().Float64())
43+ }
44+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventEnd):
45+ ev = append(ev, float64(e.Uint32()), float64(e.Uint32()), float64(e.Int32()))
46+ }
47+ p.events <- ev
48+}
49+
50+func gestureObjects(t *testing.T, c *wlturbo.Display, manager uint32) (swipe, pinch, hold chan []float64) {
51+ t.Helper()
52+ seat := bindProtocol(t, c, "wl_seat")
53+ registerProtocol(t, c, seat)
54+ pointer := c.AllocateID()
55+ registerProtocol(t, c, pointer)
56+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
57+ var logs [3]chan []float64
58+ for i, op := range []uint32{pointergestures.ZwpPointerGesturesV1RequestGetSwipeGesture, pointergestures.ZwpPointerGesturesV1RequestGetPinchGesture, pointergestures.ZwpPointerGesturesV1RequestGetHoldGesture} {
59+ id := c.AllocateID()
60+ p := &gestureLog{kind: ports.GestureKind(i), events: make(chan []float64, 16)}
61+ p.SetID(id)
62+ registerWireProxy(c, p)
63+ requestProtocol(t, c, manager, op, id, pointer)
64+ logs[i] = p.events
65+ }
66+ return logs[0], logs[1], logs[2]
67+}
68+
69+// quiet fails when the log got an event after a roundtrip.
70+func quiet(t *testing.T, c *wlturbo.Display, events chan []float64) {
71+ t.Helper()
72+ if err := c.Roundtrip(); err != nil {
73+ t.Fatal(err)
74+ }
75+ select {
76+ case ev := <-events:
77+ t.Fatalf("unexpected event %v", ev)
78+ default:
79+ }
80+}
81+
82+func TestPointerGestures(t *testing.T) {
83+ s, events, commands, dir := lifecycleServer(t)
84+ c := protocolClient(t, s, dir)
85+ manager := bindVersion(t, c, "zwp_pointer_gestures_v1", 3)
86+ swipe, pinch, hold := gestureObjects(t, c, manager)
87+ w, surf, _ := surfaceMapper(t, c, events)()
88+ // Another client with gestures never has the pointer.
89+ other := protocolClient(t, s, dir)
90+ oSwipe, oPinch, oHold := gestureObjects(t, other, bindVersion(t, other, "zwp_pointer_gestures_v1", 3))
91+
92+ // No gesture reaches a client before it has the pointer.
93+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2}
94+ commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 10}
95+ quiet(t, c, pinch)
96+
97+ // A pinch: begin carries the serial, time, surface and fingers; updates
98+ // the centre, scale and rotation; the end a later serial.
99+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2, Time: 1000 * 1e6}
100+ begin := next(t, c, pinch)
101+ if begin[0] != 0 || begin[2] != 1000 || begin[3] != float64(surf) || begin[4] != 2 {
102+ t.Fatalf("pinch begin %v", begin)
103+ }
104+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, DX: 1.5, DY: -2, Scale: 1.25, Rotation: 10, Time: 1008 * 1e6}
105+ if ev := next(t, c, pinch); !slices.Equal(ev, []float64{1, 1008, 1.5, -2, 1.25, 10}) {
106+ t.Fatalf("pinch update %v", ev)
107+ }
108+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch, Time: 1016 * 1e6}
109+ end := next(t, c, pinch)
110+ if end[0] != 2 || end[1] <= begin[1] || end[2] != 1016 || end[3] != 0 {
111+ t.Fatalf("pinch end %v (begin %v)", end, begin)
112+ }
113+
114+ // A swipe and a hold, the hold ended cancelled.
115+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureSwipe, Fingers: 5, Time: 2000 * 1e6}
116+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GestureSwipe, DX: 3, DY: 4, Time: 2008 * 1e6}
117+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GestureSwipe, Time: 2016 * 1e6}
118+ if ev := next(t, c, swipe); ev[0] != 0 || ev[4] != 5 {
119+ t.Fatalf("swipe begin %v", ev)
120+ }
121+ if ev := next(t, c, swipe); !slices.Equal(ev, []float64{1, 2008, 3, 4}) {
122+ t.Fatalf("swipe update %v", ev)
123+ }
124+ if ev := next(t, c, swipe); ev[0] != 2 || ev[3] != 0 {
125+ t.Fatalf("swipe end %v", ev)
126+ }
127+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureHold, Fingers: 3, Time: 3000 * 1e6}
128+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GestureHold, Cancelled: true, Time: 3008 * 1e6}
129+ if ev := next(t, c, hold); ev[0] != 0 || ev[4] != 3 {
130+ t.Fatalf("hold begin %v", ev)
131+ }
132+ if ev := next(t, c, hold); ev[0] != 1 || ev[3] != 1 {
133+ t.Fatalf("hold end %v", ev)
134+ }
135+
136+ // A gesture of one kind does not reach the objects of the others, and an
137+ // update of a gesture that is not running is dropped.
138+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, Scale: 2}
139+ quiet(t, c, pinch)
140+ quiet(t, c, swipe)
141+ quiet(t, c, hold)
142+
143+ // The pointer leaving the window cancels the gesture.
144+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2, Time: 4000 * 1e6}
145+ next(t, c, pinch)
146+ commands <- ports.PointerFocus{}
147+ if ev := next(t, c, pinch); ev[0] != 2 || ev[3] != 1 {
148+ t.Fatalf("pinch not cancelled on leave: %v", ev)
149+ }
150+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch, Time: 4008 * 1e6}
151+ quiet(t, c, pinch)
152+
153+ // Releasing the manager keeps its objects.
154+ commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 10}
155+ requestProtocol(t, c, manager, pointergestures.ZwpPointerGesturesV1RequestRelease)
156+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureHold, Fingers: 4, Time: 5000 * 1e6}
157+ if ev := next(t, c, hold); ev[0] != 0 || ev[4] != 4 {
158+ t.Fatalf("hold begin after release %v", ev)
159+ }
160+
161+ // The client without the pointer saw nothing.
162+ quiet(t, other, oSwipe)
163+ quiet(t, other, oPinch)
164+ quiet(t, other, oHold)
165+}
166+
167+// A window with the pointer but no gesture object gets nothing, and a
168+// gesture on a window that does not have the pointer is refused.
169+func TestPointerGesturesWithoutObjects(t *testing.T) {
170+ s, events, commands, dir := lifecycleServer(t)
171+ c := protocolClient(t, s, dir)
172+ w, _, _ := surfaceMapper(t, c, events)()
173+ commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1}
174+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2}
175+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, Scale: 2}
176+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch}
177+ if err := c.Roundtrip(); err != nil {
178+ t.Fatal(err)
179+ }
180+}
M internal/adapters/wayland/globals.go +1 -0
@@ -33,6 +33,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
3333 func() error { return registerDMABuf(d, s, o.DMABuf) },
3434 func() error { return registerSinglePixelBuffer(d, s) },
3535 func() error { return registerPointerConstraints(d, s) },
36+ func() error { return registerPointerGestures(d, s) },
3637 func() error { return registerTearing(d, s) },
3738 func() error { return registerColorManagement(d, s) },
3839 func() error { return registerColorRepresentation(d, s) },
@@ -33,6 +33,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
33 func() error { return registerDMABuf(d, s, o.DMABuf) },33 func() error { return registerDMABuf(d, s, o.DMABuf) },
34 func() error { return registerSinglePixelBuffer(d, s) },34 func() error { return registerSinglePixelBuffer(d, s) },
35 func() error { return registerPointerConstraints(d, s) },35 func() error { return registerPointerConstraints(d, s) },
36+ func() error { return registerPointerGestures(d, s) },
36 func() error { return registerTearing(d, s) },37 func() error { return registerTearing(d, s) },
37 func() error { return registerColorManagement(d, s) },38 func() error { return registerColorManagement(d, s) },
38 func() error { return registerColorRepresentation(d, s) },39 func() error { return registerColorRepresentation(d, s) },
M internal/adapters/wayland/seat.go +17 -1
@@ -23,7 +23,9 @@ type seatState struct {
2323 pointerX, pointerY float64
2424 // enters holds the serial of the enter event each wl_pointer received
2525 // for pointerFocus; a pointer warp must name its pointer's serial.
26- enters map[*server.Resource]uint32
26+ enters map[*server.Resource]uint32
27+ // gesture is the touchpad gesture sent to pointerFocus's client.
28+ gesture *activeGesture
2729 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
2830 wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
2931 pointers map[server.Client][]*wayland.Pointer
@@ -64,6 +66,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
6466 target = c.ID
6567 case ports.PointerAxisTo:
6668 target = c.ID
69+ case ports.GestureBeginTo:
70+ target = c.ID
71+ case ports.GestureUpdateTo:
72+ target = c.ID
73+ case ports.GestureEndTo:
74+ target = c.ID
6775 case ports.FocusWindow:
6876 target = c.ID
6977 case ports.ForwardKey:
@@ -142,6 +150,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
142150 for _, p := range pointers {
143151 s.sendAxis(p, c.Axis, steps, values)
144152 }
153+ case ports.GestureBeginTo:
154+ s.beginGesture(c)
155+ case ports.GestureUpdateTo:
156+ s.updateGesture(c)
157+ case ports.GestureEndTo:
158+ s.endGesture(c)
145159 case ports.SetKeymap:
146160 s.setKeymap(c)
147161 case ports.FocusWindow:
@@ -486,6 +500,8 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
486500 return
487501 }
488502 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
503+ // A gesture does not follow the pointer to another surface.
504+ s.cancelGesture()
489505 // The new client sets its own cursor on enter; until then, the arrow.
490506 s.cursorSurface = nil
491507 s.setCursor(ports.CursorChange{})
@@ -23,7 +23,9 @@ type seatState struct {
23 pointerX, pointerY float6423 pointerX, pointerY float64
24 // enters holds the serial of the enter event each wl_pointer received24 // enters holds the serial of the enter event each wl_pointer received
25 // for pointerFocus; a pointer warp must name its pointer's serial.25 // for pointerFocus; a pointer warp must name its pointer's serial.
26- enters map[*server.Resource]uint3226+ enters map[*server.Resource]uint32
27+ // gesture is the touchpad gesture sent to pointerFocus's client.
28+ gesture *activeGesture
27 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis29 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
28 wheelHeld [2]float64 // axis value held back with it for pre-v8 clients30 wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
29 pointers map[server.Client][]*wayland.Pointer31 pointers map[server.Client][]*wayland.Pointer
@@ -64,6 +66,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
64 target = c.ID66 target = c.ID
65 case ports.PointerAxisTo:67 case ports.PointerAxisTo:
66 target = c.ID68 target = c.ID
69+ case ports.GestureBeginTo:
70+ target = c.ID
71+ case ports.GestureUpdateTo:
72+ target = c.ID
73+ case ports.GestureEndTo:
74+ target = c.ID
67 case ports.FocusWindow:75 case ports.FocusWindow:
68 target = c.ID76 target = c.ID
69 case ports.ForwardKey:77 case ports.ForwardKey:
@@ -142,6 +150,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
142 for _, p := range pointers {150 for _, p := range pointers {
143 s.sendAxis(p, c.Axis, steps, values)151 s.sendAxis(p, c.Axis, steps, values)
144 }152 }
153+ case ports.GestureBeginTo:
154+ s.beginGesture(c)
155+ case ports.GestureUpdateTo:
156+ s.updateGesture(c)
157+ case ports.GestureEndTo:
158+ s.endGesture(c)
145 case ports.SetKeymap:159 case ports.SetKeymap:
146 s.setKeymap(c)160 s.setKeymap(c)
147 case ports.FocusWindow:161 case ports.FocusWindow:
@@ -486,6 +500,8 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
486 return500 return
487 }501 }
488 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}502 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
503+ // A gesture does not follow the pointer to another surface.
504+ s.cancelGesture()
489 // The new client sets its own cursor on enter; until then, the arrow.505 // The new client sets its own cursor on enter; until then, the arrow.
490 s.cursorSurface = nil506 s.cursorSurface = nil
491 s.setCursor(ports.CursorChange{})507 s.setCursor(ports.CursorChange{})
M internal/adapters/wayland/server.go +7 -4
@@ -256,8 +256,10 @@ type Server struct {
256256 clipDevices []*clipDevice
257257 dataSources, primarySources, controlSources map[*server.Resource]*clipSource
258258 // Pointer constraints (constraints.go).
259- regions map[*server.Resource]*region
260- relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
259+ regions map[*server.Resource]*region
260+ relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
261+ // gestures are each client's zwp_pointer_gestures_v1 objects (gestures.go).
262+ gestures map[server.Client][]*gestureObject
261263 constraints map[*surface]*constraint
262264 constraint *constraint // the active one
263265 positioners map[*server.Resource]*positioner
@@ -330,6 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
330332 contentNotify: make(chan struct{}),
331333 regions: map[*server.Resource]*region{},
332334 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
335+ gestures: map[server.Client][]*gestureObject{},
333336 constraints: map[*surface]*constraint{},
334337 positioners: map[*server.Resource]*positioner{},
335338 seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
@@ -737,12 +740,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
737740 // Only input/focus commands require an owner envelope. Config relay
738741 // also produces SetKeymap and other epoch-independent commands.
739742 switch cmd.(type) {
740- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.FocusWindow, ports.ForwardKey:
743+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey:
741744 return
742745 }
743746 }
744747 switch c := cmd.(type) {
745- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
748+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
746749 s.applyInput(c)
747750 case ports.ConfigurePopup:
748751 if w := s.windows[c.ID]; w != nil && w.popup != nil {
@@ -256,8 +256,10 @@ type Server struct {
256 clipDevices []*clipDevice256 clipDevices []*clipDevice
257 dataSources, primarySources, controlSources map[*server.Resource]*clipSource257 dataSources, primarySources, controlSources map[*server.Resource]*clipSource
258 // Pointer constraints (constraints.go).258 // Pointer constraints (constraints.go).
259- regions map[*server.Resource]*region259+ regions map[*server.Resource]*region
260- relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1260+ relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
261+ // gestures are each client's zwp_pointer_gestures_v1 objects (gestures.go).
262+ gestures map[server.Client][]*gestureObject
261 constraints map[*surface]*constraint263 constraints map[*surface]*constraint
262 constraint *constraint // the active one264 constraint *constraint // the active one
263 positioners map[*server.Resource]*positioner265 positioners map[*server.Resource]*positioner
@@ -330,6 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
330 contentNotify: make(chan struct{}),332 contentNotify: make(chan struct{}),
331 regions: map[*server.Resource]*region{},333 regions: map[*server.Resource]*region{},
332 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},334 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
335+ gestures: map[server.Client][]*gestureObject{},
333 constraints: map[*surface]*constraint{},336 constraints: map[*surface]*constraint{},
334 positioners: map[*server.Resource]*positioner{},337 positioners: map[*server.Resource]*positioner{},
335 seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},338 seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
@@ -737,12 +740,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
737 // Only input/focus commands require an owner envelope. Config relay740 // Only input/focus commands require an owner envelope. Config relay
738 // also produces SetKeymap and other epoch-independent commands.741 // also produces SetKeymap and other epoch-independent commands.
739 switch cmd.(type) {742 switch cmd.(type) {
740- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.FocusWindow, ports.ForwardKey:743+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey:
741 return744 return
742 }745 }
743 }746 }
744 switch c := cmd.(type) {747 switch c := cmd.(type) {
745- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:748+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
746 s.applyInput(c)749 s.applyInput(c)
747 case ports.ConfigurePopup:750 case ports.ConfigurePopup:
748 if w := s.windows[c.ID]; w != nil && w.popup != nil {751 if w := s.windows[c.ID]; w != nil && w.popup != nil {
M internal/adapters/wayland/wire_client_test.go +5 -0
@@ -32,6 +32,7 @@ import (
3232 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
3333 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"
3434 wire_pointerconstraints "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
35+ wire_pointergestures "github.com/bnema/go-wayland-bindings/server/pointergestures"
3536 wire_pointerwarp "github.com/bnema/go-wayland-bindings/server/pointerwarp"
3637 wire_presentationtime "github.com/bnema/go-wayland-bindings/server/presentationtime"
3738 wire_primaryselection "github.com/bnema/go-wayland-bindings/server/primaryselection"
@@ -239,6 +240,10 @@ var wireSchemas = map[string]wireSchema{
239240 "zwp_primary_selection_device_v1": {wire_primaryselection.ZwpPrimarySelectionDeviceV1Interface, &client_primaryselection.PrimarySelectionDevice{}, map[uint16]bool{1: true}, nil},
240241 "zwp_primary_selection_offer_v1": {wire_primaryselection.ZwpPrimarySelectionOfferV1Interface, &client_primaryselection.PrimarySelectionOffer{}, map[uint16]bool{1: true}, nil},
241242 "zwp_primary_selection_source_v1": {wire_primaryselection.ZwpPrimarySelectionSourceV1Interface, &client_primaryselection.PrimarySelectionSource{}, map[uint16]bool{1: true}, nil},
243+ "zwp_pointer_gestures_v1": {wire_pointergestures.ZwpPointerGesturesV1Interface, nil, map[uint16]bool{2: true}, nil},
244+ "zwp_pointer_gesture_swipe_v1": {wire_pointergestures.ZwpPointerGestureSwipeV1Interface, nil, map[uint16]bool{0: true}, nil},
245+ "zwp_pointer_gesture_pinch_v1": {wire_pointergestures.ZwpPointerGesturePinchV1Interface, nil, map[uint16]bool{0: true}, nil},
246+ "zwp_pointer_gesture_hold_v1": {wire_pointergestures.ZwpPointerGestureHoldV1Interface, nil, map[uint16]bool{0: true}, nil},
242247 "zwp_relative_pointer_manager_v1": {wire_relativepointer.ZwpRelativePointerManagerV1Interface, &client_relativepointer.RelativePointerManager{}, map[uint16]bool{0: true}, nil},
243248 "zwp_relative_pointer_v1": {wire_relativepointer.ZwpRelativePointerV1Interface, &client_relativepointer.RelativePointer{}, map[uint16]bool{0: true}, nil},
244249 "zwp_text_input_manager_v3": {wire_textinput.ZwpTextInputManagerV3Interface, &client_textinput.TextInputManagerV3{}, map[uint16]bool{0: true}, nil},
@@ -32,6 +32,7 @@ import (
32 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"32 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
33 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"33 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"
34 wire_pointerconstraints "github.com/bnema/go-wayland-bindings/server/pointerconstraints"34 wire_pointerconstraints "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
35+ wire_pointergestures "github.com/bnema/go-wayland-bindings/server/pointergestures"
35 wire_pointerwarp "github.com/bnema/go-wayland-bindings/server/pointerwarp"36 wire_pointerwarp "github.com/bnema/go-wayland-bindings/server/pointerwarp"
36 wire_presentationtime "github.com/bnema/go-wayland-bindings/server/presentationtime"37 wire_presentationtime "github.com/bnema/go-wayland-bindings/server/presentationtime"
37 wire_primaryselection "github.com/bnema/go-wayland-bindings/server/primaryselection"38 wire_primaryselection "github.com/bnema/go-wayland-bindings/server/primaryselection"
@@ -239,6 +240,10 @@ var wireSchemas = map[string]wireSchema{
239 "zwp_primary_selection_device_v1": {wire_primaryselection.ZwpPrimarySelectionDeviceV1Interface, &client_primaryselection.PrimarySelectionDevice{}, map[uint16]bool{1: true}, nil},240 "zwp_primary_selection_device_v1": {wire_primaryselection.ZwpPrimarySelectionDeviceV1Interface, &client_primaryselection.PrimarySelectionDevice{}, map[uint16]bool{1: true}, nil},
240 "zwp_primary_selection_offer_v1": {wire_primaryselection.ZwpPrimarySelectionOfferV1Interface, &client_primaryselection.PrimarySelectionOffer{}, map[uint16]bool{1: true}, nil},241 "zwp_primary_selection_offer_v1": {wire_primaryselection.ZwpPrimarySelectionOfferV1Interface, &client_primaryselection.PrimarySelectionOffer{}, map[uint16]bool{1: true}, nil},
241 "zwp_primary_selection_source_v1": {wire_primaryselection.ZwpPrimarySelectionSourceV1Interface, &client_primaryselection.PrimarySelectionSource{}, map[uint16]bool{1: true}, nil},242 "zwp_primary_selection_source_v1": {wire_primaryselection.ZwpPrimarySelectionSourceV1Interface, &client_primaryselection.PrimarySelectionSource{}, map[uint16]bool{1: true}, nil},
243+ "zwp_pointer_gestures_v1": {wire_pointergestures.ZwpPointerGesturesV1Interface, nil, map[uint16]bool{2: true}, nil},
244+ "zwp_pointer_gesture_swipe_v1": {wire_pointergestures.ZwpPointerGestureSwipeV1Interface, nil, map[uint16]bool{0: true}, nil},
245+ "zwp_pointer_gesture_pinch_v1": {wire_pointergestures.ZwpPointerGesturePinchV1Interface, nil, map[uint16]bool{0: true}, nil},
246+ "zwp_pointer_gesture_hold_v1": {wire_pointergestures.ZwpPointerGestureHoldV1Interface, nil, map[uint16]bool{0: true}, nil},
242 "zwp_relative_pointer_manager_v1": {wire_relativepointer.ZwpRelativePointerManagerV1Interface, &client_relativepointer.RelativePointerManager{}, map[uint16]bool{0: true}, nil},247 "zwp_relative_pointer_manager_v1": {wire_relativepointer.ZwpRelativePointerManagerV1Interface, &client_relativepointer.RelativePointerManager{}, map[uint16]bool{0: true}, nil},
243 "zwp_relative_pointer_v1": {wire_relativepointer.ZwpRelativePointerV1Interface, &client_relativepointer.RelativePointer{}, map[uint16]bool{0: true}, nil},248 "zwp_relative_pointer_v1": {wire_relativepointer.ZwpRelativePointerV1Interface, &client_relativepointer.RelativePointer{}, map[uint16]bool{0: true}, nil},
244 "zwp_text_input_manager_v3": {wire_textinput.ZwpTextInputManagerV3Interface, &client_textinput.TextInputManagerV3{}, map[uint16]bool{0: true}, nil},249 "zwp_text_input_manager_v3": {wire_textinput.ZwpTextInputManagerV3Interface, &client_textinput.TextInputManagerV3{}, map[uint16]bool{0: true}, nil},
M internal/core/client.go +1 -1
@@ -114,7 +114,7 @@ func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error {
114114 c.releaseSlots()
115115 if c.pointer == v.ID {
116116 c.pointer = 0
117- if err := c.command(ctx, ports.PointerFocus{}); err != nil {
117+ if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
118118 return err
119119 }
120120 }
@@ -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 = 0116 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 err118 return err
119 }119 }
120 }120 }
A internal/core/clientgesture.go +114 -0
@@ -0,0 +1,114 @@
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+ if at == 0 {
77+ // A removed device's end has no time: the client's clock must
78+ // not run backwards.
79+ at = g.at
80+ }
81+ return c.command(ctx, ports.GestureEndTo{ID: g.id, Kind: kind, Cancelled: cancelled || g.id != c.pointer, Time: at})
82+}
83+
84+// gestureAbort ends whatever gesture is forwarded, cancelled.
85+func (c *Core) gestureAbort(ctx context.Context, at time.Duration) error {
86+ if !c.gesture.on {
87+ return nil
88+ }
89+ return c.gestureEnd(ctx, c.gesture.kind, true, at)
90+}
91+
92+// gestureCancel ends the gesture of the window that lost the pointer, as
93+// cancelled. The rest of the device's gesture is swallowed.
94+func (c *Core) gestureCancel(ctx context.Context) error {
95+ g := &c.gesture
96+ if !g.on || g.id == 0 {
97+ return nil
98+ }
99+ id := g.id
100+ g.id = 0
101+ return c.command(ctx, ports.GestureEndTo{ID: id, Kind: g.kind, Cancelled: true, Time: g.at})
102+}
103+
104+// pointerFocus sends a pointer focus change; the window leaving the
105+// pointer first gets its gesture cancelled, so its client sees the end
106+// before the leave.
107+func (c *Core) pointerFocus(ctx context.Context, f ports.PointerFocus) error {
108+ if c.gesture.id != 0 && c.gesture.id != f.ID {
109+ if err := c.gestureCancel(ctx); err != nil {
110+ return err
111+ }
112+ }
113+ return c.command(ctx, f)
114+}
@@ -0,0 +1,114 @@
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+ if at == 0 {
77+ // A removed device's end has no time: the client's clock must
78+ // not run backwards.
79+ at = g.at
80+ }
81+ return c.command(ctx, ports.GestureEndTo{ID: g.id, Kind: kind, Cancelled: cancelled || g.id != c.pointer, Time: at})
82+}
83+
84+// gestureAbort ends whatever gesture is forwarded, cancelled.
85+func (c *Core) gestureAbort(ctx context.Context, at time.Duration) error {
86+ if !c.gesture.on {
87+ return nil
88+ }
89+ return c.gestureEnd(ctx, c.gesture.kind, true, at)
90+}
91+
92+// gestureCancel ends the gesture of the window that lost the pointer, as
93+// cancelled. The rest of the device's gesture is swallowed.
94+func (c *Core) gestureCancel(ctx context.Context) error {
95+ g := &c.gesture
96+ if !g.on || g.id == 0 {
97+ return nil
98+ }
99+ id := g.id
100+ g.id = 0
101+ return c.command(ctx, ports.GestureEndTo{ID: id, Kind: g.kind, Cancelled: true, Time: g.at})
102+}
103+
104+// pointerFocus sends a pointer focus change; the window leaving the
105+// pointer first gets its gesture cancelled, so its client sees the end
106+// before the leave.
107+func (c *Core) pointerFocus(ctx context.Context, f ports.PointerFocus) error {
108+ if c.gesture.id != 0 && c.gesture.id != f.ID {
109+ if err := c.gestureCancel(ctx); err != nil {
110+ return err
111+ }
112+ }
113+ return c.command(ctx, f)
114+}
A internal/core/clientgesture_test.go +298 -0
@@ -0,0 +1,298 @@
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+// A removed touchpad ends its gesture with no time: the client gets the
79+// last one, so its clock never runs backwards.
80+func TestGestureEndWithoutTime(t *testing.T) {
81+ r := gestureRig(t)
82+ r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
83+ r.input <- ports.PinchUpdate{Scale: 1, Time: 2 * time.Second}
84+ r.input <- ports.PinchEnd{Cancelled: true}
85+ pointerCommand(t, r.commands)
86+ pointerCommand(t, r.commands)
87+ if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) {
88+ t.Fatal(v)
89+ }
90+}
91+
92+func TestHoldGoesToPointerFocus(t *testing.T) {
93+ r := gestureRig(t)
94+ r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second}
95+ r.input <- ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}
96+ if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) {
97+ t.Fatal(v)
98+ }
99+ if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: 2 * time.Second}) {
100+ t.Fatal(v)
101+ }
102+}
103+
104+// Swipes of other finger counts than three and four go to the client; the
105+// compositor keeps those.
106+func TestSwipeFingerCounts(t *testing.T) {
107+ for _, fingers := range []int{2, 5} {
108+ r := gestureRig(t)
109+ r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second}
110+ // The client gets the accelerated deltas.
111+ r.input <- ports.SwipeUpdate{DX: 3, DY: 4, AccelDX: 6, AccelDY: 8, Time: 2 * time.Second}
112+ r.input <- ports.SwipeEnd{Time: 3 * time.Second}
113+ for _, want := range []ports.ClientCommand{
114+ ports.GestureBeginTo{ID: 1, Kind: ports.GestureSwipe, Fingers: fingers, Time: time.Second},
115+ ports.GestureUpdateTo{ID: 1, Kind: ports.GestureSwipe, DX: 6, DY: 8, Time: 2 * time.Second},
116+ ports.GestureEndTo{ID: 1, Kind: ports.GestureSwipe, Time: 3 * time.Second},
117+ } {
118+ if v := pointerCommand(t, r.commands); v != want {
119+ t.Fatalf("%d fingers: got %#v, want %#v", fingers, v, want)
120+ }
121+ }
122+ }
123+ for _, fingers := range []int{3, 4} {
124+ r := gestureRig(t)
125+ r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second}
126+ r.input <- ports.SwipeUpdate{DX: 40, DY: 0, Time: 2 * time.Second}
127+ r.input <- ports.SwipeEnd{Time: 3 * time.Second}
128+ r.idle(t)
129+ }
130+}
131+
132+// A gesture that began with no window under the pointer reaches nobody,
133+// even when the pointer enters a window meanwhile.
134+func TestGestureWithoutPointerFocus(t *testing.T) {
135+ r := gestureRig(t)
136+ r.input <- ports.PointerMotion{X: 300, Y: 50}
137+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
138+ t.Fatal(v)
139+ }
140+ r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
141+ r.input <- ports.PointerMotion{X: 50, Y: 50}
142+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
143+ t.Fatal(v)
144+ }
145+ if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) {
146+ t.Fatal(v)
147+ }
148+ r.input <- ports.PinchUpdate{Scale: 2, Time: 2 * time.Second}
149+ r.input <- ports.PinchEnd{Time: 3 * time.Second}
150+ r.idle(t)
151+}
152+
153+// The window losing the pointer focus gets its gesture cancelled before the
154+// leave, and nothing of the rest.
155+func TestGestureCancelledOnFocusChange(t *testing.T) {
156+ r := gestureRig(t)
157+ r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
158+ if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}) {
159+ t.Fatal(v)
160+ }
161+ r.input <- ports.PinchUpdate{Scale: 1.1, Time: 2 * time.Second}
162+ if v := pointerCommand(t, r.commands); v != (ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, Scale: 1.1, Time: 2 * time.Second}) {
163+ t.Fatal(v)
164+ }
165+ r.input <- ports.PointerMotion{X: 300, Y: 50}
166+ if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) {
167+ t.Fatal(v)
168+ }
169+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
170+ t.Fatal(v)
171+ }
172+ r.input <- ports.PinchUpdate{Scale: 1.2, Time: 3 * time.Second}
173+ r.input <- ports.PinchEnd{Time: 4 * time.Second}
174+ r.input <- ports.PointerMotion{X: 50, Y: 50}
175+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
176+ t.Fatal(v)
177+ }
178+}
179+
180+// A window that goes away mid-gesture has its gesture cancelled with the
181+// pointer focus.
182+func TestGestureCancelledOnUnmap(t *testing.T) {
183+ r := gestureRig(t)
184+ r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second}
185+ if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) {
186+ t.Fatal(v)
187+ }
188+ r.client <- ports.WindowUnmapped{ID: 1}
189+ if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: time.Second}) {
190+ t.Fatal(v)
191+ }
192+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
193+ t.Fatal(v)
194+ }
195+}
196+
197+// A gesture whose end was lost ends cancelled when the next one begins.
198+func TestGestureBeginEndsPrevious(t *testing.T) {
199+ r := gestureRig(t)
200+ r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
201+ r.input <- ports.HoldBegin{Fingers: 3, Time: 2 * time.Second}
202+ for _, want := range []ports.ClientCommand{
203+ ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
204+ ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second},
205+ ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: 2 * time.Second},
206+ } {
207+ if v := pointerCommand(t, r.commands); v != want {
208+ t.Fatalf("got %#v, want %#v", v, want)
209+ }
210+ }
211+}
212+
213+// A protected session forwards no gesture to a window; one running when
214+// the session locks ends with the pointer focus.
215+func TestGesturesRefusedWhileLocked(t *testing.T) {
216+ var epoch atomic.Value
217+ epoch.Store(ports.SecurityState{})
218+ gate := portsmocks.NewMockSessionSecurity(t)
219+ gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
220+ r := &multiRig{
221+ client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 32),
222+ output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4),
223+ commands: make(chan ports.ClientCommand, 256), scenes: make(chan []ports.Scene, 1),
224+ }
225+ cfg := config.Defaults()
226+ 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})
227+ if err != nil {
228+ t.Fatal(err)
229+ }
230+ ctx, cancel := context.WithCancel(context.Background())
231+ done := make(chan error, 1)
232+ go func() { done <- c.Run(ctx) }()
233+ t.Cleanup(func() { cancel(); <-done })
234+ r.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 300, Height: 200}}
235+ scene(t, r.scenes)
236+ r.client <- ports.WindowMapped{ID: 1}
237+ scene(t, r.scenes)
238+ unlocked := ports.SecurityState{}
239+ r.input <- ports.SecurityInput{State: unlocked, Event: ports.PointerMotion{X: 50, Y: 50}}
240+ r.input <- ports.SecurityInput{State: unlocked, Event: ports.PinchBegin{Fingers: 2, Time: time.Second}}
241+ for _, want := range []ports.ClientCommand{
242+ ports.PointerFocus{ID: 1, X: 50, Y: 50},
243+ ports.PointerMotionTo{ID: 1, X: 50, Y: 50},
244+ ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
245+ } {
246+ if v := unwrap(t, r.commands, unlocked); v != want {
247+ t.Fatalf("got %#v, want %#v", v, want)
248+ }
249+ }
250+
251+ // The lock clears the pointer focus, which cancels the gesture in wayland.
252+ locked := ports.SecurityState{Generation: 1, Protected: true}
253+ epoch.Store(locked)
254+ r.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 300, Height: 200}}}
255+ sceneMatch(t, r.scenes, func(s ports.Scene) bool { return s.Security == locked && len(s.Windows) == 1 })
256+ r.input <- ports.SecurityInput{State: locked, Event: ports.PointerMotion{X: 50, Y: 50}}
257+ r.input <- ports.SecurityInput{State: locked, Event: ports.PinchBegin{Fingers: 2, Time: 2 * time.Second}}
258+ r.input <- ports.SecurityInput{State: locked, Event: ports.PinchUpdate{Scale: 2, Time: 3 * time.Second}}
259+ r.input <- ports.SecurityInput{State: locked, Event: ports.PinchEnd{Time: 4 * time.Second}}
260+ r.input <- ports.SecurityInput{State: locked, Event: ports.HoldBegin{Fingers: 3, Time: 5 * time.Second}}
261+ r.input <- ports.SecurityInput{State: locked, Event: ports.HoldEnd{Time: 6 * time.Second}}
262+ r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeBegin{Fingers: 2, Time: 7 * time.Second}}
263+ r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeEnd{Time: 8 * time.Second}}
264+ r.input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "marker", Keycode: 30, Pressed: true}}
265+ for {
266+ wrapped, ok := receive(t, r.commands).(ports.SecurityCommand)
267+ if !ok {
268+ t.Fatal("unwrapped command")
269+ }
270+ switch v := wrapped.Command.(type) {
271+ case ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
272+ if wrapped.State == locked {
273+ t.Fatalf("gesture forwarded while locked: %#v", v)
274+ }
275+ case ports.ForwardKey:
276+ if wrapped.State == locked {
277+ return
278+ }
279+ }
280+ }
281+}
282+
283+// unwrap returns the next pointer command of an envelope of the given epoch.
284+func unwrap(t *testing.T, ch <-chan ports.ClientCommand, state ports.SecurityState) ports.ClientCommand {
285+ t.Helper()
286+ for {
287+ wrapped, ok := receive(t, ch).(ports.SecurityCommand)
288+ if !ok {
289+ t.Fatal("unwrapped command")
290+ }
291+ if wrapped.State != state {
292+ t.Fatalf("epoch %+v, want %+v", wrapped.State, state)
293+ }
294+ if isPointerCommand(wrapped.Command) {
295+ return wrapped.Command
296+ }
297+ }
298+}
@@ -0,0 +1,298 @@
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+// A removed touchpad ends its gesture with no time: the client gets the
79+// last one, so its clock never runs backwards.
80+func TestGestureEndWithoutTime(t *testing.T) {
81+ r := gestureRig(t)
82+ r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
83+ r.input <- ports.PinchUpdate{Scale: 1, Time: 2 * time.Second}
84+ r.input <- ports.PinchEnd{Cancelled: true}
85+ pointerCommand(t, r.commands)
86+ pointerCommand(t, r.commands)
87+ if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) {
88+ t.Fatal(v)
89+ }
90+}
91+
92+func TestHoldGoesToPointerFocus(t *testing.T) {
93+ r := gestureRig(t)
94+ r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second}
95+ r.input <- ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}
96+ if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) {
97+ t.Fatal(v)
98+ }
99+ if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: 2 * time.Second}) {
100+ t.Fatal(v)
101+ }
102+}
103+
104+// Swipes of other finger counts than three and four go to the client; the
105+// compositor keeps those.
106+func TestSwipeFingerCounts(t *testing.T) {
107+ for _, fingers := range []int{2, 5} {
108+ r := gestureRig(t)
109+ r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second}
110+ // The client gets the accelerated deltas.
111+ r.input <- ports.SwipeUpdate{DX: 3, DY: 4, AccelDX: 6, AccelDY: 8, Time: 2 * time.Second}
112+ r.input <- ports.SwipeEnd{Time: 3 * time.Second}
113+ for _, want := range []ports.ClientCommand{
114+ ports.GestureBeginTo{ID: 1, Kind: ports.GestureSwipe, Fingers: fingers, Time: time.Second},
115+ ports.GestureUpdateTo{ID: 1, Kind: ports.GestureSwipe, DX: 6, DY: 8, Time: 2 * time.Second},
116+ ports.GestureEndTo{ID: 1, Kind: ports.GestureSwipe, Time: 3 * time.Second},
117+ } {
118+ if v := pointerCommand(t, r.commands); v != want {
119+ t.Fatalf("%d fingers: got %#v, want %#v", fingers, v, want)
120+ }
121+ }
122+ }
123+ for _, fingers := range []int{3, 4} {
124+ r := gestureRig(t)
125+ r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second}
126+ r.input <- ports.SwipeUpdate{DX: 40, DY: 0, Time: 2 * time.Second}
127+ r.input <- ports.SwipeEnd{Time: 3 * time.Second}
128+ r.idle(t)
129+ }
130+}
131+
132+// A gesture that began with no window under the pointer reaches nobody,
133+// even when the pointer enters a window meanwhile.
134+func TestGestureWithoutPointerFocus(t *testing.T) {
135+ r := gestureRig(t)
136+ r.input <- ports.PointerMotion{X: 300, Y: 50}
137+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
138+ t.Fatal(v)
139+ }
140+ r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
141+ r.input <- ports.PointerMotion{X: 50, Y: 50}
142+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
143+ t.Fatal(v)
144+ }
145+ if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) {
146+ t.Fatal(v)
147+ }
148+ r.input <- ports.PinchUpdate{Scale: 2, Time: 2 * time.Second}
149+ r.input <- ports.PinchEnd{Time: 3 * time.Second}
150+ r.idle(t)
151+}
152+
153+// The window losing the pointer focus gets its gesture cancelled before the
154+// leave, and nothing of the rest.
155+func TestGestureCancelledOnFocusChange(t *testing.T) {
156+ r := gestureRig(t)
157+ r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
158+ if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}) {
159+ t.Fatal(v)
160+ }
161+ r.input <- ports.PinchUpdate{Scale: 1.1, Time: 2 * time.Second}
162+ if v := pointerCommand(t, r.commands); v != (ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, Scale: 1.1, Time: 2 * time.Second}) {
163+ t.Fatal(v)
164+ }
165+ r.input <- ports.PointerMotion{X: 300, Y: 50}
166+ if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) {
167+ t.Fatal(v)
168+ }
169+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
170+ t.Fatal(v)
171+ }
172+ r.input <- ports.PinchUpdate{Scale: 1.2, Time: 3 * time.Second}
173+ r.input <- ports.PinchEnd{Time: 4 * time.Second}
174+ r.input <- ports.PointerMotion{X: 50, Y: 50}
175+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
176+ t.Fatal(v)
177+ }
178+}
179+
180+// A window that goes away mid-gesture has its gesture cancelled with the
181+// pointer focus.
182+func TestGestureCancelledOnUnmap(t *testing.T) {
183+ r := gestureRig(t)
184+ r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second}
185+ if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) {
186+ t.Fatal(v)
187+ }
188+ r.client <- ports.WindowUnmapped{ID: 1}
189+ if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: time.Second}) {
190+ t.Fatal(v)
191+ }
192+ if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
193+ t.Fatal(v)
194+ }
195+}
196+
197+// A gesture whose end was lost ends cancelled when the next one begins.
198+func TestGestureBeginEndsPrevious(t *testing.T) {
199+ r := gestureRig(t)
200+ r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
201+ r.input <- ports.HoldBegin{Fingers: 3, Time: 2 * time.Second}
202+ for _, want := range []ports.ClientCommand{
203+ ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
204+ ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second},
205+ ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: 2 * time.Second},
206+ } {
207+ if v := pointerCommand(t, r.commands); v != want {
208+ t.Fatalf("got %#v, want %#v", v, want)
209+ }
210+ }
211+}
212+
213+// A protected session forwards no gesture to a window; one running when
214+// the session locks ends with the pointer focus.
215+func TestGesturesRefusedWhileLocked(t *testing.T) {
216+ var epoch atomic.Value
217+ epoch.Store(ports.SecurityState{})
218+ gate := portsmocks.NewMockSessionSecurity(t)
219+ gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
220+ r := &multiRig{
221+ client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 32),
222+ output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4),
223+ commands: make(chan ports.ClientCommand, 256), scenes: make(chan []ports.Scene, 1),
224+ }
225+ cfg := config.Defaults()
226+ 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})
227+ if err != nil {
228+ t.Fatal(err)
229+ }
230+ ctx, cancel := context.WithCancel(context.Background())
231+ done := make(chan error, 1)
232+ go func() { done <- c.Run(ctx) }()
233+ t.Cleanup(func() { cancel(); <-done })
234+ r.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 300, Height: 200}}
235+ scene(t, r.scenes)
236+ r.client <- ports.WindowMapped{ID: 1}
237+ scene(t, r.scenes)
238+ unlocked := ports.SecurityState{}
239+ r.input <- ports.SecurityInput{State: unlocked, Event: ports.PointerMotion{X: 50, Y: 50}}
240+ r.input <- ports.SecurityInput{State: unlocked, Event: ports.PinchBegin{Fingers: 2, Time: time.Second}}
241+ for _, want := range []ports.ClientCommand{
242+ ports.PointerFocus{ID: 1, X: 50, Y: 50},
243+ ports.PointerMotionTo{ID: 1, X: 50, Y: 50},
244+ ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
245+ } {
246+ if v := unwrap(t, r.commands, unlocked); v != want {
247+ t.Fatalf("got %#v, want %#v", v, want)
248+ }
249+ }
250+
251+ // The lock clears the pointer focus, which cancels the gesture in wayland.
252+ locked := ports.SecurityState{Generation: 1, Protected: true}
253+ epoch.Store(locked)
254+ r.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 300, Height: 200}}}
255+ sceneMatch(t, r.scenes, func(s ports.Scene) bool { return s.Security == locked && len(s.Windows) == 1 })
256+ r.input <- ports.SecurityInput{State: locked, Event: ports.PointerMotion{X: 50, Y: 50}}
257+ r.input <- ports.SecurityInput{State: locked, Event: ports.PinchBegin{Fingers: 2, Time: 2 * time.Second}}
258+ r.input <- ports.SecurityInput{State: locked, Event: ports.PinchUpdate{Scale: 2, Time: 3 * time.Second}}
259+ r.input <- ports.SecurityInput{State: locked, Event: ports.PinchEnd{Time: 4 * time.Second}}
260+ r.input <- ports.SecurityInput{State: locked, Event: ports.HoldBegin{Fingers: 3, Time: 5 * time.Second}}
261+ r.input <- ports.SecurityInput{State: locked, Event: ports.HoldEnd{Time: 6 * time.Second}}
262+ r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeBegin{Fingers: 2, Time: 7 * time.Second}}
263+ r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeEnd{Time: 8 * time.Second}}
264+ r.input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "marker", Keycode: 30, Pressed: true}}
265+ for {
266+ wrapped, ok := receive(t, r.commands).(ports.SecurityCommand)
267+ if !ok {
268+ t.Fatal("unwrapped command")
269+ }
270+ switch v := wrapped.Command.(type) {
271+ case ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
272+ if wrapped.State == locked {
273+ t.Fatalf("gesture forwarded while locked: %#v", v)
274+ }
275+ case ports.ForwardKey:
276+ if wrapped.State == locked {
277+ return
278+ }
279+ }
280+ }
281+}
282+
283+// unwrap returns the next pointer command of an envelope of the given epoch.
284+func unwrap(t *testing.T, ch <-chan ports.ClientCommand, state ports.SecurityState) ports.ClientCommand {
285+ t.Helper()
286+ for {
287+ wrapped, ok := receive(t, ch).(ports.SecurityCommand)
288+ if !ok {
289+ t.Fatal("unwrapped command")
290+ }
291+ if wrapped.State != state {
292+ t.Fatalf("epoch %+v, want %+v", wrapped.State, state)
293+ }
294+ if isPointerCommand(wrapped.Command) {
295+ return wrapped.Command
296+ }
297+ }
298+}
M internal/core/core.go +6 -3
@@ -135,7 +135,10 @@ type Core struct {
135135 // flip came in time to move a running slide; nil when not armed. It is
136136 // the channel of frameTimer, made once and reset per frame: an
137137 // animation frame allocates no timer.
138- swipe *swipeGesture
138+ swipe *swipeGesture
139+ // gesture is the touchpad gesture forwarded to a client
140+ // (clientgesture.go).
141+ gesture clientGesture
139142 frameC <-chan time.Time
140143 frameTimer ports.Timer
141144 // shots is the snapshot of what the screens show before an action
@@ -626,7 +629,7 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error {
626629 if c.pointer != 0 && !c.visible(c.pointer) {
627630 c.pointer = 0
628631 if c.grab == 0 {
629- if err := c.command(ctx, ports.PointerFocus{}); err != nil {
632+ if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
630633 return err
631634 }
632635 }
@@ -708,7 +711,7 @@ func (c *Core) rehit(ctx context.Context) error {
708711 id, x, y := c.hit(c.cursorX, c.cursorY)
709712 if id != c.pointer {
710713 c.pointer, c.pointerAt = id, [2]float64{x, y}
711- return c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
714+ return c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
712715 }
713716 if id == 0 || c.pointerAt == [2]float64{x, y} {
714717 return nil
@@ -135,7 +135,10 @@ type Core struct {
135 // flip came in time to move a running slide; nil when not armed. It is135 // flip came in time to move a running slide; nil when not armed. It is
136 // the channel of frameTimer, made once and reset per frame: an136 // the channel of frameTimer, made once and reset per frame: an
137 // animation frame allocates no timer.137 // animation frame allocates no timer.
138- swipe *swipeGesture138+ swipe *swipeGesture
139+ // gesture is the touchpad gesture forwarded to a client
140+ // (clientgesture.go).
141+ gesture clientGesture
139 frameC <-chan time.Time142 frameC <-chan time.Time
140 frameTimer ports.Timer143 frameTimer ports.Timer
141 // shots is the snapshot of what the screens show before an action144 // shots is the snapshot of what the screens show before an action
@@ -626,7 +629,7 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error {
626 if c.pointer != 0 && !c.visible(c.pointer) {629 if c.pointer != 0 && !c.visible(c.pointer) {
627 c.pointer = 0630 c.pointer = 0
628 if c.grab == 0 {631 if c.grab == 0 {
629- if err := c.command(ctx, ports.PointerFocus{}); err != nil {632+ if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
630 return err633 return err
631 }634 }
632 }635 }
@@ -708,7 +711,7 @@ func (c *Core) rehit(ctx context.Context) error {
708 id, x, y := c.hit(c.cursorX, c.cursorY)711 id, x, y := c.hit(c.cursorX, c.cursorY)
709 if id != c.pointer {712 if id != c.pointer {
710 c.pointer, c.pointerAt = id, [2]float64{x, y}713 c.pointer, c.pointerAt = id, [2]float64{x, y}
711- return c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y})714+ return c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y})
712 }715 }
713 if id == 0 || c.pointerAt == [2]float64{x, y} {716 if id == 0 || c.pointerAt == [2]float64{x, y} {
714 return nil717 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
165165 c.grab = 0
166166 if c.pointer != 0 {
167167 c.pointer = 0
168- if err := c.command(ctx, ports.PointerFocus{}); err != nil {
168+ if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
169169 return err
170170 }
171171 }
@@ -165,7 +165,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize
165 c.grab = 0165 c.grab = 0
166 if c.pointer != 0 {166 if c.pointer != 0 {
167 c.pointer = 0167 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 err169 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 {
2626 case ports.PointerAxis:
2727 return c.pointerAxis(ctx, v)
2828 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+ }
2935 if c.swipeBegin(v) {
3036 return c.slid(ctx, false)
3137 }
3238 return nil
3339 case ports.SwipeUpdate:
40+ if c.gesture.on {
41+ return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.AccelDX, DY: v.AccelDY, Time: v.Time})
42+ }
3443 before := c.cur().mon.Current()
3544 if c.swipeUpdate(v) {
3645 return c.slid(ctx, c.cur().mon.Current() != before)
3746 }
3847 return nil
3948 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+ }
4052 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)
4163 case ports.KeyEvent:
4264 return c.keyEvent(ctx, v)
4365 }
@@ -73,7 +95,7 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
7395 // Layout changes are intentionally re-hit-tested only on motion.
7496 if id != c.pointer {
7597 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 {
7799 return err
78100 }
79101 }
@@ -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 nil38 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.AccelDX, DY: v.AccelDY, 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 nil47 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 = id97 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 err99 return err
78 }100 }
79 }101 }
M internal/core/security.go +6 -3
@@ -41,6 +41,9 @@ func (c *Core) syncSecurity() bool {
4141 }
4242 c.dropSwipe()
4343 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{}
4447 c.stopAnimations()
4548 c.stopPulse()
4649 if state.Protected {
@@ -64,7 +67,7 @@ func (c *Core) securityCheckpoint(ctx context.Context) error {
6467 return nil
6568 }
6669 // 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 {
6871 return err
6972 }
7073 if err := c.command(ctx, ports.FocusWindow{}); err != nil {
@@ -204,7 +207,7 @@ func (c *Core) publishProtected(ctx context.Context) error {
204207 if !c.validLockID(c.pointer) {
205208 c.pointer = 0
206209 }
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 {
208211 return err
209212 }
210213 latest(c.ch.Scenes, scenes)
@@ -259,7 +262,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
259262 id, x, y := c.lockHit(c.cursorX, c.cursorY)
260263 if id != c.pointer {
261264 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 {
263266 return err
264267 }
265268 }
@@ -41,6 +41,9 @@ func (c *Core) syncSecurity() bool {
41 }41 }
42 c.dropSwipe()42 c.dropSwipe()
43 c.swipe = nil43 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 nil67 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 err71 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 = 0208 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 err211 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 = id264 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 err266 return err
264 }267 }
265 }268 }
M internal/ports/client_commands.go +49 -0
@@ -192,6 +192,55 @@ type PointerAxisTo struct {
192192
193193 func (PointerAxisTo) clientCommand() {}
194194
195+// GestureKind is a touchpad gesture a client can receive
196+// (zwp_pointer_gestures_v1).
197+type GestureKind uint8
198+
199+const (
200+ GestureSwipe GestureKind = iota
201+ GesturePinch
202+ GestureHold
203+)
204+
205+// GestureBeginTo starts a gesture on a window with the pointer focus.
206+// Wayland assigns the serial. Time is the device timestamp
207+// (CLOCK_MONOTONIC), like the updates and the end of the gesture.
208+type GestureBeginTo struct {
209+ ID WindowID
210+ Kind GestureKind
211+ Fingers int
212+ Time time.Duration
213+}
214+
215+func (GestureBeginTo) clientCommand() {}
216+
217+// GestureUpdateTo moves the gesture of a window. DX and DY are the
218+// centre's movement; Scale and Rotation are only for a pinch (see
219+// PinchUpdate). A hold has no updates.
220+type GestureUpdateTo struct {
221+ ID WindowID
222+ Kind GestureKind
223+ DX, DY float64
224+ Scale float64
225+ Rotation float64
226+ Time time.Duration
227+}
228+
229+func (GestureUpdateTo) clientCommand() {}
230+
231+// GestureEndTo ends the gesture of a window. Cancelled is set when the
232+// gesture did not complete: the pointer focus moved, the window went away
233+// or the device cancelled it. Ending a gesture that is not running does
234+// nothing.
235+type GestureEndTo struct {
236+ ID WindowID
237+ Kind GestureKind
238+ Cancelled bool
239+ Time time.Duration
240+}
241+
242+func (GestureEndTo) clientCommand() {}
243+
195244 // ForwardKey carries core → wayland unbound keys.
196245 type ForwardKey struct {
197246 ID WindowID
@@ -192,6 +192,55 @@ type PointerAxisTo struct {
192 192
193 func (PointerAxisTo) clientCommand() {}193 func (PointerAxisTo) clientCommand() {}
194 194
195+// GestureKind is a touchpad gesture a client can receive
196+// (zwp_pointer_gestures_v1).
197+type GestureKind uint8
198+
199+const (
200+ GestureSwipe GestureKind = iota
201+ GesturePinch
202+ GestureHold
203+)
204+
205+// GestureBeginTo starts a gesture on a window with the pointer focus.
206+// Wayland assigns the serial. Time is the device timestamp
207+// (CLOCK_MONOTONIC), like the updates and the end of the gesture.
208+type GestureBeginTo struct {
209+ ID WindowID
210+ Kind GestureKind
211+ Fingers int
212+ Time time.Duration
213+}
214+
215+func (GestureBeginTo) clientCommand() {}
216+
217+// GestureUpdateTo moves the gesture of a window. DX and DY are the
218+// centre's movement; Scale and Rotation are only for a pinch (see
219+// PinchUpdate). A hold has no updates.
220+type GestureUpdateTo struct {
221+ ID WindowID
222+ Kind GestureKind
223+ DX, DY float64
224+ Scale float64
225+ Rotation float64
226+ Time time.Duration
227+}
228+
229+func (GestureUpdateTo) clientCommand() {}
230+
231+// GestureEndTo ends the gesture of a window. Cancelled is set when the
232+// gesture did not complete: the pointer focus moved, the window went away
233+// or the device cancelled it. Ending a gesture that is not running does
234+// nothing.
235+type GestureEndTo struct {
236+ ID WindowID
237+ Kind GestureKind
238+ Cancelled bool
239+ Time time.Duration
240+}
241+
242+func (GestureEndTo) clientCommand() {}
243+
195 // ForwardKey carries core → wayland unbound keys.244 // ForwardKey carries core → wayland unbound keys.
196 type ForwardKey struct {245 type ForwardKey struct {
197 ID WindowID246 ID WindowID
M internal/ports/input_events.go +56 -5
@@ -88,18 +88,23 @@ type PointerAxis struct {
8888
8989 func (PointerAxis) inputEvent() {}
9090
91-// SwipeBegin starts a three- or four-finger touchpad swipe. Time is the
92-// device timestamp (CLOCK_MONOTONIC), shared by the updates and the end.
91+// SwipeBegin starts a touchpad swipe of any finger count; core keeps three
92+// and four fingers and forwards the others to the window under the
93+// pointer. Time is the device timestamp (CLOCK_MONOTONIC), shared by the
94+// updates and the end.
9395 type SwipeBegin struct {
9496 Fingers int
9597 Time time.Duration
9698 }
9799
98100 // SwipeUpdate moves the fingers by the unaccelerated deltas DX, DY
99-// (touchpad units, as libinput reports them, not natural-scroll inverted).
101+// (touchpad units, as libinput reports them, not natural-scroll inverted),
102+// which the compositor's own swipes follow. AccelDX, AccelDY are the same
103+// move with pointer acceleration, what a client gets.
100104 type SwipeUpdate struct {
101- DX, DY float64
102- Time time.Duration
105+ DX, DY float64
106+ AccelDX, AccelDY float64
107+ Time time.Duration
103108 }
104109
105110 // SwipeEnd lifts the fingers. Cancelled is set when libinput cancelled
@@ -112,3 +117,49 @@ type SwipeEnd struct {
112117 func (SwipeBegin) inputEvent() {}
113118 func (SwipeUpdate) inputEvent() {}
114119 func (SwipeEnd) inputEvent() {}
120+
121+// PinchBegin starts a touchpad pinch. Time is the device timestamp
122+// (CLOCK_MONOTONIC), shared by the updates and the end. A pinch always goes
123+// to the window under the pointer.
124+type PinchBegin struct {
125+ Fingers int
126+ Time time.Duration
127+}
128+
129+// PinchUpdate moves a pinch: the fingers' centre by the deltas DX, DY with
130+// pointer acceleration, as clients get them, Scale is the spread of the fingers
131+// relative to the begin (1.0 there) and Rotation the angle turned since the
132+// previous event, in degrees clockwise.
133+type PinchUpdate struct {
134+ DX, DY float64
135+ Scale float64
136+ Rotation float64
137+ Time time.Duration
138+}
139+
140+// PinchEnd lifts the fingers. Cancelled is set when libinput cancelled the
141+// pinch.
142+type PinchEnd struct {
143+ Cancelled bool
144+ Time time.Duration
145+}
146+
147+// HoldBegin starts a touchpad hold: fingers resting on the pad. Like a
148+// pinch it goes to the window under the pointer.
149+type HoldBegin struct {
150+ Fingers int
151+ Time time.Duration
152+}
153+
154+// HoldEnd lifts the fingers. Cancelled is set when the fingers moved or
155+// another finger landed, so the hold became something else.
156+type HoldEnd struct {
157+ Cancelled bool
158+ Time time.Duration
159+}
160+
161+func (PinchBegin) inputEvent() {}
162+func (PinchUpdate) inputEvent() {}
163+func (PinchEnd) inputEvent() {}
164+func (HoldBegin) inputEvent() {}
165+func (HoldEnd) inputEvent() {}
@@ -88,18 +88,23 @@ type PointerAxis struct {
88 88
89 func (PointerAxis) inputEvent() {}89 func (PointerAxis) inputEvent() {}
90 90
91-// SwipeBegin starts a three- or four-finger touchpad swipe. Time is the91+// SwipeBegin starts a touchpad swipe of any finger count; core keeps three
92-// device timestamp (CLOCK_MONOTONIC), shared by the updates and the end.92+// and four fingers and forwards the others to the window under the
93+// pointer. Time is the device timestamp (CLOCK_MONOTONIC), shared by the
94+// updates and the end.
93 type SwipeBegin struct {95 type SwipeBegin struct {
94 Fingers int96 Fingers int
95 Time time.Duration97 Time time.Duration
96 }98 }
97 99
98 // SwipeUpdate moves the fingers by the unaccelerated deltas DX, DY100 // SwipeUpdate moves the fingers by the unaccelerated deltas DX, DY
99-// (touchpad units, as libinput reports them, not natural-scroll inverted).101+// (touchpad units, as libinput reports them, not natural-scroll inverted),
102+// which the compositor's own swipes follow. AccelDX, AccelDY are the same
103+// move with pointer acceleration, what a client gets.
100 type SwipeUpdate struct {104 type SwipeUpdate struct {
101- DX, DY float64105+ DX, DY float64
102- Time time.Duration106+ AccelDX, AccelDY float64
107+ Time time.Duration
103 }108 }
104 109
105 // SwipeEnd lifts the fingers. Cancelled is set when libinput cancelled110 // SwipeEnd lifts the fingers. Cancelled is set when libinput cancelled
@@ -112,3 +117,49 @@ type SwipeEnd struct {
112 func (SwipeBegin) inputEvent() {}117 func (SwipeBegin) inputEvent() {}
113 func (SwipeUpdate) inputEvent() {}118 func (SwipeUpdate) inputEvent() {}
114 func (SwipeEnd) inputEvent() {}119 func (SwipeEnd) inputEvent() {}
120+
121+// PinchBegin starts a touchpad pinch. Time is the device timestamp
122+// (CLOCK_MONOTONIC), shared by the updates and the end. A pinch always goes
123+// to the window under the pointer.
124+type PinchBegin struct {
125+ Fingers int
126+ Time time.Duration
127+}
128+
129+// PinchUpdate moves a pinch: the fingers' centre by the deltas DX, DY with
130+// pointer acceleration, as clients get them, Scale is the spread of the fingers
131+// relative to the begin (1.0 there) and Rotation the angle turned since the
132+// previous event, in degrees clockwise.
133+type PinchUpdate struct {
134+ DX, DY float64
135+ Scale float64
136+ Rotation float64
137+ Time time.Duration
138+}
139+
140+// PinchEnd lifts the fingers. Cancelled is set when libinput cancelled the
141+// pinch.
142+type PinchEnd struct {
143+ Cancelled bool
144+ Time time.Duration
145+}
146+
147+// HoldBegin starts a touchpad hold: fingers resting on the pad. Like a
148+// pinch it goes to the window under the pointer.
149+type HoldBegin struct {
150+ Fingers int
151+ Time time.Duration
152+}
153+
154+// HoldEnd lifts the fingers. Cancelled is set when the fingers moved or
155+// another finger landed, so the hold became something else.
156+type HoldEnd struct {
157+ Cancelled bool
158+ Time time.Duration
159+}
160+
161+func (PinchBegin) inputEvent() {}
162+func (PinchUpdate) inputEvent() {}
163+func (PinchEnd) inputEvent() {}
164+func (HoldBegin) inputEvent() {}
165+func (HoldEnd) inputEvent() {}

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