Skip to content

Commit 6280295

Browse files
authored
[macOS] Implement hit testing and handle platform view cursor changes (flutter#43101)
Fixes flutter#129085 ## Changes to `FlutterMouseCursorPlugin` - `none` cursor is not handled by hiding the cursor anymore. That was too big of a hammer as it also affects other views (and platform views). Instead empty cursor is created from empty `NSImage`. - Cursor plugin now notifies the engine when cursor has changed. The engine forwards the change to last `FlutterView` that handled mouse event. This is necessary because on occasion `FlutterView` needs to be able to restore cursor without framework being involved. ## Preventing PlatformView from changing cursor when it is obscured by Flutter Content. Generally in Cocoa cursor changes are done as response to `mouseMoved` event, which is driven by a `NSTrackingArea`. The issue here is that this is not affected by hit testing and tracking areas form a hierarchy parallel to view hierarchy and are not affected by being obscured by another view (or tracking area). This means that platform view will receive `mouseMoved` event even when is obscured by Flutter content. To work around this, the mutator view puts a tracking area above platform view, which means it gets the mouseMove event first, and when it decides that mouse is over Flutter content, it will prevent platform view from changing the cursor for the rest of RunLoop turn (see `NSCursor+IgnoreChange`). ## Actual hit testing This part is rather straightforward, the area where FlutterContent obscures mutator view is provided to the mutator view, which will return `nil` from `hitTest:` when the point is in the obscured area. ## Example of hit testing https://github.com/flutter/engine/assets/96958/bbac0cfd-8c44-44d3-addd-921c91a8a539 ## 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 Hixie said 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]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [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
1 parent 412ce8a commit 6280295

10 files changed

Lines changed: 484 additions & 69 deletions

shell/platform/darwin/macos/framework/Source/FlutterCompositor.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,25 @@
88
#include <functional>
99
#include <list>
1010
#include <unordered_map>
11+
#include <variant>
1112

1213
#include "flutter/fml/macros.h"
14+
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h"
1315
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h"
1416
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTimeConverter.h"
1517
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h"
1618
#include "flutter/shell/platform/embedder/embedder.h"
1719

1820
@class FlutterMutatorView;
21+
@class FlutterCursorCoordinator;
1922

2023
namespace flutter {
2124

22-
class PlatformViewLayer;
25+
struct BackingStoreLayer {
26+
std::vector<FlutterRect> paint_region;
27+
};
2328

24-
typedef std::pair<PlatformViewLayer, size_t> PlatformViewLayerWithIndex;
29+
using LayerVariant = std::variant<PlatformViewLayer, BackingStoreLayer>;
2530

2631
// FlutterCompositor creates and manages the backing stores used for
2732
// rendering Flutter content and presents Flutter content and Platform views.
@@ -67,13 +72,16 @@ class FlutterCompositor {
6772
ViewPresenter();
6873

6974
void PresentPlatformViews(FlutterView* default_base_view,
70-
const std::vector<PlatformViewLayerWithIndex>& platform_views,
75+
const std::vector<LayerVariant>& layers,
7176
const FlutterPlatformViewController* platform_views_controller);
7277

7378
private:
7479
// Platform view to FlutterMutatorView that contains it.
7580
NSMapTable<NSView*, FlutterMutatorView*>* mutator_views_;
7681

82+
// Coordinates mouse cursor changes between platform views and overlays.
83+
FlutterCursorCoordinator* cursor_coordinator_;
84+
7785
// Presents the platform view layer represented by `layer`. `layer_index` is
7886
// used to position the layer in the z-axis. If the layer does not have a
7987
// superview, it will become subview of `default_base_view`.

shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,31 @@
33
// found in the LICENSE file.
44

55
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h"
6-
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h"
76

87
#include "flutter/fml/logging.h"
98

109
namespace flutter {
1110

1211
namespace {
13-
std::vector<PlatformViewLayerWithIndex> CopyPlatformViewLayers(const FlutterLayer** layers,
14-
size_t layer_count) {
15-
std::vector<PlatformViewLayerWithIndex> platform_views;
12+
std::vector<LayerVariant> CopyLayers(const FlutterLayer** layers, size_t layer_count) {
13+
std::vector<LayerVariant> layers_copy;
1614
for (size_t i = 0; i < layer_count; i++) {
17-
if (layers[i]->type == kFlutterLayerContentTypePlatformView) {
18-
platform_views.push_back(std::make_pair(PlatformViewLayer(layers[i]), i));
15+
const auto& layer = layers[i];
16+
if (layer->type == kFlutterLayerContentTypePlatformView) {
17+
layers_copy.push_back(PlatformViewLayer(layer));
18+
} else if (layer->type == kFlutterLayerContentTypeBackingStore) {
19+
std::vector<FlutterRect> rects;
20+
auto present_info = layer->backing_store_present_info;
21+
if (present_info != nullptr && present_info->paint_region != nullptr) {
22+
rects.reserve(present_info->paint_region->rects_count);
23+
std::copy(present_info->paint_region->rects,
24+
present_info->paint_region->rects + present_info->paint_region->rects_count,
25+
std::back_inserter(rects));
26+
}
27+
layers_copy.push_back(BackingStoreLayer{rects});
1928
}
2029
}
21-
return platform_views;
30+
return layers_copy;
2231
}
2332
} // namespace
2433

@@ -91,17 +100,16 @@
91100

92101
// Notify block below may be called asynchronously, hence the need to copy
93102
// the layer information instead of passing the original pointers from embedder.
94-
auto platform_views_layers = std::make_shared<std::vector<PlatformViewLayerWithIndex>>(
95-
CopyPlatformViewLayers(layers, layers_count));
96-
97-
[view.surfaceManager presentSurfaces:surfaces
98-
atTime:presentation_time
99-
notify:^{
100-
// Gets a presenter or create a new one for the view.
101-
ViewPresenter& presenter = presenters_[view_id];
102-
presenter.PresentPlatformViews(view, *platform_views_layers,
103-
platform_view_controller_);
104-
}];
103+
auto layers_copy = std::make_shared<std::vector<LayerVariant>>(CopyLayers(layers, layers_count));
104+
105+
[view.surfaceManager
106+
presentSurfaces:surfaces
107+
atTime:presentation_time
108+
notify:^{
109+
// Gets a presenter or create a new one for the view.
110+
ViewPresenter& presenter = presenters_[view_id];
111+
presenter.PresentPlatformViews(view, *layers_copy, platform_view_controller_);
112+
}];
105113

106114
return true;
107115
}
@@ -111,18 +119,45 @@
111119

112120
void FlutterCompositor::ViewPresenter::PresentPlatformViews(
113121
FlutterView* default_base_view,
114-
const std::vector<PlatformViewLayerWithIndex>& platform_views,
122+
const std::vector<LayerVariant>& layers,
115123
const FlutterPlatformViewController* platform_view_controller) {
116124
FML_DCHECK([[NSThread currentThread] isMainThread])
117125
<< "Must be on the main thread to present platform views";
118126

119127
// Active mutator views for this frame.
120128
NSMutableArray<FlutterMutatorView*>* present_mutators = [NSMutableArray array];
121129

122-
for (const auto& platform_view : platform_views) {
123-
FlutterMutatorView* container = PresentPlatformView(
124-
default_base_view, platform_view.first, platform_view.second, platform_view_controller);
125-
[present_mutators addObject:container];
130+
for (size_t i = 0; i < layers.size(); i++) {
131+
const auto& layer = layers[i];
132+
if (!std::holds_alternative<PlatformViewLayer>(layer)) {
133+
continue;
134+
}
135+
const auto& platform_view = std::get<PlatformViewLayer>(layer);
136+
FlutterMutatorView* mutator_view =
137+
PresentPlatformView(default_base_view, platform_view, i, platform_view_controller);
138+
[present_mutators addObject:mutator_view];
139+
140+
// Gather all overlay regions above this mutator view.
141+
[mutator_view resetHitTestRegion];
142+
for (size_t j = i + 1; j < layers.size(); j++) {
143+
const auto& overlay_layer = layers[j];
144+
if (!std::holds_alternative<BackingStoreLayer>(overlay_layer)) {
145+
continue;
146+
}
147+
const auto& backing_store_layer = std::get<BackingStoreLayer>(overlay_layer);
148+
for (const auto& flutter_rect : backing_store_layer.paint_region) {
149+
double scale = default_base_view.layer.contentsScale;
150+
CGRect rect = CGRectMake(flutter_rect.left / scale, flutter_rect.top / scale,
151+
(flutter_rect.right - flutter_rect.left) / scale,
152+
(flutter_rect.bottom - flutter_rect.top) / scale);
153+
CGRect intersection = CGRectIntersection(rect, mutator_view.frame);
154+
if (!CGRectIsNull(intersection)) {
155+
intersection.origin.x -= mutator_view.frame.origin.x;
156+
intersection.origin.y -= mutator_view.frame.origin.y;
157+
[mutator_view addHitTestIgnoreRegion:intersection];
158+
}
159+
}
160+
}
126161
}
127162

128163
NSMutableArray<FlutterMutatorView*>* obsolete_mutators =
@@ -150,10 +185,15 @@
150185

151186
FML_DCHECK(platform_view) << "Platform view not found for id: " << platform_view_id;
152187

188+
if (cursor_coordinator_ == nil) {
189+
cursor_coordinator_ = [[FlutterCursorCoordinator alloc] initWithFlutterView:default_base_view];
190+
}
191+
153192
FlutterMutatorView* container = [mutator_views_ objectForKey:platform_view];
154193

155194
if (!container) {
156-
container = [[FlutterMutatorView alloc] initWithPlatformView:platform_view];
195+
container = [[FlutterMutatorView alloc] initWithPlatformView:platform_view
196+
cursorCoordiator:cursor_coordinator_];
157197
[mutator_views_ setObject:container forKey:platform_view];
158198
[default_base_view addSubview:container];
159199
}

shell/platform/darwin/macos/framework/Source/FlutterEngine.mm

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ - (instancetype)initWithConnection:(NSNumber*)connection
8686
/**
8787
* Private interface declaration for FlutterEngine.
8888
*/
89-
@interface FlutterEngine () <FlutterBinaryMessenger>
89+
@interface FlutterEngine () <FlutterBinaryMessenger, FlutterMouseCursorPluginDelegate>
9090

9191
/**
9292
* A mutable array that holds one bool value that determines if responses to platform messages are
@@ -466,6 +466,10 @@ @implementation FlutterEngine {
466466
// Map from ViewId to vsync waiter. Note that this is modified on main thread
467467
// but accessed on UI thread, so access must be @synchronized.
468468
NSMapTable<NSNumber*, FlutterVSyncWaiter*>* _vsyncWaiters;
469+
470+
// Weak reference to last view that received a pointer event. This is used to
471+
// pair cursor change with a view.
472+
__weak FlutterView* _lastViewWithPointerEvent;
469473
}
470474

471475
- (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project {
@@ -981,6 +985,7 @@ - (void)updateWindowMetricsForViewController:(FlutterViewController*)viewControl
981985

982986
- (void)sendPointerEvent:(const FlutterPointerEvent&)event {
983987
_embedderAPI.SendPointerEvent(_engine, &event, 1);
988+
_lastViewWithPointerEvent = [self viewControllerForId:kFlutterImplicitViewId].flutterView;
984989
}
985990

986991
- (void)sendKeyEvent:(const FlutterKeyEvent&)event
@@ -1167,7 +1172,8 @@ - (void)setUpNotificationCenterListeners {
11671172

11681173
- (void)addInternalPlugins {
11691174
__weak FlutterEngine* weakSelf = self;
1170-
[FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]];
1175+
[FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]
1176+
delegate:self];
11711177
[FlutterMenuPlugin registerWithRegistrar:[self registrarForPlugin:@"menu"]];
11721178
_settingsChannel =
11731179
[FlutterBasicMessageChannel messageChannelWithName:kFlutterSettingsChannel
@@ -1182,6 +1188,13 @@ - (void)addInternalPlugins {
11821188
}];
11831189
}
11841190

1191+
- (void)didUpdateMouseCursor:(NSCursor*)cursor {
1192+
// Mouse cursor plugin does not specify which view is responsible for changing the cursor,
1193+
// so the reasonable assumption here is that cursor change is a result of a mouse movement
1194+
// and thus the cursor will be paired with last Flutter view that reveived mouse event.
1195+
[_lastViewWithPointerEvent didUpdateMouseCursor:cursor];
1196+
}
1197+
11851198
- (void)applicationWillTerminate:(NSNotification*)notification {
11861199
[self shutDownEngine];
11871200
}

shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
1111
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
1212

13+
@protocol FlutterMouseCursorPluginDelegate <NSObject>
14+
- (void)didUpdateMouseCursor:(nonnull NSCursor*)cursor;
15+
@end
16+
1317
/**
1418
* A plugin to handle mouse cursor.
1519
*
@@ -18,6 +22,9 @@
1822
*/
1923
@interface FlutterMouseCursorPlugin : NSObject <FlutterPlugin>
2024

25+
+ (void)registerWithRegistrar:(nonnull id<FlutterPluginRegistrar>)registrar
26+
delegate:(nullable id<FlutterMouseCursorPluginDelegate>)delegate;
27+
2128
@end
2229

2330
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERMOUSECURSORPLUGIN_H_

shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
// The following mapping must be kept in sync with Flutter framework's
2626
// mouse_cursor.dart
2727

28+
if ([kind isEqualToString:kKindValueNone]) {
29+
NSImage* image = [[NSImage alloc] initWithSize:NSMakeSize(1, 1)];
30+
return [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint(0, 0)];
31+
}
32+
2833
if (systemCursors == nil) {
2934
systemCursors = @{
3035
@"alias" : [NSCursor dragLinkCursor],
@@ -58,10 +63,8 @@
5863
}
5964

6065
@interface FlutterMouseCursorPlugin ()
61-
/**
62-
* Whether the cursor is currently hidden.
63-
*/
64-
@property(nonatomic) BOOL hidden;
66+
67+
@property(nonatomic, weak) id<FlutterMouseCursorPluginDelegate> delegate;
6568

6669
/**
6770
* Handles the method call that activates a system cursor.
@@ -79,11 +82,6 @@ - (FlutterError*)activateSystemCursor:(nonnull NSDictionary*)arguments;
7982
*/
8083
- (void)displayCursorObject:(nonnull NSCursor*)cursorObject;
8184

82-
/**
83-
* Hides the cursor.
84-
*/
85-
- (void)hide;
86-
8785
/**
8886
* Handles all method calls from Flutter.
8987
*/
@@ -105,41 +103,22 @@ - (instancetype)init {
105103
return self;
106104
}
107105

108-
- (void)dealloc {
109-
if (_hidden) {
110-
[NSCursor unhide];
111-
}
112-
}
113-
114106
- (FlutterError*)activateSystemCursor:(nonnull NSDictionary*)arguments {
115107
NSString* kindArg = arguments[kKindKey];
116108
if (!kindArg) {
117109
return [FlutterError errorWithCode:@"error"
118110
message:@"Missing argument"
119111
details:@"Missing argument while trying to activate system cursor"];
120112
}
121-
if ([kindArg isEqualToString:kKindValueNone]) {
122-
[self hide];
123-
return nil;
124-
}
113+
125114
NSCursor* cursorObject = [FlutterMouseCursorPlugin cursorFromKind:kindArg];
126115
[self displayCursorObject:cursorObject];
127116
return nil;
128117
}
129118

130119
- (void)displayCursorObject:(nonnull NSCursor*)cursorObject {
131120
[cursorObject set];
132-
if (_hidden) {
133-
[NSCursor unhide];
134-
}
135-
_hidden = NO;
136-
}
137-
138-
- (void)hide {
139-
if (!_hidden) {
140-
[NSCursor hide];
141-
}
142-
_hidden = YES;
121+
[self.delegate didUpdateMouseCursor:cursorObject];
143122
}
144123

145124
+ (NSCursor*)cursorFromKind:(NSString*)kind {
@@ -154,9 +133,15 @@ + (NSCursor*)cursorFromKind:(NSString*)kind {
154133
#pragma mark - FlutterPlugin implementation
155134

156135
+ (void)registerWithRegistrar:(id<FlutterPluginRegistrar>)registrar {
136+
[self registerWithRegistrar:registrar delegate:nil];
137+
}
138+
139+
+ (void)registerWithRegistrar:(id<FlutterPluginRegistrar>)registrar
140+
delegate:(id<FlutterMouseCursorPluginDelegate>)delegate {
157141
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:kMouseCursorChannel
158142
binaryMessenger:registrar.messenger];
159143
FlutterMouseCursorPlugin* instance = [[FlutterMouseCursorPlugin alloc] init];
144+
instance.delegate = delegate;
160145
[registrar addMethodCallDelegate:instance channel:channel];
161146
}
162147

0 commit comments

Comments
 (0)