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