[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
This commit is contained in:
Chris Bracken 2023-06-02 16:35:43 -07:00 committed by GitHub
parent ce41b91c3b
commit 49e25a2dee
2 changed files with 65 additions and 7 deletions

View File

@ -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.

View File

@ -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<FlutterPlatformViewMutation> 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];