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 #include <vector>
9 
10 #include "flutter/fml/logging.h"
11 #include "flutter/shell/platform/embedder/embedder.h"
12 
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.
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