diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h index 8d2e5c6b33ed8..ef9773ce6a635 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h @@ -8,20 +8,25 @@ #include #include #include +#include #include "flutter/fml/macros.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTimeConverter.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h" #include "flutter/shell/platform/embedder/embedder.h" @class FlutterMutatorView; +@class FlutterCursorCoordinator; namespace flutter { -class PlatformViewLayer; +struct BackingStoreLayer { + std::vector paint_region; +}; -typedef std::pair PlatformViewLayerWithIndex; +using LayerVariant = std::variant; // FlutterCompositor creates and manages the backing stores used for // rendering Flutter content and presents Flutter content and Platform views. @@ -67,13 +72,16 @@ class FlutterCompositor { ViewPresenter(); void PresentPlatformViews(FlutterView* default_base_view, - const std::vector& platform_views, + const std::vector& layers, const FlutterPlatformViewController* platform_views_controller); private: // Platform view to FlutterMutatorView that contains it. NSMapTable* mutator_views_; + // Coordinates mouse cursor changes between platform views and overlays. + FlutterCursorCoordinator* cursor_coordinator_; + // Presents the platform view layer represented by `layer`. `layer_index` is // used to position the layer in the z-axis. If the layer does not have a // superview, it will become subview of `default_base_view`. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm index 4c1630356a863..3af34ff386d02 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm @@ -3,22 +3,31 @@ // found in the LICENSE file. #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h" #include "flutter/fml/logging.h" namespace flutter { namespace { -std::vector CopyPlatformViewLayers(const FlutterLayer** layers, - size_t layer_count) { - std::vector platform_views; +std::vector CopyLayers(const FlutterLayer** layers, size_t layer_count) { + std::vector layers_copy; for (size_t i = 0; i < layer_count; i++) { - if (layers[i]->type == kFlutterLayerContentTypePlatformView) { - platform_views.push_back(std::make_pair(PlatformViewLayer(layers[i]), i)); + const auto& layer = layers[i]; + if (layer->type == kFlutterLayerContentTypePlatformView) { + layers_copy.push_back(PlatformViewLayer(layer)); + } else if (layer->type == kFlutterLayerContentTypeBackingStore) { + std::vector rects; + auto present_info = layer->backing_store_present_info; + if (present_info != nullptr && present_info->paint_region != nullptr) { + rects.reserve(present_info->paint_region->rects_count); + std::copy(present_info->paint_region->rects, + present_info->paint_region->rects + present_info->paint_region->rects_count, + std::back_inserter(rects)); + } + layers_copy.push_back(BackingStoreLayer{rects}); } } - return platform_views; + return layers_copy; } } // namespace @@ -91,17 +100,16 @@ // Notify block below may be called asynchronously, hence the need to copy // the layer information instead of passing the original pointers from embedder. - auto platform_views_layers = std::make_shared>( - CopyPlatformViewLayers(layers, layers_count)); - - [view.surfaceManager presentSurfaces:surfaces - atTime:presentation_time - notify:^{ - // Gets a presenter or create a new one for the view. - ViewPresenter& presenter = presenters_[view_id]; - presenter.PresentPlatformViews(view, *platform_views_layers, - platform_view_controller_); - }]; + auto layers_copy = std::make_shared>(CopyLayers(layers, layers_count)); + + [view.surfaceManager + presentSurfaces:surfaces + atTime:presentation_time + notify:^{ + // Gets a presenter or create a new one for the view. + ViewPresenter& presenter = presenters_[view_id]; + presenter.PresentPlatformViews(view, *layers_copy, platform_view_controller_); + }]; return true; } @@ -111,7 +119,7 @@ void FlutterCompositor::ViewPresenter::PresentPlatformViews( FlutterView* default_base_view, - const std::vector& platform_views, + const std::vector& layers, const FlutterPlatformViewController* platform_view_controller) { FML_DCHECK([[NSThread currentThread] isMainThread]) << "Must be on the main thread to present platform views"; @@ -119,10 +127,37 @@ // Active mutator views for this frame. NSMutableArray* present_mutators = [NSMutableArray array]; - for (const auto& platform_view : platform_views) { - FlutterMutatorView* container = PresentPlatformView( - default_base_view, platform_view.first, platform_view.second, platform_view_controller); - [present_mutators addObject:container]; + for (size_t i = 0; i < layers.size(); i++) { + const auto& layer = layers[i]; + if (!std::holds_alternative(layer)) { + continue; + } + const auto& platform_view = std::get(layer); + FlutterMutatorView* mutator_view = + PresentPlatformView(default_base_view, platform_view, i, platform_view_controller); + [present_mutators addObject:mutator_view]; + + // Gather all overlay regions above this mutator view. + [mutator_view resetHitTestRegion]; + for (size_t j = i + 1; j < layers.size(); j++) { + const auto& overlay_layer = layers[j]; + if (!std::holds_alternative(overlay_layer)) { + continue; + } + const auto& backing_store_layer = std::get(overlay_layer); + for (const auto& flutter_rect : backing_store_layer.paint_region) { + double scale = default_base_view.layer.contentsScale; + CGRect rect = CGRectMake(flutter_rect.left / scale, flutter_rect.top / scale, + (flutter_rect.right - flutter_rect.left) / scale, + (flutter_rect.bottom - flutter_rect.top) / scale); + CGRect intersection = CGRectIntersection(rect, mutator_view.frame); + if (!CGRectIsNull(intersection)) { + intersection.origin.x -= mutator_view.frame.origin.x; + intersection.origin.y -= mutator_view.frame.origin.y; + [mutator_view addHitTestIgnoreRegion:intersection]; + } + } + } } NSMutableArray* obsolete_mutators = @@ -150,10 +185,15 @@ FML_DCHECK(platform_view) << "Platform view not found for id: " << platform_view_id; + if (cursor_coordinator_ == nil) { + cursor_coordinator_ = [[FlutterCursorCoordinator alloc] initWithFlutterView:default_base_view]; + } + FlutterMutatorView* container = [mutator_views_ objectForKey:platform_view]; if (!container) { - container = [[FlutterMutatorView alloc] initWithPlatformView:platform_view]; + container = [[FlutterMutatorView alloc] initWithPlatformView:platform_view + cursorCoordiator:cursor_coordinator_]; [mutator_views_ setObject:container forKey:platform_view]; [default_base_view addSubview:container]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index f354a23675952..47d71718ef590 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -86,7 +86,7 @@ - (instancetype)initWithConnection:(NSNumber*)connection /** * Private interface declaration for FlutterEngine. */ -@interface FlutterEngine () +@interface FlutterEngine () /** * A mutable array that holds one bool value that determines if responses to platform messages are @@ -466,6 +466,10 @@ @implementation FlutterEngine { // Map from ViewId to vsync waiter. Note that this is modified on main thread // but accessed on UI thread, so access must be @synchronized. NSMapTable* _vsyncWaiters; + + // Weak reference to last view that received a pointer event. This is used to + // pair cursor change with a view. + __weak FlutterView* _lastViewWithPointerEvent; } - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project { @@ -981,6 +985,7 @@ - (void)updateWindowMetricsForViewController:(FlutterViewController*)viewControl - (void)sendPointerEvent:(const FlutterPointerEvent&)event { _embedderAPI.SendPointerEvent(_engine, &event, 1); + _lastViewWithPointerEvent = [self viewControllerForId:kFlutterImplicitViewId].flutterView; } - (void)sendKeyEvent:(const FlutterKeyEvent&)event @@ -1167,7 +1172,8 @@ - (void)setUpNotificationCenterListeners { - (void)addInternalPlugins { __weak FlutterEngine* weakSelf = self; - [FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]]; + [FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"] + delegate:self]; [FlutterMenuPlugin registerWithRegistrar:[self registrarForPlugin:@"menu"]]; _settingsChannel = [FlutterBasicMessageChannel messageChannelWithName:kFlutterSettingsChannel @@ -1182,6 +1188,13 @@ - (void)addInternalPlugins { }]; } +- (void)didUpdateMouseCursor:(NSCursor*)cursor { + // Mouse cursor plugin does not specify which view is responsible for changing the cursor, + // so the reasonable assumption here is that cursor change is a result of a mouse movement + // and thus the cursor will be paired with last Flutter view that reveived mouse event. + [_lastViewWithPointerEvent didUpdateMouseCursor:cursor]; +} + - (void)applicationWillTerminate:(NSNotification*)notification { [self shutDownEngine]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h b/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h index ac986d6fb81c4..4cc4790a1e0a7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h @@ -10,6 +10,10 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" +@protocol FlutterMouseCursorPluginDelegate +- (void)didUpdateMouseCursor:(nonnull NSCursor*)cursor; +@end + /** * A plugin to handle mouse cursor. * @@ -18,6 +22,9 @@ */ @interface FlutterMouseCursorPlugin : NSObject ++ (void)registerWithRegistrar:(nonnull id)registrar + delegate:(nullable id)delegate; + @end #endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERMOUSECURSORPLUGIN_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm index 273f4c3533270..a108b7078a493 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm @@ -25,6 +25,11 @@ // The following mapping must be kept in sync with Flutter framework's // mouse_cursor.dart + if ([kind isEqualToString:kKindValueNone]) { + NSImage* image = [[NSImage alloc] initWithSize:NSMakeSize(1, 1)]; + return [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint(0, 0)]; + } + if (systemCursors == nil) { systemCursors = @{ @"alias" : [NSCursor dragLinkCursor], @@ -58,10 +63,8 @@ } @interface FlutterMouseCursorPlugin () -/** - * Whether the cursor is currently hidden. - */ -@property(nonatomic) BOOL hidden; + +@property(nonatomic, weak) id delegate; /** * Handles the method call that activates a system cursor. @@ -79,11 +82,6 @@ - (FlutterError*)activateSystemCursor:(nonnull NSDictionary*)arguments; */ - (void)displayCursorObject:(nonnull NSCursor*)cursorObject; -/** - * Hides the cursor. - */ -- (void)hide; - /** * Handles all method calls from Flutter. */ @@ -105,12 +103,6 @@ - (instancetype)init { return self; } -- (void)dealloc { - if (_hidden) { - [NSCursor unhide]; - } -} - - (FlutterError*)activateSystemCursor:(nonnull NSDictionary*)arguments { NSString* kindArg = arguments[kKindKey]; if (!kindArg) { @@ -118,10 +110,7 @@ - (FlutterError*)activateSystemCursor:(nonnull NSDictionary*)arguments { message:@"Missing argument" details:@"Missing argument while trying to activate system cursor"]; } - if ([kindArg isEqualToString:kKindValueNone]) { - [self hide]; - return nil; - } + NSCursor* cursorObject = [FlutterMouseCursorPlugin cursorFromKind:kindArg]; [self displayCursorObject:cursorObject]; return nil; @@ -129,17 +118,7 @@ - (FlutterError*)activateSystemCursor:(nonnull NSDictionary*)arguments { - (void)displayCursorObject:(nonnull NSCursor*)cursorObject { [cursorObject set]; - if (_hidden) { - [NSCursor unhide]; - } - _hidden = NO; -} - -- (void)hide { - if (!_hidden) { - [NSCursor hide]; - } - _hidden = YES; + [self.delegate didUpdateMouseCursor:cursorObject]; } + (NSCursor*)cursorFromKind:(NSString*)kind { @@ -154,9 +133,15 @@ + (NSCursor*)cursorFromKind:(NSString*)kind { #pragma mark - FlutterPlugin implementation + (void)registerWithRegistrar:(id)registrar { + [self registerWithRegistrar:registrar delegate:nil]; +} + ++ (void)registerWithRegistrar:(id)registrar + delegate:(id)delegate { FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:kMouseCursorChannel binaryMessenger:registrar.messenger]; FlutterMouseCursorPlugin* instance = [[FlutterMouseCursorPlugin alloc] init]; + instance.delegate = delegate; [registrar addMethodCallDelegate:instance channel:channel]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h b/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h index d970a66e055b3..390da0e5830ba 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h @@ -37,11 +37,35 @@ class PlatformViewLayer { }; } // namespace flutter +@class FlutterView; +@class FlutterMutatorView; + +/// FlutterCursorCoordinator is responsible for coordinating cursor changes between +/// platform views and overlays of single FlutterView. +@interface FlutterCursorCoordinator : NSObject + +- (nonnull FlutterCursorCoordinator*)initWithFlutterView:(nonnull FlutterView*)flutterView; + +@end + +/// Exposed methods for testing. +@interface FlutterCursorCoordinator (Private) + +@property(readonly, nonatomic) BOOL cleanupScheduled; + +- (void)processMouseMoveEvent:(nonnull NSEvent*)event + forMutatorView:(nonnull FlutterMutatorView*)view + overlayRegion:(const std::vector&)region; +@end + /// FlutterMutatorView contains platform view and is responsible for applying /// FlutterLayer mutations to it. @interface FlutterMutatorView : NSView /// Designated initializer. +- (nonnull instancetype)initWithPlatformView:(nonnull NSView*)platformView + cursorCoordiator:(nullable FlutterCursorCoordinator*)coordinator; + - (nonnull instancetype)initWithPlatformView:(nonnull NSView*)platformView; /// Returns wrapped platform view. @@ -52,6 +76,13 @@ class PlatformViewLayer { /// requested mutations. - (void)applyFlutterLayer:(nonnull const flutter::PlatformViewLayer*)layer; +/// Resets hit hit testing region for this mutator view. +- (void)resetHitTestRegion; + +/// Adds rectangle (in local vie coordinates) to hit test ignore region +/// (part of view obscured by Flutter contents). +- (void)addHitTestIgnoreRegion:(CGRect)region; + @end #endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERMUTATORVIEW_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm b/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm index 9e5a3e5fe084e..e5bfd3ad707a6 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm @@ -3,9 +3,9 @@ // found in the LICENSE file. #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" #include -#include #include "flutter/fml/logging.h" #include "flutter/shell/platform/embedder/embedder.h" @@ -30,6 +30,65 @@ : identifier_(identifier), mutations_(mutations), offset_(offset), size_(size) {} } // namespace flutter +@implementation FlutterCursorCoordinator { + __weak FlutterView* _flutterView; + BOOL _cleanupScheduled; + BOOL _mouseMoveHandled; +} + +- (FlutterCursorCoordinator*)initWithFlutterView:(FlutterView*)flutterView { + if (self = [super init]) { + _flutterView = flutterView; + } + return self; +} + +- (void)frameCleanup { + _cleanupScheduled = NO; + _mouseMoveHandled = NO; +} + +- (BOOL)cleanupScheduled { + return _cleanupScheduled; +} + +// Processes the mouse event from given mutator view. This is called for each mutator view, in +// z-order, from the top most down. +- (void)processMouseMoveEvent:(NSEvent*)event + forMutatorView:(FlutterMutatorView*)view + overlayRegion:(const std::vector&)region { + // [self frameCleanup] will be called once after current run loop turn. + if (!_cleanupScheduled) { + [[NSRunLoop mainRunLoop] performBlock:^{ + [self frameCleanup]; + }]; + _cleanupScheduled = YES; + } + + // Mouse move was already handled by a mutator view above. + if (_mouseMoveHandled) { + return; + } + + NSPoint point = [view convertPoint:event.locationInWindow fromView:nil]; + + // If the mouse is above overlay region restore current Flutter cursor. + for (const auto& r : region) { + if (CGRectContainsPoint(r, point)) { + [_flutterView cursorUpdate:event]; + _mouseMoveHandled = YES; + return; + } + } + NSView* platformView = view.platformView; + // It is possible that Flutter changed mouse cursor while the mouse was inside + // cursor rect. Unfocused NSTextField uses legacy cursor rects for changing + // its cursor. + [platformView.window invalidateCursorRectsForView:platformView]; + _mouseMoveHandled = YES; +} +@end + @interface FlutterMutatorView () { // Each of these views clips to a CGPathRef. These views, if present, // are nested (first is child of FlutterMutatorView and last is parent of @@ -41,6 +100,18 @@ @interface FlutterMutatorView () { NSView* _platformViewContainer; NSView* _platformView; + + FlutterCursorCoordinator* _cursorCoordinator; + + // Container view that hosts the tracking area. Must be above platform view + // so that it gets the mouseMove event first. + NSView* _trackingAreaContainer; + + // Tracking area used to update cursor when moving over overlay region. + NSTrackingArea* _trackingArea; + + // Region of the overlay that should be ignored for hit testing. + std::vector _hitTestIgnoreRegion; } @end @@ -51,6 +122,11 @@ @interface FlutterPlatformViewContainer : NSView @implementation FlutterPlatformViewContainer +- (NSView*)hitTest:(NSPoint)point { + NSView* res = [super hitTest:point]; + return res != self ? res : nil; +} + - (BOOL)isFlipped { // Flutter transforms assume a coordinate system with an upper-left corner origin, with y // coordinate values increasing downwards. This affects the view, view transforms, and @@ -83,6 +159,11 @@ - (BOOL)isFlipped { return YES; } +- (NSView*)hitTest:(NSPoint)point { + NSView* res = [super hitTest:point]; + return res != self ? res : nil; +} + /// Clip the view to the given path. Offset top left corner of platform view /// in global logical coordinates. - (void)maskToPath:(CGPathRef)path withOrigin:(CGPoint)origin { @@ -392,6 +473,15 @@ CGRect MasterClipFromMutations(CGRect bounds, const MutationVector& mutations) { } } // namespace +@interface FlutterTrackingAreaContainer : NSView +@end + +@implementation FlutterTrackingAreaContainer +- (NSView*)hitTest:(NSPoint)point { + return nil; +} +@end + @implementation FlutterMutatorView - (NSView*)platformView { @@ -407,17 +497,56 @@ - (NSView*)platformViewContainer { } - (instancetype)initWithPlatformView:(NSView*)platformView { + return [self initWithPlatformView:platformView cursorCoordiator:nil]; +} + +- (instancetype)initWithPlatformView:(NSView*)platformView + cursorCoordiator:(FlutterCursorCoordinator*)coordinator { if (self = [super initWithFrame:NSZeroRect]) { _platformView = platformView; _pathClipViews = [NSMutableArray array]; + _cursorCoordinator = coordinator; self.wantsLayer = YES; self.clipsToBounds = YES; + + _trackingAreaContainer = [[FlutterTrackingAreaContainer alloc] initWithFrame:NSZeroRect]; + [self addSubview:_trackingAreaContainer]; + + NSTrackingAreaOptions options = NSTrackingMouseMoved | NSTrackingInVisibleRect | + NSTrackingEnabledDuringMouseDrag | NSTrackingActiveAlways; + _trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect + options:options + owner:self + userInfo:nil]; + [_trackingAreaContainer addTrackingArea:_trackingArea]; } return self; } +- (void)resetHitTestRegion { + self->_hitTestIgnoreRegion.clear(); +} + +- (void)addHitTestIgnoreRegion:(CGRect)region { + self->_hitTestIgnoreRegion.push_back(region); +} + +- (void)mouseMoved:(NSEvent*)event { + [_cursorCoordinator processMouseMoveEvent:event + forMutatorView:self + overlayRegion:_hitTestIgnoreRegion]; +} + - (NSView*)hitTest:(NSPoint)point { - return nil; + CGPoint localPoint = point; + localPoint.x -= self.frame.origin.x; + localPoint.y -= self.frame.origin.y; + for (const auto& region : _hitTestIgnoreRegion) { + if (CGRectContainsPoint(region, localPoint)) { + return nil; + } + } + return [super hitTest:point]; } - (BOOL)isFlipped { @@ -486,8 +615,10 @@ - (void)updatePlatformViewWithBounds:(CGRect)untransformedBounds // By default NSView clips children to frame. If masterClip is tighter than mutator view frame, // the frame is set to masterClip and child offset adjusted to compensate for the difference. if (!CGRectEqualToRect(clipRect, transformedBounds)) { - FML_DCHECK(self.subviews.count == 1); - auto subview = self.subviews.firstObject; + NSMutableArray* subviews = [NSMutableArray arrayWithArray:self.subviews]; + [subviews removeObject:_trackingAreaContainer]; + FML_DCHECK(subviews.count == 1); + auto subview = subviews.firstObject; FML_DCHECK(subview.frame.origin.x == 0 && subview.frame.origin.y == 0); subview.frame = CGRectMake(transformedBounds.origin.x - clipRect.origin.x, transformedBounds.origin.y - clipRect.origin.y, @@ -535,6 +666,9 @@ - (void)applyFlutterLayer:(const flutter::PlatformViewLayer*)layer { transformedBounds:finalBoundingRect transform:finalTransform clipRect:masterClip]; + + [self addSubview:_trackingAreaContainer positioned:(NSWindowAbove)relativeTo:nil]; + _trackingAreaContainer.frame = self.bounds; } @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterMutatorViewTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterMutatorViewTest.mm index 6d6b6b21c4024..576ab9b3d4de3 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterMutatorViewTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterMutatorViewTest.mm @@ -2,7 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" #include "third_party/googletest/googletest/include/gtest/gtest.h" @@ -532,3 +534,178 @@ void ExpectTransform3DEqual(const CATransform3D& t, const CATransform3D& u) { EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(100, 50, 30, 20))); EXPECT_EQ(mutatorView.pathClipViews.count, 0ull); } + +TEST(FlutterMutatorViewTest, HitTestIgnoreRegion) { + NSView* platformView = [[NSView alloc] init]; + FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView]; + ApplyFlutterLayer(mutatorView, FlutterSize{100, 100}, {}); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(10, 10)], platformView); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(50, 10)], platformView); + + [mutatorView resetHitTestRegion]; + [mutatorView addHitTestIgnoreRegion:CGRectMake(0, 0, 50, 50)]; + [mutatorView addHitTestIgnoreRegion:CGRectMake(50, 50, 50, 50)]; + + EXPECT_EQ([mutatorView hitTest:NSMakePoint(10, 10)], nil); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(49, 10)], nil); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(10, 49)], nil); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(50, 50)], nil); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(50, 10)], platformView); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(10, 50)], platformView); + + [mutatorView resetHitTestRegion]; + EXPECT_EQ([mutatorView hitTest:NSMakePoint(10, 10)], platformView); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(49, 10)], platformView); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(10, 49)], platformView); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(50, 50)], platformView); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(50, 10)], platformView); + EXPECT_EQ([mutatorView hitTest:NSMakePoint(10, 50)], platformView); +} + +@interface FlutterCursorCoordinatorTest : NSObject + +@end + +@implementation FlutterCursorCoordinatorTest +- (void)testCoordinatorEventWithinFlutterContent { + id flutterView = OCMClassMock([FlutterView class]); + FlutterCursorCoordinator* coordinator = + [[FlutterCursorCoordinator alloc] initWithFlutterView:flutterView]; + { + id platformView = OCMClassMock([NSView class]); + OCMStub([flutterView cursorUpdate:[OCMArg any]]); + id mutatorView = OCMStrictClassMock([FlutterMutatorView class]); + OCMStub([mutatorView platformView]).andReturn(platformView); + CGPoint location = NSMakePoint(50, 50); + OCMStub([mutatorView convertPoint:location fromView:[OCMArg any]]).andReturn(location); + NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved + location:location + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + eventNumber:0 + clickCount:0 + pressure:0]; + [coordinator processMouseMoveEvent:event + forMutatorView:mutatorView + overlayRegion:{CGRectMake(0, 0, 100, 100)}]; + OCMVerify([flutterView cursorUpdate:event]); + } + { + id platformView = OCMClassMock([NSView class]); + // Make sure once event is handled the coordinator will not send cursorUpdate again. + OCMReject([flutterView cursorUpdate:[OCMArg any]]); + id mutatorView = OCMStrictClassMock([FlutterMutatorView class]); + OCMStub([mutatorView platformView]).andReturn(platformView); + CGPoint location = NSMakePoint(50, 50); + OCMStub([mutatorView convertPoint:location fromView:[OCMArg any]]).andReturn(location); + NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved + location:location + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + eventNumber:0 + clickCount:0 + pressure:0]; + [coordinator processMouseMoveEvent:event + forMutatorView:mutatorView + overlayRegion:{CGRectMake(0, 0, 100, 100)}]; + } + EXPECT_TRUE(coordinator.cleanupScheduled); + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + EXPECT_FALSE(coordinator.cleanupScheduled); +} + +- (void)testCoordinatorEventOutsideFlutterContent { + id flutterView = OCMClassMock([FlutterView class]); + OCMReject([flutterView cursorUpdate:[OCMArg any]]); + FlutterCursorCoordinator* coordinator = + [[FlutterCursorCoordinator alloc] initWithFlutterView:flutterView]; + id platformViewWindow = OCMClassMock([NSWindow class]); + { + id platformView = OCMClassMock([NSView class]); + OCMStub([platformViewWindow invalidateCursorRectsForView:platformView]); + OCMStub([platformView window]).andReturn(platformViewWindow); + OCMStub([flutterView cursorUpdate:[OCMArg any]]); + id mutatorView = OCMStrictClassMock([FlutterMutatorView class]); + OCMStub([mutatorView platformView]).andReturn(platformView); + CGPoint location = NSMakePoint(150, 150); + OCMStub([mutatorView convertPoint:location fromView:[OCMArg any]]).andReturn(location); + NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved + location:location + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + eventNumber:0 + clickCount:0 + pressure:0]; + [coordinator processMouseMoveEvent:event + forMutatorView:mutatorView + overlayRegion:{CGRectMake(0, 0, 100, 100)}]; + OCMVerify([platformViewWindow invalidateCursorRectsForView:platformView]); + } + { + // Make sure this is not called again for subsequent invocation during same run loop turn. + OCMReject([platformViewWindow invalidateCursorRectsForView:[OCMArg any]]); + + id platformView = OCMClassMock([NSView class]); + OCMStub([platformViewWindow invalidateCursorRectsForView:platformView]); + OCMStub([platformView window]).andReturn(platformViewWindow); + OCMStub([flutterView cursorUpdate:[OCMArg any]]); + id mutatorView = OCMStrictClassMock([FlutterMutatorView class]); + OCMStub([mutatorView platformView]).andReturn(platformView); + CGPoint location = NSMakePoint(150, 150); + OCMStub([mutatorView convertPoint:location fromView:[OCMArg any]]).andReturn(location); + NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved + location:location + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + eventNumber:0 + clickCount:0 + pressure:0]; + [coordinator processMouseMoveEvent:event + forMutatorView:mutatorView + overlayRegion:{CGRectMake(0, 0, 100, 100)}]; + } + EXPECT_TRUE(coordinator.cleanupScheduled); + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + EXPECT_FALSE(coordinator.cleanupScheduled); + + // Check that invalidateCursorRectsForView is called again + platformViewWindow = OCMClassMock([NSWindow class]); + { + id platformView = OCMClassMock([NSView class]); + OCMStub([platformViewWindow invalidateCursorRectsForView:platformView]); + OCMStub([platformView window]).andReturn(platformViewWindow); + OCMStub([flutterView cursorUpdate:[OCMArg any]]); + id mutatorView = OCMStrictClassMock([FlutterMutatorView class]); + OCMStub([mutatorView platformView]).andReturn(platformView); + CGPoint location = NSMakePoint(150, 150); + OCMStub([mutatorView convertPoint:location fromView:[OCMArg any]]).andReturn(location); + NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved + location:location + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + eventNumber:0 + clickCount:0 + pressure:0]; + [coordinator processMouseMoveEvent:event + forMutatorView:mutatorView + overlayRegion:{CGRectMake(0, 0, 100, 100)}]; + OCMVerify([platformViewWindow invalidateCursorRectsForView:platformView]); + } + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; +} +@end + +TEST(FlutterMutatorViewTest, CursorCoordinator) { + [[[FlutterCursorCoordinatorTest alloc] init] testCoordinatorEventWithinFlutterContent]; + [[[FlutterCursorCoordinatorTest alloc] init] testCoordinatorEventOutsideFlutterContent]; +} \ No newline at end of file diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.h b/shell/platform/darwin/macos/framework/Source/FlutterView.h index add6131930795..a143890329372 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -77,6 +77,13 @@ constexpr FlutterViewId kFlutterImplicitViewId = 0ll; */ - (void)setBackgroundColor:(nonnull NSColor*)color; +/** + * Called from the engine to notify the view that mouse cursor was updated while + * the mouse is over the view. The view is responsible from restoring the cursor + * when the mouse enters the view from another subview. + */ +- (void)didUpdateMouseCursor:(nonnull NSCursor*)cursor; + @end #endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVIEW_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.mm b/shell/platform/darwin/macos/framework/Source/FlutterView.mm index 79607d0f759e8..29eec7364d457 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.mm @@ -14,6 +14,7 @@ @interface FlutterView () { __weak id _viewDelegate; FlutterThreadSynchronizer* _threadSynchronizer; FlutterSurfaceManager* _surfaceManager; + NSCursor* _lastCursor; } @end @@ -94,13 +95,25 @@ - (BOOL)acceptsFirstResponder { return [_viewDelegate viewShouldAcceptFirstResponder:self]; } +- (void)didUpdateMouseCursor:(NSCursor*)cursor { + _lastCursor = cursor; +} + +// Restores mouse cursor. There are few cases when this is needed and framework will not handle this +// automatically: +// - When mouse cursor leaves subview of FlutterView (technically still within bound of FlutterView +// tracking area so the framework won't be notified) +// - When context menu above FlutterView is closed. Context menu will change current cursor to arrow +// and will not restore it back. - (void)cursorUpdate:(NSEvent*)event { - // When adding/removing views AppKit will schedule call to current hit-test view - // cursorUpdate: at the end of frame to determine possible cursor change. If - // the view doesn't implement cursorUpdate: AppKit will set the default (arrow) cursor - // instead. This would replace the cursor set by FlutterMouseCursorPlugin. - // Empty cursorUpdate: implementation prevents this behavior. - // https://github.com/flutter/flutter/issues/111425 + [_lastCursor set]; + // It is possible that there is a platform view with NSTrackingArea below flutter content. + // This could override the mouse cursor as a result of mouse move event. There is no good way + // to prevent that short of swizzling [NSCursor set], so as a workaround force flutter cursor + // in next runloop turn. This is not ideal, as it may cause the cursor flicker a bit. + [[NSRunLoop currentRunLoop] performBlock:^{ + [_lastCursor set]; + }]; } - (void)viewDidChangeBackingProperties {