Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Fix inertia cancel event on macOS Ventura #37067

Merged
merged 3 commits into from
Nov 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
static constexpr int32_t kMousePointerDeviceId = 0;
static constexpr int32_t kPointerPanZoomDeviceId = 1;

// A trackpad touch following inertial scrolling should cause an inertia cancel
// event to be issued. Use a window of 50 milliseconds after the scroll to account
// for delays in event propagation observed in macOS Ventura.
static constexpr double kTrackpadTouchInertiaCancelWindowMs = 0.050;

/**
* State tracking for mouse events, to adapt between the events coming from the system and the
* events that the embedding API expects.
Expand Down Expand Up @@ -91,9 +96,9 @@
bool rotate_gesture_active = false;

/**
* System scroll inertia is currently sending us events.
* Time of last scroll momentum event.
*/
bool system_scroll_inertia_active = false;
NSTimeInterval last_scroll_momentum_changed_time = 0;

/**
* Resets all gesture state to default values.
Expand Down Expand Up @@ -521,11 +526,11 @@ - (void)dispatchGestureEvent:(nonnull NSEvent*)event {
} else if (event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone) {
[self dispatchMouseEvent:event phase:kHover];
} else {
if (event.momentumPhase == NSEventPhaseBegan) {
_mouseState.system_scroll_inertia_active = true;
} else if (event.momentumPhase == NSEventPhaseEnded ||
event.momentumPhase == NSEventPhaseCancelled) {
_mouseState.system_scroll_inertia_active = false;
// Waiting until the first momentum change event is a workaround for an issue where
// touchesBegan: is called unexpectedly while in low power mode within the interval between
// momentum start and the first momentum change.
if (event.momentumPhase == NSEventPhaseChanged) {
_mouseState.last_scroll_momentum_changed_time = event.timestamp;
}
// Skip momentum update events, the framework will generate scroll momentum.
NSAssert(event.momentumPhase != NSEventPhaseNone,
Expand All @@ -549,6 +554,8 @@ - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
_mouseState.rotate_gesture_active;
if (event.type == NSEventTypeScrollWheel) {
_mouseState.pan_gesture_active = true;
// Ensure scroll inertia cancel event is not sent afterwards.
_mouseState.last_scroll_momentum_changed_time = 0;
} else if (event.type == NSEventTypeMagnify) {
_mouseState.scale_gesture_active = true;
} else if (event.type == NSEventTypeRotate) {
Expand Down Expand Up @@ -841,8 +848,9 @@ - (void)swipeWithEvent:(NSEvent*)event {
- (void)touchesBeganWithEvent:(NSEvent*)event {
NSTouch* touch = event.allTouches.anyObject;
if (touch != nil) {
if (_mouseState.system_scroll_inertia_active) {
// The trackpad has been touched and a scroll gesture is still sending inertia events.
if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) <
kTrackpadTouchInertiaCancelWindowMs) {
// The trackpad has been touched following a scroll momentum event.
// A scroll inertia cancel message should be sent to the framework.
NSPoint locationInView = [self.flutterView convertPoint:event.locationInWindow fromView:nil];
NSPoint locationInBackingCoordinates =
Expand All @@ -858,6 +866,8 @@ - (void)touchesBeganWithEvent:(NSEvent*)event {
};

[_engine sendPointerEvent:flutterEvent];
// Ensure no further scroll inertia cancel event will be sent.
_mouseState.last_scroll_momentum_changed_time = 0;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,17 +541,39 @@ - (bool)testTrackpadGesturesAreSentToFramework {
[viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumStart]];
EXPECT_FALSE(called);

// Advance system momentum.
CGEventRef cgEventMomentumUpdate = CGEventCreateCopy(cgEventStart);
CGEventSetIntegerValueField(cgEventMomentumUpdate, kCGScrollWheelEventScrollPhase, 0);
CGEventSetIntegerValueField(cgEventMomentumUpdate, kCGScrollWheelEventMomentumPhase,
kCGMomentumScrollPhaseContinue);

called = false;
[viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumUpdate]];
EXPECT_FALSE(called);

// Mock a touch on the trackpad.
id touchMock = OCMClassMock([NSTouch class]);
NSSet* touchSet = [NSSet setWithObject:touchMock];
id touchEventMock = OCMClassMock([NSEvent class]);
OCMStub([touchEventMock allTouches]).andReturn(touchSet);
id touchEventMock1 = OCMClassMock([NSEvent class]);
OCMStub([touchEventMock1 allTouches]).andReturn(touchSet);
CGPoint touchLocation = {0, 0};
OCMStub([touchEventMock locationInWindow]).andReturn(touchLocation);
OCMStub([touchEventMock1 locationInWindow]).andReturn(touchLocation);
OCMStub([(NSEvent*)touchEventMock1 timestamp]).andReturn(0.150); // 150 milliseconds.

// Scroll inertia cancel event should not be issued (timestamp too far in the future).
called = false;
[viewController touchesBeganWithEvent:touchEventMock1];
EXPECT_FALSE(called);

// Mock another touch on the trackpad.
id touchEventMock2 = OCMClassMock([NSEvent class]);
OCMStub([touchEventMock2 allTouches]).andReturn(touchSet);
OCMStub([touchEventMock2 locationInWindow]).andReturn(touchLocation);
OCMStub([(NSEvent*)touchEventMock2 timestamp]).andReturn(0.005); // 5 milliseconds.

// Scroll inertia cancel event should be issued.
called = false;
[viewController touchesBeganWithEvent:touchEventMock];
[viewController touchesBeganWithEvent:touchEventMock2];
EXPECT_TRUE(called);
EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindScrollInertiaCancel);
EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
Expand All @@ -566,11 +588,6 @@ - (bool)testTrackpadGesturesAreSentToFramework {
[viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumEnd]];
EXPECT_FALSE(called);

// Scroll inertia cancel event should not be issued after momentum has ended.
called = false;
[viewController touchesBeganWithEvent:touchEventMock];
EXPECT_FALSE(called);

// May-begin and cancel are used while macOS determines which type of gesture to choose.
CGEventRef cgEventMayBegin = CGEventCreateCopy(cgEventStart);
CGEventSetIntegerValueField(cgEventMayBegin, kCGScrollWheelEventScrollPhase,
Expand Down