| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/ozone/platform/flatland/flatland_window.h" |
| |
| #include <fidl/fuchsia.ui.pointer/cpp/fidl.h> |
| #include <fidl/fuchsia.ui.pointer/cpp/hlcpp_conversion.h> |
| #include <fuchsia/ui/composition/cpp/fidl.h> |
| #include <fuchsia/ui/views/cpp/fidl.h> |
| #include <lib/ui/scenic/cpp/testing/fake_flatland.h> |
| #include <lib/ui/scenic/cpp/testing/fake_touch_source.h> |
| #include <lib/ui/scenic/cpp/testing/fake_view_ref_focused.h> |
| #include <lib/zx/channel.h> |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/fuchsia/koid.h" |
| #include "base/fuchsia/scoped_service_publisher.h" |
| #include "base/fuchsia/test_component_context_for_process.h" |
| #include "base/logging.h" |
| #include "base/test/fidl_matchers.h" |
| #include "base/test/task_environment.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/events/fuchsia/util/pointer_event_utility.h" |
| #include "ui/ozone/platform/flatland/flatland_window_manager.h" |
| #include "ui/ozone/test/mock_platform_window_delegate.h" |
| #include "ui/platform_window/fuchsia/view_ref_pair.h" |
| |
| using ::scenic::FakeGraph; |
| using ::scenic::FakeTransform; |
| using ::scenic::FakeTransformPtr; |
| using ::scenic::FakeView; |
| using ::scenic::FakeViewport; |
| using ::testing::_; |
| using ::testing::AllOf; |
| using ::testing::Contains; |
| using ::testing::Field; |
| using ::testing::IsEmpty; |
| using ::testing::Matcher; |
| using ::testing::Property; |
| using ::testing::SaveArg; |
| using ::testing::VariantWith; |
| |
| namespace ui { |
| |
| namespace { |
| |
| Matcher<FakeGraph> IsWindowGraph( |
| const fuchsia::ui::composition::ParentViewportWatcherPtr& |
| parent_viewport_watcher, |
| const fuchsia::ui::views::ViewportCreationToken& viewport_creation_token, |
| Matcher<std::vector<FakeTransformPtr>> children_transform_matcher) { |
| std::optional<zx_koid_t> view_token_koid = |
| base::GetRelatedKoid(viewport_creation_token.value); |
| EXPECT_TRUE(view_token_koid.has_value()); |
| std::optional<zx_koid_t> watcher_koid = |
| base::GetRelatedKoid(parent_viewport_watcher.channel()); |
| EXPECT_TRUE(watcher_koid.has_value()); |
| |
| return AllOf( |
| Field("root_transform", &FakeGraph::root_transform, |
| Pointee(AllOf( |
| Field("translation", &FakeTransform::translation, |
| ::base::test::FidlEq(FakeTransform::kDefaultTranslation)), |
| Field("scale", &FakeTransform::scale, |
| ::base::test::FidlEq(FakeTransform::kDefaultScale)), |
| Field("opacity", &FakeTransform::opacity, |
| FakeTransform::kDefaultOpacity), |
| Field("children", &FakeTransform::children, |
| children_transform_matcher)))), |
| Field("view", &FakeGraph::view, |
| Optional(AllOf( |
| Field("view_token", &FakeView::view_token, view_token_koid), |
| Field("parent_viewport_watcher", |
| &FakeView::parent_viewport_watcher, watcher_koid))))); |
| } |
| |
| Matcher<fuchsia::ui::composition::ViewportProperties> IsViewportProperties( |
| const fuchsia::math::SizeU& logical_size) { |
| return AllOf( |
| Property("has_logical_size", |
| &fuchsia::ui::composition::ViewportProperties::has_logical_size, |
| true), |
| Property("logical_size", |
| &fuchsia::ui::composition::ViewportProperties::logical_size, |
| ::base::test::FidlEq(logical_size))); |
| } |
| |
| Matcher<FakeTransformPtr> IsViewport( |
| const fuchsia::ui::views::ViewCreationToken& view_token, |
| const fuchsia::math::SizeU& viewport_logical_size) { |
| auto viewport_koid = base::GetRelatedKoid(view_token.value); |
| |
| return Pointee(AllOf( |
| Field("translation", &FakeTransform::translation, |
| ::base::test::FidlEq(FakeTransform::kDefaultTranslation)), |
| Field("scale", &FakeTransform::scale, |
| ::base::test::FidlEq(FakeTransform::kDefaultScale)), |
| Field("opacity", &FakeTransform::opacity, FakeTransform::kDefaultOpacity), |
| Field("children", &FakeTransform::children, IsEmpty()), |
| Field("content", &FakeTransform::content, |
| Pointee(VariantWith<FakeViewport>(AllOf( |
| Field("viewport_properties", &FakeViewport::viewport_properties, |
| IsViewportProperties(viewport_logical_size)), |
| Field("viewport_token", &FakeViewport::viewport_token, |
| viewport_koid))))))); |
| } |
| |
| Matcher<FakeTransformPtr> IsHitShield() { |
| return Pointee(AllOf( |
| // Must not clip the hit region. |
| Field("clip_bounds", &FakeTransform::clip_bounds, |
| testing::Eq(std::nullopt)), |
| // Hit region must be "infinite". |
| Field("hit_regions", &FakeTransform::hit_regions, |
| testing::Contains( |
| ::base::test::FidlEq(scenic::kInfiniteHitRegion))))); |
| } |
| |
| } // namespace |
| |
| class FlatlandWindowTest : public ::testing::Test { |
| protected: |
| FlatlandWindowTest() |
| : fake_flatland_publisher_(test_context_.additional_services(), |
| fake_flatland_.GetFlatlandRequestHandler()) {} |
| ~FlatlandWindowTest() override = default; |
| |
| void CreateWindow() { |
| EXPECT_FALSE(flatland_window_); |
| |
| fuchsia::ui::views::ViewCreationToken view_token; |
| fuchsia::ui::views::ViewportCreationToken viewport_token; |
| auto status = |
| zx::channel::create(0, &viewport_token.value, &view_token.value); |
| CHECK_EQ(ZX_OK, status); |
| viewport_token_ = std::move(viewport_token); |
| |
| EXPECT_CALL(window_delegate_, OnAcceleratedWidgetAvailable(_)) |
| .WillOnce(SaveArg<0>(&window_widget_)); |
| |
| PlatformWindowInitProperties properties; |
| properties.view_ref_pair = ViewRefPair::New(); |
| properties.view_creation_token = std::move(view_token); |
| flatland_window_ = std::make_unique<FlatlandWindow>( |
| &window_manager_, &window_delegate_, std::move(properties)); |
| } |
| |
| void SetLayoutInfo(uint32_t width, |
| uint32_t height, |
| float dpr, |
| fuchsia::math::Inset inset = {0, 0, 0, 0}) { |
| fuchsia::ui::composition::LayoutInfo layout_info; |
| layout_info.set_logical_size({width, height}); |
| layout_info.set_device_pixel_ratio({dpr, dpr}); |
| layout_info.set_inset(inset); |
| flatland_window_->OnGetLayout(std::move(layout_info)); |
| } |
| |
| void SetWindowStatus(fuchsia::ui::composition::ParentViewportStatus status) { |
| flatland_window_->OnGetStatus(status); |
| } |
| |
| void SetViewRefFocusedHandle( |
| fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused_handle) { |
| flatland_window_->view_ref_focused_.Bind( |
| std::move(view_ref_focused_handle)); |
| flatland_window_->view_ref_focused_->Watch(fit::bind_member( |
| flatland_window_.get(), &FlatlandWindow::OnViewRefFocusedWatchResult)); |
| } |
| |
| void SetTouchSource( |
| fidl::ClientEnd<fuchsia_ui_pointer::TouchSource> touch_source) { |
| auto mouse_endpoints = |
| fidl::CreateEndpoints<fuchsia_ui_pointer::MouseSource>(); |
| EXPECT_TRUE(mouse_endpoints.is_ok()) << mouse_endpoints.status_string(); |
| flatland_window_->pointer_handler_ = std::make_unique<PointerEventsHandler>( |
| std::move(touch_source), std::move(mouse_endpoints->client)); |
| flatland_window_->pointer_handler_->StartWatching(base::BindRepeating( |
| &FlatlandWindow::DispatchEvent, |
| // This is safe since |flatland_window_| is a class member. |
| base::Unretained(flatland_window_.get()))); |
| } |
| |
| bool HasPendingAttachSurfaceContentClosure() { |
| return !!flatland_window_->pending_attach_surface_content_closure_; |
| } |
| |
| const fuchsia::ui::composition::ParentViewportWatcherPtr& |
| parent_viewport_watcher() { |
| return flatland_window_->parent_viewport_watcher_; |
| } |
| |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::SingleThreadTaskEnvironment::MainThreadType::IO}; |
| scenic::FakeFlatland fake_flatland_; |
| |
| base::TestComponentContextForProcess test_context_; |
| // Injects binding for responding to Flatland protocol connection requests. |
| const base::ScopedServicePublisher<fuchsia::ui::composition::Flatland> |
| fake_flatland_publisher_; |
| |
| FlatlandWindowManager window_manager_; |
| |
| MockPlatformWindowDelegate window_delegate_; |
| std::unique_ptr<FlatlandWindow> flatland_window_; |
| |
| gfx::AcceleratedWidget window_widget_ = gfx::kNullAcceleratedWidget; |
| |
| fuchsia::ui::views::ViewportCreationToken viewport_token_; |
| }; |
| |
| TEST_F(FlatlandWindowTest, Initialization) { |
| CreateWindow(); |
| ASSERT_NE(window_widget_, gfx::kNullAcceleratedWidget); |
| |
| // Check that there are no crashes after flushing tasks. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(FlatlandWindowTest, PresentsOnShow) { |
| size_t presents_called = 0u; |
| fake_flatland_.SetPresentHandler( |
| [&presents_called](auto) { ++presents_called; }); |
| |
| CreateWindow(); |
| ASSERT_NE(window_widget_, gfx::kNullAcceleratedWidget); |
| |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(0u, presents_called); |
| |
| flatland_window_->Show(/*inactive=*/false); |
| EXPECT_EQ(0u, presents_called); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(1u, presents_called); |
| EXPECT_THAT( |
| fake_flatland_.graph(), |
| IsWindowGraph(parent_viewport_watcher(), viewport_token_, IsEmpty())); |
| } |
| |
| // Tests that FlatlandWindow processes and delegates focus signal. |
| TEST_F(FlatlandWindowTest, ProcessesFocusedSignal) { |
| CreateWindow(); |
| flatland_window_->Show(/*inactive=*/false); |
| task_environment_.RunUntilIdle(); |
| |
| scenic::FakeViewRefFocused fake_view_ref_focused; |
| fidl::Binding<fuchsia::ui::views::ViewRefFocused> |
| fake_view_ref_focused_binding(&fake_view_ref_focused); |
| SetViewRefFocusedHandle(fake_view_ref_focused_binding.NewBinding()); |
| |
| // FlatlandWindow should start watching. |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(fake_view_ref_focused.times_watched(), 1u); |
| |
| // Send focused=true signal. |
| bool focus_delegated = false; |
| EXPECT_CALL(window_delegate_, OnActivationChanged(_)) |
| .WillRepeatedly(SaveArg<0>(&focus_delegated)); |
| fake_view_ref_focused.ScheduleCallback(true); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(fake_view_ref_focused.times_watched(), 2u); |
| EXPECT_TRUE(focus_delegated); |
| |
| // Send focused=false signal. |
| fake_view_ref_focused.ScheduleCallback(false); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(fake_view_ref_focused.times_watched(), 3u); |
| EXPECT_FALSE(focus_delegated); |
| } |
| |
| TEST_F(FlatlandWindowTest, AppliesDevicePixelRatio) { |
| CreateWindow(); |
| EXPECT_CALL(window_delegate_, OnBoundsChanged(_)).Times(1); |
| SetLayoutInfo(100, 100, 1.f); |
| |
| scenic::FakeTouchSource fake_touch_source; |
| fidl::Binding<fuchsia::ui::pointer::TouchSource> fake_touch_source_binding( |
| &fake_touch_source); |
| SetTouchSource(fidl::HLCPPToNatural(fake_touch_source_binding.NewBinding())); |
| task_environment_.RunUntilIdle(); |
| |
| // Send a touch event and expect coordinates to be the same as TouchEvent. |
| const float kLocationX = 9.f; |
| const float kLocationY = 10.f; |
| bool event_received = false; |
| EXPECT_CALL(window_delegate_, DispatchEvent(_)) |
| .WillOnce([&event_received, kLocationX, kLocationY](ui::Event* event) { |
| EXPECT_EQ(event->AsTouchEvent()->location_f().x(), kLocationX); |
| EXPECT_EQ(event->AsTouchEvent()->location_f().y(), kLocationY); |
| event_received = true; |
| }); |
| std::vector<fuchsia_ui_pointer::TouchEvent> events; |
| events.push_back(TouchEventBuilder() |
| .SetPosition({kLocationX, kLocationY}) |
| .SetTouchInteractionStatus( |
| fuchsia_ui_pointer::TouchInteractionStatus::kGranted) |
| .Build()); |
| fake_touch_source.ScheduleCallback(fidl::NaturalToHLCPP(std::move(events))); |
| task_environment_.RunUntilIdle(); |
| EXPECT_TRUE(event_received); |
| |
| // Update device pixel ratio. |
| const float kDPR = 2.f; |
| EXPECT_CALL(window_delegate_, OnBoundsChanged(_)).Times(1); |
| SetLayoutInfo(100, 100, kDPR); |
| |
| // Send the same touch event and expect coordinates to be scaled from |
| // TouchEvent. |
| event_received = false; |
| EXPECT_CALL(window_delegate_, DispatchEvent(_)) |
| .WillOnce([&event_received, kLocationX, kLocationY, |
| kDPR](ui::Event* event) { |
| EXPECT_EQ(event->AsTouchEvent()->location_f().x(), kLocationX * kDPR); |
| EXPECT_EQ(event->AsTouchEvent()->location_f().y(), kLocationY * kDPR); |
| event_received = true; |
| }); |
| events.clear(); |
| events.push_back(TouchEventBuilder() |
| .SetPosition({kLocationX, kLocationY}) |
| .SetTouchInteractionStatus( |
| fuchsia_ui_pointer::TouchInteractionStatus::kGranted) |
| .Build()); |
| fake_touch_source.ScheduleCallback(fidl::NaturalToHLCPP(std::move(events))); |
| task_environment_.RunUntilIdle(); |
| EXPECT_TRUE(event_received); |
| } |
| |
| TEST_F(FlatlandWindowTest, WaitsForNonZeroSizeToAttachSurfaceContent) { |
| size_t presents_called = 0u; |
| fake_flatland_.SetPresentHandler( |
| [&presents_called](auto) { ++presents_called; }); |
| |
| CreateWindow(); |
| |
| // FlatlandWindow should start watching callbacks in ctor. |
| task_environment_.RunUntilIdle(); |
| |
| // Try attaching the content. It should only be a closure. |
| fuchsia::ui::views::ViewCreationToken view_token; |
| fuchsia::ui::views::ViewportCreationToken viewport_token; |
| auto status = |
| zx::channel::create(0, &viewport_token.value, &view_token.value); |
| CHECK_EQ(ZX_OK, status); |
| flatland_window_->AttachSurfaceContent(std::move(viewport_token)); |
| EXPECT_TRUE(HasPendingAttachSurfaceContentClosure()); |
| |
| // Setting layout info should trigger the closure and delegate calls. |
| EXPECT_CALL(window_delegate_, OnWindowStateChanged(_, _)).Times(1); |
| EXPECT_CALL(window_delegate_, OnBoundsChanged(_)).Times(1); |
| |
| const uint32_t kWidth = 200; |
| const uint32_t kHeight = 100; |
| const fuchsia::math::SizeU expected_size = {kWidth, kHeight}; |
| |
| SetWindowStatus( |
| fuchsia::ui::composition::ParentViewportStatus::CONNECTED_TO_DISPLAY); |
| SetLayoutInfo(kWidth, kHeight, 1.f); |
| EXPECT_FALSE(HasPendingAttachSurfaceContentClosure()); |
| |
| // There should be a present call in FakeFlatland after flushing the tasks. |
| EXPECT_EQ(0u, presents_called); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(1u, presents_called); |
| |
| // Show to attach the scene graph. |
| flatland_window_->Show(/*inactive=*/false); |
| fuchsia::ui::composition::OnNextFrameBeginValues on_next_frame_begin_values; |
| on_next_frame_begin_values.set_additional_present_credits(1); |
| fake_flatland_.FireOnNextFrameBeginEvent( |
| std::move(on_next_frame_begin_values)); |
| |
| // Spin the loop to process Present(). |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(2u, presents_called); |
| EXPECT_THAT(fake_flatland_.graph(), |
| IsWindowGraph(parent_viewport_watcher(), viewport_token_, |
| Contains(IsViewport(view_token, expected_size)))); |
| } |
| |
| // Verify that surface is cleared when the window is disconnected from the |
| // display. |
| TEST_F(FlatlandWindowTest, ResetSurfaceOnDisconnect) { |
| CreateWindow(); |
| |
| EXPECT_CALL(window_delegate_, OnBoundsChanged(_)); |
| SetLayoutInfo(100, 100, 1.f); |
| task_environment_.RunUntilIdle(); |
| |
| // Try attaching the content. It should only be a closure. |
| fuchsia::ui::views::ViewCreationToken view_token; |
| fuchsia::ui::views::ViewportCreationToken viewport_token; |
| auto status = |
| zx::channel::create(0, &viewport_token.value, &view_token.value); |
| CHECK_EQ(ZX_OK, status); |
| flatland_window_->AttachSurfaceContent(std::move(viewport_token)); |
| |
| SetWindowStatus( |
| fuchsia::ui::composition::ParentViewportStatus::CONNECTED_TO_DISPLAY); |
| |
| // Show to attach the scene graph. |
| flatland_window_->Show(/*inactive=*/false); |
| fuchsia::ui::composition::OnNextFrameBeginValues on_next_frame_begin_values; |
| on_next_frame_begin_values.set_additional_present_credits(1); |
| fake_flatland_.FireOnNextFrameBeginEvent( |
| std::move(on_next_frame_begin_values)); |
| |
| // Spin the loop to process Present(). |
| task_environment_.RunUntilIdle(); |
| |
| // surface view should be attached once the window is shown. |
| EXPECT_THAT(fake_flatland_.graph(), |
| IsWindowGraph(parent_viewport_watcher(), viewport_token_, _)); |
| |
| // Remove the window from the screen and verify that it simulates destruction |
| // of AcceleratedWidget, which is necessary to ensure that WindowSurface is |
| // re-initialized. |
| testing::Mock::VerifyAndClearExpectations(&window_delegate_); |
| EXPECT_CALL(window_delegate_, OnAcceleratedWidgetDestroyed()); |
| EXPECT_CALL(window_delegate_, OnAcceleratedWidgetAvailable(window_widget_)); |
| |
| SetWindowStatus(fuchsia::ui::composition::ParentViewportStatus:: |
| DISCONNECTED_FROM_DISPLAY); |
| |
| on_next_frame_begin_values.set_additional_present_credits(1); |
| fake_flatland_.FireOnNextFrameBeginEvent( |
| std::move(on_next_frame_begin_values)); |
| |
| // Spin the loop to process Present(). |
| task_environment_.RunUntilIdle(); |
| |
| // Verify that the surface view is cleared. |
| EXPECT_THAT( |
| fake_flatland_.graph(), |
| IsWindowGraph(parent_viewport_watcher(), viewport_token_, IsEmpty())); |
| } |
| |
| // Verify that when surface is attached, a hit region accompanies the surface. |
| TEST_F(FlatlandWindowTest, SurfaceHasHitTestHitShield) { |
| CreateWindow(); |
| |
| EXPECT_CALL(window_delegate_, OnBoundsChanged(_)); |
| const uint32_t kWidth = 200; |
| const uint32_t kHeight = 100; |
| const fuchsia::math::SizeU expected_size = {kWidth, kHeight}; |
| SetLayoutInfo(kWidth, kHeight, 1.f); |
| |
| // Spin the loop to propagate layout. |
| task_environment_.RunUntilIdle(); |
| |
| fuchsia::ui::views::ViewCreationToken view_token; |
| fuchsia::ui::views::ViewportCreationToken viewport_token; |
| auto status = |
| zx::channel::create(0, &viewport_token.value, &view_token.value); |
| CHECK_EQ(ZX_OK, status); |
| flatland_window_->AttachSurfaceContent(std::move(viewport_token)); |
| |
| // Show() the window, to trigger creation of the scene graph, including |
| // surface and hit shield. |
| flatland_window_->Show(/*inactive=*/false); |
| fuchsia::ui::composition::OnNextFrameBeginValues on_next_frame_begin_values; |
| on_next_frame_begin_values.set_additional_present_credits(1); |
| fake_flatland_.FireOnNextFrameBeginEvent( |
| std::move(on_next_frame_begin_values)); |
| |
| // Spin the loop to process Present(). |
| task_environment_.RunUntilIdle(); |
| |
| // Surface should be accompanied by input shield, in that order. |
| EXPECT_THAT( |
| fake_flatland_.graph(), |
| Field("root_transform", &FakeGraph::root_transform, |
| Pointee(Field("children", &FakeTransform::children, |
| ElementsAre(IsViewport(view_token, expected_size), |
| IsHitShield()))))); |
| } |
| |
| class ParameterizedViewInsetTest : public FlatlandWindowTest, |
| public testing::WithParamInterface<float> {}; |
| |
| INSTANTIATE_TEST_SUITE_P(ViewInsetTest, |
| ParameterizedViewInsetTest, |
| testing::Values(1.f, 2.f, 3.f)); |
| |
| // Tests whether view insets are properly set in |FlatlandWindow|. |
| TEST_P(ParameterizedViewInsetTest, ViewInsetsTest) { |
| CreateWindow(); |
| EXPECT_CALL(window_delegate_, OnBoundsChanged(_)).Times(1); |
| SetLayoutInfo(100, 100, 1.f); |
| |
| const fuchsia::math::Inset inset = {1, 1, 1, 1}; |
| const float dpr = GetParam(); |
| |
| // Setting LayoutInfo should trigger a change in the bounds. |
| PlatformWindowDelegate::BoundsChange bounds(false); |
| EXPECT_CALL(window_delegate_, OnBoundsChanged(_)) |
| .WillOnce(SaveArg<0>(&bounds)); |
| SetLayoutInfo(100, 100, dpr, inset); |
| |
| EXPECT_EQ(bounds.system_ui_overlap.top(), dpr * inset.top); |
| EXPECT_EQ(bounds.system_ui_overlap.left(), dpr * inset.left); |
| EXPECT_EQ(bounds.system_ui_overlap.bottom(), dpr * inset.bottom); |
| EXPECT_EQ(bounds.system_ui_overlap.right(), dpr * inset.right); |
| } |
| |
| } // namespace ui |