From d960962a564f8348b32b36e298b9ecad89e6bc17 Mon Sep 17 00:00:00 2001 From: Callum Moffat Date: Thu, 27 Oct 2022 01:06:20 -0400 Subject: [PATCH 1/3] Fix inertia cancel event on macOS Ventura --- .../framework/Source/FlutterViewController.mm | 22 +++++++----- .../Source/FlutterViewControllerTest.mm | 35 ++++++++++++++----- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 7c40c8bb0f2fa..b22afb739cdb1 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -91,9 +91,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. @@ -521,11 +521,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, @@ -549,6 +549,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) { @@ -841,8 +843,8 @@ - (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) < 0.010) { + // The trackpad has been touched within 10 ms 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 = @@ -858,6 +860,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; } } } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm index eb373819a8ff9..9ec0f3aec65bb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -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); @@ -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, From 145c2ab2f7b1e92d7f062bb132f04ed2c07da74b Mon Sep 17 00:00:00 2001 From: Callum Moffat Date: Fri, 28 Oct 2022 17:21:09 -0400 Subject: [PATCH 2/3] Increase allowed time delay on event to 50 ms --- .../darwin/macos/framework/Source/FlutterViewController.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index b22afb739cdb1..cce60f475c524 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -843,8 +843,8 @@ - (void)swipeWithEvent:(NSEvent*)event { - (void)touchesBeganWithEvent:(NSEvent*)event { NSTouch* touch = event.allTouches.anyObject; if (touch != nil) { - if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) < 0.010) { - // The trackpad has been touched within 10 ms following a scroll momentum event. + if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) < 0.050) { + // The trackpad has been touched within 50 ms 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 = From fccefae05758fccfa66596dc44740b51de79eafe Mon Sep 17 00:00:00 2001 From: Callum Moffat Date: Sat, 12 Nov 2022 18:23:54 -0500 Subject: [PATCH 3/3] Address feedback --- .../macos/framework/Source/FlutterViewController.mm | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index cce60f475c524..b6ffe70bea8ac 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -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. @@ -843,8 +848,9 @@ - (void)swipeWithEvent:(NSEvent*)event { - (void)touchesBeganWithEvent:(NSEvent*)event { NSTouch* touch = event.allTouches.anyObject; if (touch != nil) { - if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) < 0.050) { - // The trackpad has been touched within 50 ms following a scroll momentum event. + 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 =