blob: 146b1604215a27dab544975fd7ada9e68dd2f8f9 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h"
#include "flutter/fml/logging.h"
#include <QuartzCore/QuartzCore.h>
#include <vector>
@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
// _platformView).
NSMutableArray* _pathClipViews;
// View right above the platform view. Used to apply the final transform
// (sans the translation) to the platform view.
NSView* _platformViewContainer;
NSView* _platformView;
}
@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.
@interface FlutterPathClipView : NSView
@end
@implementation FlutterPathClipView
- (instancetype)initWithFrame:(NSRect)frameRect {
if (self = [super initWithFrame:frameRect]) {
self.wantsLayer = YES;
}
return self;
}
- (BOOL)isFlipped {
return YES;
}
/// Clip the view to the given path. Offset top left corner of platform view
/// in global logical coordinates.
- (void)maskToPath:(CGPathRef)path withOrigin:(CGPoint)origin {
CAShapeLayer* maskLayer = self.layer.mask;
if (maskLayer == nil) {
maskLayer = [CAShapeLayer layer];
self.layer.mask = maskLayer;
}
maskLayer.path = path;
maskLayer.transform = CATransform3DMakeTranslation(-origin.x, -origin.y, 0);
}
@end
namespace {
CATransform3D ToCATransform3D(const FlutterTransformation& t) {
CATransform3D transform = CATransform3DIdentity;
transform.m11 = t.scaleX;
transform.m21 = t.skewX;
transform.m41 = t.transX;
transform.m14 = t.pers0;
transform.m12 = t.skewY;
transform.m22 = t.scaleY;
transform.m42 = t.transY;
transform.m24 = t.pers1;
return transform;
}
CGRect FromFlutterRect(const FlutterRect& rect) {
return CGRectMake(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
}
FlutterRect ToFlutterRect(const CGRect& rect) {
return FlutterRect{
.left = rect.origin.x,
.top = rect.origin.y,
.right = rect.origin.x + rect.size.width,
.bottom = rect.origin.y + rect.size.height,
};
}
/// Returns whether the point is inside ellipse with given radius (centered at 0, 0).
bool PointInsideEllipse(const CGPoint& point, const FlutterSize& radius) {
return (point.x * point.x) / (radius.width * radius.width) +
(point.y * point.y) / (radius.height * radius.height) <
1.0;
}
bool RoundRectCornerIntersects(const FlutterRoundedRect& roundRect, const FlutterRect& rect) {
// Inner coordinate of the top left corner of the round rect.
CGPoint inner_top_left =
CGPointMake(roundRect.rect.left + roundRect.upper_left_corner_radius.width,
roundRect.rect.top + roundRect.upper_left_corner_radius.height);
// Position of `rect` corner relative to inner_top_left.
CGPoint relative_top_left =
CGPointMake(rect.left - inner_top_left.x, rect.top - inner_top_left.y);
// `relative_top_left` is in upper left quadrant.
if (relative_top_left.x < 0 && relative_top_left.y < 0) {
if (!PointInsideEllipse(relative_top_left, roundRect.upper_left_corner_radius)) {
return true;
}
}
// Inner coordinate of the top right corner of the round rect.
CGPoint inner_top_right =
CGPointMake(roundRect.rect.right - roundRect.upper_right_corner_radius.width,
roundRect.rect.top + roundRect.upper_right_corner_radius.height);
// Positon of `rect` corner relative to inner_top_right.
CGPoint relative_top_right =
CGPointMake(rect.right - inner_top_right.x, rect.top - inner_top_right.y);
// `relative_top_right` is in top right quadrant.
if (relative_top_right.x > 0 && relative_top_right.y < 0) {
if (!PointInsideEllipse(relative_top_right, roundRect.upper_right_corner_radius)) {
return true;
}
}
// Inner coordinate of the bottom left corner of the round rect.
CGPoint inner_bottom_left =
CGPointMake(roundRect.rect.left + roundRect.lower_left_corner_radius.width,
roundRect.rect.bottom - roundRect.lower_left_corner_radius.height);
// Position of `rect` corner relative to inner_bottom_left.
CGPoint relative_bottom_left =
CGPointMake(rect.left - inner_bottom_left.x, rect.bottom - inner_bottom_left.y);
// `relative_bottom_left` is in bottom left quadrant.
if (relative_bottom_left.x < 0 && relative_bottom_left.y > 0) {
if (!PointInsideEllipse(relative_bottom_left, roundRect.lower_left_corner_radius)) {
return true;
}
}
// Inner coordinate of the bottom right corner of the round rect.
CGPoint inner_bottom_right =
CGPointMake(roundRect.rect.right - roundRect.lower_right_corner_radius.width,
roundRect.rect.bottom - roundRect.lower_right_corner_radius.height);
// Position of `rect` corner relative to inner_bottom_right.
CGPoint relative_bottom_right =
CGPointMake(rect.right - inner_bottom_right.x, rect.bottom - inner_bottom_right.y);
// `relative_bottom_right` is in bottom right quadrant.
if (relative_bottom_right.x > 0 && relative_bottom_right.y > 0) {
if (!PointInsideEllipse(relative_bottom_right, roundRect.lower_right_corner_radius)) {
return true;
}
}
return false;
}
CGPathRef PathFromRoundedRect(const FlutterRoundedRect& roundedRect) {
CGMutablePathRef path = CGPathCreateMutable();
const auto& rect = roundedRect.rect;
const auto& topLeft = roundedRect.upper_left_corner_radius;
const auto& topRight = roundedRect.upper_right_corner_radius;
const auto& bottomLeft = roundedRect.lower_left_corner_radius;
const auto& bottomRight = roundedRect.lower_right_corner_radius;
CGPathMoveToPoint(path, nullptr, rect.left + topLeft.width, rect.top);
CGPathAddLineToPoint(path, nullptr, rect.right - topRight.width, rect.top);
CGPathAddCurveToPoint(path, nullptr, rect.right, rect.top, rect.right, rect.top + topRight.height,
rect.right, rect.top + topRight.height);
CGPathAddLineToPoint(path, nullptr, rect.right, rect.bottom - bottomRight.height);
CGPathAddCurveToPoint(path, nullptr, rect.right, rect.bottom, rect.right - bottomRight.width,
rect.bottom, rect.right - bottomRight.width, rect.bottom);
CGPathAddLineToPoint(path, nullptr, rect.left + bottomLeft.width, rect.bottom);
CGPathAddCurveToPoint(path, nullptr, rect.left, rect.bottom, rect.left,
rect.bottom - bottomLeft.height, rect.left,
rect.bottom - bottomLeft.height);
CGPathAddLineToPoint(path, nullptr, rect.left, rect.top + topLeft.height);
CGPathAddCurveToPoint(path, nullptr, rect.left, rect.top, rect.left + topLeft.width, rect.top,
rect.left + topLeft.width, rect.top);
CGPathCloseSubpath(path);
return path;
}
} // namespace
@implementation FlutterMutatorView
- (NSView*)platformView {
return _platformView;
}
- (NSMutableArray*)pathClipViews {
return _pathClipViews;
}
- (NSView*)platformViewContainer {
return _platformViewContainer;
}
- (instancetype)initWithPlatformView:(NSView*)platformView {
if (self = [super initWithFrame:NSZeroRect]) {
_platformView = platformView;
_pathClipViews = [NSMutableArray array];
self.wantsLayer = YES;
}
return self;
}
- (NSView*)hitTest:(NSPoint)point {
return nil;
}
- (BOOL)isFlipped {
return YES;
}
/// Whenever possible view will be clipped using layer bounds.
/// If clipping to path is needed, CAShapeLayer(s) will be used as mask.
/// Clipping to round rect only clips to path if round corners are intersected.
- (void)applyFlutterLayer:(const FlutterLayer*)layer {
CGFloat scale = self.superview != nil ? self.superview.layer.contentsScale : 1.0;
// Initial transform to compensate for scale factor. This is needed because all
// cocoa coordinates are logical but Flutter will send the physical to logical
// transform in mutations.
CATransform3D transform = CATransform3DMakeScale(1.0 / scale, 1.0 / scale, 1);
// Platform view transform after applying all transformation mutations.
CATransform3D finalTransform = transform;
for (size_t i = 0; i < layer->platform_view->mutations_count; ++i) {
auto mutation = layer->platform_view->mutations[i];
if (mutation->type == kFlutterPlatformViewMutationTypeTransformation) {
finalTransform =
CATransform3DConcat(ToCATransform3D(mutation->transformation), finalTransform);
}
}
CGRect untransformedBoundingRect =
CGRectMake(0, 0, layer->size.width / scale, layer->size.height / scale);
CGRect finalBoundingRect = CGRectApplyAffineTransform(
untransformedBoundingRect, CATransform3DGetAffineTransform(finalTransform));
self.frame = finalBoundingRect;
// Master clip in global logical coordinates. This is intersection of all clip rectangles
// present in mutators.
CGRect masterClip = finalBoundingRect;
self.layer.opacity = 1.0;
// Gathered pairs of rounded rect in local coordinates + appropriate transform.
std::vector<std::pair<FlutterRoundedRect, CGAffineTransform>> roundedRects;
for (size_t i = 0; i < layer->platform_view->mutations_count; ++i) {
auto mutation = layer->platform_view->mutations[i];
if (mutation->type == kFlutterPlatformViewMutationTypeTransformation) {
transform = CATransform3DConcat(ToCATransform3D(mutation->transformation), transform);
} else if (mutation->type == kFlutterPlatformViewMutationTypeClipRect) {
CGRect rect = CGRectApplyAffineTransform(FromFlutterRect(mutation->clip_rect),
CATransform3DGetAffineTransform(transform));
masterClip = CGRectIntersection(rect, masterClip);
} else if (mutation->type == kFlutterPlatformViewMutationTypeClipRoundedRect) {
CGAffineTransform affineTransform = CATransform3DGetAffineTransform(transform);
roundedRects.push_back(std::make_pair(mutation->clip_rounded_rect, affineTransform));
CGRect rect = CGRectApplyAffineTransform(FromFlutterRect(mutation->clip_rounded_rect.rect),
affineTransform);
masterClip = CGRectIntersection(rect, masterClip);
} else if (mutation->type == kFlutterPlatformViewMutationTypeOpacity) {
self.layer.opacity *= mutation->opacity;
}
}
if (CGRectIsNull(masterClip)) {
self.hidden = YES;
return;
}
self.hidden = NO;
/// Paths in global logical coordinates that need to be clipped to.
NSMutableArray* paths = [NSMutableArray array];
for (const auto& r : roundedRects) {
CGAffineTransform inverse = CGAffineTransformInvert(r.second);
// Transform master clip to clip rect coordinates and check if this view intersects one of the
// corners, which means we need to use path clipping.
CGRect localMasterClip = CGRectApplyAffineTransform(masterClip, inverse);
// Only clip to rounded rectangle path if the view intersects some of the round corners. If
// not, clipping to masterClip is enough.
if (RoundRectCornerIntersects(r.first, ToFlutterRect(localMasterClip))) {
CGPathRef path = PathFromRoundedRect(r.first);
CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &r.second);
[paths addObject:(__bridge id)transformedPath];
CGPathRelease(transformedPath);
CGPathRelease(path);
}
}
// Add / remove path clip views depending on the number of paths.
while (_pathClipViews.count > paths.count) {
NSView* view = _pathClipViews.lastObject;
[view removeFromSuperview];
[_pathClipViews removeLastObject];
}
NSView* lastView = self;
for (size_t i = 0; i < paths.count; ++i) {
FlutterPathClipView* pathClipView = nil;
if (i < _pathClipViews.count) {
pathClipView = _pathClipViews[i];
} else {
pathClipView = [[FlutterPathClipView alloc] initWithFrame:self.bounds];
[_pathClipViews addObject:pathClipView];
[lastView addSubview:pathClipView];
}
pathClipView.frame = self.bounds;
[pathClipView maskToPath:(__bridge CGPathRef)[paths objectAtIndex:i]
withOrigin:finalBoundingRect.origin];
lastView = pathClipView;
}
// Used to apply sublayer transform.
if (_platformViewContainer == nil) {
_platformViewContainer = [[NSView alloc] initWithFrame:self.bounds];
_platformViewContainer.wantsLayer = YES;
}
[lastView addSubview:_platformViewContainer];
_platformViewContainer.frame = self.bounds;
[_platformViewContainer addSubview:_platformView];
_platformView.frame = untransformedBoundingRect;
// Transform for the platform view is finalTransform adjusted for bounding rect origin.
_platformViewContainer.layer.sublayerTransform = CATransform3DTranslate(
finalTransform, -finalBoundingRect.origin.x / finalTransform.m11 /* scaleX */,
-finalBoundingRect.origin.y / finalTransform.m22 /* scaleY */, 0);
// 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.
if (!CGRectEqualToRect(masterClip, finalBoundingRect)) {
FML_DCHECK(self.subviews.count == 1);
auto subview = self.subviews.firstObject;
FML_DCHECK(subview.frame.origin.x == 0 && subview.frame.origin.y == 0);
subview.frame = CGRectMake(finalBoundingRect.origin.x - masterClip.origin.x,
finalBoundingRect.origin.y - masterClip.origin.y,
subview.frame.size.width, subview.frame.size.height);
self.frame = masterClip;
}
}
@end