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 
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.
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