diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 1839a8aa2e3f8..cc252ae527835 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -248,6 +248,11 @@ class EmbeddedViewParams { // Clippings are ignored. const SkRect& finalBoundingRect() const { return final_bounding_rect_; } + // Pushes the stored DlImageFilter object to the mutators stack. + void PushImageFilter(std::shared_ptr filter) { + mutators_stack_.PushBackdropFilter(filter); + } + // Whether the embedder should construct DisplayList objects to hold the // rendering commands for each between-view slice of the layer tree. bool display_list_enabled() const { return display_list_enabled_; } @@ -439,6 +444,18 @@ class ExternalViewEmbedder { // 'EndFrame', otherwise returns false. bool GetUsedThisFrame() const { return used_this_frame_; } + // Pushes the platform view id of a visited platform view to a list of + // visited platform views. + virtual void PushVisitedPlatformView(int64_t view_id) {} + + // Pushes a DlImageFilter object to each platform view within a list of + // visited platform views. + // + // See also: |PushVisitedPlatformView| for pushing platform view ids to the + // visited platform views list. + virtual void PushFilterToVisitedPlatformViews( + std::shared_ptr filter) {} + private: bool used_this_frame_ = false; diff --git a/flow/layers/backdrop_filter_layer.cc b/flow/layers/backdrop_filter_layer.cc index d52ac787c44c0..9f67b746b1c9b 100644 --- a/flow/layers/backdrop_filter_layer.cc +++ b/flow/layers/backdrop_filter_layer.cc @@ -43,6 +43,9 @@ void BackdropFilterLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { Layer::AutoPrerollSaveLayerState save = Layer::AutoPrerollSaveLayerState::Create(context, true, bool(filter_)); + if (context->view_embedder != nullptr) { + context->view_embedder->PushFilterToVisitedPlatformViews(filter_); + } SkRect child_paint_bounds = SkRect::MakeEmpty(); PrerollChildren(context, matrix, &child_paint_bounds); child_paint_bounds.join(context->cull_rect); diff --git a/flow/layers/platform_view_layer.cc b/flow/layers/platform_view_layer.cc index 8a46cbbce127c..a86886fb1b04f 100644 --- a/flow/layers/platform_view_layer.cc +++ b/flow/layers/platform_view_layer.cc @@ -29,6 +29,7 @@ void PlatformViewLayer::Preroll(PrerollContext* context, context->display_list_enabled); context->view_embedder->PrerollCompositeEmbeddedView(view_id_, std::move(params)); + context->view_embedder->PushVisitedPlatformView(view_id_); } void PlatformViewLayer::Paint(PaintContext& context) const { diff --git a/flow/mutators_stack_unittests.cc b/flow/mutators_stack_unittests.cc index a460125ef9627..c93838cfa68bf 100644 --- a/flow/mutators_stack_unittests.cc +++ b/flow/mutators_stack_unittests.cc @@ -163,6 +163,8 @@ TEST(MutatorsStack, Equality) { stack.PushClipPath(path); int alpha = 240; stack.PushOpacity(alpha); + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + stack.PushBackdropFilter(filter); MutatorsStack stackOther; SkMatrix matrixOther = SkMatrix::Scale(1, 1); @@ -175,6 +177,9 @@ TEST(MutatorsStack, Equality) { stackOther.PushClipPath(otherPath); int otherAlpha = 240; stackOther.PushOpacity(otherAlpha); + auto otherFilter = + std::make_shared(5, 5, DlTileMode::kClamp); + stackOther.PushBackdropFilter(otherFilter); ASSERT_TRUE(stack == stackOther); } @@ -204,6 +209,11 @@ TEST(Mutator, Initialization) { int alpha = 240; Mutator mutator5 = Mutator(alpha); ASSERT_TRUE(mutator5.GetType() == MutatorType::kOpacity); + + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + Mutator mutator6 = Mutator(filter); + ASSERT_TRUE(mutator6.GetType() == MutatorType::kBackdropFilter); + ASSERT_TRUE(mutator6.GetFilter() == *filter); } TEST(Mutator, CopyConstructor) { @@ -232,6 +242,11 @@ TEST(Mutator, CopyConstructor) { Mutator mutator5 = Mutator(alpha); Mutator copy5 = Mutator(mutator5); ASSERT_TRUE(mutator5 == copy5); + + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + Mutator mutator6 = Mutator(filter); + Mutator copy6 = Mutator(mutator6); + ASSERT_TRUE(mutator6 == copy6); } TEST(Mutator, Equality) { @@ -260,6 +275,11 @@ TEST(Mutator, Equality) { Mutator mutator5 = Mutator(alpha); Mutator otherMutator5 = Mutator(alpha); ASSERT_TRUE(mutator5 == otherMutator5); + + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + Mutator mutator6 = Mutator(filter); + Mutator otherMutator6 = Mutator(filter); + ASSERT_TRUE(mutator6 == otherMutator6); } TEST(Mutator, UnEquality) { @@ -275,6 +295,13 @@ TEST(Mutator, UnEquality) { Mutator mutator2 = Mutator(alpha); Mutator otherMutator2 = Mutator(alpha2); ASSERT_TRUE(mutator2 != otherMutator2); + + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + auto filter2 = + std::make_shared(10, 10, DlTileMode::kClamp); + Mutator mutator3 = Mutator(filter); + Mutator otherMutator3 = Mutator(filter2); + ASSERT_TRUE(mutator3 != otherMutator3); } } // namespace testing diff --git a/shell/common/shell_test_external_view_embedder.cc b/shell/common/shell_test_external_view_embedder.cc index a366e6f99075c..579f682089517 100644 --- a/shell/common/shell_test_external_view_embedder.cc +++ b/shell/common/shell_test_external_view_embedder.cc @@ -28,6 +28,14 @@ SkISize ShellTestExternalViewEmbedder::GetLastSubmittedFrameSize() { return last_submitted_frame_size_; } +std::vector ShellTestExternalViewEmbedder::GetVisitedPlatformViews() { + return visited_platform_views_; +} + +MutatorsStack ShellTestExternalViewEmbedder::GetStack(int64_t view_id) { + return mutators_stacks_[view_id]; +} + // |ExternalViewEmbedder| void ShellTestExternalViewEmbedder::CancelFrame() {} @@ -41,7 +49,16 @@ void ShellTestExternalViewEmbedder::BeginFrame( // |ExternalViewEmbedder| void ShellTestExternalViewEmbedder::PrerollCompositeEmbeddedView( int view_id, - std::unique_ptr params) {} + std::unique_ptr params) { + SkRect view_bounds = SkRect::Make(frame_size_); + std::unique_ptr view; + if (params->display_list_enabled()) { + view = std::make_unique(view_bounds); + } else { + view = std::make_unique(view_bounds); + } + slices_.insert_or_assign(view_id, std::move(view)); +} // |ExternalViewEmbedder| PostPrerollResult ShellTestExternalViewEmbedder::PostPrerollAction( @@ -62,9 +79,24 @@ ShellTestExternalViewEmbedder::GetCurrentBuilders() { } // |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::PushVisitedPlatformView(int64_t view_id) { + visited_platform_views_.push_back(view_id); +} + +// |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::PushFilterToVisitedPlatformViews( + std::shared_ptr filter) { + for (int64_t id : visited_platform_views_) { + EmbeddedViewParams params = current_composition_params_[id]; + params.PushImageFilter(filter); + current_composition_params_[id] = params; + mutators_stacks_[id] = params.mutatorsStack(); + } +} + EmbedderPaintContext ShellTestExternalViewEmbedder::CompositeEmbeddedView( int view_id) { - return {nullptr, nullptr}; + return {slices_[view_id]->canvas(), slices_[view_id]->builder()}; } // |ExternalViewEmbedder| diff --git a/shell/common/shell_test_external_view_embedder.h b/shell/common/shell_test_external_view_embedder.h index 1bb70fd131e07..583a09182e5fc 100644 --- a/shell/common/shell_test_external_view_embedder.h +++ b/shell/common/shell_test_external_view_embedder.h @@ -7,6 +7,7 @@ #include "flutter/flow/embedded_views.h" #include "flutter/fml/raster_thread_merger.h" +#include "third_party/skia/include/core/SkPictureRecorder.h" namespace flutter { @@ -32,9 +33,15 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { // the external view embedder. int GetSubmittedFrameCount(); - // Returns the size of last submitted frame surface + // Returns the size of last submitted frame surface. SkISize GetLastSubmittedFrameSize(); + // Returns the mutators stack for the given platform view. + MutatorsStack GetStack(int64_t); + + // Returns the list of visited platform views. + std::vector GetVisitedPlatformViews(); + private: // |ExternalViewEmbedder| void CancelFrame() override; @@ -64,6 +71,13 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { // |ExternalViewEmbedder| EmbedderPaintContext CompositeEmbeddedView(int view_id) override; + // |ExternalViewEmbedder| + void PushVisitedPlatformView(int64_t view_id) override; + + // |ExternalViewEmbedder| + void PushFilterToVisitedPlatformViews( + std::shared_ptr filter) override; + // |ExternalViewEmbedder| void SubmitFrame(GrDirectContext* context, std::unique_ptr frame) override; @@ -84,7 +98,11 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { PostPrerollResult post_preroll_result_; bool support_thread_merging_; - + SkISize frame_size_; + std::map> slices_; + std::map mutators_stacks_; + std::map current_composition_params_; + std::vector visited_platform_views_; std::atomic submitted_frame_count_; std::atomic last_submitted_frame_size_; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index e2193d9be7d24..5d513b5239541 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -13,8 +13,10 @@ #include "assets/directory_asset_bundle.h" #include "common/graphics/persistent_cache.h" +#include "flutter/flow/layers/backdrop_filter_layer.h" #include "flutter/flow/layers/display_list_layer.h" #include "flutter/flow/layers/layer_raster_cache_item.h" +#include "flutter/flow/layers/platform_view_layer.h" #include "flutter/flow/layers/transform_layer.h" #include "flutter/fml/command_line.h" #include "flutter/fml/dart/dart_converter.h" @@ -765,12 +767,66 @@ TEST_F(ShellTest, ExternalEmbedderNoThreadMerger) { PumpOneFrame(shell.get(), 100, 100, builder); end_frame_latch.Wait(); - ASSERT_TRUE(end_frame_called); DestroyShell(std::move(shell)); } +TEST_F(ShellTest, PushBackdropFilterToVisitedPlatformViews) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + bool end_frame_called = false; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + ASSERT_TRUE(raster_thread_merger.get() == nullptr); + ASSERT_FALSE(should_resubmit_frame); + end_frame_called = true; + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kResubmitFrame, false); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + auto platform_view_layer = std::make_shared( + SkPoint::Make(10, 10), SkSize::Make(10, 10), 50); + root->Add(platform_view_layer); + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + auto backdrop_filter_layer = + std::make_shared(filter, DlBlendMode::kSrcOver); + root->Add(backdrop_filter_layer); + auto platform_view_layer2 = std::make_shared( + SkPoint::Make(10, 10), SkSize::Make(10, 10), 75); + backdrop_filter_layer->Add(platform_view_layer2); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + end_frame_latch.Wait(); + ASSERT_EQ(external_view_embedder->GetVisitedPlatformViews().size(), + (const unsigned long)2); + ASSERT_EQ(external_view_embedder->GetVisitedPlatformViews()[0], 50); + ASSERT_EQ(external_view_embedder->GetVisitedPlatformViews()[1], 75); + ASSERT_TRUE(external_view_embedder->GetStack(75).is_empty()); + ASSERT_FALSE(external_view_embedder->GetStack(50).is_empty()); + + auto filter = DlBlurImageFilter(5, 5, DlTileMode::kClamp); + auto mutator = *external_view_embedder->GetStack(50).Begin(); + ASSERT_EQ(mutator->GetType(), MutatorType::kBackdropFilter); + ASSERT_EQ(mutator->GetFilter(), filter); + + DestroyShell(std::move(shell)); +} + // TODO(https://github.com/flutter/flutter/issues/59816): Enable on fuchsia. TEST_F(ShellTest, #if defined(OS_FUCHSIA) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index adb5e6514e89f..d4c96358007cb 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -33,6 +33,8 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { @end namespace flutter { +// Becomes NO if Apple's API changes and blurred backdrop filters cannot be applied. +BOOL canApplyBlurBackdrop = YES; std::shared_ptr FlutterPlatformViewLayerPool::GetLayer( GrDirectContext* gr_context, @@ -318,6 +320,15 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { } } +void FlutterPlatformViewsController::PushFilterToVisitedPlatformViews( + std::shared_ptr filter) { + for (int64_t id : visited_platform_views_) { + EmbeddedViewParams params = current_composition_params_[id]; + params.PushImageFilter(filter); + current_composition_params_[id] = params; + } +} + void FlutterPlatformViewsController::PrerollCompositeEmbeddedView( int view_id, std::unique_ptr params) { @@ -414,6 +425,8 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CGRectGetWidth(flutter_view.bounds), CGRectGetHeight(flutter_view.bounds))] autorelease]; + NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; + auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { switch ((*iter)->GetType()) { @@ -434,11 +447,25 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kOpacity: embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha; break; - case kBackdropFilter: + case kBackdropFilter: { + // We only support DlBlurImageFilter for BackdropFilter. + if ((*iter)->GetFilter().asBlur() && canApplyBlurBackdrop) { + // sigma_x is arbitrarily chosen as the radius value because Quartz sets + // sigma_x and sigma_y equal to each other. DlBlurImageFilter's Tile Mode + // is not supported in Quartz's gaussianBlur CAFilter, so it is not used + // to blur the PlatformView. + [blurRadii addObject:@((*iter)->GetFilter().asBlur()->sigma_x())]; + } break; + } } ++iter; } + + if (canApplyBlurBackdrop) { + canApplyBlurBackdrop = [clipView applyBlurBackdropFilters:blurRadii]; + } + // Reverse the offset of the clipView. // The clipView's frame includes the final translate of the final transform matrix. // So we need to revese this translate so the platform view can layout at the correct offset. @@ -517,6 +544,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { clip_count_.clear(); views_to_recomposite_.clear(); layer_pool_->RecycleLayers(); + visited_platform_views_.clear(); } SkRect FlutterPlatformViewsController::GetPlatformViewRect(int view_id) { @@ -777,6 +805,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { void FlutterPlatformViewsController::ResetFrameState() { slices_.clear(); composition_order_.clear(); + visited_platform_views_.clear(); } } // namespace flutter diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index cc205e91bd3f6..5c6c8d0653797 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -3,6 +3,7 @@ // found in the LICENSE file. #import +#import #import #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" @@ -242,6 +243,805 @@ - (void)testChildClippingViewHitTests { XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 199) withEvent:nil]); } +- (void)testApplyBackdropFilter { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a backdrop filter + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + stack.PushBackdropFilter(filter); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // sigmaX is chosen for input radius, regardless of sigmaY + NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); +} + +- (void)testApplyMultipleBackdropFilters { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push backdrop filters + for (int i = 0; i < 50; i++) { + auto filter = std::make_shared(i, 2, flutter::DlTileMode::kClamp); + stack.PushBackdropFilter(filter); + } + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(50, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // All filters have sigma X radius + for (int i = 0; i < 50; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(i), [gaussianFilter valueForKey:@"inputRadius"]); + } +} + +- (void)testAddBackdropFilters { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a backdrop filter + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + stack.PushBackdropFilter(filter); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // + // Simulate adding 1 backdrop filter (create a new mutators stack) + // Create embedded view params + flutter::MutatorsStack stack2; + // Layer tree always pushes a screen scale factor to the stack + stack2.PushTransform(screenScaleMatrix); + // Push backdrop filters + for (int i = 0; i < 2; i++) { + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(2, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // All filters have sigma X radius + for (int i = 0; i < 2; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } +} + +- (void)testRemoveBackdropFilters { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push backdrop filters + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + for (int i = 0; i < 5; i++) { + stack.PushBackdropFilter(filter); + } + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Simulate removing 1 backdrop filter (create a new mutators stack) + // Create embedded view params + flutter::MutatorsStack stack2; + // Layer tree always pushes a screen scale factor to the stack + stack2.PushTransform(screenScaleMatrix); + // Push backdrop filters + for (int i = 0; i < 4; i++) { + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + + // All filters have sigma X radius + for (int i = 0; i < 4; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // Simulate removing all backdrop filters (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // No backdrop filters in the stack, so no nothing to push + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has no CAFilters because no backdrop filters were added + XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +} + +- (void)testEditBackdropFilters { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push backdrop filters + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + for (int i = 0; i < 5; i++) { + stack.PushBackdropFilter(filter); + } + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Simulate editing 1 backdrop filter in the middle of the stack (create a new mutators stack) + // Create embedded view params + flutter::MutatorsStack stack2; + // Layer tree always pushes a screen scale factor to the stack + stack2.PushTransform(screenScaleMatrix); + // Push backdrop filters + for (int i = 0; i < 5; i++) { + if (i == 3) { + auto filter2 = + std::make_shared(2, 5, flutter::DlTileMode::kClamp); + + stack2.PushBackdropFilter(filter2); + continue; + } + + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The edited backdrop filter has the new radius value + for (int i = 0; i < 5; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + if (i == 3) { + XCTAssertEqualObjects(@(2), [gaussianFilter valueForKey:@"inputRadius"]); + } else { + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + } + + // Simulate editing 1 backdrop filter in the beginning of the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters + for (int i = 0; i < 5; i++) { + if (i == 0) { + auto filter2 = + std::make_shared(2, 5, flutter::DlTileMode::kClamp); + stack2.PushBackdropFilter(filter2); + continue; + } + + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The edited backdrop filter has the new radius value + for (int i = 0; i < 5; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + if (i == 0) { + XCTAssertEqualObjects(@(2), [gaussianFilter valueForKey:@"inputRadius"]); + } else { + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + } + + // Simulate editing 1 backdrop filter in the end of the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters + for (int i = 0; i < 5; i++) { + if (i == 4) { + auto filter2 = + std::make_shared(2, 5, flutter::DlTileMode::kClamp); + stack2.PushBackdropFilter(filter2); + continue; + } + + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The edited backdrop filter has the new radius value + for (int i = 0; i < 5; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + if (i == 4) { + XCTAssertEqualObjects(@(2), [gaussianFilter valueForKey:@"inputRadius"]); + } else { + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + } + + // Simulate editing all backdrop filters in the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters + for (int i = 0; i < 5; i++) { + auto filter2 = std::make_shared(i, 2, flutter::DlTileMode::kClamp); + + stack2.PushBackdropFilter(filter2); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The edited backdrop filter has the new radius value + for (int i = 0; i < 5; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + + XCTAssertEqualObjects(@(i), [gaussianFilter valueForKey:@"inputRadius"]); + } +} + +- (void)testApplyBackdropFilterNotDlBlurImageFilter { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a dilate backdrop filter + auto dilateFilter = std::make_shared(5, 2); + stack.PushBackdropFilter(dilateFilter); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // No filters were added + XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // Simulate adding a non-DlBlurImageFilter in the middle of the stack (create a new mutators + // stack) Create embedded view params + flutter::MutatorsStack stack2; + // Layer tree always pushes a screen scale factor to the stack + stack2.PushTransform(screenScaleMatrix); + // Push backdrop filters and dilate filter + auto blurFilter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + + for (int i = 0; i < 5; i++) { + if (i == 2) { + stack2.PushBackdropFilter(dilateFilter); + continue; + } + + stack2.PushBackdropFilter(blurFilter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Filters were only added for DlBlurImageFilters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The added filters are all gaussianBlur filters + for (int i = 0; i < 4; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + + // Simulate adding a non-DlBlurImageFilter to the beginning of the stack (replace the mutators + // stack) Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters and dilate filter + for (int i = 0; i < 5; i++) { + if (i == 0) { + stack2.PushBackdropFilter(dilateFilter); + continue; + } + + stack2.PushBackdropFilter(blurFilter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Filters were only added for DlBlurImageFilters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The added filters are all gaussianBlur filters + for (int i = 0; i < 4; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + + // Simulate adding a non-DlBlurImageFilter to the end of the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters and dilate filter + for (int i = 0; i < 5; i++) { + if (i == 4) { + stack2.PushBackdropFilter(dilateFilter); + continue; + } + + stack2.PushBackdropFilter(blurFilter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Filters were only added for DlBlurImageFilters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The added filters are all gaussianBlur filters + for (int i = 0; i < 4; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + + // Simulate adding only non-DlBlurImageFilter to the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push dilate filters + for (int i = 0; i < 5; i++) { + stack2.PushBackdropFilter(dilateFilter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // No filters were added + XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +} + +- (void)testApplyBackdropFilterAPIChanged { + NSArray* blurRadii = @[ @(1), @(5), @(10) ]; + + // The gaussianBlur filter is extracted once for each childClippingView. + // Each test requires a new childClippingView + // Valid UIVisualEffectView API + ChildClippingView* childClippingView1 = [[ChildClippingView alloc] init]; + childClippingView1.blurEffectView = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + XCTAssertTrue([childClippingView1 applyBlurBackdropFilters:blurRadii]); + + // Invalid UIVisualEffectView initialization + ChildClippingView* childClippingView2 = [[ChildClippingView alloc] init]; + childClippingView2.blurEffectView = [[UIVisualEffectView alloc] init]; + XCTAssertFalse([childClippingView2 applyBlurBackdropFilters:blurRadii]); + + // Invalid UIView + ChildClippingView* childClippingView3 = [[ChildClippingView alloc] init]; + childClippingView3.blurEffectView = [[UIView alloc] init]; + XCTAssertFalse([childClippingView3 applyBlurBackdropFilters:blurRadii]); + + // Invalid UIVisualEffectView API for "name" + UIVisualEffectView* editedUIVisualEffectView1 = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + NSArray* subviews1 = editedUIVisualEffectView1.subviews; + for (UIView* view in subviews1) { + if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + for (CIFilter* filter in view.layer.filters) { + if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + [filter setValue:@"notGaussianBlur" forKey:@"name"]; + break; + } + } + break; + } + } + + ChildClippingView* childClippingView4 = [[ChildClippingView alloc] init]; + childClippingView4.blurEffectView = editedUIVisualEffectView1; + XCTAssertFalse([childClippingView4 applyBlurBackdropFilters:blurRadii]); + + // Invalid UIVisualEffectView API for "inputRadius" + UIVisualEffectView* editedUIVisualEffectView2 = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + NSArray* subviews2 = editedUIVisualEffectView2.subviews; + for (UIView* view in subviews2) { + if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + for (CIFilter* filter in view.layer.filters) { + if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + [filter setValue:@"invalidInputRadius" forKey:@"inputRadius"]; + break; + } + } + break; + } + } + + ChildClippingView* childClippingView5 = [[ChildClippingView alloc] init]; + childClippingView5.blurEffectView = editedUIVisualEffectView2; + XCTAssertFalse([childClippingView5 applyBlurBackdropFilters:blurRadii]); +} + - (void)testCompositePlatformView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); @@ -296,6 +1096,86 @@ - (void)testCompositePlatformView { XCTAssertTrue(CGRectEqualToRect(platformViewRectInFlutterView, CGRectMake(100, 100, 300, 300))); } +- (void)testBackdropFilterCorrectlyPushedAndReset { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->BeginFrame(SkISize::Make(0, 0)); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->PushVisitedPlatformView(2); + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + flutterPlatformViewsController->PushFilterToVisitedPlatformViews(filter); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // sigmaX is chosen for input radius, regardless of sigmaY + NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + + // New frame, with no filter pushed. + auto embeddedViewParams2 = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + flutterPlatformViewsController->BeginFrame(SkISize::Make(0, 0)); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams2)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // No filter in this frame. + XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); +} + - (void)testChildClippingViewShouldBeTheBoundingRectOfPlatformView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 096cf8cf51052..542f39b736f4f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -48,6 +48,16 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView +// Applies blur backdrop filters to the ChildClippingView with blur radius values from +// blurRadii. Returns NO if Apple's API has changed and blurred backdrop filters cannot +// be applied, otherwise returns YES. +- (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii; + +// The UIView used to extract the gaussianBlur filter. This must be a UIVisualEffectView +// initalized with UIBlurEffect to extract the correct filter. Made a public property +// for custom unit tests. +@property(nonatomic, retain) UIView* blurEffectView; + @end namespace flutter { @@ -186,6 +196,12 @@ class FlutterPlatformViewsController { // responder. Returns -1 if no such platform view is found. long FindFirstResponderPlatformViewId(); + // Pushes backdrop filter mutation to the mutator stack of each visited platform view. + void PushFilterToVisitedPlatformViews(std::shared_ptr filter); + + // Pushes the view id of a visted platform view to the list of visied platform views. + void PushVisitedPlatformView(int64_t view_id) { visited_platform_views_.push_back(view_id); } + private: static const size_t kMaxLayerAllocations = 2; @@ -293,6 +309,9 @@ class FlutterPlatformViewsController { // The last ID in this vector belond to the that is composited on top of all others. std::vector composition_order_; + // A vector of visited platform view IDs. + std::vector visited_platform_views_; + // The latest composition order that was presented in Present(). std::vector active_composition_order_; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index c662a078f2a16..e3c3389080a7d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -4,6 +4,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#include "flutter/display_list/display_list_image_filter.h" #include "flutter/fml/platform/darwin/cf_utils.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" @@ -56,7 +57,23 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter -@implementation ChildClippingView +@implementation ChildClippingView { + // A gaussianFilter from UIVisualEffectView that can be copied for new backdrop filters. + NSObject* _gaussianFilter; +} + +// Lazy initializes blurEffectView as the expected UIVisualEffectView. The backdropFilter blur +// requires this UIVisualEffectView initialization. The lazy initalization is only used to allow +// custom unit tests. +- (UIView*)blurEffectView { + if (!_blurEffectView) { + // blurEffectView is only needed to extract its gaussianBlur filter. It is released after + // searching its subviews and extracting the filter. + _blurEffectView = [[[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; + } + return _blurEffectView; +} // The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to // be hit tested and consumed by this view if they are inside the embedded platform view which could @@ -70,6 +87,86 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { return NO; } +// Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its +// gaussianFilter. Returns nil if Apple's API has changed and the filter cannot be extracted. +- (NSObject*)extractGaussianFilter { + NSObject* gaussianFilter = nil; + + for (UIView* view in self.blurEffectView.subviews) { + if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + for (CIFilter* filter in view.layer.filters) { + if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + if ([[filter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { + gaussianFilter = filter; + } + // No need to look at other CIFilters. If the API structure has not changed, the + // gaussianBlur filter was succesfully saved. Otherwise, still exit the loop because the + // filter cannot be extracted. + break; + } + } + // No need to look at other UIViews. If the API structure has not changed, the gaussianBlur + // filter was succesfully saved. Otherwise, still exit the loop because the filter cannot + // be extracted. + break; + } + } + + return gaussianFilter; +} + +- (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { + // The outer if-statement checks for the first time this method is called and _gaussianFilter is + // not initialized. The inner if-statement checks if extracting the gaussianBlur was successful. + // If it was not successful, this method will not be called again. Thus the if-statements check + // for different conditions. + if (!_gaussianFilter) { + _gaussianFilter = [self extractGaussianFilter]; + + if (!_gaussianFilter) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the gaussianBlur CAFilter."; + return NO; + } + } + + BOOL newRadiusValues = NO; + + if ([blurRadii count] != [self.layer.filters count]) { + newRadiusValues = YES; + } else { + for (NSUInteger i = 0; i < [blurRadii count]; i++) { + if ([self.layer.filters[i] valueForKey:@"inputRadius"] != blurRadii[i]) { + newRadiusValues = YES; + break; + } + } + } + + if (newRadiusValues) { + NSMutableArray* newGaussianFilters = [[[NSMutableArray alloc] init] autorelease]; + + for (NSUInteger i = 0; i < [blurRadii count]; i++) { + NSObject* newGaussianFilter = [[_gaussianFilter copy] autorelease]; + [newGaussianFilter setValue:blurRadii[i] forKey:@"inputRadius"]; + [newGaussianFilters addObject:newGaussianFilter]; + } + + self.layer.filters = newGaussianFilters; + } + + return YES; +} + +- (void)dealloc { + [_blurEffectView release]; + _blurEffectView = nil; + + [_gaussianFilter release]; + _gaussianFilter = nil; + [super dealloc]; +} + @end @interface FlutterClippingMaskView () diff --git a/shell/platform/darwin/ios/ios_external_view_embedder.h b/shell/platform/darwin/ios/ios_external_view_embedder.h index 3a5a0b3c17613..03cec910bae39 100644 --- a/shell/platform/darwin/ios/ios_external_view_embedder.h +++ b/shell/platform/darwin/ios/ios_external_view_embedder.h @@ -67,6 +67,13 @@ class IOSExternalViewEmbedder : public ExternalViewEmbedder { // |ExternalViewEmbedder| bool SupportsDynamicThreadMerging() override; + // |ExternalViewEmbedder| + void PushFilterToVisitedPlatformViews( + std::shared_ptr filter) override; + + // |ExternalViewEmbedder| + void PushVisitedPlatformView(int64_t view_id) override; + FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalViewEmbedder); }; diff --git a/shell/platform/darwin/ios/ios_external_view_embedder.mm b/shell/platform/darwin/ios/ios_external_view_embedder.mm index e67983321c770..76995be2c56ad 100644 --- a/shell/platform/darwin/ios/ios_external_view_embedder.mm +++ b/shell/platform/darwin/ios/ios_external_view_embedder.mm @@ -98,4 +98,15 @@ return true; } +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PushFilterToVisitedPlatformViews( + std::shared_ptr filter) { + platform_views_controller_->PushFilterToVisitedPlatformViews(filter); +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PushVisitedPlatformView(int64_t view_id) { + platform_views_controller_->PushVisitedPlatformView(view_id); +} + } // namespace flutter diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_iPhone 8_13.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_iPhone 8_13.0_simulator.png index fdba44db11fcf..307864c09a863 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_iPhone 8_13.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_iPhone 8_13.0_simulator.png differ