Skip to content

Commit

Permalink
[macOS] Top-left origin for PlatformView container (flutter#42523)
Browse files Browse the repository at this point in the history
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: flutter/flutter#124490

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
  • Loading branch information
cbracken authored Jun 2, 2023
1 parent 8f7dc77 commit 5883356
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 7 deletions.
33 changes: 26 additions & 7 deletions shell/platform/darwin/macos/framework/Source/FlutterMutatorView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -26,6 +26,21 @@ @interface FlutterMutatorView () {

@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.
Expand All @@ -43,6 +58,9 @@ - (instancetype)initWithFrame:(NSRect)frameRect {
}

- (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;
}

Expand Down Expand Up @@ -400,7 +418,7 @@ - (void)updatePlatformViewWithBounds:(CGRect)untransformedBounds
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;
}

Expand All @@ -409,14 +427,15 @@ - (void)updatePlatformViewWithBounds:(CGRect)untransformedBounds
[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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,45 @@ void ExpectTransform3DEqual(const CATransform3D& t, const CATransform3D& u) {
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];
Expand Down

0 comments on commit 5883356

Please sign in to comment.