Skip to content

Commit c931fb4

Browse files
moffatmanschwa423
authored andcommitted
Fix inertia cancel event on macOS Ventura (flutter#37067)
* Fix inertia cancel event on macOS Ventura * Increase allowed time delay on event to 50 ms * Address feedback
1 parent 9dc71c6 commit c931fb4

File tree

2 files changed

+45
-18
lines changed

2 files changed

+45
-18
lines changed

shell/platform/darwin/macos/framework/Source/FlutterViewController.mm

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
static constexpr int32_t kMousePointerDeviceId = 0;
2929
static constexpr int32_t kPointerPanZoomDeviceId = 1;
3030

31+
// A trackpad touch following inertial scrolling should cause an inertia cancel
32+
// event to be issued. Use a window of 50 milliseconds after the scroll to account
33+
// for delays in event propagation observed in macOS Ventura.
34+
static constexpr double kTrackpadTouchInertiaCancelWindowMs = 0.050;
35+
3136
/**
3237
* State tracking for mouse events, to adapt between the events coming from the system and the
3338
* events that the embedding API expects.
@@ -91,9 +96,9 @@
9196
bool rotate_gesture_active = false;
9297

9398
/**
94-
* System scroll inertia is currently sending us events.
99+
* Time of last scroll momentum event.
95100
*/
96-
bool system_scroll_inertia_active = false;
101+
NSTimeInterval last_scroll_momentum_changed_time = 0;
97102

98103
/**
99104
* Resets all gesture state to default values.
@@ -521,11 +526,11 @@ - (void)dispatchGestureEvent:(nonnull NSEvent*)event {
521526
} else if (event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone) {
522527
[self dispatchMouseEvent:event phase:kHover];
523528
} else {
524-
if (event.momentumPhase == NSEventPhaseBegan) {
525-
_mouseState.system_scroll_inertia_active = true;
526-
} else if (event.momentumPhase == NSEventPhaseEnded ||
527-
event.momentumPhase == NSEventPhaseCancelled) {
528-
_mouseState.system_scroll_inertia_active = false;
529+
// Waiting until the first momentum change event is a workaround for an issue where
530+
// touchesBegan: is called unexpectedly while in low power mode within the interval between
531+
// momentum start and the first momentum change.
532+
if (event.momentumPhase == NSEventPhaseChanged) {
533+
_mouseState.last_scroll_momentum_changed_time = event.timestamp;
529534
}
530535
// Skip momentum update events, the framework will generate scroll momentum.
531536
NSAssert(event.momentumPhase != NSEventPhaseNone,
@@ -549,6 +554,8 @@ - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
549554
_mouseState.rotate_gesture_active;
550555
if (event.type == NSEventTypeScrollWheel) {
551556
_mouseState.pan_gesture_active = true;
557+
// Ensure scroll inertia cancel event is not sent afterwards.
558+
_mouseState.last_scroll_momentum_changed_time = 0;
552559
} else if (event.type == NSEventTypeMagnify) {
553560
_mouseState.scale_gesture_active = true;
554561
} else if (event.type == NSEventTypeRotate) {
@@ -841,8 +848,9 @@ - (void)swipeWithEvent:(NSEvent*)event {
841848
- (void)touchesBeganWithEvent:(NSEvent*)event {
842849
NSTouch* touch = event.allTouches.anyObject;
843850
if (touch != nil) {
844-
if (_mouseState.system_scroll_inertia_active) {
845-
// The trackpad has been touched and a scroll gesture is still sending inertia events.
851+
if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) <
852+
kTrackpadTouchInertiaCancelWindowMs) {
853+
// The trackpad has been touched following a scroll momentum event.
846854
// A scroll inertia cancel message should be sent to the framework.
847855
NSPoint locationInView = [self.flutterView convertPoint:event.locationInWindow fromView:nil];
848856
NSPoint locationInBackingCoordinates =
@@ -858,6 +866,8 @@ - (void)touchesBeganWithEvent:(NSEvent*)event {
858866
};
859867

860868
[_engine sendPointerEvent:flutterEvent];
869+
// Ensure no further scroll inertia cancel event will be sent.
870+
_mouseState.last_scroll_momentum_changed_time = 0;
861871
}
862872
}
863873
}

shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -541,17 +541,39 @@ - (bool)testTrackpadGesturesAreSentToFramework {
541541
[viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumStart]];
542542
EXPECT_FALSE(called);
543543

544+
// Advance system momentum.
545+
CGEventRef cgEventMomentumUpdate = CGEventCreateCopy(cgEventStart);
546+
CGEventSetIntegerValueField(cgEventMomentumUpdate, kCGScrollWheelEventScrollPhase, 0);
547+
CGEventSetIntegerValueField(cgEventMomentumUpdate, kCGScrollWheelEventMomentumPhase,
548+
kCGMomentumScrollPhaseContinue);
549+
550+
called = false;
551+
[viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumUpdate]];
552+
EXPECT_FALSE(called);
553+
544554
// Mock a touch on the trackpad.
545555
id touchMock = OCMClassMock([NSTouch class]);
546556
NSSet* touchSet = [NSSet setWithObject:touchMock];
547-
id touchEventMock = OCMClassMock([NSEvent class]);
548-
OCMStub([touchEventMock allTouches]).andReturn(touchSet);
557+
id touchEventMock1 = OCMClassMock([NSEvent class]);
558+
OCMStub([touchEventMock1 allTouches]).andReturn(touchSet);
549559
CGPoint touchLocation = {0, 0};
550-
OCMStub([touchEventMock locationInWindow]).andReturn(touchLocation);
560+
OCMStub([touchEventMock1 locationInWindow]).andReturn(touchLocation);
561+
OCMStub([(NSEvent*)touchEventMock1 timestamp]).andReturn(0.150); // 150 milliseconds.
562+
563+
// Scroll inertia cancel event should not be issued (timestamp too far in the future).
564+
called = false;
565+
[viewController touchesBeganWithEvent:touchEventMock1];
566+
EXPECT_FALSE(called);
567+
568+
// Mock another touch on the trackpad.
569+
id touchEventMock2 = OCMClassMock([NSEvent class]);
570+
OCMStub([touchEventMock2 allTouches]).andReturn(touchSet);
571+
OCMStub([touchEventMock2 locationInWindow]).andReturn(touchLocation);
572+
OCMStub([(NSEvent*)touchEventMock2 timestamp]).andReturn(0.005); // 5 milliseconds.
551573

552574
// Scroll inertia cancel event should be issued.
553575
called = false;
554-
[viewController touchesBeganWithEvent:touchEventMock];
576+
[viewController touchesBeganWithEvent:touchEventMock2];
555577
EXPECT_TRUE(called);
556578
EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindScrollInertiaCancel);
557579
EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
@@ -566,11 +588,6 @@ - (bool)testTrackpadGesturesAreSentToFramework {
566588
[viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumEnd]];
567589
EXPECT_FALSE(called);
568590

569-
// Scroll inertia cancel event should not be issued after momentum has ended.
570-
called = false;
571-
[viewController touchesBeganWithEvent:touchEventMock];
572-
EXPECT_FALSE(called);
573-
574591
// May-begin and cancel are used while macOS determines which type of gesture to choose.
575592
CGEventRef cgEventMayBegin = CGEventCreateCopy(cgEventStart);
576593
CGEventSetIntegerValueField(cgEventMayBegin, kCGScrollWheelEventScrollPhase,

0 commit comments

Comments
 (0)