From 5c08658ec2be2c65e4985a698a8f1ba69c04424e Mon Sep 17 00:00:00 2001 From: Hari07 <22373191+Hari-07@users.noreply.github.com> Date: Wed, 17 Dec 2025 01:04:39 +0530 Subject: [PATCH] Platform view blur clipping - Rounded Rect (iOS) (#177551) This PR aims to fix blurs on platform views not being clipped on ios. Currently the clips were not forwarded to the mutator stack and thus just ignored. The frame the blur is in was being used which would always behave like a clip rect ignoring any corner radii or clip paths etc Now all rrect, rse and path clips are being forwarded to the mutator stack. **Currently only rounded rect has been implemented though, and even in that just a single uniform corner radii, which seems to be most of the use cases from users who have run into this issue.** Example: ```dart Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(50), child: Container( width: 200, height: 200, decoration: BoxDecoration( color: Colors.black.withOpacity(0.2), borderRadius: BorderRadius.circular(500), ), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), child: const Center( child: Text('Blurred block 1'), ), ), ), ); ``` Before: After: *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* https://github.com/flutter/flutter/issues/115926 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Gray Mackall <34871572+gmackall@users.noreply.github.com> --- engine/src/flutter/flow/embedded_views.cc | 22 ++++ engine/src/flutter/flow/embedded_views.h | 102 +++++++++++++++++- .../flutter/flow/layers/clip_path_layer.cc | 5 + .../src/flutter/flow/layers/clip_path_layer.h | 2 + .../flutter/flow/layers/clip_rect_layer.cc | 5 + .../src/flutter/flow/layers/clip_rect_layer.h | 2 + .../flutter/flow/layers/clip_rrect_layer.cc | 5 + .../flutter/flow/layers/clip_rrect_layer.h | 2 + .../flow/layers/clip_rsuperellipse_layer.cc | 5 + .../flow/layers/clip_rsuperellipse_layer.h | 2 + .../flutter/flow/layers/clip_shape_layer.h | 7 ++ .../flutter/flow/mutators_stack_unittests.cc | 38 +++++++ .../android/platform_view_android_jni_impl.cc | 8 ++ .../framework/Source/FlutterPlatformViews.mm | 8 ++ .../Source/FlutterPlatformViewsController.h | 16 +++ .../Source/FlutterPlatformViewsController.mm | 73 +++++++++++++ .../Source/FlutterPlatformViewsTest.mm | 89 +++++++++++++++ .../Source/FlutterPlatformViews_Internal.h | 12 +++ .../darwin/ios/ios_external_view_embedder.h | 14 +++ .../darwin/ios/ios_external_view_embedder.mm | 21 ++++ .../embedder_external_view_embedder.cc | 4 + .../platform/embedder/embedder_layers.cc | 4 + 22 files changed, 444 insertions(+), 2 deletions(-) diff --git a/engine/src/flutter/flow/embedded_views.cc b/engine/src/flutter/flow/embedded_views.cc index 57b5997faab..3fc44d0f4e8 100644 --- a/engine/src/flutter/flow/embedded_views.cc +++ b/engine/src/flutter/flow/embedded_views.cc @@ -96,6 +96,28 @@ void MutatorsStack::PushBackdropFilter( vector_.push_back(element); } +void MutatorsStack::PushPlatformViewClipRect(const DlRect& rect) { + std::shared_ptr element = + std::make_shared(BackdropClipRect(rect)); + vector_.push_back(element); +} +void MutatorsStack::PushPlatformViewClipRRect(const DlRoundRect& rrect) { + std::shared_ptr element = + std::make_shared(BackdropClipRRect(rrect)); + vector_.push_back(element); +} +void MutatorsStack::PushPlatformViewClipRSuperellipse( + const DlRoundSuperellipse& rse) { + std::shared_ptr element = + std::make_shared(BackdropClipRSuperellipse(rse)); + vector_.push_back(element); +} +void MutatorsStack::PushPlatformViewClipPath(const DlPath& path) { + std::shared_ptr element = + std::make_shared(BackdropClipPath(path)); + vector_.push_back(element); +} + void MutatorsStack::Pop() { vector_.pop_back(); } diff --git a/engine/src/flutter/flow/embedded_views.h b/engine/src/flutter/flow/embedded_views.h index b9ddd9f3ab3..0800e71ce20 100644 --- a/engine/src/flutter/flow/embedded_views.h +++ b/engine/src/flutter/flow/embedded_views.h @@ -10,6 +10,8 @@ #include #include +#include "display_list/geometry/dl_geometry_types.h" +#include "display_list/geometry/dl_path.h" #include "flutter/display_list/dl_builder.h" #include "flutter/display_list/geometry/dl_geometry_conversions.h" #include "flutter/flow/surface_frame.h" @@ -37,7 +39,11 @@ enum class MutatorType { kClipPath, kTransform, kOpacity, - kBackdropFilter + kBackdropFilter, + kBackdropClipRect, + kBackdropClipRRect, + kBackdropClipRSuperellipse, + kBackdropClipPath, }; // Represents an image filter mutation. @@ -61,6 +67,42 @@ class ImageFilterMutation { const DlRect filter_rect_; }; +struct BackdropClipRect { + DlRect rect; + explicit BackdropClipRect(const DlRect& r) : rect(r) {} + + bool operator==(const BackdropClipRect& other) const { + return rect == other.rect; + } +}; + +struct BackdropClipRRect { + DlRoundRect rrect; + explicit BackdropClipRRect(const DlRoundRect& r) : rrect(r) {} + + bool operator==(const BackdropClipRRect& other) const { + return rrect == other.rrect; + } +}; + +struct BackdropClipRSuperellipse { + DlRoundSuperellipse rse; + explicit BackdropClipRSuperellipse(const DlRoundSuperellipse& r) : rse(r) {} + + bool operator==(const BackdropClipRSuperellipse& other) const { + return rse == other.rse; + } +}; + +struct BackdropClipPath { + DlPath path; + explicit BackdropClipPath(const DlPath& r) : path(r) {} + + bool operator==(const BackdropClipPath& other) const { + return path == other.path; + } +}; + // Stores mutation information like clipping or kTransform. // // The `type` indicates the type of the mutation: kClipRect, kTransform and etc. @@ -81,6 +123,15 @@ class Mutator { const DlRect& filter_rect) : data_(ImageFilterMutation(filter, filter_rect)) {} + explicit Mutator(const BackdropClipRect& backdrop_rect) + : data_(backdrop_rect) {} + explicit Mutator(const BackdropClipRRect& backdrop_rrect) + : data_(backdrop_rrect) {} + explicit Mutator(const BackdropClipRSuperellipse& backdrop_rse) + : data_(backdrop_rse) {} + explicit Mutator(const BackdropClipPath& backdrop_path) + : data_(backdrop_path) {} + MutatorType GetType() const { return static_cast(data_.index()); } @@ -98,6 +149,18 @@ class Mutator { const ImageFilterMutation& GetFilterMutation() const { return std::get(data_); } + const BackdropClipRect& GetBackdropClipRect() const { + return std::get(data_); + } + const BackdropClipRRect& GetBackdropClipRRect() const { + return std::get(data_); + } + const BackdropClipRSuperellipse& GetBackdropClipRSuperellipse() const { + return std::get(data_); + } + const BackdropClipPath& GetBackdropClipPath() const { + return std::get(data_); + } const uint8_t& GetAlpha() const { return std::get(data_); } float GetAlphaFloat() const { return DlColor::toOpacity(GetAlpha()); } @@ -110,6 +173,10 @@ class Mutator { case MutatorType::kClipRRect: case MutatorType::kClipRSE: return true; + case MutatorType::kBackdropClipRect: + case MutatorType::kBackdropClipRRect: + case MutatorType::kBackdropClipRSuperellipse: + case MutatorType::kBackdropClipPath: case MutatorType::kOpacity: case MutatorType::kTransform: case MutatorType::kBackdropFilter: @@ -124,7 +191,11 @@ class Mutator { DlPath, DlMatrix, uint8_t, - ImageFilterMutation> + ImageFilterMutation, + BackdropClipRect, + BackdropClipRRect, + BackdropClipRSuperellipse, + BackdropClipPath> data_; }; // Mutator @@ -150,6 +221,10 @@ class MutatorsStack { // `filter_rect` is in global coordinates. void PushBackdropFilter(const std::shared_ptr& filter, const DlRect& filter_rect); + void PushPlatformViewClipRect(const DlRect& rect); + void PushPlatformViewClipRRect(const DlRoundRect& rrect); + void PushPlatformViewClipRSuperellipse(const DlRoundSuperellipse& rse); + void PushPlatformViewClipPath(const DlPath& path); // Removes the `Mutator` on the top of the stack // and destroys it. @@ -249,6 +324,22 @@ class EmbeddedViewParams { mutators_stack_.PushBackdropFilter(filter, filter_rect); } + void PushPlatformViewClipRect(const DlRect& clip_rect) { + mutators_stack_.PushPlatformViewClipRect(clip_rect); + } + + void PushPlatformViewClipRRect(const DlRoundRect& clip_rrect) { + mutators_stack_.PushPlatformViewClipRRect(clip_rrect); + } + + void PushPlatformViewClipRSuperellipse(const DlRoundSuperellipse& clip_rse) { + mutators_stack_.PushPlatformViewClipRSuperellipse(clip_rse); + } + + void PushPlatformViewClipPath(const DlPath& clip_path) { + mutators_stack_.PushPlatformViewClipPath(clip_path); + } + bool operator==(const EmbeddedViewParams& other) const { return size_points_ == other.size_points_ && mutators_stack_ == other.mutators_stack_ && @@ -459,6 +550,13 @@ class ExternalViewEmbedder { const std::shared_ptr& filter, const DlRect& filter_rect) {} + virtual void PushClipRectToVisitedPlatformViews(const DlRect& clip_rect) {} + virtual void PushClipRRectToVisitedPlatformViews( + const DlRoundRect& clip_rrect) {} + virtual void PushClipRSuperellipseToVisitedPlatformViews( + const DlRoundSuperellipse& clip_rse) {} + virtual void PushClipPathToVisitedPlatformViews(const DlPath& clip_path) {} + private: bool used_this_frame_ = false; diff --git a/engine/src/flutter/flow/layers/clip_path_layer.cc b/engine/src/flutter/flow/layers/clip_path_layer.cc index d2006cb25ce..dd578ccd7bf 100644 --- a/engine/src/flutter/flow/layers/clip_path_layer.cc +++ b/engine/src/flutter/flow/layers/clip_path_layer.cc @@ -31,4 +31,9 @@ void ClipPathLayer::ApplyClip(LayerStateStack::MutatorContext& mutator) const { } } +void ClipPathLayer::PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const { + view_embedder->PushClipPathToVisitedPlatformViews(clip_shape()); +} + } // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_path_layer.h b/engine/src/flutter/flow/layers/clip_path_layer.h index 5ac6de72539..30f6225cd3a 100644 --- a/engine/src/flutter/flow/layers/clip_path_layer.h +++ b/engine/src/flutter/flow/layers/clip_path_layer.h @@ -20,6 +20,8 @@ class ClipPathLayer : public ClipShapeLayer { const DlRect clip_shape_bounds() const override; void ApplyClip(LayerStateStack::MutatorContext& mutator) const override; + void PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const override; private: FML_DISALLOW_COPY_AND_ASSIGN(ClipPathLayer); diff --git a/engine/src/flutter/flow/layers/clip_rect_layer.cc b/engine/src/flutter/flow/layers/clip_rect_layer.cc index f67518696a0..92f93c5d860 100644 --- a/engine/src/flutter/flow/layers/clip_rect_layer.cc +++ b/engine/src/flutter/flow/layers/clip_rect_layer.cc @@ -17,4 +17,9 @@ void ClipRectLayer::ApplyClip(LayerStateStack::MutatorContext& mutator) const { mutator.clipRect(clip_shape(), clip_behavior() != Clip::kHardEdge); } +void ClipRectLayer::PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const { + view_embedder->PushClipRectToVisitedPlatformViews(clip_shape()); +} + } // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_rect_layer.h b/engine/src/flutter/flow/layers/clip_rect_layer.h index b35401a3214..a59fe28ca5c 100644 --- a/engine/src/flutter/flow/layers/clip_rect_layer.h +++ b/engine/src/flutter/flow/layers/clip_rect_layer.h @@ -17,6 +17,8 @@ class ClipRectLayer : public ClipShapeLayer { const DlRect clip_shape_bounds() const override; void ApplyClip(LayerStateStack::MutatorContext& mutator) const override; + void PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const override; private: FML_DISALLOW_COPY_AND_ASSIGN(ClipRectLayer); diff --git a/engine/src/flutter/flow/layers/clip_rrect_layer.cc b/engine/src/flutter/flow/layers/clip_rrect_layer.cc index 6b172d50c58..87aead37124 100644 --- a/engine/src/flutter/flow/layers/clip_rrect_layer.cc +++ b/engine/src/flutter/flow/layers/clip_rrect_layer.cc @@ -23,4 +23,9 @@ void ClipRRectLayer::ApplyClip(LayerStateStack::MutatorContext& mutator) const { } } +void ClipRRectLayer::PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const { + view_embedder->PushClipRRectToVisitedPlatformViews(clip_shape()); +} + } // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_rrect_layer.h b/engine/src/flutter/flow/layers/clip_rrect_layer.h index 091eaf0e0f7..92b8889fd4b 100644 --- a/engine/src/flutter/flow/layers/clip_rrect_layer.h +++ b/engine/src/flutter/flow/layers/clip_rrect_layer.h @@ -17,6 +17,8 @@ class ClipRRectLayer : public ClipShapeLayer { const DlRect clip_shape_bounds() const override; void ApplyClip(LayerStateStack::MutatorContext& mutator) const override; + void PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const override; private: FML_DISALLOW_COPY_AND_ASSIGN(ClipRRectLayer); diff --git a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.cc b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.cc index d7f05013958..ef4e8d55ec5 100644 --- a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.cc +++ b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.cc @@ -20,4 +20,9 @@ void ClipRSuperellipseLayer::ApplyClip( mutator.clipRSuperellipse(clip_shape(), clip_behavior() != Clip::kHardEdge); } +void ClipRSuperellipseLayer::PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const { + view_embedder->PushClipRSuperellipseToVisitedPlatformViews(clip_shape()); +} + } // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.h b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.h index 8f1ba25f614..47577930b45 100644 --- a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.h +++ b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.h @@ -18,6 +18,8 @@ class ClipRSuperellipseLayer : public ClipShapeLayer { const DlRect clip_shape_bounds() const override; void ApplyClip(LayerStateStack::MutatorContext& mutator) const override; + void PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const override; private: FML_DISALLOW_COPY_AND_ASSIGN(ClipRSuperellipseLayer); diff --git a/engine/src/flutter/flow/layers/clip_shape_layer.h b/engine/src/flutter/flow/layers/clip_shape_layer.h index d2baa33b427..220f05c802e 100644 --- a/engine/src/flutter/flow/layers/clip_shape_layer.h +++ b/engine/src/flutter/flow/layers/clip_shape_layer.h @@ -56,6 +56,11 @@ class ClipShapeLayer : public CacheableContainerLayer { Layer::AutoPrerollSaveLayerState save = Layer::AutoPrerollSaveLayerState::Create(context, UsesSaveLayer()); + ExternalViewEmbedder* view_embedder = context->view_embedder; + if (view_embedder) { + PushClipToEmbeddedNativeViewMutatorStack(view_embedder); + } + auto mutator = context->state_stack.save(); ApplyClip(mutator); @@ -107,6 +112,8 @@ class ClipShapeLayer : public CacheableContainerLayer { protected: virtual const DlRect clip_shape_bounds() const = 0; virtual void ApplyClip(LayerStateStack::MutatorContext& mutator) const = 0; + virtual void PushClipToEmbeddedNativeViewMutatorStack( + ExternalViewEmbedder* view_embedder) const = 0; virtual ~ClipShapeLayer() = default; const ClipShape& clip_shape() const { return clip_shape_; } diff --git a/engine/src/flutter/flow/mutators_stack_unittests.cc b/engine/src/flutter/flow/mutators_stack_unittests.cc index 7944d74cf12..d21e0c3df32 100644 --- a/engine/src/flutter/flow/mutators_stack_unittests.cc +++ b/engine/src/flutter/flow/mutators_stack_unittests.cc @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "display_list/geometry/dl_geometry_types.h" #include "flutter/display_list/effects/dl_image_filters.h" #include "flutter/flow/embedded_views.h" #include "gtest/gtest.h" @@ -98,6 +99,43 @@ TEST(MutatorsStack, PushOpacity) { ASSERT_TRUE(iter->get()->GetAlpha() == 240); } +TEST(MutatorsStack, PushPlatformViewClipRect) { + MutatorsStack stack; + auto rect = DlRect(); + stack.PushPlatformViewClipRect(rect); + auto iter = stack.Bottom(); + ASSERT_TRUE(iter->get()->GetType() == MutatorType::kBackdropClipRect); + ASSERT_TRUE(iter->get()->GetBackdropClipRect().rect == rect); +} + +TEST(MutatorsStack, PushPlatformViewClipRRect) { + MutatorsStack stack; + auto rrect = DlRoundRect(); + stack.PushPlatformViewClipRRect(rrect); + auto iter = stack.Bottom(); + ASSERT_TRUE(iter->get()->GetType() == MutatorType::kBackdropClipRRect); + ASSERT_TRUE(iter->get()->GetBackdropClipRRect().rrect == rrect); +} + +TEST(MutatorsStack, PushPlatformViewClipRSuperellipse) { + MutatorsStack stack; + auto rse = DlRoundSuperellipse(); + stack.PushPlatformViewClipRSuperellipse(rse); + auto iter = stack.Bottom(); + ASSERT_TRUE(iter->get()->GetType() == + MutatorType::kBackdropClipRSuperellipse); + ASSERT_TRUE(iter->get()->GetBackdropClipRSuperellipse().rse == rse); +} + +TEST(MutatorsStack, PushPlatformViewClipPath) { + MutatorsStack stack; + auto path = DlPath(); + stack.PushPlatformViewClipPath(path); + auto iter = stack.Bottom(); + ASSERT_TRUE(iter->get()->GetType() == MutatorType::kBackdropClipPath); + ASSERT_TRUE(iter->get()->GetBackdropClipPath().path == path); +} + TEST(MutatorsStack, PushBackdropFilter) { MutatorsStack stack; const int num_of_mutators = 10; diff --git a/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc b/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc index ed3324fefb8..2b3340d5b4e 100644 --- a/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc +++ b/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc @@ -1785,6 +1785,10 @@ void PlatformViewAndroidJNIImpl::FlutterViewOnDisplayPlatformView( case MutatorType::kClipPath: case MutatorType::kOpacity: case MutatorType::kBackdropFilter: + case MutatorType::kBackdropClipRect: + case MutatorType::kBackdropClipRRect: + case MutatorType::kBackdropClipRSuperellipse: + case MutatorType::kBackdropClipPath: break; } ++iter; @@ -2294,6 +2298,10 @@ void PlatformViewAndroidJNIImpl::onDisplayPlatformView2( // TODO(cyanglaz): Implement other mutators. // https://github.com/flutter/flutter/issues/58426 case MutatorType::kBackdropFilter: + case MutatorType::kBackdropClipRect: + case MutatorType::kBackdropClipRRect: + case MutatorType::kBackdropClipRSuperellipse: + case MutatorType::kBackdropClipPath: break; } ++iter; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 4055681a9c5..7efa0d11831 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -94,10 +94,12 @@ static BOOL _preparedOnce = NO; - (instancetype)initWithFrame:(CGRect)frame blurRadius:(CGFloat)blurRadius + cornerRadius:(CGFloat)cornerRadius visualEffectView:(UIVisualEffectView*)visualEffectView { if (self = [super init]) { _frame = frame; _blurRadius = blurRadius; + _cornerRadius = cornerRadius; [PlatformViewFilter prepareOnce:visualEffectView]; if (![PlatformViewFilter isUIVisualEffectViewImplementationValid]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " @@ -163,6 +165,9 @@ static BOOL _preparedOnce = NO; visualEffectSubview.layer.backgroundColor = UIColor.clearColor.CGColor; visualEffectView.frame = _frame; + visualEffectView.layer.cornerRadius = _cornerRadius; + visualEffectView.clipsToBounds = YES; + self.backdropFilterView = visualEffectView; } @@ -831,3 +836,6 @@ static BOOL _preparedOnce = NO; return YES; } @end + +@implementation PendingRRectClip +@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.h index bb90c1aa954..82a3718f182 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.h @@ -114,6 +114,22 @@ NS_ASSUME_NONNULL_BEGIN /// @brief Pushes the view id of a visted platform view to the list of visied platform views. - (void)pushVisitedPlatformViewId:(int64_t)viewId; +/// @brief Pushes the outstanding rectangular clips to the mutator stack of each visited platform +/// view +- (void)pushClipRectToVisitedPlatformViews:(const flutter::DlRect&)clipRect; + +/// @brief Pushes the outstanding rounded rectangular clips to the mutator stack of each visited +/// platform view +- (void)pushClipRRectToVisitedPlatformViews:(const flutter::DlRoundRect&)clipRRect; + +/// @brief Pushes the outstanding round super elliptical clips to the mutator stack of each visited +/// platform view +- (void)pushClipRSuperellipseToVisitedPlatformViews:(const flutter::DlRoundSuperellipse&)clipRse; + +/// @brief Pushes the outstanding path clips to the mutator stack of each visited platform +/// view +- (void)pushClipPathToVisitedPlatformViews:(const flutter::DlPath&)clipPath; + @end @interface FlutterPlatformViewsController (Testing) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm index 4dfc483b46b..2ff23159517 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.mm @@ -3,6 +3,8 @@ // found in the LICENSE file. #import "shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsController.h" +#include "display_list/geometry/dl_geometry_types.h" +#include "impeller/geometry/rounding_radii.h" #include "flutter/display_list/effects/image_filters/dl_blur_image_filter.h" #include "flutter/display_list/utils/dl_matrix_clip_tracker.h" @@ -525,6 +527,8 @@ static CGRect GetCGRectFromDlRect(const DlRect& clipDlRect) { DlMatrix transformMatrix; NSMutableArray* blurFilters = [[NSMutableArray alloc] init]; + NSMutableArray* pendingClipRRects = [[NSMutableArray alloc] init]; + FML_DCHECK(!clipView.maskView || [clipView.maskView isKindOfClass:[FlutterClippingMaskView class]]); if (clipView.maskView) { @@ -605,8 +609,19 @@ static CGRect GetCGRectFromDlRect(const DlRect& clipDlRect) { CGFloat blurRadius = (*iter)->GetFilterMutation().GetFilter().asBlur()->sigma_x(); UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + + // TODO(https://github.com/flutter/flutter/issues/179126) + CGFloat cornerRadius = 0.0; + if ([pendingClipRRects count] > 0) { + cornerRadius = pendingClipRRects[0].topLeftRadius; + [pendingClipRRects removeAllObjects]; + } + visualEffectView.layer.cornerRadius = cornerRadius; + visualEffectView.clipsToBounds = YES; + PlatformViewFilter* filter = [[PlatformViewFilter alloc] initWithFrame:frameInClipView blurRadius:blurRadius + cornerRadius:cornerRadius visualEffectView:visualEffectView]; if (!filter) { self.canApplyBlurBackdrop = NO; @@ -615,6 +630,32 @@ static CGRect GetCGRectFromDlRect(const DlRect& clipDlRect) { } break; } + case flutter::MutatorType::kBackdropClipRect: { + // The frame already handles cropping into the rect so this can + // no-op + break; + } + case flutter::MutatorType::kBackdropClipRRect: { + PendingRRectClip* clip = [[PendingRRectClip alloc] init]; + DlRoundRect rrect = (*iter)->GetBackdropClipRRect().rrect; + + clip.rect = boundingRect; + impeller::RoundingRadii radii = rrect.GetRadii(); + clip.topLeftRadius = radii.top_left.width; + clip.topRightRadius = radii.top_right.width; + clip.bottomLeftRadius = radii.bottom_left.width; + clip.bottomRightRadius = radii.bottom_right.width; + [pendingClipRRects addObject:clip]; + break; + } + case flutter::MutatorType::kBackdropClipRSuperellipse: { + // TODO(https://github.com/flutter/flutter/issues/179125) + break; + } + case flutter::MutatorType::kBackdropClipPath: { + // TODO(https://github.com/flutter/flutter/issues/179127) + break; + } } ++iter; } @@ -1001,6 +1042,38 @@ static CGRect GetCGRectFromDlRect(const DlRect& clipDlRect) { self.visitedPlatformViews.push_back(viewId); } +- (void)pushClipRectToVisitedPlatformViews:(const flutter::DlRect&)clipRect { + for (int64_t id : self.visitedPlatformViews) { + flutter::EmbeddedViewParams params = self.currentCompositionParams[id]; + params.PushPlatformViewClipRect(clipRect); + self.currentCompositionParams[id] = params; + } +} + +- (void)pushClipRRectToVisitedPlatformViews:(const flutter::DlRoundRect&)clipRRect { + for (int64_t id : self.visitedPlatformViews) { + flutter::EmbeddedViewParams params = self.currentCompositionParams[id]; + params.PushPlatformViewClipRRect(clipRRect); + self.currentCompositionParams[id] = params; + } +} + +- (void)pushClipRSuperellipseToVisitedPlatformViews:(const flutter::DlRoundSuperellipse&)clipRse { + for (int64_t id : self.visitedPlatformViews) { + flutter::EmbeddedViewParams params = self.currentCompositionParams[id]; + params.PushPlatformViewClipRSuperellipse(clipRse); + self.currentCompositionParams[id] = params; + } +} + +- (void)pushClipPathToVisitedPlatformViews:(const flutter::DlPath&)clipPath { + for (int64_t id : self.visitedPlatformViews) { + flutter::EmbeddedViewParams params = self.currentCompositionParams[id]; + params.PushPlatformViewClipPath(clipPath); + self.currentCompositionParams[id] = params; + } +} + - (const flutter::EmbeddedViewParams&)compositionParamsForView:(int64_t)viewId { return self.currentCompositionParams.find(viewId)->second; } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 75376a4560c..2ef07694ac8 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "display_list/geometry/dl_geometry_types.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h" #import @@ -432,6 +433,7 @@ fml::RefPtr GetDefaultTaskRunner() { PlatformViewFilter* platformViewFilter1 = [[PlatformViewFilter alloc] initWithFrame:CGRectMake(0, 0, 10, 10) blurRadius:5 + cornerRadius:0 visualEffectView:visualEffectView1]; [clippingView applyBlurBackdropFilters:@[ platformViewFilter1 ]]; @@ -443,6 +445,7 @@ fml::RefPtr GetDefaultTaskRunner() { PlatformViewFilter* platformViewFilter2 = [[PlatformViewFilter alloc] initWithFrame:CGRectMake(0, 0, 10, 10) blurRadius:5 + cornerRadius:0 visualEffectView:visualEffectView2]; [clippingView applyBlurBackdropFilters:@[ platformViewFilter2 ]]; @@ -1545,6 +1548,7 @@ fml::RefPtr GetDefaultTaskRunner() { PlatformViewFilter* platformViewFilter = [[PlatformViewFilter alloc] initWithFrame:CGRectMake(0, 0, 10, 10) blurRadius:5 + cornerRadius:0 visualEffectView:visualEffectView]; XCTAssertNotNil(platformViewFilter); } @@ -1555,6 +1559,7 @@ fml::RefPtr GetDefaultTaskRunner() { PlatformViewFilter* platformViewFilter = [[PlatformViewFilter alloc] initWithFrame:CGRectMake(0, 0, 10, 10) blurRadius:5 + cornerRadius:0 visualEffectView:visualEffectView]; XCTAssertNil(platformViewFilter); } @@ -1578,6 +1583,7 @@ fml::RefPtr GetDefaultTaskRunner() { PlatformViewFilter* platformViewFilter = [[PlatformViewFilter alloc] initWithFrame:CGRectMake(0, 0, 10, 10) blurRadius:5 + cornerRadius:0 visualEffectView:editedUIVisualEffectView]; XCTAssertNil(platformViewFilter); } @@ -1602,10 +1608,92 @@ fml::RefPtr GetDefaultTaskRunner() { PlatformViewFilter* platformViewFilter = [[PlatformViewFilter alloc] initWithFrame:CGRectMake(0, 0, 10, 10) blurRadius:5 + cornerRadius:0 visualEffectView:editedUIVisualEffectView]; XCTAssertNil(platformViewFilter); } +- (void)testApplyBackdropFilterRespectsClipRRect { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/GetDefaultTaskRunner()); + FlutterPlatformViewsController* flutterPlatformViewsController = + [[FlutterPlatformViewsController alloc] init]; + flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_jsync_switch=*/std::make_shared()); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init]; + [flutterPlatformViewsController + registerViewFactory:factory + withId:@"MockFlutterPlatformView" + gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; + FlutterResult result = ^(id result) { + }; + [flutterPlatformViewsController + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create" + arguments:@{ + @"id" : @2, + @"viewType" : @"MockFlutterPlatformView" + }] + result:result]; + + XCTAssertNotNil(gMockPlatformView); + + UIView* flutterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + flutterPlatformViewsController.flutterView = flutterView; + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + flutter::DlScalar screenScale = [UIScreen mainScreen].scale; + flutter::DlMatrix screenScaleMatrix = flutter::DlMatrix::MakeScale({screenScale, screenScale, 1}); + stack.PushTransform(screenScaleMatrix); + + // Push a rounded rect clip + auto clipRect = flutter::DlRect::MakeXYWH(2, 2, 6, 6); + auto clipRRect = flutter::DlRoundRect::MakeRectXY(clipRect, 3, 3); + stack.PushPlatformViewClipRRect(clipRRect); + + // Push a backdrop filter + auto filter = flutter::DlBlurImageFilter::Make(5, 2, flutter::DlTileMode::kClamp); + stack.PushBackdropFilter(filter, + flutter::DlRect::MakeXYWH(0, 0, screenScale * 10, screenScale * 10)); + + auto embeddedViewParams = std::make_unique( + screenScaleMatrix, flutter::DlSize(10, 10), stack); + + [flutterPlatformViewsController prerollCompositeEmbeddedView:2 + withParams:std::move(embeddedViewParams)]; + [flutterPlatformViewsController + compositeView:2 + withParams:[flutterPlatformViewsController compositionParamsForView:2]]; + + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [flutterView addSubview:childClippingView]; + + [flutterView setNeedsLayout]; + [flutterView layoutIfNeeded]; + + NSArray* filters = childClippingView.backdropFilterSubviews; + XCTAssertEqual(filters.count, 1u); + + UIVisualEffectView* visualEffectView = filters[0]; + auto radii = clipRRect.GetRadii(); + + XCTAssertEqual(visualEffectView.layer.cornerRadius, radii.top_left.width); +} + - (void)testBackdropFilterVisualEffectSubviewBackgroundColor { __weak UIVisualEffectView* weakVisualEffectView; @@ -1616,6 +1704,7 @@ fml::RefPtr GetDefaultTaskRunner() { PlatformViewFilter* platformViewFilter = [[PlatformViewFilter alloc] initWithFrame:CGRectMake(0, 0, 10, 10) blurRadius:5 + cornerRadius:0 visualEffectView:visualEffectView]; CGColorRef visualEffectSubviewBackgroundColor = nil; for (UIView* view in [platformViewFilter backdropFilterView].subviews) { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 649b4d28804..3009d1f28e7 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -94,6 +94,9 @@ // The inputRadius can be customized and it doesn't add any color saturation to the blurred view. @property(nonatomic, readonly) UIVisualEffectView* backdropFilterView; +// Determines the corner radius of the backdrop filter view. +@property(nonatomic, readonly) CGFloat cornerRadius; + // For testing only. + (void)resetPreparation; @@ -111,6 +114,7 @@ // implementation in `PlatformViewFilter`, this method will return nil. - (instancetype)initWithFrame:(CGRect)frame blurRadius:(CGFloat)blurRadius + cornerRadius:(CGFloat)cornerRadius visualEffectView:(UIVisualEffectView*)visualEffectView NS_DESIGNATED_INITIALIZER; @end @@ -195,4 +199,12 @@ - (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target; @end +@interface PendingRRectClip : NSObject +@property(nonatomic) flutter::DlRect rect; +@property(nonatomic) CGFloat topLeftRadius; +@property(nonatomic) CGFloat topRightRadius; +@property(nonatomic) CGFloat bottomRightRadius; +@property(nonatomic) CGFloat bottomLeftRadius; +@end + #endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_external_view_embedder.h b/engine/src/flutter/shell/platform/darwin/ios/ios_external_view_embedder.h index 188547f89e3..066bc88b078 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_external_view_embedder.h +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_external_view_embedder.h @@ -71,6 +71,20 @@ class IOSExternalViewEmbedder : public ExternalViewEmbedder { const std::shared_ptr& filter, const DlRect& filter_rect) override; + // |ExternalViewEmbedder| + void PushClipRectToVisitedPlatformViews(const DlRect& clip_rect) override; + + // |ExternalViewEmbedder| + void PushClipRRectToVisitedPlatformViews( + const DlRoundRect& clip_rrect) override; + + // |ExternalViewEmbedder| + void PushClipRSuperellipseToVisitedPlatformViews( + const DlRoundSuperellipse& clip_rse) override; + + // |ExternalViewEmbedder| + void PushClipPathToVisitedPlatformViews(const DlPath& clip_path) override; + // |ExternalViewEmbedder| void PushVisitedPlatformView(int64_t view_id) override; diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_external_view_embedder.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_external_view_embedder.mm index 6d2fbfcfaa3..6e46d890ff0 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_external_view_embedder.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_external_view_embedder.mm @@ -113,4 +113,25 @@ void IOSExternalViewEmbedder::PushVisitedPlatformView(int64_t view_id) { [platform_views_controller_ pushVisitedPlatformViewId:view_id]; } +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PushClipRectToVisitedPlatformViews(const DlRect& clip_rect) { + [platform_views_controller_ pushClipRectToVisitedPlatformViews:clip_rect]; +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PushClipRRectToVisitedPlatformViews(const DlRoundRect& clip_rrect) { + [platform_views_controller_ pushClipRRectToVisitedPlatformViews:clip_rrect]; +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PushClipRSuperellipseToVisitedPlatformViews( + const DlRoundSuperellipse& clip_rse) { + [platform_views_controller_ pushClipRSuperellipseToVisitedPlatformViews:clip_rse]; +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PushClipPathToVisitedPlatformViews(const DlPath& clip_path) { + [platform_views_controller_ pushClipPathToVisitedPlatformViews:clip_path]; +} + } // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc b/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc index f4ef3e75fcc..72674a38e07 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc +++ b/engine/src/flutter/shell/platform/embedder/embedder_external_view_embedder.cc @@ -177,6 +177,10 @@ struct PlatformView { } case MutatorType::kOpacity: case MutatorType::kBackdropFilter: + case MutatorType::kBackdropClipRect: + case MutatorType::kBackdropClipRRect: + case MutatorType::kBackdropClipRSuperellipse: + case MutatorType::kBackdropClipPath: break; } } diff --git a/engine/src/flutter/shell/platform/embedder/embedder_layers.cc b/engine/src/flutter/shell/platform/embedder/embedder_layers.cc index 4c648288785..b671e860b59 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder_layers.cc +++ b/engine/src/flutter/shell/platform/embedder/embedder_layers.cc @@ -186,6 +186,10 @@ void EmbedderLayers::PushPlatformViewLayer( } } break; case MutatorType::kBackdropFilter: + case MutatorType::kBackdropClipRect: + case MutatorType::kBackdropClipRRect: + case MutatorType::kBackdropClipRSuperellipse: + case MutatorType::kBackdropClipPath: break; } }