diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index 8491708fa372f..37e144d8e2590 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -48,7 +48,8 @@ extern NSNotificationName const FlutterSemanticsUpdateNotification; * FlutterViewController and other `UIViewController`s. */ FLUTTER_EXPORT -@interface FlutterViewController : UIViewController +@interface FlutterViewController + : UIViewController /** * Initializes this FlutterViewController with the specified `FlutterEngine`. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 7f593583a54c8..a0cffaace53b3 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -34,6 +34,28 @@ NSNotificationName const FlutterViewControllerShowHomeIndicator = @"FlutterViewControllerShowHomeIndicator"; +// Struct holding the mouse state. The engine doesn't keep track of which +// mouse buttons have been pressed, so it's the embedding's responsibility. +typedef struct MouseState { + // True if the last event sent to Flutter had at least one mouse button. + // pressed. + bool flutter_state_is_down = false; + + // True if kAdd has been sent to Flutter. Used to determine whether + // to send a kAdd event before sending an incoming mouse event, since + // Flutter expects pointers to be added before events are sent for them. + bool flutter_state_is_added = false; + + // Current coordinate of the mouse cursor in physical device pixels. + CGPoint location = CGPointZero; + + // Last reported translation for an in-flight pan gesture in physical device pixels. + CGPoint last_translation = CGPointZero; + + // The currently pressed buttons, as represented in FlutterPointerEvent. + uint64_t buttons = 0; +} MouseState; + // This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the // change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are // just a warning. @@ -78,6 +100,9 @@ @implementation FlutterViewController { // UIScrollView with height zero and a content offset so we can get those events. See also: // https://github.com/flutter/flutter/issues/35050 fml::scoped_nsobject _scrollView; + fml::scoped_nsobject _pointerInteraction API_AVAILABLE(ios(13.4)); + fml::scoped_nsobject _panGestureRecognizer API_AVAILABLE(ios(13.4)); + MouseState _mouseState; } @synthesize displayingFlutterUI = _displayingFlutterUI; @@ -603,6 +628,17 @@ - (void)viewDidLoad { [_engine.get() attachView]; + if (@available(iOS 13.4, *)) { + _pointerInteraction.reset([[UIPointerInteraction alloc] initWithDelegate:self]); + [self.view addInteraction:_pointerInteraction]; + + _panGestureRecognizer.reset( + [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(scrollEvent:)]); + _panGestureRecognizer.get().allowedScrollTypesMask = UIScrollTypeMaskAll; + _panGestureRecognizer.get().allowedTouchTypes = @[ @(UITouchTypeIndirectPointer) ]; + [_flutterView.get() addGestureRecognizer:_panGestureRecognizer.get()]; + } + [super viewDidLoad]; } @@ -759,8 +795,9 @@ - (void)goToApplicationLifecycle:(nonnull NSString*)state { return flutter::PointerData::DeviceKind::kTouch; case UITouchTypeStylus: return flutter::PointerData::DeviceKind::kStylus; + case UITouchTypeIndirectPointer: + return flutter::PointerData::DeviceKind::kMouse; default: - // TODO(53696): Handle the UITouchTypeIndirectPointer enum value. FML_DLOG(INFO) << "Unhandled touch type: " << touch.type; break; } @@ -1423,4 +1460,67 @@ - (BOOL)isPresentingViewController { return self.presentedViewController != nil || self.isPresentingViewControllerAnimating; } +- (flutter::PointerData)generatePointerDataForMouse API_AVAILABLE(ios(13.4)) { + flutter::PointerData pointer_data; + + pointer_data.Clear(); + + pointer_data.kind = flutter::PointerData::DeviceKind::kMouse; + pointer_data.change = _mouseState.flutter_state_is_added ? flutter::PointerData::Change::kAdd + : flutter::PointerData::Change::kHover; + pointer_data.pointer_identifier = reinterpret_cast(_pointerInteraction.get()); + + pointer_data.physical_x = _mouseState.location.x; + pointer_data.physical_y = _mouseState.location.y; + + return pointer_data; +} + +- (UIPointerRegion*)pointerInteraction:(UIPointerInteraction*)interaction + regionForRequest:(UIPointerRegionRequest*)request + defaultRegion:(UIPointerRegion*)defaultRegion API_AVAILABLE(ios(13.4)) { + if (request != nil) { + auto packet = std::make_unique(1); + const CGFloat scale = [UIScreen mainScreen].scale; + _mouseState.location = {request.location.x * scale, request.location.y * scale}; + + flutter::PointerData pointer_data = [self generatePointerDataForMouse]; + + pointer_data.signal_kind = flutter::PointerData::SignalKind::kNone; + packet->SetPointerData(/*index=*/0, pointer_data); + + [_engine.get() dispatchPointerDataPacket:std::move(packet)]; + } + return nil; +} + +- (void)scrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) { + CGPoint translation = [recognizer translationInView:self.view]; + const CGFloat scale = [UIScreen mainScreen].scale; + + translation.x *= scale; + translation.y *= scale; + + auto packet = std::make_unique(1); + + flutter::PointerData pointer_data = [self generatePointerDataForMouse]; + pointer_data.signal_kind = flutter::PointerData::SignalKind::kScroll; + pointer_data.scroll_delta_x = (translation.x - _mouseState.last_translation.x); + pointer_data.scroll_delta_y = -(translation.y - _mouseState.last_translation.y); + + // The translation reported by UIPanGestureRecognizer is the total translation + // generated by the pan gesture since the gesture began. We need to be able + // to keep track of the last translation value in order to generate the deltaX + // and deltaY coordinates for each subsequent scroll event. + if (recognizer.state != UIGestureRecognizerStateEnded) { + _mouseState.last_translation = translation; + } else { + _mouseState.last_translation = CGPointZero; + } + + packet->SetPointerData(/*index=*/0, pointer_data); + + [_engine.get() dispatchPointerDataPacket:std::move(packet)]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 48a1c1ee2cbd1..a70387182e185 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -12,10 +12,15 @@ FLUTTER_ASSERT_ARC +namespace flutter { +class PointerDataPacket {}; +} + @interface FlutterEngine () - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI initialRoute:(NSString*)initialRoute; +- (void)dispatchPointerDataPacket:(std::unique_ptr)packet; @end @interface FlutterEngine (TestLowMemory) @@ -64,6 +69,7 @@ @interface FlutterViewController (Tests) - (void)surfaceUpdated:(BOOL)appeared; - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences; - (void)dispatchPresses:(NSSet*)presses; +- (void)scrollEvent:(UIPanGestureRecognizer*)recognizer; @end @implementation FlutterViewControllerTest @@ -645,6 +651,53 @@ - (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) { [keyEventChannel stopMocking]; } +- (void)testPanGestureRecognizer API_AVAILABLE(ios(13.4)) { + if (@available(iOS 13.4, *)) { + // noop + } else { + return; + } + + FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine + nibName:nil + bundle:nil]; + XCTAssertNotNil(vc); + UIView* view = vc.view; + XCTAssertNotNil(view); + NSArray* gestureRecognizers = view.gestureRecognizers; + XCTAssertNotNil(gestureRecognizers); + + BOOL found = NO; + for (id gesture in gestureRecognizers) { + if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) { + found = YES; + break; + } + } + XCTAssertTrue(found); +} + +- (void)testMouseSupport API_AVAILABLE(ios(13.4)) { + if (@available(iOS 13.4, *)) { + // noop + } else { + return; + } + + FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine + nibName:nil + bundle:nil]; + XCTAssertNotNil(vc); + + id mockPanGestureRecognizer = OCMClassMock([UIPanGestureRecognizer class]); + XCTAssertNotNil(mockPanGestureRecognizer); + + [vc scrollEvent:mockPanGestureRecognizer]; + + [[[self.mockEngine verify] ignoringNonObjectArgs] + dispatchPointerDataPacket:std::make_unique()]; +} + - (NSSet*)fakeUiPressSetForPhase:(UIPressPhase)phase keyCode:(UIKeyboardHIDUsage)keyCode modifierFlags:(UIKeyModifierFlags)modifierFlags