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