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