From 49e25a2dee41211330c195363e7cbabb704ae022 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 2 Jun 2023 16:35:43 -0700 Subject: [PATCH] [macOS] Top-left origin for PlatformView container (flutter/engine#42523) For consistency with Flutter (and all other platforms), Flutter views in the macOS embedder set `isFlipped` to ensure a co-ordinate system with the origin in the top-left, with y co-ordinates increasing towards the bottom edge of the view. Previously, we were using a stock NSView as the container, which uses a bottom-left origin by default. Instead we extract the PlatformView container view as its own class with `isFlipped` true. This doesn't correct the issue of the view anchorpoint/position but does correct rotation direction. This also applies the transform back to origin prior to other transforms when adjusting the platformview position rather than after. Issue: https://github.com/flutter/flutter/issues/124490 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- .../framework/Source/FlutterMutatorView.mm | 33 ++++++++++++---- .../Source/FlutterMutatorViewTest.mm | 39 +++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm index de10cd4680..e0cf184d0c 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm @@ -12,8 +12,8 @@ #include "flutter/shell/platform/embedder/embedder.h" @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 + // Each of these views clips to a CGPathRef. These views, if present, + // are nested (first is child of FlutterMutatorView and last is parent of // _platformView). NSMutableArray* _pathClipViews; @@ -26,6 +26,21 @@ @end +/// Superview container for platform views, to which sublayer transforms are applied. +@interface FlutterPlatformViewContainer : NSView +@end + +@implementation FlutterPlatformViewContainer + +- (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 + // sublayerTransforms. + return YES; +} + +@end + /// View that clips that content to a specific CGPathRef. /// Clipping is done through a CAShapeLayer mask, which avoids the need to /// rasterize the mask. @@ -43,6 +58,9 @@ } - (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 + // sublayerTransforms. return YES; } @@ -400,7 +418,7 @@ NSMutableArray* RoundedRectClipsFromMutations(CGRect master_clip, const Mutation clipRect:(CGRect)clipRect { // Create the PlatformViewContainer view if necessary. if (_platformViewContainer == nil) { - _platformViewContainer = [[NSView alloc] initWithFrame:self.bounds]; + _platformViewContainer = [[FlutterPlatformViewContainer alloc] initWithFrame:self.bounds]; _platformViewContainer.wantsLayer = YES; } @@ -409,14 +427,15 @@ NSMutableArray* RoundedRectClipsFromMutations(CGRect master_clip, const Mutation [containerSuperview addSubview:_platformViewContainer]; _platformViewContainer.frame = self.bounds; - // Add the + // Nest the platform view in the PlatformViewContainer. [_platformViewContainer addSubview:_platformView]; _platformView.frame = untransformedBounds; // Transform for the platform view is finalTransform adjusted for bounding rect origin. - _platformViewContainer.layer.sublayerTransform = - CATransform3DTranslate(transform, -transformedBounds.origin.x / transform.m11 /* scaleX */, - -transformedBounds.origin.y / transform.m22 /* scaleY */, 0); + CATransform3D translation = + CATransform3DMakeTranslation(-transformedBounds.origin.x, -transformedBounds.origin.y, 0); + transform = CATransform3DConcat(transform, translation); + _platformViewContainer.layer.sublayerTransform = transform; // 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. diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorViewTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorViewTest.mm index 78a59a9213..f9e7bb6f30 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorViewTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorViewTest.mm @@ -301,6 +301,45 @@ TEST(FlutterMutatorViewTest, RoundRectClipsToSimpleRectangle) { EXPECT_EQ(mutatorView.pathClipViews.count, 0ul); } +// Ensure that the mutator view, clip views, and container all use a flipped y axis. The transforms +// sent from the framework assume this, and so aside from the consistency with every other embedder, +// we can avoid a lot of extra math. +TEST(FlutterMutatorViewTest, ViewsSetIsFlipped) { + NSView* platformView = [[NSView alloc] init]; + FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView]; + + std::vector mutations{ + { + .type = kFlutterPlatformViewMutationTypeClipRoundedRect, + .clip_rounded_rect = + FlutterRoundedRect{ + .rect = FlutterRect{110, 60, 150, 150}, + .upper_left_corner_radius = FlutterSize{10, 10}, + .upper_right_corner_radius = FlutterSize{10, 10}, + .lower_right_corner_radius = FlutterSize{10, 10}, + .lower_left_corner_radius = FlutterSize{10, 10}, + }, + }, + { + .type = kFlutterPlatformViewMutationTypeTransformation, + .transformation = + FlutterTransformation{ + .scaleX = 1, + .transX = 100, + .scaleY = 1, + .transY = 50, + }, + }, + }; + + ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations); + + EXPECT_TRUE(mutatorView.isFlipped); + ASSERT_EQ(mutatorView.pathClipViews.count, 1ul); + EXPECT_TRUE(mutatorView.pathClipViews.firstObject.isFlipped); + EXPECT_TRUE(mutatorView.platformViewContainer.isFlipped); +} + TEST(FlutterMutatorViewTest, RoundRectClipsToPath) { NSView* platformView = [[NSView alloc] init]; FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];