| Flutter macOS Embedder |

FlutterMutatorView.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h"
6
7 #include <QuartzCore/QuartzCore.h>
8 #include <vector>
9
10 #include "flutter/fml/logging.h"
11 #include "flutter/shell/platform/embedder/embedder.h"
12
13 #import "flutter/shell/platform/darwin/macos/framework/Source/NSView+ClipsToBounds.h"
14
15 @interface FlutterMutatorView () {
16 // Each of these views clips to a CGPathRef. These views, if present,
17 // are nested (first is child of FlutterMutatorView and last is parent of
18 // _platformView).
19 NSMutableArray* _pathClipViews;
20
21 // View right above the platform view. Used to apply the final transform
22 // (sans the translation) to the platform view.
23 NSView* _platformViewContainer;
24
25 NSView* _platformView;
26 }
27
28 @end
29
30 /// Superview container for platform views, to which sublayer transforms are applied.
31 @interface FlutterPlatformViewContainer : NSView
32 @end
33
34 @implementation FlutterPlatformViewContainer
35
36 - (BOOL)isFlipped {
37 // Flutter transforms assume a coordinate system with an upper-left corner origin, with y
38 // coordinate values increasing downwards. This affects the view, view transforms, and
39 // sublayerTransforms.
40 return YES;
41 }
42
43 @end
44
45 /// View that clips that content to a specific CGPathRef.
46 /// Clipping is done through a CAShapeLayer mask, which avoids the need to
47 /// rasterize the mask.
48 @interface FlutterPathClipView : NSView
49
50 @end
51
52 @implementation FlutterPathClipView
53
54 - (instancetype)initWithFrame:(NSRect)frameRect {
55 if (self = [super initWithFrame:frameRect]) {
56 self.wantsLayer = YES;
57 }
58 return self;
59 }
60
61 - (BOOL)isFlipped {
62 // Flutter transforms assume a coordinate system with an upper-left corner origin, with y
63 // coordinate values increasing downwards. This affects the view, view transforms, and
64 // sublayerTransforms.
65 return YES;
66 }
67
68 /// Clip the view to the given path. Offset top left corner of platform view
69 /// in global logical coordinates.
70 - (void)maskToPath:(CGPathRef)path withOrigin:(CGPoint)origin {
71 CAShapeLayer* maskLayer = self.layer.mask;
72 if (maskLayer == nil) {
73 maskLayer = [CAShapeLayer layer];
74 self.layer.mask = maskLayer;
75 }
76 maskLayer.path = path;
77 maskLayer.transform = CATransform3DMakeTranslation(-origin.x, -origin.y, 0);
78 }
79
80 @end
81
82 namespace {
83 CATransform3D ToCATransform3D(const FlutterTransformation& t) {
84 CATransform3D transform = CATransform3DIdentity;
85 transform.m11 = t.scaleX;
86 transform.m21 = t.skewX;
87 transform.m41 = t.transX;
88 transform.m14 = t.pers0;
89 transform.m12 = t.skewY;
90 transform.m22 = t.scaleY;
91 transform.m42 = t.transY;
92 transform.m24 = t.pers1;
93 return transform;
94 }
95
96 bool AffineTransformIsOnlyScaleOrTranslate(const CGAffineTransform& transform) {
97 return transform.b == 0 && transform.c == 0;
98 }
99
100 bool IsZeroSize(const FlutterSize size) {
101 return size.width == 0 && size.height == 0;
102 }
103
104 CGRect FromFlutterRect(const FlutterRect& rect) {
105 return CGRectMake(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
106 }
107
108 FlutterRect ToFlutterRect(const CGRect& rect) {
109 return FlutterRect{
110 .left = rect.origin.x,
111 .top = rect.origin.y,
112 .right = rect.origin.x + rect.size.width,
113 .bottom = rect.origin.y + rect.size.height,
114
115 };
116 }
117
118 /// Returns whether the point is inside ellipse with given radius (centered at 0, 0).
119 bool PointInsideEllipse(const CGPoint& point, const FlutterSize& radius) {
120 return (point.x * point.x) / (radius.width * radius.width) +
121 (point.y * point.y) / (radius.height * radius.height) <
122 1.0;
123 }
124
125 bool RoundRectCornerIntersects(const FlutterRoundedRect& roundRect, const FlutterRect& rect) {
126 // Inner coordinate of the top left corner of the round rect.
127 CGPoint inner_top_left =
128 CGPointMake(roundRect.rect.left + roundRect.upper_left_corner_radius.width,
129 roundRect.rect.top + roundRect.upper_left_corner_radius.height);
130
131 // Position of `rect` corner relative to inner_top_left.
132 CGPoint relative_top_left =
133 CGPointMake(rect.left - inner_top_left.x, rect.top - inner_top_left.y);
134
135 // `relative_top_left` is in upper left quadrant.
136 if (relative_top_left.x < 0 && relative_top_left.y < 0) {
137 if (!PointInsideEllipse(relative_top_left, roundRect.upper_left_corner_radius)) {
138 return true;
139 }
140 }
141
142 // Inner coordinate of the top right corner of the round rect.
143 CGPoint inner_top_right =
144 CGPointMake(roundRect.rect.right - roundRect.upper_right_corner_radius.width,
145 roundRect.rect.top + roundRect.upper_right_corner_radius.height);
146
147 // Positon of `rect` corner relative to inner_top_right.
148 CGPoint relative_top_right =
149 CGPointMake(rect.right - inner_top_right.x, rect.top - inner_top_right.y);
150
151 // `relative_top_right` is in top right quadrant.
152 if (relative_top_right.x > 0 && relative_top_right.y < 0) {
153 if (!PointInsideEllipse(relative_top_right, roundRect.upper_right_corner_radius)) {
154 return true;
155 }
156 }
157
158 // Inner coordinate of the bottom left corner of the round rect.
159 CGPoint inner_bottom_left =
160 CGPointMake(roundRect.rect.left + roundRect.lower_left_corner_radius.width,
161 roundRect.rect.bottom - roundRect.lower_left_corner_radius.height);
162
163 // Position of `rect` corner relative to inner_bottom_left.
164 CGPoint relative_bottom_left =
165 CGPointMake(rect.left - inner_bottom_left.x, rect.bottom - inner_bottom_left.y);
166
167 // `relative_bottom_left` is in bottom left quadrant.
168 if (relative_bottom_left.x < 0 && relative_bottom_left.y > 0) {
169 if (!PointInsideEllipse(relative_bottom_left, roundRect.lower_left_corner_radius)) {
170 return true;
171 }
172 }
173
174 // Inner coordinate of the bottom right corner of the round rect.
175 CGPoint inner_bottom_right =
176 CGPointMake(roundRect.rect.right - roundRect.lower_right_corner_radius.width,
177 roundRect.rect.bottom - roundRect.lower_right_corner_radius.height);
178
179 // Position of `rect` corner relative to inner_bottom_right.
180 CGPoint relative_bottom_right =
181 CGPointMake(rect.right - inner_bottom_right.x, rect.bottom - inner_bottom_right.y);
182
183 // `relative_bottom_right` is in bottom right quadrant.
184 if (relative_bottom_right.x > 0 && relative_bottom_right.y > 0) {
185 if (!PointInsideEllipse(relative_bottom_right, roundRect.lower_right_corner_radius)) {
186 return true;
187 }
188 }
189
190 return false;
191 }
192
193 CGPathRef PathFromRoundedRect(const FlutterRoundedRect& roundedRect) {
194 if (IsZeroSize(roundedRect.lower_left_corner_radius) &&
195 IsZeroSize(roundedRect.lower_right_corner_radius) &&
196 IsZeroSize(roundedRect.upper_left_corner_radius) &&
197 IsZeroSize(roundedRect.upper_right_corner_radius)) {
198 return CGPathCreateWithRect(FromFlutterRect(roundedRect.rect), nullptr);
199 }
200
201 CGMutablePathRef path = CGPathCreateMutable();
202
203 const auto& rect = roundedRect.rect;
204 const auto& topLeft = roundedRect.upper_left_corner_radius;
205 const auto& topRight = roundedRect.upper_right_corner_radius;
206 const auto& bottomLeft = roundedRect.lower_left_corner_radius;
207 const auto& bottomRight = roundedRect.lower_right_corner_radius;
208
209 CGPathMoveToPoint(path, nullptr, rect.left + topLeft.width, rect.top);
210 CGPathAddLineToPoint(path, nullptr, rect.right - topRight.width, rect.top);
211 CGPathAddCurveToPoint(path, nullptr, rect.right, rect.top, rect.right, rect.top + topRight.height,
212 rect.right, rect.top + topRight.height);
213 CGPathAddLineToPoint(path, nullptr, rect.right, rect.bottom - bottomRight.height);
214 CGPathAddCurveToPoint(path, nullptr, rect.right, rect.bottom, rect.right - bottomRight.width,
215 rect.bottom, rect.right - bottomRight.width, rect.bottom);
216 CGPathAddLineToPoint(path, nullptr, rect.left + bottomLeft.width, rect.bottom);
217 CGPathAddCurveToPoint(path, nullptr, rect.left, rect.bottom, rect.left,
218 rect.bottom - bottomLeft.height, rect.left,
219 rect.bottom - bottomLeft.height);
220 CGPathAddLineToPoint(path, nullptr, rect.left, rect.top + topLeft.height);
221 CGPathAddCurveToPoint(path, nullptr, rect.left, rect.top, rect.left + topLeft.width, rect.top,
222 rect.left + topLeft.width, rect.top);
223 CGPathCloseSubpath(path);
224 return path;
225 }
226
227 using MutationVector = std::vector<FlutterPlatformViewMutation>;
228
229 /// Returns a vector of FlutterPlatformViewMutation object pointers associated with a platform view.
230 /// The transforms sent from the engine include a transform from logical to physical coordinates.
231 /// Since Cocoa deals only in logical points, this function prepends a scale transform that scales
232 /// back from physical to logical coordinates to compensate.
233 MutationVector MutationsForPlatformView(const FlutterPlatformView* view, float scale) {
234 MutationVector mutations;
235 mutations.reserve(view->mutations_count + 1);
236 mutations.push_back({
237 .type = kFlutterPlatformViewMutationTypeTransformation,
238 .transformation{
239 .scaleX = 1.0 / scale,
240 .scaleY = 1.0 / scale,
241 },
242 });
243 for (size_t i = 0; i < view->mutations_count; ++i) {
244 mutations.push_back(*view->mutations[i]);
245 }
246 return mutations;
247 }
248
249 /// Returns the composition of all transformation mutations in the mutations vector.
250 CATransform3D CATransformFromMutations(const MutationVector& mutations) {
251 CATransform3D transform = CATransform3DIdentity;
252 for (auto mutation : mutations) {
253 switch (mutation.type) {
254 case kFlutterPlatformViewMutationTypeTransformation: {
255 CATransform3D mutationTransform = ToCATransform3D(mutation.transformation);
256 transform = CATransform3DConcat(mutationTransform, transform);
257 break;
258 }
259 case kFlutterPlatformViewMutationTypeClipRect:
260 case kFlutterPlatformViewMutationTypeClipRoundedRect:
261 case kFlutterPlatformViewMutationTypeOpacity:
262 break;
263 }
264 }
265 return transform;
266 }
267
268 /// Returns the opacity for all opacity mutations in the mutations vector.
269 float OpacityFromMutations(const MutationVector& mutations) {
270 float opacity = 1.0;
271 for (auto mutation : mutations) {
272 switch (mutation.type) {
273 case kFlutterPlatformViewMutationTypeOpacity:
274 opacity *= mutation.opacity;
275 break;
276 case kFlutterPlatformViewMutationTypeClipRect:
277 case kFlutterPlatformViewMutationTypeClipRoundedRect:
278 case kFlutterPlatformViewMutationTypeTransformation:
279 break;
280 }
281 }
282 return opacity;
283 }
284
285 /// Returns the clip rect generated by the intersection of clips in the mutations vector.
286 CGRect MasterClipFromMutations(CGRect bounds, const MutationVector& mutations) {
287 // Master clip in global logical coordinates. This is intersection of all clip rectangles
288 // present in mutators.
289 CGRect master_clip = bounds;
290
291 // Create the initial transform.
292 CATransform3D transform = CATransform3DIdentity;
293 for (auto mutation : mutations) {
294 switch (mutation.type) {
295 case kFlutterPlatformViewMutationTypeClipRect: {
296 CGRect rect = CGRectApplyAffineTransform(FromFlutterRect(mutation.clip_rect),
297 CATransform3DGetAffineTransform(transform));
298 master_clip = CGRectIntersection(rect, master_clip);
299 break;
300 }
301 case kFlutterPlatformViewMutationTypeClipRoundedRect: {
302 CGAffineTransform affineTransform = CATransform3DGetAffineTransform(transform);
303 CGRect rect = CGRectApplyAffineTransform(FromFlutterRect(mutation.clip_rounded_rect.rect),
304 affineTransform);
305 master_clip = CGRectIntersection(rect, master_clip);
306 break;
307 }
308 case kFlutterPlatformViewMutationTypeTransformation:
309 transform = CATransform3DConcat(ToCATransform3D(mutation.transformation), transform);
310 break;
311 case kFlutterPlatformViewMutationTypeOpacity:
312 break;
313 }
314 }
315 return master_clip;
316 }
317
318 /// A rounded rectangle and transform associated with it.
319 typedef struct {
320 FlutterRoundedRect rrect;
321 CGAffineTransform transform;
322 } ClipRoundedRect;
323
324 /// Returns the set of all rounded rect paths generated by clips in the mutations vector.
325 NSMutableArray* ClipPathFromMutations(CGRect master_clip, const MutationVector& mutations) {
326 std::vector<ClipRoundedRect> rounded_rects;
327
328 CATransform3D transform = CATransform3DIdentity;
329 for (auto mutation : mutations) {
330 switch (mutation.type) {
331 case kFlutterPlatformViewMutationTypeClipRoundedRect: {
332 CGAffineTransform affineTransform = CATransform3DGetAffineTransform(transform);
333 rounded_rects.push_back({mutation.clip_rounded_rect, affineTransform});
334 break;
335 }
336 case kFlutterPlatformViewMutationTypeTransformation:
337 transform = CATransform3DConcat(ToCATransform3D(mutation.transformation), transform);
338 break;
339 case kFlutterPlatformViewMutationTypeClipRect: {
340 CGAffineTransform affineTransform = CATransform3DGetAffineTransform(transform);
341 // Shearing or rotation requires path clipping.
342 if (!AffineTransformIsOnlyScaleOrTranslate(affineTransform)) {
343 rounded_rects.push_back(
344 {FlutterRoundedRect{mutation.clip_rect, FlutterSize{0, 0}, FlutterSize{0, 0},
345 FlutterSize{0, 0}, FlutterSize{0, 0}},
346 affineTransform});
347 }
348 break;
349 }
350 case kFlutterPlatformViewMutationTypeOpacity:
351 break;
352 }
353 }
354
355 NSMutableArray* paths = [NSMutableArray array];
356 for (const auto& r : rounded_rects) {
357 bool requiresPath = !AffineTransformIsOnlyScaleOrTranslate(r.transform);
358 if (!requiresPath) {
359 CGAffineTransform inverse = CGAffineTransformInvert(r.transform);
360 // Transform master clip to clip rect coordinates and check if this view intersects one of the
361 // corners, which means we need to use path clipping.
362 CGRect localMasterClip = CGRectApplyAffineTransform(master_clip, inverse);
363 requiresPath = RoundRectCornerIntersects(r.rrect, ToFlutterRect(localMasterClip));
364 }
365
366 // Only clip to rounded rectangle path if the view intersects some of the round corners. If
367 // not, clipping to masterClip is enough.
368 if (requiresPath) {
369 CGPathRef path = PathFromRoundedRect(r.rrect);
370 CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &r.transform);
371 [paths addObject:(__bridge id)transformedPath];
372 CGPathRelease(transformedPath);
373 CGPathRelease(path);
374 }
375 }
376 return paths;
377 }
378 } // namespace
379
380 @implementation FlutterMutatorView
381
382 - (NSView*)platformView {
383 return _platformView;
384 }
385
386 - (NSMutableArray*)pathClipViews {
387 return _pathClipViews;
388 }
389
390 - (NSView*)platformViewContainer {
391 return _platformViewContainer;
392 }
393
394 - (instancetype)initWithPlatformView:(NSView*)platformView {
395 if (self = [super initWithFrame:NSZeroRect]) {
396 _platformView = platformView;
397 _pathClipViews = [NSMutableArray array];
398 self.wantsLayer = YES;
399 self.clipsToBounds = YES;
400 }
401 return self;
402 }
403
404 - (NSView*)hitTest:(NSPoint)point {
405 return nil;
406 }
407
408 - (BOOL)isFlipped {
409 return YES;
410 }
411
412 /// Returns the scale factor to translate logical pixels to physical pixels for this view.
413 - (CGFloat)contentsScale {
414 return self.superview != nil ? self.superview.layer.contentsScale : 1.0;
415 }
416
417 /// Updates the nested stack of clip views that host the platform view.
418 - (void)updatePathClipViewsWithPaths:(NSArray*)paths {
419 // Remove path clip views depending on the number of paths.
420 while (_pathClipViews.count > paths.count) {
421 NSView* view = _pathClipViews.lastObject;
422 [view removeFromSuperview];
423 [_pathClipViews removeLastObject];
424 }
425 // Otherwise, add path clip views to the end.
426 for (size_t i = _pathClipViews.count; i < paths.count; ++i) {
427 NSView* superView = _pathClipViews.count == 0 ? self : _pathClipViews.lastObject;
428 FlutterPathClipView* pathClipView = [[FlutterPathClipView alloc] initWithFrame:self.bounds];
429 [_pathClipViews addObject:pathClipView];
430 [superView addSubview:pathClipView];
431 }
432 // Update bounds and apply clip paths.
433 for (size_t i = 0; i < _pathClipViews.count; ++i) {
434 FlutterPathClipView* pathClipView = _pathClipViews[i];
435 pathClipView.frame = self.bounds;
436 [pathClipView maskToPath:(__bridge CGPathRef)[paths objectAtIndex:i]
437 withOrigin:self.frame.origin];
438 }
439 }
440
441 /// Updates the PlatformView and PlatformView container views.
442 ///
443 /// Re-nests _platformViewContainer in the innermost clip view, applies transforms to the underlying
444 /// CALayer, adds the platform view as a subview of the container, and sets the axis-aligned clip
445 /// rect around the tranformed view.
446 - (void)updatePlatformViewWithBounds:(CGRect)untransformedBounds
447 transformedBounds:(CGRect)transformedBounds
448 transform:(CATransform3D)transform
449 clipRect:(CGRect)clipRect {
450 // Create the PlatformViewContainer view if necessary.
451 if (_platformViewContainer == nil) {
452 _platformViewContainer = [[FlutterPlatformViewContainer alloc] initWithFrame:self.bounds];
453 _platformViewContainer.wantsLayer = YES;
454 }
455
456 // Nest the PlatformViewContainer view in the innermost path clip view.
457 NSView* containerSuperview = _pathClipViews.count == 0 ? self : _pathClipViews.lastObject;
458 [containerSuperview addSubview:_platformViewContainer];
459 _platformViewContainer.frame = self.bounds;
460
461 // Nest the platform view in the PlatformViewContainer.
462 [_platformViewContainer addSubview:_platformView];
463 _platformView.frame = untransformedBounds;
464
465 // Transform for the platform view is finalTransform adjusted for bounding rect origin.
466 CATransform3D translation =
467 CATransform3DMakeTranslation(-transformedBounds.origin.x, -transformedBounds.origin.y, 0);
468 transform = CATransform3DConcat(transform, translation);
469 _platformViewContainer.layer.sublayerTransform = transform;
470
471 // By default NSView clips children to frame. If masterClip is tighter than mutator view frame,
472 // the frame is set to masterClip and child offset adjusted to compensate for the difference.
473 if (!CGRectEqualToRect(clipRect, transformedBounds)) {
474 FML_DCHECK(self.subviews.count == 1);
475 auto subview = self.subviews.firstObject;
476 FML_DCHECK(subview.frame.origin.x == 0 && subview.frame.origin.y == 0);
477 subview.frame = CGRectMake(transformedBounds.origin.x - clipRect.origin.x,
478 transformedBounds.origin.y - clipRect.origin.y,
479 subview.frame.size.width, subview.frame.size.height);
480 self.frame = clipRect;
481 }
482 }
483
484 /// Whenever possible view will be clipped using layer bounds.
485 /// If clipping to path is needed, CAShapeLayer(s) will be used as mask.
486 /// Clipping to round rect only clips to path if round corners are intersected.
487 - (void)applyFlutterLayer:(const FlutterLayer*)layer {
488 // Compute the untransformed bounding rect for the platform view in logical pixels.
489 // FlutterLayer.size is in physical pixels but Cocoa uses logical points.
490 CGFloat scale = [self contentsScale];
491 MutationVector mutations = MutationsForPlatformView(layer->platform_view, scale);
492
493 CATransform3D finalTransform = CATransformFromMutations(mutations);
494
495 // Compute the untransformed bounding rect for the platform view in logical pixels.
496 // FlutterLayer.size is in physical pixels but Cocoa uses logical points.
497 CGRect untransformedBoundingRect =
498 CGRectMake(0, 0, layer->size.width / scale, layer->size.height / scale);
499 CGRect finalBoundingRect = CGRectApplyAffineTransform(
500 untransformedBoundingRect, CATransform3DGetAffineTransform(finalTransform));
501 self.frame = finalBoundingRect;
502
503 // Compute the layer opacity.
504 self.layer.opacity = OpacityFromMutations(mutations);
505
506 // Compute the master clip in global logical coordinates.
507 CGRect masterClip = MasterClipFromMutations(finalBoundingRect, mutations);
508 if (CGRectIsNull(masterClip)) {
509 self.hidden = YES;
510 return;
511 }
512 self.hidden = NO;
513
514 /// Paths in global logical coordinates that need to be clipped to.
515 NSMutableArray* paths = ClipPathFromMutations(masterClip, mutations);
516 [self updatePathClipViewsWithPaths:paths];
517
518 /// Update PlatformViewContainer, PlatformView, and apply transforms and axis-aligned clip rect.
519 [self updatePlatformViewWithBounds:untransformedBoundingRect
520 transformedBounds:finalBoundingRect
521 transform:finalTransform
522 clipRect:masterClip];
523 }
524
525 @end
FlutterMutatorView.h
FlutterMutatorView
Definition: FlutterMutatorView.h:11
FlutterMutatorView::platformView
NSView * platformView
Returns wrapped platform view.
Definition: FlutterMutatorView.h:17
FlutterMutatorView()::_platformViewContainer
NSView * _platformViewContainer
Definition: FlutterMutatorView.mm:23
FlutterMutatorView()::_platformView
NSView * _platformView
Definition: FlutterMutatorView.mm:25
FlutterPlatformViewContainer
Superview container for platform views, to which sublayer transforms are applied.
Definition: FlutterMutatorView.mm:31
NSView+ClipsToBounds.h
FlutterMutatorView()::_pathClipViews
NSMutableArray * _pathClipViews
Definition: FlutterMutatorView.mm:19
FlutterPathClipView
Definition: FlutterMutatorView.mm:48