From e016cf4d04e505dc04dff03c99fa0e5b01bdae2f Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Wed, 17 Apr 2024 21:25:18 +0200 Subject: [PATCH] [macOS] Handle interleaved movement and gesture events (flutter/engine#52201) Fixes https://github.com/flutter/flutter/issues/146893 The crash happens, because with magic mouse, it is quite common to receive `mouseExited:` event while the gesture event is still in progress. This would reset the gesture state while the gesture event is still active, leading to assertion when the gesture event ends. The state machine assumes that when mouse exists that also ends any gesture events. However that is not the case with magic mouse, which handles the movement and gesture events separately. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making or feature I am adding, or the PR is [test-exempt]. See [testing the engine] for instructions on writing and running engine tests. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I signed the [CLA]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat --- .../framework/Source/FlutterViewController.mm | 1 - .../Source/FlutterViewControllerTest.mm | 43 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 291340b3764..9928fb03c40 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -129,7 +129,6 @@ struct MouseState { flutter_state_is_down = false; has_pending_exit = false; buttons = 0; - GestureReset(); } }; diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm index aa0a729b898..1d630e03001 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -101,6 +101,7 @@ - (bool)testFlagsChangedEventsArePropagatedIfNotHandled:(id)mockEngine; - (bool)testKeyboardIsRestartedOnEngineRestart:(id)mockEngine; - (bool)testTrackpadGesturesAreSentToFramework:(id)mockEngine; +- (bool)mouseAndGestureEventsAreHandledSeparately:(id)engineMock; - (bool)testMouseDownUpEventsSentToNextResponder:(id)mockEngine; - (bool)testModifierKeysAreSynthesizedOnMouseMove:(id)mockEngine; - (bool)testViewWillAppearCalledMultipleTimes:(id)mockEngine; @@ -287,6 +288,12 @@ TEST_F(FlutterViewControllerMockEngineTest, TestTrackpadGesturesAreSentToFramewo [[FlutterViewControllerTestObjC alloc] testTrackpadGesturesAreSentToFramework:mockEngine]); } +TEST_F(FlutterViewControllerMockEngineTest, TestmouseAndGestureEventsAreHandledSeparately) { + id mockEngine = GetMockEngine(); + ASSERT_TRUE( + [[FlutterViewControllerTestObjC alloc] mouseAndGestureEventsAreHandledSeparately:mockEngine]); +} + TEST_F(FlutterViewControllerMockEngineTest, TestMouseDownUpEventsSentToNextResponder) { id mockEngine = GetMockEngine(); ASSERT_TRUE( @@ -1004,6 +1011,42 @@ TEST_F(FlutterViewControllerTest, testViewControllerIsReleased) { return true; } +// Magic mouse can interleave mouse events with scroll events. This must not crash. +- (bool)mouseAndGestureEventsAreHandledSeparately:(id)engineMock { + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + [viewController loadView]; + + // Test for pan events. + // Start gesture. + CGEventRef cgEventStart = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 1, 0); + CGEventSetType(cgEventStart, kCGEventScrollWheel); + CGEventSetIntegerValueField(cgEventStart, kCGScrollWheelEventScrollPhase, kCGScrollPhaseBegan); + CGEventSetIntegerValueField(cgEventStart, kCGScrollWheelEventIsContinuous, 1); + [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventStart]]; + CFRelease(cgEventStart); + + CGEventRef cgEventUpdate = CGEventCreateCopy(cgEventStart); + CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventScrollPhase, kCGScrollPhaseChanged); + CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventDeltaAxis2, 1); // pan_x + CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventDeltaAxis1, 2); // pan_y + [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventUpdate]]; + CFRelease(cgEventUpdate); + + NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00); + [viewController mouseEntered:mouseEvent]; + [viewController mouseExited:mouseEvent]; + + // End gesture. + CGEventRef cgEventEnd = CGEventCreateCopy(cgEventStart); + CGEventSetIntegerValueField(cgEventEnd, kCGScrollWheelEventScrollPhase, kCGScrollPhaseEnded); + [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventEnd]]; + CFRelease(cgEventEnd); + + return true; +} + - (bool)testViewWillAppearCalledMultipleTimes:(id)engineMock { FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock nibName:@""