diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index 6f434af1047f7..c44774885a302 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -47,8 +47,13 @@ extern NSNotificationName const FlutterSemanticsUpdateNotification; * Dart-related state and asynchronous tasks when navigating back and forth between a * FlutterViewController and other `UIViewController`s. */ +#ifdef __IPHONE_13_4 FLUTTER_EXPORT +@interface FlutterViewController + : UIViewController +#else @interface FlutterViewController : UIViewController +#endif /** * 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 d788b461586f6..fb739b7bd6a2d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -33,6 +33,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 + CGPoint location = CGPointZero; + + // Last reported translation for an in-flight pan gesture + 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. @@ -77,6 +99,11 @@ @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; +#ifdef __IPHONE_13_4 + fml::scoped_nsobject _pointerInteraction API_AVAILABLE(ios(13.4)); + fml::scoped_nsobject _panGestureRecognizer API_AVAILABLE(ios(13.4)); + MouseState _mouseState; +#endif } @synthesize displayingFlutterUI = _displayingFlutterUI; @@ -528,6 +555,19 @@ - (void)viewDidLoad { << "FlutterViewController's view is loaded but is not attached to a FlutterEngine"; [_engine.get() attachView]; +#ifdef __IPHONE_13_4 + 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()]; + } +#endif + [super viewDidLoad]; } @@ -685,8 +725,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; } @@ -1286,4 +1327,67 @@ - (BOOL)isPresentingViewController { return self.presentedViewController != nil || self.isPresentingViewControllerAnimating; } +#ifdef __IPHONE_13_4 +- (flutter::PointerData)generatePointerDataForMouse { + const CGFloat scale = [UIScreen mainScreen].scale; + + 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 * scale; + pointer_data.physical_y = _mouseState.location.y * scale; + + 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); + _mouseState.location = request.location; + + flutter::PointerData pointer_data = [self generatePointerDataForMouse]; + + pointer_data.signal_kind = flutter::PointerData::SignalKind::kNone; + packet->SetPointerData(0, pointer_data); + + [_engine.get() dispatchPointerDataPacket:std::move(packet)]; + } + return nil; +} + +- (void)scrollEvent:(UIPanGestureRecognizer*)recognizer { + CGPoint translation = [recognizer translationInView:self.view]; + const CGFloat scale = [UIScreen mainScreen].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) * scale; + pointer_data.scroll_delta_y = -(translation.y - _mouseState.last_translation.y) * scale; + + // 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(0, pointer_data); + + [_engine.get() dispatchPointerDataPacket:std::move(packet)]; +} +#endif + @end