diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc index 2746db78e76c4..617f22bd46277 100644 --- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc +++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "flatland_external_view_embedder.h" +#include #include #include "flutter/fml/trace_event.h" @@ -17,6 +18,24 @@ namespace { // overflows on operations (like FLT_MAX would). constexpr float kMaxHitRegionSize = 1'000'000.f; +void AttachClipTransformChild( + FlatlandConnection* flatland, + FlatlandExternalViewEmbedder::ClipTransform* parent_clip_transform, + const fuchsia::ui::composition::TransformId& child_transform_id) { + flatland->flatland()->AddChild(parent_clip_transform->transform_id, + child_transform_id); + parent_clip_transform->children.push_back(child_transform_id); +} + +void DetachClipTransformChildren( + FlatlandConnection* flatland, + FlatlandExternalViewEmbedder::ClipTransform* clip_transform) { + for (auto& child : clip_transform->children) { + flatland->flatland()->RemoveChild(clip_transform->transform_id, child); + } + clip_transform->children.clear(); +} + } // namespace FlatlandExternalViewEmbedder::FlatlandExternalViewEmbedder( @@ -263,7 +282,6 @@ void FlatlandExternalViewEmbedder::SubmitFrame( viewport.transform_id, {static_cast(view_mutators.transform.getTranslateX()), static_cast(view_mutators.transform.getTranslateY())}); - flatland_->flatland()->SetScale( viewport.transform_id, {view_mutators.transform.getScaleX(), view_mutators.transform.getScaleY()}); @@ -271,7 +289,54 @@ void FlatlandExternalViewEmbedder::SubmitFrame( } // TODO(fxbug.dev/94000): Set HitTestBehavior. - // TODO(fxbug.dev/94000): Set ClipRegions. + + // Set clip regions. + if (view_mutators.clips != viewport.mutators.clips) { + // Expand the clip_transforms array to fit any new transforms. + while (viewport.clip_transforms.size() < view_mutators.clips.size()) { + ClipTransform clip_transform; + clip_transform.transform_id = flatland_->NextTransformId(); + flatland_->flatland()->CreateTransform(clip_transform.transform_id); + viewport.clip_transforms.emplace_back(std::move(clip_transform)); + } + FML_CHECK(viewport.clip_transforms.size() >= + view_mutators.clips.size()); + + // Adjust and re-parent all clip transforms. + for (auto& clip_transform : viewport.clip_transforms) { + DetachClipTransformChildren(flatland_.get(), &clip_transform); + } + + for (size_t c = 0; c < view_mutators.clips.size(); c++) { + const SkMatrix& clip_matrix = view_mutators.clips[c].transform; + const SkRect& clip_rect = view_mutators.clips[c].rect; + + flatland_->flatland()->SetTranslation( + viewport.clip_transforms[c].transform_id, + {static_cast(clip_matrix.getTranslateX()), + static_cast(clip_matrix.getTranslateY())}); + flatland_->flatland()->SetScale( + viewport.clip_transforms[c].transform_id, + {clip_matrix.getScaleX(), clip_matrix.getScaleY()}); + fuchsia::math::Rect rect = { + static_cast(clip_rect.x()), + static_cast(clip_rect.y()), + static_cast(clip_rect.width()), + static_cast(clip_rect.height())}; + flatland_->flatland()->SetClipBoundary( + viewport.clip_transforms[c].transform_id, + std::make_unique(std::move(rect))); + + const auto child_transform_id = + c != (view_mutators.clips.size() - 1) + ? viewport.clip_transforms[c + 1].transform_id + : viewport.transform_id; + AttachClipTransformChild(flatland_.get(), + &(viewport.clip_transforms[c]), + child_transform_id); + } + viewport.mutators.clips = view_mutators.clips; + } // Set opacity. if (view_mutators.opacity != viewport.mutators.opacity) { @@ -299,9 +364,13 @@ void FlatlandExternalViewEmbedder::SubmitFrame( } // Attach the FlatlandView to the main scene graph. + const auto main_child_transform = + viewport.mutators.clips.empty() + ? viewport.transform_id + : viewport.clip_transforms[0].transform_id; flatland_->flatland()->AddChild(root_transform_id_, - viewport.transform_id); - child_transforms_.emplace_back(viewport.transform_id); + main_child_transform); + child_transforms_.emplace_back(main_child_transform); } // Acquire the surface associated with the layer. @@ -485,19 +554,29 @@ void FlatlandExternalViewEmbedder::DestroyView( auto viewport_id = flatland_view->second.viewport_id; auto transform_id = flatland_view->second.transform_id; + auto& clip_transforms = flatland_view->second.clip_transforms; if (!flatland_view->second.pending_create_viewport_callback) { flatland_->flatland()->ReleaseViewport(viewport_id, [](auto) {}); } - auto itr = - std::find_if(child_transforms_.begin(), child_transforms_.end(), - [transform_id](fuchsia::ui::composition::TransformId id) { - return id.value == transform_id.value; - }); + auto itr = std::find_if( + child_transforms_.begin(), child_transforms_.end(), + [transform_id, + &clip_transforms](fuchsia::ui::composition::TransformId id) { + return id.value == transform_id.value || + (!clip_transforms.empty() && + (id.value == clip_transforms[0].transform_id.value)); + }); if (itr != child_transforms_.end()) { - flatland_->flatland()->RemoveChild(root_transform_id_, transform_id); + flatland_->flatland()->RemoveChild(root_transform_id_, *itr); child_transforms_.erase(itr); } + for (auto& clip_transform : clip_transforms) { + DetachClipTransformChildren(flatland_.get(), &clip_transform); + } flatland_->flatland()->ReleaseTransform(transform_id); + for (auto& clip_transform : clip_transforms) { + flatland_->flatland()->ReleaseTransform(clip_transform.transform_id); + } flatland_views_.erase(flatland_view); on_view_unbound(viewport_id); diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h index c6d670785d70e..b12ef0f8c1074 100644 --- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h +++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h @@ -114,6 +114,12 @@ class FlatlandExternalViewEmbedder final bool hit_testable, bool focusable); + // Holds the clip transform that may be applied on a FlatlandView. + struct ClipTransform { + fuchsia::ui::composition::TransformId transform_id; + std::vector children; + }; + private: void Reset(); // Reset state for a new frame. @@ -173,6 +179,7 @@ class FlatlandExternalViewEmbedder final constexpr static EmbedderLayerId kRootLayerId = EmbedderLayerId{}; struct FlatlandView { + std::vector clip_transforms; fuchsia::ui::composition::TransformId transform_id; fuchsia::ui::composition::ContentId viewport_id; ViewMutators mutators; diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h index 1b0fc40076e0a..40f1648707e10 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h @@ -124,6 +124,16 @@ inline bool operator==( return true; } +inline bool operator==(const std::optional& a, + const std::optional& b) { + if (a.has_value() != b.has_value()) { + return false; + } + if (!a.has_value()) { + } + return a.value() == b.value(); +} + namespace flutter_runner::testing { constexpr static fuchsia::ui::composition::TransformId kInvalidTransformId{0}; diff --git a/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc b/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc index 213efaa2af131..eb9b79d46d111 100644 --- a/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc +++ b/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc @@ -284,6 +284,19 @@ Matcher> IsViewportLayer( /*hit_regions*/ _)); } +Matcher> IsClipTransformLayer( + const fuchsia::math::Vec& transform_translation, + const fuchsia::math::VecF& transform_scale, + std::optional clip_bounds, + Matcher> viewport_matcher) { + return Pointee(FieldsAre( + /* id */ _, transform_translation, transform_scale, + FakeTransform::kDefaultOrientation, /*clip_bounds*/ clip_bounds, + FakeTransform::kDefaultOpacity, + /*children*/ ElementsAre(viewport_matcher), + /*content*/ _, + /*hit_regions*/ _)); +} fuchsia::ui::composition::OnNextFrameBeginValues WithPresentCredits( uint32_t additional_present_credits) { fuchsia::ui::composition::OnNextFrameBeginValues values; @@ -725,6 +738,252 @@ TEST_F(FlatlandExternalViewEmbedderTest, SceneWithOneView) { fuchsia::ui::composition::HitTestInteraction::DEFAULT)})})); } +TEST_F(FlatlandExternalViewEmbedderTest, SceneWithOneClippedView) { + fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher; + fuchsia::ui::views::ViewportCreationToken viewport_creation_token; + fuchsia::ui::views::ViewCreationToken view_creation_token; + fuchsia::ui::views::ViewRef view_ref; + auto view_creation_token_status = zx::channel::create( + 0u, &viewport_creation_token.value, &view_creation_token.value); + ASSERT_EQ(view_creation_token_status, ZX_OK); + auto view_ref_pair = scenic::ViewRefPair::New(); + view_ref_pair.view_ref.Clone(&view_ref); + + // Create the `FlatlandExternalViewEmbedder` and pump the message loop until + // the initial scene graph is setup. + FlatlandExternalViewEmbedder external_view_embedder( + std::move(view_creation_token), + fuchsia::ui::views::ViewIdentityOnCreation{ + .view_ref = std::move(view_ref_pair.view_ref), + .view_ref_control = std::move(view_ref_pair.control_ref), + }, + fuchsia::ui::composition::ViewBoundProtocols{}, + parent_viewport_watcher.NewRequest(), flatland_connection(), + fake_surface_producer()); + flatland_connection()->Present(); + loop().RunUntilIdle(); + fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u)); + loop().RunUntilIdle(); + EXPECT_THAT(fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, + view_ref)); + + // Create the view before drawing the scene. + const SkSize child_view_size_signed = SkSize::Make(256.f, 512.f); + const fuchsia::math::SizeU child_view_size{ + static_cast(child_view_size_signed.width()), + static_cast(child_view_size_signed.height())}; + auto [child_view_token, child_viewport_token] = ViewTokenPair::New(); + const uint32_t child_view_id = child_viewport_token.value.get(); + + const int kOpacity = 200; + const float kOpacityFloat = 200 / 255.0f; + const fuchsia::math::VecF kScale{3.0f, 4.0f}; + const int kTranslateX = 10; + const int kTranslateY = 20; + + auto matrix = SkMatrix::I(); + matrix.setScaleX(kScale.x); + matrix.setScaleY(kScale.y); + matrix.setTranslateX(kTranslateX); + matrix.setTranslateY(kTranslateY); + + SkRect kClipRect = + SkRect::MakeXYWH(30, 40, child_view_size_signed.width() - 50, + child_view_size_signed.height() - 60); + fuchsia::math::Rect kClipInMathRect = { + static_cast(kClipRect.x()), static_cast(kClipRect.y()), + static_cast(kClipRect.width()), + static_cast(kClipRect.height())}; + + auto mutators_stack = flutter::MutatorsStack(); + mutators_stack.PushOpacity(kOpacity); + mutators_stack.PushTransform(matrix); + mutators_stack.PushClipRect(kClipRect); + + flutter::EmbeddedViewParams child_view_params(matrix, child_view_size_signed, + mutators_stack); + external_view_embedder.CreateView( + child_view_id, []() {}, + [](fuchsia::ui::composition::ContentId, + fuchsia::ui::composition::ChildViewWatcherHandle) {}); + const SkRect child_view_occlusion_hint = SkRect::MakeLTRB(1, 2, 3, 4); + const fuchsia::math::Inset child_view_inset{ + static_cast(child_view_occlusion_hint.top()), + static_cast(child_view_occlusion_hint.right()), + static_cast(child_view_occlusion_hint.bottom()), + static_cast(child_view_occlusion_hint.left())}; + external_view_embedder.SetViewProperties( + child_view_id, child_view_occlusion_hint, /*hit_testable=*/false, + /*focusable=*/false); + + // We must take into account the effect of DPR on the view scale. + const float kDPR = 2.0f; + const float kInvDPR = 1.f / kDPR; + + // Draw the scene. The scene graph shouldn't change yet. + const SkISize frame_size_signed = SkISize::Make(512, 512); + const fuchsia::math::SizeU frame_size{ + static_cast(frame_size_signed.width()), + static_cast(frame_size_signed.height())}; + DrawFrameWithView( + external_view_embedder, frame_size_signed, kDPR, child_view_id, + child_view_params, + [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorGREEN); + canvas->translate(canvas_size.width() / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }, + [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorRED); + canvas->translate(canvas_size.width() * 3.f / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }); + EXPECT_THAT(fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, + view_ref)); + + // Pump the message loop. The scene updates should propagate to flatland. + loop().RunUntilIdle(); + fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u)); + loop().RunUntilIdle(); + + EXPECT_THAT( + fake_flatland().graph(), + IsFlutterGraph( + parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/ + {IsImageLayer( + frame_size, kFirstLayerBlendMode, + {IsHitRegion( + /* x */ 128.f, + /* y */ 256.f, + /* width */ 16.f, + /* height */ 16.f, + /* hit_test */ + fuchsia::ui::composition::HitTestInteraction::DEFAULT)}), + IsClipTransformLayer( + {kTranslateX, kTranslateY}, kScale, kClipInMathRect, + IsViewportLayer(child_view_token, child_view_size, + child_view_inset, {0, 0}, + FakeTransform::kDefaultScale, kOpacityFloat)), + IsImageLayer( + frame_size, kUpperLayerBlendMode, + {IsHitRegion( + /* x */ 384.f, + /* y */ 256.f, + /* width */ 16.f, + /* height */ 16.f, + /* hit_test */ + fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}, + {kInvDPR, kInvDPR})); + + // Draw another frame with view, but get rid of the clips this time. This + // should remove all ClipTransformLayer instances. + auto new_matrix = SkMatrix::I(); + new_matrix.setScaleX(kScale.x); + new_matrix.setScaleY(kScale.y); + auto new_mutators_stack = flutter::MutatorsStack(); + new_mutators_stack.PushOpacity(kOpacity); + new_mutators_stack.PushTransform(new_matrix); + flutter::EmbeddedViewParams new_child_view_params( + new_matrix, child_view_size_signed, new_mutators_stack); + DrawFrameWithView( + external_view_embedder, frame_size_signed, kDPR, child_view_id, + new_child_view_params, + [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorGREEN); + canvas->translate(canvas_size.width() / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }, + [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorRED); + canvas->translate(canvas_size.width() * 3.f / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }); + loop().RunUntilIdle(); + fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u)); + loop().RunUntilIdle(); + EXPECT_THAT( + fake_flatland().graph(), + IsFlutterGraph( + parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/ + {IsImageLayer( + frame_size, kFirstLayerBlendMode, + {IsHitRegion( + /* x */ 128.f, + /* y */ 256.f, + /* width */ 16.f, + /* height */ 16.f, + /* hit_test */ + fuchsia::ui::composition::HitTestInteraction::DEFAULT)}), + IsViewportLayer(child_view_token, child_view_size, child_view_inset, + {0, 0}, kScale, kOpacityFloat), + IsImageLayer( + frame_size, kUpperLayerBlendMode, + {IsHitRegion( + /* x */ 384.f, + /* y */ 256.f, + /* width */ 16.f, + /* height */ 16.f, + /* hit_test */ + fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}, + {kInvDPR, kInvDPR})); + + // Destroy the view and draw another frame without the view. + external_view_embedder.DestroyView( + child_view_id, [](fuchsia::ui::composition::ContentId) {}); + DrawSimpleFrame( + external_view_embedder, frame_size_signed, 1.f, [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorGREEN); + canvas->translate(canvas_size.width() / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }); + loop().RunUntilIdle(); + EXPECT_THAT( + fake_flatland().graph(), + IsFlutterGraph( + parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/ + {IsImageLayer( + frame_size, kFirstLayerBlendMode, + {IsHitRegion( + /* x */ 128.f, + /* y */ 256.f, + /* width */ 16.f, + /* height */ 16.f, + /* hit_test */ + fuchsia::ui::composition::HitTestInteraction::DEFAULT)})})); +} + TEST_F(FlatlandExternalViewEmbedderTest, SceneWithOneView_NoOverlay) { fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher; fuchsia::ui::views::ViewportCreationToken viewport_creation_token;