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
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 | |||
| 186 | 186 | ||
| 187 | 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 | 191 | ## HDR | |
| 190 | 192 | ||
| 191 | 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. | |
| @@ -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 | ## HDR | 191 | ## 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 ( | |||
| 33 | 33 | evSwipeBegin = 800 | |
| 34 | 34 | evSwipeUpdate = 801 | |
| 35 | 35 | evSwipeEnd = 802 | |
| 36 | + | evPinchBegin = 803 | |
| 37 | + | evPinchUpdate = 804 | |
| 38 | + | evPinchEnd = 805 | |
| 39 | + | evHoldBegin = 806 | |
| 40 | + | evHoldEnd = 807 | |
| 36 | 41 | ) | |
| 37 | 42 | ||
| 38 | 43 | var ( | |
| @@ -75,7 +80,11 @@ var ( | |||
| 75 | 80 | gestureCanceled func(gev uintptr) int32 | |
| 76 | 81 | gestureDX func(gev uintptr) float64 | |
| 77 | 82 | gestureDY func(gev uintptr) float64 | |
| 83 | + | gestureAccelDX func(gev uintptr) float64 | |
| 84 | + | gestureAccelDY func(gev uintptr) float64 | |
| 78 | 85 | gestureUsec func(gev uintptr) uint64 | |
| 86 | + | gestureScale func(gev uintptr) float64 | |
| 87 | + | gestureAngle func(gev uintptr) float64 | |
| 79 | 88 | deviceRef func(dev uintptr) uintptr | |
| 80 | 89 | deviceUnref func(dev uintptr) uintptr | |
| 81 | 90 | hasNatural func(dev uintptr) int32 | |
| @@ -148,7 +157,11 @@ func load() error { | |||
| 148 | 157 | reg(&gestureCanceled, "event_gesture_get_cancelled") | |
| 149 | 158 | reg(&gestureDX, "event_gesture_get_dx_unaccelerated") | |
| 150 | 159 | reg(&gestureDY, "event_gesture_get_dy_unaccelerated") | |
| 160 | + | reg(&gestureAccelDX, "event_gesture_get_dx") | |
| 161 | + | reg(&gestureAccelDY, "event_gesture_get_dy") | |
| 151 | 162 | reg(&gestureUsec, "event_gesture_get_time_usec") | |
| 163 | + | reg(&gestureScale, "event_gesture_get_scale") | |
| 164 | + | reg(&gestureAngle, "event_gesture_get_angle_delta") | |
| 152 | 165 | reg(&deviceRef, "device_ref") | |
| 153 | 166 | reg(&deviceUnref, "device_unref") | |
| 154 | 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 | 340 | // 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 | |
| 330 | 343 | // type, so storing it in the interface allocates nothing. | |
| 331 | 344 | 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 | |
| 336 | 349 | } | |
| 337 | 350 | ||
| 338 | 351 | // deviceConfig is libinput's device configuration side: what a device is | |
| @@ -478,33 +491,74 @@ func (s *inputState) release() { | |||
| 478 | 491 | clear(s.devices) | |
| 479 | 492 | } | |
| 480 | 493 | ||
| 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 | + | } | |
| 487 | 504 | ||
| 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 { | |
| 490 | 507 | return nil | |
| 491 | 508 | } | |
| 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 | + | } | |
| 493 | 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 { | |
| 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) { | |
| 498 | 533 | 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 { | |
| 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) { | |
| 505 | 541 | return nil | |
| 506 | 542 | } | |
| 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 | + | } | |
| 508 | 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 | 606 | delete(in.devices, dev) | |
| 553 | 607 | deviceUnref(dev) | |
| 554 | 608 | } | |
| 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: | |
| 558 | 612 | 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 | |
| 560 | 620 | case evSwipeUpdate: | |
| 561 | 621 | 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 | |
| 563 | 626 | case evSwipeEnd: | |
| 564 | 627 | 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 | |
| 566 | 635 | case evKeyboardKey: | |
| 567 | 636 | k := keyboardEvent(ev) | |
| 568 | 637 | code, pressed := keyboardKey(k), keyboardState(k) == 1 | |
| @@ -33,6 +33,11 @@ const ( | |||||
| 33 | evSwipeBegin = 800 | 33 | evSwipeBegin = 800 | ||
| 34 | evSwipeUpdate = 801 | 34 | evSwipeUpdate = 801 | ||
| 35 | evSwipeEnd = 802 | 35 | 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) int32 | 80 | gestureCanceled func(gev uintptr) int32 | ||
| 76 | gestureDX func(gev uintptr) float64 | 81 | gestureDX func(gev uintptr) float64 | ||
| 77 | gestureDY func(gev uintptr) float64 | 82 | gestureDY func(gev uintptr) float64 | ||
| 83 | + | gestureAccelDX func(gev uintptr) float64 | |||
| 84 | + | gestureAccelDY func(gev uintptr) float64 | |||
| 78 | gestureUsec func(gev uintptr) uint64 | 85 | gestureUsec func(gev uintptr) uint64 | ||
| 86 | + | gestureScale func(gev uintptr) float64 | |||
| 87 | + | gestureAngle func(gev uintptr) float64 | |||
| 79 | deviceRef func(dev uintptr) uintptr | 88 | deviceRef func(dev uintptr) uintptr | ||
| 80 | deviceUnref func(dev uintptr) uintptr | 89 | deviceUnref func(dev uintptr) uintptr | ||
| 81 | hasNatural func(dev uintptr) int32 | 90 | 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 devices | 340 | // inputState is the device state Run owns: the device config, the devices | ||
| 328 | - | // it configures (referenced until removed) and the swipe in progress on | 341 | + | // it configures (referenced until removed) and the gesture in progress on |
| 329 | - | // each touchpad. dev applies the config to libinput; it is a zero-size | 342 | + | // 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.InputDevicesConfig | 345 | + | cfg ports.InputDevicesConfig |
| 333 | - | dev deviceConfig | 346 | + | dev deviceConfig |
| 334 | - | devices map[uintptr]bool | 347 | + | devices map[uintptr]bool |
| 335 | - | swipes swipes | 348 | + | gestures gestures |
| 336 | } | 349 | } | ||
| 337 | 350 | ||||
| 338 | // deviceConfig is libinput's device configuration side: what a device is | 351 | // 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, which | 494 | + | // gestures streams one touchpad gesture at a time to core: a swipe of any |
| 482 | - | // follows | 495 | + | // 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 sees | 497 | + | // 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 nil | 507 | return nil | ||
| 491 | } | 508 | } | ||
| 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 | + | } | |||
| 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 nil | 533 | 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 nil | 541 | return nil | ||
| 506 | } | 542 | } | ||
| 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 | + | } | |||
| 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), nil | 610 | + | 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))), 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 | |||
| 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))), 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 | |||
| 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))), 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 | |||
| 566 | case evKeyboardKey: | 635 | case evKeyboardKey: | ||
| 567 | k := keyboardEvent(ev) | 636 | k := keyboardEvent(ev) | ||
| 568 | code, pressed := keyboardKey(k), keyboardState(k) == 1 | 637 | 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) { | |||
| 42 | 42 | } | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | - | func TestSwipesIgnoreSecondTouchpad(t *testing.T) { | |
| 46 | - | var s swipes | |
| 45 | + | func TestGesturesIgnoreSecondTouchpad(t *testing.T) { | |
| 46 | + | var g gestures | |
| 47 | 47 | 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 { | |
| 50 | 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 | 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 | 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 | 59 | t.Fatal("first touchpad lost its swipe") | |
| 60 | 60 | } | |
| 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 { | |
| 63 | 63 | t.Fatal("second touchpad cannot swipe after the first ended") | |
| 64 | 64 | } | |
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | - | func TestSwipesStreamThreeFingers(t *testing.T) { | |
| 68 | - | var s swipes | |
| 67 | + | func TestGesturesStreamSwipe(t *testing.T) { | |
| 68 | + | var g gestures | |
| 69 | 69 | 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) | |
| 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 | 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 | 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 | 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 | 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 | 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 | 89 | t.Fatalf("second end: %v", ev) | |
| 90 | 90 | } | |
| 91 | 91 | } | |
| 92 | 92 | ||
| 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) | |
| 97 | 114 | } | |
| 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 | + | } | |
| 100 | 165 | } | |
| 101 | 166 | } | |
| 102 | 167 | ||
| @@ -262,6 +327,7 @@ func TestSecurityInputAllKindsProductionEpoch(t *testing.T) { | |||
| 262 | 327 | ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1}, | |
| 263 | 328 | ports.PointerButton{Button: 272}, ports.PointerAxis{}, | |
| 264 | 329 | ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{}, | |
| 330 | + | ports.PinchBegin{}, ports.PinchUpdate{}, ports.PinchEnd{}, ports.HoldBegin{}, ports.HoldEnd{}, | |
| 265 | 331 | } | |
| 266 | 332 | security.EXPECT().Snapshot().Return(old).Times(len(events)) | |
| 267 | 333 | 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 swipes | 46 | + | var g gestures |
| 47 | const pad, other = 1, 2 | 47 | 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 swipes | 68 | + | var g gestures |
| 69 | const pad, other = 1, 2 | 69 | 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 swipes | 94 | + | 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 { | |||
| 33 | 33 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | |
| 34 | 34 | func() error { return registerSinglePixelBuffer(d, s) }, | |
| 35 | 35 | func() error { return registerPointerConstraints(d, s) }, | |
| 36 | + | func() error { return registerPointerGestures(d, s) }, | |
| 36 | 37 | func() error { return registerTearing(d, s) }, | |
| 37 | 38 | func() error { return registerColorManagement(d, s) }, | |
| 38 | 39 | 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 { | |||
| 23 | 23 | pointerX, pointerY float64 | |
| 24 | 24 | // enters holds the serial of the enter event each wl_pointer received | |
| 25 | 25 | // 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 | |
| 27 | 29 | wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis | |
| 28 | 30 | wheelHeld [2]float64 // axis value held back with it for pre-v8 clients | |
| 29 | 31 | pointers map[server.Client][]*wayland.Pointer | |
| @@ -64,6 +66,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 64 | 66 | target = c.ID | |
| 65 | 67 | case ports.PointerAxisTo: | |
| 66 | 68 | 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 | 75 | case ports.FocusWindow: | |
| 68 | 76 | target = c.ID | |
| 69 | 77 | case ports.ForwardKey: | |
| @@ -142,6 +150,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 142 | 150 | for _, p := range pointers { | |
| 143 | 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 | 159 | case ports.SetKeymap: | |
| 146 | 160 | s.setKeymap(c) | |
| 147 | 161 | case ports.FocusWindow: | |
| @@ -486,6 +500,8 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||
| 486 | 500 | return | |
| 487 | 501 | } | |
| 488 | 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 | 505 | // The new client sets its own cursor on enter; until then, the arrow. | |
| 490 | 506 | s.cursorSurface = nil | |
| 491 | 507 | s.setCursor(ports.CursorChange{}) | |
| @@ -23,7 +23,9 @@ type seatState struct { | |||||
| 23 | pointerX, pointerY float64 | 23 | pointerX, pointerY float64 | ||
| 24 | // enters holds the serial of the enter event each wl_pointer received | 24 | // 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]uint32 | 26 | + | 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 axis | 29 | 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 clients | 30 | wheelHeld [2]float64 // axis value held back with it for pre-v8 clients | ||
| 29 | pointers map[server.Client][]*wayland.Pointer | 31 | pointers map[server.Client][]*wayland.Pointer | ||
| @@ -64,6 +66,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 64 | target = c.ID | 66 | target = c.ID | ||
| 65 | case ports.PointerAxisTo: | 67 | case ports.PointerAxisTo: | ||
| 66 | target = c.ID | 68 | 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.ID | 76 | 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 | return | 500 | 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 = nil | 506 | 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 { | |||
| 256 | 256 | clipDevices []*clipDevice | |
| 257 | 257 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | |
| 258 | 258 | // 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 | |
| 261 | 263 | constraints map[*surface]*constraint | |
| 262 | 264 | constraint *constraint // the active one | |
| 263 | 265 | positioners map[*server.Resource]*positioner | |
| @@ -330,6 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 330 | 332 | contentNotify: make(chan struct{}), | |
| 331 | 333 | regions: map[*server.Resource]*region{}, | |
| 332 | 334 | relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, | |
| 335 | + | gestures: map[server.Client][]*gestureObject{}, | |
| 333 | 336 | constraints: map[*surface]*constraint{}, | |
| 334 | 337 | positioners: map[*server.Resource]*positioner{}, | |
| 335 | 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 | 740 | // Only input/focus commands require an owner envelope. Config relay | |
| 738 | 741 | // also produces SetKeymap and other epoch-independent commands. | |
| 739 | 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 | 744 | return | |
| 742 | 745 | } | |
| 743 | 746 | } | |
| 744 | 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 | 749 | s.applyInput(c) | |
| 747 | 750 | case ports.ConfigurePopup: | |
| 748 | 751 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | |
| @@ -256,8 +256,10 @@ type Server struct { | |||||
| 256 | clipDevices []*clipDevice | 256 | clipDevices []*clipDevice | ||
| 257 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | 257 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | ||
| 258 | // Pointer constraints (constraints.go). | 258 | // Pointer constraints (constraints.go). | ||
| 259 | - | regions map[*server.Resource]*region | 259 | + | regions map[*server.Resource]*region |
| 260 | - | relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1 | 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 | |||
| 261 | constraints map[*surface]*constraint | 263 | constraints map[*surface]*constraint | ||
| 262 | constraint *constraint // the active one | 264 | constraint *constraint // the active one | ||
| 263 | positioners map[*server.Resource]*positioner | 265 | 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 relay | 740 | // 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 | return | 744 | 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 ( | |||
| 32 | 32 | wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf" | |
| 33 | 33 | wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj" | |
| 34 | 34 | wire_pointerconstraints "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |
| 35 | + | wire_pointergestures "github.com/bnema/go-wayland-bindings/server/pointergestures" | |
| 35 | 36 | wire_pointerwarp "github.com/bnema/go-wayland-bindings/server/pointerwarp" | |
| 36 | 37 | wire_presentationtime "github.com/bnema/go-wayland-bindings/server/presentationtime" | |
| 37 | 38 | wire_primaryselection "github.com/bnema/go-wayland-bindings/server/primaryselection" | |
| @@ -239,6 +240,10 @@ var wireSchemas = map[string]wireSchema{ | |||
| 239 | 240 | "zwp_primary_selection_device_v1": {wire_primaryselection.ZwpPrimarySelectionDeviceV1Interface, &client_primaryselection.PrimarySelectionDevice{}, map[uint16]bool{1: true}, nil}, | |
| 240 | 241 | "zwp_primary_selection_offer_v1": {wire_primaryselection.ZwpPrimarySelectionOfferV1Interface, &client_primaryselection.PrimarySelectionOffer{}, map[uint16]bool{1: true}, nil}, | |
| 241 | 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 | 247 | "zwp_relative_pointer_manager_v1": {wire_relativepointer.ZwpRelativePointerManagerV1Interface, &client_relativepointer.RelativePointerManager{}, map[uint16]bool{0: true}, nil}, | |
| 243 | 248 | "zwp_relative_pointer_v1": {wire_relativepointer.ZwpRelativePointerV1Interface, &client_relativepointer.RelativePointer{}, map[uint16]bool{0: true}, nil}, | |
| 244 | 249 | "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 { | |||
| 114 | 114 | c.releaseSlots() | |
| 115 | 115 | if c.pointer == v.ID { | |
| 116 | 116 | c.pointer = 0 | |
| 117 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | |
| 117 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 118 | 118 | return err | |
| 119 | 119 | } | |
| 120 | 120 | } | |
| @@ -114,7 +114,7 @@ func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error { | |||||
| 114 | c.releaseSlots() | 114 | c.releaseSlots() | ||
| 115 | if c.pointer == v.ID { | 115 | if c.pointer == v.ID { | ||
| 116 | c.pointer = 0 | 116 | c.pointer = 0 | ||
| 117 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | 117 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { |
| 118 | return err | 118 | return err | ||
| 119 | } | 119 | } | ||
| 120 | } | 120 | } | ||
A internal/core/clientgesture.go +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 { | |||
| 135 | 135 | // flip came in time to move a running slide; nil when not armed. It is | |
| 136 | 136 | // the channel of frameTimer, made once and reset per frame: an | |
| 137 | 137 | // 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 | |
| 139 | 142 | frameC <-chan time.Time | |
| 140 | 143 | frameTimer ports.Timer | |
| 141 | 144 | // 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 | 629 | if c.pointer != 0 && !c.visible(c.pointer) { | |
| 627 | 630 | c.pointer = 0 | |
| 628 | 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 | 633 | return err | |
| 631 | 634 | } | |
| 632 | 635 | } | |
| @@ -708,7 +711,7 @@ func (c *Core) rehit(ctx context.Context) error { | |||
| 708 | 711 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 709 | 712 | if id != c.pointer { | |
| 710 | 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 | 716 | if id == 0 || c.pointerAt == [2]float64{x, y} { | |
| 714 | 717 | return nil | |
| @@ -135,7 +135,10 @@ type Core struct { | |||||
| 135 | // flip came in time to move a running slide; nil when not armed. It is | 135 | // 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: an | 136 | // 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 *swipeGesture | 138 | + | swipe *swipeGesture |
| 139 | + | // gesture is the touchpad gesture forwarded to a client | |||
| 140 | + | // (clientgesture.go). | |||
| 141 | + | gesture clientGesture | |||
| 139 | frameC <-chan time.Time | 142 | frameC <-chan time.Time | ||
| 140 | frameTimer ports.Timer | 143 | frameTimer ports.Timer | ||
| 141 | // shots is the snapshot of what the screens show before an action | 144 | // 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 = 0 | 630 | 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 err | 633 | 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 nil | 717 | return nil | ||
M internal/core/drag.go +1 -1
| @@ -165,7 +165,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize | |||
| 165 | 165 | c.grab = 0 | |
| 166 | 166 | if c.pointer != 0 { | |
| 167 | 167 | c.pointer = 0 | |
| 168 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | |
| 168 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 169 | 169 | return err | |
| 170 | 170 | } | |
| 171 | 171 | } | |
| @@ -165,7 +165,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize | |||||
| 165 | c.grab = 0 | 165 | c.grab = 0 | ||
| 166 | if c.pointer != 0 { | 166 | if c.pointer != 0 { | ||
| 167 | c.pointer = 0 | 167 | c.pointer = 0 | ||
| 168 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | 168 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { |
| 169 | return err | 169 | return err | ||
| 170 | } | 170 | } | ||
| 171 | } | 171 | } | ||
M internal/core/input.go +23 -1
| @@ -26,18 +26,40 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 26 | 26 | case ports.PointerAxis: | |
| 27 | 27 | return c.pointerAxis(ctx, v) | |
| 28 | 28 | case ports.SwipeBegin: | |
| 29 | + | if forwardsSwipe(v.Fingers) { | |
| 30 | + | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | |
| 31 | + | } | |
| 32 | + | if err := c.gestureAbort(ctx, v.Time); err != nil { | |
| 33 | + | return err | |
| 34 | + | } | |
| 29 | 35 | if c.swipeBegin(v) { | |
| 30 | 36 | return c.slid(ctx, false) | |
| 31 | 37 | } | |
| 32 | 38 | return nil | |
| 33 | 39 | case ports.SwipeUpdate: | |
| 40 | + | if c.gesture.on { | |
| 41 | + | return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.AccelDX, DY: v.AccelDY, Time: v.Time}) | |
| 42 | + | } | |
| 34 | 43 | before := c.cur().mon.Current() | |
| 35 | 44 | if c.swipeUpdate(v) { | |
| 36 | 45 | return c.slid(ctx, c.cur().mon.Current() != before) | |
| 37 | 46 | } | |
| 38 | 47 | return nil | |
| 39 | 48 | case ports.SwipeEnd: | |
| 49 | + | if c.gesture.on && c.gesture.kind == ports.GestureSwipe { | |
| 50 | + | return c.gestureEnd(ctx, ports.GestureSwipe, v.Cancelled, v.Time) | |
| 51 | + | } | |
| 40 | 52 | return c.slid(ctx, c.swipeEnd(v)) | |
| 53 | + | case ports.PinchBegin: | |
| 54 | + | return c.gestureBegin(ctx, ports.GesturePinch, v.Fingers, v.Time) | |
| 55 | + | case ports.PinchUpdate: | |
| 56 | + | return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GesturePinch, DX: v.DX, DY: v.DY, Scale: v.Scale, Rotation: v.Rotation, Time: v.Time}) | |
| 57 | + | case ports.PinchEnd: | |
| 58 | + | return c.gestureEnd(ctx, ports.GesturePinch, v.Cancelled, v.Time) | |
| 59 | + | case ports.HoldBegin: | |
| 60 | + | return c.gestureBegin(ctx, ports.GestureHold, v.Fingers, v.Time) | |
| 61 | + | case ports.HoldEnd: | |
| 62 | + | return c.gestureEnd(ctx, ports.GestureHold, v.Cancelled, v.Time) | |
| 41 | 63 | case ports.KeyEvent: | |
| 42 | 64 | return c.keyEvent(ctx, v) | |
| 43 | 65 | } | |
| @@ -73,7 +95,7 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error { | |||
| 73 | 95 | // Layout changes are intentionally re-hit-tested only on motion. | |
| 74 | 96 | if id != c.pointer { | |
| 75 | 97 | c.pointer = id | |
| 76 | - | if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | |
| 98 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | |
| 77 | 99 | return err | |
| 78 | 100 | } | |
| 79 | 101 | } | |
| @@ -26,18 +26,40 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 26 | case ports.PointerAxis: | 26 | case ports.PointerAxis: | ||
| 27 | return c.pointerAxis(ctx, v) | 27 | return c.pointerAxis(ctx, v) | ||
| 28 | case ports.SwipeBegin: | 28 | case ports.SwipeBegin: | ||
| 29 | + | if forwardsSwipe(v.Fingers) { | |||
| 30 | + | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | |||
| 31 | + | } | |||
| 32 | + | if err := c.gestureAbort(ctx, v.Time); err != nil { | |||
| 33 | + | return err | |||
| 34 | + | } | |||
| 29 | if c.swipeBegin(v) { | 35 | if c.swipeBegin(v) { | ||
| 30 | return c.slid(ctx, false) | 36 | return c.slid(ctx, false) | ||
| 31 | } | 37 | } | ||
| 32 | return nil | 38 | return nil | ||
| 33 | case ports.SwipeUpdate: | 39 | case ports.SwipeUpdate: | ||
| 40 | + | if c.gesture.on { | |||
| 41 | + | return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.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 nil | 47 | return nil | ||
| 39 | case ports.SwipeEnd: | 48 | case ports.SwipeEnd: | ||
| 49 | + | if c.gesture.on && c.gesture.kind == ports.GestureSwipe { | |||
| 50 | + | return c.gestureEnd(ctx, ports.GestureSwipe, v.Cancelled, v.Time) | |||
| 51 | + | } | |||
| 40 | return c.slid(ctx, c.swipeEnd(v)) | 52 | return c.slid(ctx, c.swipeEnd(v)) | ||
| 53 | + | case ports.PinchBegin: | |||
| 54 | + | return c.gestureBegin(ctx, ports.GesturePinch, v.Fingers, v.Time) | |||
| 55 | + | case ports.PinchUpdate: | |||
| 56 | + | return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GesturePinch, DX: v.DX, DY: v.DY, Scale: v.Scale, Rotation: v.Rotation, Time: v.Time}) | |||
| 57 | + | case ports.PinchEnd: | |||
| 58 | + | return c.gestureEnd(ctx, ports.GesturePinch, v.Cancelled, v.Time) | |||
| 59 | + | case ports.HoldBegin: | |||
| 60 | + | return c.gestureBegin(ctx, ports.GestureHold, v.Fingers, v.Time) | |||
| 61 | + | case ports.HoldEnd: | |||
| 62 | + | return c.gestureEnd(ctx, ports.GestureHold, v.Cancelled, v.Time) | |||
| 41 | case ports.KeyEvent: | 63 | case ports.KeyEvent: | ||
| 42 | return c.keyEvent(ctx, v) | 64 | return c.keyEvent(ctx, v) | ||
| 43 | } | 65 | } | ||
| @@ -73,7 +95,7 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error { | |||||
| 73 | // Layout changes are intentionally re-hit-tested only on motion. | 95 | // Layout changes are intentionally re-hit-tested only on motion. | ||
| 74 | if id != c.pointer { | 96 | if id != c.pointer { | ||
| 75 | c.pointer = id | 97 | c.pointer = id | ||
| 76 | - | if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | 98 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { |
| 77 | return err | 99 | return err | ||
| 78 | } | 100 | } | ||
| 79 | } | 101 | } | ||
M internal/core/security.go +6 -3
| @@ -41,6 +41,9 @@ func (c *Core) syncSecurity() bool { | |||
| 41 | 41 | } | |
| 42 | 42 | c.dropSwipe() | |
| 43 | 43 | c.swipe = nil | |
| 44 | + | // The gesture ends with the pointer focus: wayland cancels it when the | |
| 45 | + | // protected epoch clears that focus. | |
| 46 | + | c.gesture = clientGesture{} | |
| 44 | 47 | c.stopAnimations() | |
| 45 | 48 | c.stopPulse() | |
| 46 | 49 | if state.Protected { | |
| @@ -64,7 +67,7 @@ func (c *Core) securityCheckpoint(ctx context.Context) error { | |||
| 64 | 67 | return nil | |
| 65 | 68 | } | |
| 66 | 69 | // Clear display focus even if the new epoch has no mapped lock surfaces. | |
| 67 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | |
| 70 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 68 | 71 | return err | |
| 69 | 72 | } | |
| 70 | 73 | if err := c.command(ctx, ports.FocusWindow{}); err != nil { | |
| @@ -204,7 +207,7 @@ func (c *Core) publishProtected(ctx context.Context) error { | |||
| 204 | 207 | if !c.validLockID(c.pointer) { | |
| 205 | 208 | c.pointer = 0 | |
| 206 | 209 | } | |
| 207 | - | if err := c.command(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil { | |
| 210 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil { | |
| 208 | 211 | return err | |
| 209 | 212 | } | |
| 210 | 213 | latest(c.ch.Scenes, scenes) | |
| @@ -259,7 +262,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 259 | 262 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | |
| 260 | 263 | if id != c.pointer { | |
| 261 | 264 | c.pointer = id | |
| 262 | - | if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | |
| 265 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | |
| 263 | 266 | return err | |
| 264 | 267 | } | |
| 265 | 268 | } | |
| @@ -41,6 +41,9 @@ func (c *Core) syncSecurity() bool { | |||||
| 41 | } | 41 | } | ||
| 42 | c.dropSwipe() | 42 | c.dropSwipe() | ||
| 43 | c.swipe = nil | 43 | c.swipe = nil | ||
| 44 | + | // The gesture ends with the pointer focus: wayland cancels it when the | |||
| 45 | + | // protected epoch clears that focus. | |||
| 46 | + | c.gesture = clientGesture{} | |||
| 44 | c.stopAnimations() | 47 | c.stopAnimations() | ||
| 45 | c.stopPulse() | 48 | c.stopPulse() | ||
| 46 | if state.Protected { | 49 | if state.Protected { | ||
| @@ -64,7 +67,7 @@ func (c *Core) securityCheckpoint(ctx context.Context) error { | |||||
| 64 | return nil | 67 | return nil | ||
| 65 | } | 68 | } | ||
| 66 | // Clear display focus even if the new epoch has no mapped lock surfaces. | 69 | // Clear display focus even if the new epoch has no mapped lock surfaces. | ||
| 67 | - | if err := c.command(ctx, ports.PointerFocus{}); err != nil { | 70 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { |
| 68 | return err | 71 | return err | ||
| 69 | } | 72 | } | ||
| 70 | if err := c.command(ctx, ports.FocusWindow{}); err != nil { | 73 | if err := c.command(ctx, ports.FocusWindow{}); err != nil { | ||
| @@ -204,7 +207,7 @@ func (c *Core) publishProtected(ctx context.Context) error { | |||||
| 204 | if !c.validLockID(c.pointer) { | 207 | if !c.validLockID(c.pointer) { | ||
| 205 | c.pointer = 0 | 208 | c.pointer = 0 | ||
| 206 | } | 209 | } | ||
| 207 | - | if err := c.command(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil { | 210 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: c.pointer, X: c.pointerAt[0], Y: c.pointerAt[1]}); err != nil { |
| 208 | return err | 211 | return err | ||
| 209 | } | 212 | } | ||
| 210 | latest(c.ch.Scenes, scenes) | 213 | latest(c.ch.Scenes, scenes) | ||
| @@ -259,7 +262,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 259 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | 262 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | ||
| 260 | if id != c.pointer { | 263 | if id != c.pointer { | ||
| 261 | c.pointer = id | 264 | c.pointer = id | ||
| 262 | - | if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { | 265 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil { |
| 263 | return err | 266 | return err | ||
| 264 | } | 267 | } | ||
| 265 | } | 268 | } | ||
M internal/ports/client_commands.go +49 -0
| @@ -192,6 +192,55 @@ type PointerAxisTo struct { | |||
| 192 | 192 | ||
| 193 | 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 | 244 | // ForwardKey carries core → wayland unbound keys. | |
| 196 | 245 | type ForwardKey struct { | |
| 197 | 246 | 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 WindowID | 246 | ID WindowID | ||
M internal/ports/input_events.go +56 -5
| @@ -88,18 +88,23 @@ type PointerAxis struct { | |||
| 88 | 88 | ||
| 89 | 89 | func (PointerAxis) inputEvent() {} | |
| 90 | 90 | ||
| 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. | |
| 93 | 95 | type SwipeBegin struct { | |
| 94 | 96 | Fingers int | |
| 95 | 97 | Time time.Duration | |
| 96 | 98 | } | |
| 97 | 99 | ||
| 98 | 100 | // 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 | 104 | type SwipeUpdate struct { | |
| 101 | - | DX, DY float64 | |
| 102 | - | Time time.Duration | |
| 105 | + | DX, DY float64 | |
| 106 | + | AccelDX, AccelDY float64 | |
| 107 | + | Time time.Duration | |
| 103 | 108 | } | |
| 104 | 109 | ||
| 105 | 110 | // SwipeEnd lifts the fingers. Cancelled is set when libinput cancelled | |
| @@ -112,3 +117,49 @@ type SwipeEnd struct { | |||
| 112 | 117 | func (SwipeBegin) inputEvent() {} | |
| 113 | 118 | func (SwipeUpdate) inputEvent() {} | |
| 114 | 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() {} | |
| @@ -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 the | 91 | + | // 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 int | 96 | Fingers int | ||
| 95 | Time time.Duration | 97 | Time time.Duration | ||
| 96 | } | 98 | } | ||
| 97 | 99 | ||||
| 98 | // SwipeUpdate moves the fingers by the unaccelerated deltas DX, DY | 100 | // 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 float64 | 105 | + | DX, DY float64 |
| 102 | - | Time time.Duration | 106 | + | AccelDX, AccelDY float64 |
| 107 | + | Time time.Duration | |||
| 103 | } | 108 | } | ||
| 104 | 109 | ||||
| 105 | // SwipeEnd lifts the fingers. Cancelled is set when libinput cancelled | 110 | // 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() {} | |||