Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[macOS] Implement hit testing and handle platform view cursor changes #43101

Merged
merged 4 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions shell/platform/darwin/macos/framework/Source/FlutterCompositor.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@
#include <functional>
#include <list>
#include <unordered_map>
#include <variant>

#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<FlutterRect> paint_region;
};

typedef std::pair<PlatformViewLayer, size_t> PlatformViewLayerWithIndex;
using LayerVariant = std::variant<PlatformViewLayer, BackingStoreLayer>;

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

void PresentPlatformViews(FlutterView* default_base_view,
const std::vector<PlatformViewLayerWithIndex>& platform_views,
const std::vector<LayerVariant>& layers,
const FlutterPlatformViewController* platform_views_controller);

private:
// Platform view to FlutterMutatorView that contains it.
NSMapTable<NSView*, FlutterMutatorView*>* 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`.
Expand Down
88 changes: 64 additions & 24 deletions shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlatformViewLayerWithIndex> CopyPlatformViewLayers(const FlutterLayer** layers,
size_t layer_count) {
std::vector<PlatformViewLayerWithIndex> platform_views;
std::vector<LayerVariant> CopyLayers(const FlutterLayer** layers, size_t layer_count) {
std::vector<LayerVariant> 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<FlutterRect> 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

Expand Down Expand Up @@ -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<std::vector<PlatformViewLayerWithIndex>>(
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<std::vector<LayerVariant>>(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;
}
Expand All @@ -111,18 +119,45 @@

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

// Active mutator views for this frame.
NSMutableArray<FlutterMutatorView*>* 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<PlatformViewLayer>(layer)) {
continue;
}
const auto& platform_view = std::get<PlatformViewLayer>(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<BackingStoreLayer>(overlay_layer)) {
continue;
}
const auto& backing_store_layer = std::get<BackingStoreLayer>(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<FlutterMutatorView*>* obsolete_mutators =
Expand Down Expand Up @@ -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];
}
Expand Down
17 changes: 15 additions & 2 deletions shell/platform/darwin/macos/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ - (instancetype)initWithConnection:(NSNumber*)connection
/**
* Private interface declaration for FlutterEngine.
*/
@interface FlutterEngine () <FlutterBinaryMessenger>
@interface FlutterEngine () <FlutterBinaryMessenger, FlutterMouseCursorPluginDelegate>

/**
* A mutable array that holds one bool value that determines if responses to platform messages are
Expand Down Expand Up @@ -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<NSNumber*, FlutterVSyncWaiter*>* _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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <NSObject>
- (void)didUpdateMouseCursor:(nonnull NSCursor*)cursor;
@end

/**
* A plugin to handle mouse cursor.
*
Expand All @@ -18,6 +22,9 @@
*/
@interface FlutterMouseCursorPlugin : NSObject <FlutterPlugin>

+ (void)registerWithRegistrar:(nonnull id<FlutterPluginRegistrar>)registrar
delegate:(nullable id<FlutterMouseCursorPluginDelegate>)delegate;

@end

#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERMOUSECURSORPLUGIN_H_
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -58,10 +63,8 @@
}

@interface FlutterMouseCursorPlugin ()
/**
* Whether the cursor is currently hidden.
*/
@property(nonatomic) BOOL hidden;

@property(nonatomic, weak) id<FlutterMouseCursorPluginDelegate> delegate;

/**
* Handles the method call that activates a system cursor.
Expand All @@ -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.
*/
Expand All @@ -105,41 +103,22 @@ - (instancetype)init {
return self;
}

- (void)dealloc {
if (_hidden) {
[NSCursor unhide];
}
}

- (FlutterError*)activateSystemCursor:(nonnull NSDictionary*)arguments {
NSString* kindArg = arguments[kKindKey];
if (!kindArg) {
return [FlutterError errorWithCode:@"error"
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;
}

- (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 {
Expand All @@ -154,9 +133,15 @@ + (NSCursor*)cursorFromKind:(NSString*)kind {
#pragma mark - FlutterPlugin implementation

+ (void)registerWithRegistrar:(id<FlutterPluginRegistrar>)registrar {
[self registerWithRegistrar:registrar delegate:nil];
}

+ (void)registerWithRegistrar:(id<FlutterPluginRegistrar>)registrar
delegate:(id<FlutterMouseCursorPluginDelegate>)delegate {
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:kMouseCursorChannel
binaryMessenger:registrar.messenger];
FlutterMouseCursorPlugin* instance = [[FlutterMouseCursorPlugin alloc] init];
instance.delegate = delegate;
[registrar addMethodCallDelegate:instance channel:channel];
}

Expand Down
Loading