diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc index 4d8d948e6ee67..13fb03838b2a2 100644 --- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc +++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc @@ -13,11 +13,6 @@ namespace flutter_runner { namespace { -// Since the flatland hit-region can be transformed (rotated, scaled or -// translated), we must ensure that the size of the hit-region will not cause -// overflows on operations (like FLT_MAX would). -constexpr float kMaxHitRegionSize = 1'000'000.f; - void AttachClipTransformChild( FlatlandConnection* flatland, FlatlandExternalViewEmbedder::ClipTransform* parent_clip_transform, @@ -59,19 +54,13 @@ FlatlandExternalViewEmbedder::FlatlandExternalViewEmbedder( if (intercept_all_input) { input_interceptor_transform_ = flatland_->NextTransformId(); flatland_->flatland()->CreateTransform(*input_interceptor_transform_); + flatland_->flatland()->SetInfiniteHitRegion( + *input_interceptor_transform_, + fuchsia::ui::composition::HitTestInteraction::SEMANTICALLY_INVISIBLE); flatland_->flatland()->AddChild(root_transform_id_, *input_interceptor_transform_); child_transforms_.emplace_back(*input_interceptor_transform_); - - // Attach full-screen hit testing shield. Note that since the hit-region - // may be transformed (translated, rotated), we do not want to set - // width/height to FLT_MAX. This will cause a numeric overflow. - flatland_->flatland()->SetHitRegions( - *input_interceptor_transform_, - {{{0, 0, kMaxHitRegionSize, kMaxHitRegionSize}, - fuchsia::ui::composition::HitTestInteraction:: - SEMANTICALLY_INVISIBLE}}); } } @@ -453,9 +442,9 @@ void FlatlandExternalViewEmbedder::SubmitFrame( flatland_layer_index++; } - // Set up the input interceptor at the top of the - // scene, if applicable. It will capture all input, and any unwanted input - // will be reinjected into embedded views. + // Set up the input interceptor at the top of the scene, if applicable. It + // will capture all input, and any unwanted input will be reinjected into + // embedded views. if (input_interceptor_transform_.has_value()) { flatland_->flatland()->AddChild(root_transform_id_, *input_interceptor_transform_); diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc index 4c782089df38e..8322275507ea7 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc @@ -853,6 +853,29 @@ void FakeFlatland::SetHitRegions( transform->hit_regions = std::move(regions); } +void FakeFlatland::SetInfiniteHitRegion( + fuchsia::ui::composition::TransformId transform_id, + fuchsia::ui::composition::HitTestInteraction hit_test) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::SetTranslation: TransformId 0 is invalid."; + return; + } + + auto found_transform = pending_graph_.transform_map.find(transform_id.value); + if (found_transform == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetTranslation: TransformId " + << transform_id.value << " does not exist."; + return; + } + + auto& transform = found_transform->second; + ZX_ASSERT(transform); + transform->hit_regions = {kInfiniteHitRegion}; +} + void FakeFlatland::Clear() { parents_map_.clear(); pending_graph_.Clear(); diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.h b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.h index 94ca6578f93e0..ac70c4641cc40 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.h +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.h @@ -310,6 +310,11 @@ class FakeFlatland fuchsia::ui::composition::TransformId transform_id, std::vector regions) override; + // |fuchsia::ui::composition::Flatland| + void SetInfiniteHitRegion( + fuchsia::ui::composition::TransformId transform_id, + fuchsia::ui::composition::HitTestInteraction hit_test) override; + // |fuchsia::ui::composition::Flatland| void Clear() override; 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 40f1648707e10..531c709f0183e 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 @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -138,6 +139,8 @@ namespace flutter_runner::testing { constexpr static fuchsia::ui::composition::TransformId kInvalidTransformId{0}; constexpr static fuchsia::ui::composition::ContentId kInvalidContentId{0}; +constexpr static fuchsia::ui::composition::HitRegion kInfiniteHitRegion = { + .region = {-FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX}}; // Convenience structure which allows clients to easily create a valid // `ViewCreationToken` / `ViewportCreationToken` pair for use with Flatland 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 57e740ed710d3..46d2fd37f77c3 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 @@ -38,8 +38,10 @@ using fuchsia::scenic::scheduling::FuturePresentationTimes; using fuchsia::scenic::scheduling::PresentReceivedInfo; using ::testing::_; using ::testing::AllOf; +using ::testing::Contains; using ::testing::ElementsAre; using ::testing::Eq; +using ::testing::Field; using ::testing::FieldsAre; using ::testing::IsEmpty; using ::testing::Matcher; @@ -297,6 +299,16 @@ Matcher> IsClipTransformLayer( /*content*/ _, /*hit_regions*/ _)); } + +Matcher> IsInputShield() { + return Pointee(AllOf( + // Must not clip the hit region. + Field("clip_bounds", &FakeTransform::clip_bounds, Eq(std::nullopt)), + // Hit region must be "infinite". + Field("hit_regions", &FakeTransform::hit_regions, + Contains(flutter_runner::testing::kInfiniteHitRegion)))); +} + fuchsia::ui::composition::OnNextFrameBeginValues WithPresentCredits( uint32_t additional_present_credits) { fuchsia::ui::composition::OnNextFrameBeginValues values; @@ -396,15 +408,18 @@ class FlatlandExternalViewEmbedderTest : public ::testing::Test { fuchsia::ui::composition::FlatlandHandle flatland = fake_flatland_.ConnectFlatland(session_subloop_->dispatcher()); - auto test_name = + const auto test_name = ::testing::UnitTest::GetInstance()->current_test_info()->name(); + const auto max_frames_in_flight = 1; + const auto vsync_offset = fml::TimeDelta::Zero(); return std::make_shared( - std::move(test_name), std::move(flatland), []() { FAIL(); }, - [](auto...) {}, 1, fml::TimeDelta::Zero()); + std::move(test_name), std::move(flatland), + /*error_callback=*/[] { FAIL(); }, /*ofpe_callback=*/[](auto...) {}, + max_frames_in_flight, vsync_offset); } // Primary loop and subloop for the FakeFlatland instance to process its - // messages. The subloop allocates it's own zx_port_t, allowing us to use a + // messages. The subloop allocates its own zx_port_t, allowing us to use a // separate port for each end of the message channel, rather than sharing a // single one. Dual ports allow messages and responses to be intermingled, // which is how production code behaves; this improves test realism. @@ -1476,4 +1491,143 @@ TEST_F(FlatlandExternalViewEmbedderTest, SimpleScene_OverlappingHitRegions) { fuchsia::ui::composition::HitTestInteraction::DEFAULT)})})); } +TEST_F(FlatlandExternalViewEmbedderTest, ViewportCoveredWithInputInterceptor) { + 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(), + /*intercept_all_input=*/true // Enables the interceptor. + ); + 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, {IsInputShield()})); + + // 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}; + + auto matrix = SkMatrix::I(); + matrix.setScaleX(kScale.x); + matrix.setScaleY(kScale.y); + + auto mutators_stack = flutter::MutatorsStack(); + mutators_stack.PushOpacity(kOpacity); + mutators_stack.PushTransform(matrix); + + 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, {IsInputShield()})); + + // 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)}), + 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)}), + IsInputShield()}, + {kInvDPR, kInvDPR})); +} + } // namespace flutter_runner::testing