| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| |
| #include <fuchsia/ui/input/cpp/fidl.h> |
| #include <lib/async/cpp/time.h> |
| #include <lib/zx/eventpair.h> |
| |
| #include "garnet/lib/ui/input/tests/util.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "lib/fxl/logging.h" |
| #include "lib/gtest/test_loop_fixture.h" |
| #include "lib/ui/input/cpp/formatting.h" |
| #include "lib/ui/scenic/cpp/resources.h" |
| #include "lib/ui/scenic/cpp/session.h" |
| |
| // This test exercises the focus avoidance property of a View. A pointer DOWN |
| // event typically triggers a pair of focus/unfocus events (each sent to a |
| // client). A View that has the focus avoidance property, and that would |
| // otherwise trigger focus/unfocus events, should not trigger these events. We |
| // set up a scene with two translated but overlapping Views, and see if |
| // focus/unfocus events are not conveyed to each client. |
| // |
| // The geometry is constrained to a 9x9 display and layer, with two 5x5 |
| // rectangles that intersect in one pixel, like so: |
| // |
| // x 1 1 1 1 - - - - |
| // 1 1 1 1 1 - - - - |
| // 1 1 1 1 1 - - - - |
| // 1 1 1 1 1 - - - - |
| // 1 1 1 1 y 2 2 2 2 |
| // - - - - 2 2 2 2 2 |
| // - - - - 2 2 2 2 2 |
| // - - - - 2 2 2 2 2 x - View 1 origin |
| // - - - - 2 2 2 2 2 y - View 2 origin |
| // |
| // To create this test setup, we perform translation of each View itself (i.e., |
| // (0,0) and (4,4)), in addition to aligning (translating) each View's Shape to |
| // its owning View. The setup also sets the focus avoidance property for View 2. |
| // |
| // View 1 creates its rectangle in the upper left quadrant; its origin is marked |
| // 'x'. Similarly, View 2 creates its rectangle in the bottom right quadrant; |
| // its origin marked 'y'. Here, View 1 is *underneath* View 2; the top-most |
| // pixel at 'y' belongs to View 2. |
| // |
| // The first hit test occurs at 'x' to ensure View 1 gains focus. The |
| // coordinates are: |
| // |
| // Event Finger Mark Device View-1 View-2 |
| // ADD 1 y (0,0) (0,0) n/a |
| // DOWN 1 y (0,0) (0,0) n/a |
| // |
| // The second hit test occurs at the overlap, at 'y'. Typically, View 2 would |
| // receive a focus event, and View 1 would receive an unfocus event. Since View |
| // 2 has the focus avoidance property, each View should receive the pointer |
| // events, but each View should *not* receive a focus or unfocus event. The |
| // coordinates are: |
| // |
| // Event Finger Mark Device View-1 View-2 |
| // ADD 2 y (4,4) (4,4) (0, 0) |
| // DOWN 2 y (4,4) (4,4) (0, 0) |
| // |
| // We use a different finger ID to trigger the second hit test. Each finger's |
| // state sequence is thus consistent, albeit incomplete for test brevity. |
| // |
| // N.B. This test is carefully constructed to avoid Vulkan functionality. |
| |
| namespace lib_ui_input_tests { |
| |
| using fuchsia::ui::input::InputEvent; |
| using fuchsia::ui::input::PointerEvent; |
| using fuchsia::ui::input::PointerEventPhase; |
| using fuchsia::ui::input::PointerEventType; |
| |
| // Class fixture for TEST_F. Sets up a 9x9 "display" for GfxSystem. |
| class FocusAvoidanceTest : public InputSystemTest { |
| protected: |
| uint32_t test_display_width_px() const override { return 9; } |
| uint32_t test_display_height_px() const override { return 9; } |
| }; |
| |
| // There's a lot of boilerplate that unfortunately can't be consolidated with |
| // that of ScenicStyleViewHierarchy; the scene graph is very different. |
| TEST_F(FocusAvoidanceTest, ViewHierarchyByViewManager) { |
| // Create the tokens for the Presenter to share with ViewManager. |
| zx::eventpair import_1, export_1, import_2, export_2; |
| CreateTokenPair(&import_1, &export_1); |
| CreateTokenPair(&import_2, &export_2); |
| |
| // Tie the test's dispatcher clock to the system (real) clock. |
| RunLoopUntil(zx::clock::get_monotonic()); |
| |
| // "Presenter" sets up a scene with room for two Views. |
| uint32_t compositor_id = 0; |
| SessionWrapper presenter(scenic()); |
| presenter.RunNow( |
| [this, &compositor_id, export_1 = std::move(export_1), |
| export_2 = std::move(export_2)](scenic::Session* session, |
| scenic::EntityNode* root_node) mutable { |
| // Minimal scene. |
| scenic::Compositor compositor(session); |
| compositor_id = compositor.id(); |
| |
| scenic::Scene scene(session); |
| scenic::Camera camera(scene); |
| scenic::Renderer renderer(session); |
| renderer.SetCamera(camera); |
| |
| scenic::Layer layer(session); |
| layer.SetSize(test_display_width_px(), test_display_height_px()); |
| layer.SetRenderer(renderer); |
| |
| scenic::LayerStack layer_stack(session); |
| layer_stack.AddLayer(layer); |
| compositor.SetLayerStack(layer_stack); |
| |
| // Add local root node to the scene. Add per-view translation node for |
| // each View, export these nodes to ViewManager. |
| scene.AddChild(*root_node); |
| scenic::EntityNode translate_1(session), translate_2(session); |
| |
| root_node->AddChild(translate_1); |
| translate_1.SetTranslation(0, 0, 1); |
| translate_1.SetTag(1); |
| translate_1.Export(std::move(export_1)); |
| |
| root_node->AddChild(translate_2); |
| translate_2.SetTranslation(4, 4, 2); |
| translate_2.SetTag(2); |
| translate_2.Export(std::move(export_2)); |
| |
| RequestToPresent(session); |
| }); |
| |
| // Create the tokens for ViewManager to share with each View. |
| zx::eventpair import_view_1, export_view_1, import_view_2, export_view_2; |
| CreateTokenPair(&import_view_1, &export_view_1); |
| CreateTokenPair(&import_view_2, &export_view_2); |
| |
| // "ViewManager" sets up two Views. |
| SessionWrapper view_manager(scenic()); |
| view_manager.RunNow( |
| [this, import_1 = std::move(import_1), import_2 = std::move(import_2), |
| export_view_1 = std::move(export_view_1), |
| export_view_2 = std::move(export_view_2)]( |
| scenic::Session* session, scenic::EntityNode* ignored) mutable { |
| scenic::ImportNode view_holder_1(session); // Implicit delegate node. |
| view_holder_1.Bind(std::move(import_1)); |
| scenic::EntityNode view_1(session); |
| view_1.SetLabel("<view 1>"); |
| view_1.SetTag(3); |
| view_1.Export(std::move(export_view_1)); |
| view_holder_1.AddChild(view_1); |
| |
| scenic::ImportNode view_holder_2(session); // Implicit delegate node. |
| view_holder_2.Bind(std::move(import_2)); |
| scenic::EntityNode view_2(session); |
| view_2.SetLabel("<view 2>"); |
| view_2.SetTag(4); |
| view_2.Export(std::move(export_view_2)); |
| view_holder_2.AddChild(view_2); |
| |
| // ViewManager sets "no-focus" property for view 2. |
| { |
| fuchsia::ui::gfx::SetImportFocusCmd focus_cmd; |
| focus_cmd.id = view_holder_2.id(); |
| focus_cmd.focusable = false; |
| fuchsia::ui::gfx::Command gfx_cmd; |
| gfx_cmd.set_set_import_focus(std::move(focus_cmd)); |
| session->Enqueue(std::move(gfx_cmd)); |
| } |
| |
| RequestToPresent(session); |
| }); |
| |
| // Client 1 vends a View to the global scene. |
| SessionWrapper client_1(scenic()); |
| client_1.RunNow( |
| [this, import_view_1 = std::move(import_view_1)]( |
| scenic::Session* session, scenic::EntityNode* root_node) mutable { |
| scenic::ImportNode import(session); |
| import.Bind(std::move(import_view_1)); |
| import.AddChild(*root_node); |
| |
| scenic::ShapeNode shape(session); |
| shape.SetTranslation(2, 2, 0); // Center the shape within the View. |
| root_node->AddPart(shape); |
| |
| scenic::Rectangle rec(session, 5, 5); // Simple; no real GPU work. |
| shape.SetShape(rec); |
| |
| scenic::Material material(session); |
| shape.SetMaterial(material); |
| |
| RequestToPresent(session); |
| }); |
| |
| // Client 2 vends a View to the global scene. |
| SessionWrapper client_2(scenic()); |
| client_2.RunNow( |
| [this, import_view_2 = std::move(import_view_2)]( |
| scenic::Session* session, scenic::EntityNode* root_node) mutable { |
| scenic::ImportNode import(session); |
| import.Bind(std::move(import_view_2)); |
| import.AddChild(*root_node); |
| |
| scenic::ShapeNode shape(session); |
| shape.SetTranslation(2, 2, 0); // Center the shape within the View. |
| root_node->AddPart(shape); |
| |
| scenic::Rectangle rec(session, 5, 5); // Simple; no real GPU work. |
| shape.SetShape(rec); |
| |
| scenic::Material material(session); |
| shape.SetMaterial(material); |
| |
| RequestToPresent(session); |
| }); |
| |
| // Multi-agent scene is now set up, send in the input. |
| presenter.RunNow([this, compositor_id](scenic::Session* session, |
| scenic::EntityNode* root_node) { |
| PointerCommandGenerator pointer_1(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, |
| PointerEventType::TOUCH); |
| // A touch sequence that starts in the upper left corner of the display. |
| session->Enqueue(pointer_1.Add(0, 0)); |
| session->Enqueue(pointer_1.Down(0, 0)); |
| |
| PointerCommandGenerator pointer_2(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 2, |
| PointerEventType::TOUCH); |
| // A touch sequence that starts in the middle of the display. |
| session->Enqueue(pointer_2.Add(4, 4)); |
| session->Enqueue(pointer_2.Down(4, 4)); |
| |
| RunLoopUntilIdle(); |
| |
| #if 0 |
| FXL_LOG(INFO) << DumpScenes(); // Handy debugging. |
| #endif |
| }); |
| |
| client_1.ExamineEvents([this](const std::vector<InputEvent>& events) { |
| EXPECT_EQ(events.size(), 5u) << "Should receive exactly 5 input events."; |
| |
| // ADD |
| EXPECT_TRUE(events[0].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[0].pointer(), 1u, PointerEventPhase::ADD, 0, 0)); |
| |
| // FOCUS |
| EXPECT_TRUE(events[1].is_focus()); |
| EXPECT_TRUE(events[1].focus().focused); |
| |
| // DOWN |
| EXPECT_TRUE(events[2].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[2].pointer(), 1u, PointerEventPhase::DOWN, 0, 0)); |
| |
| // ADD |
| EXPECT_TRUE(events[3].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[3].pointer(), 2u, PointerEventPhase::ADD, 4, 4)); |
| |
| // No unfocus event here! |
| |
| // DOWN |
| EXPECT_TRUE(events[4].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[4].pointer(), 2u, PointerEventPhase::DOWN, 4, 4)); |
| }); |
| |
| client_2.ExamineEvents([this](const std::vector<InputEvent>& events) { |
| EXPECT_EQ(events.size(), 2u) << "Should receive exactly 2 input events."; |
| |
| // ADD |
| EXPECT_TRUE(events[0].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[0].pointer(), 2u, PointerEventPhase::ADD, 0, 0)); |
| |
| // No focus event here! |
| |
| // DOWN |
| EXPECT_TRUE(events[1].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[1].pointer(), 2u, PointerEventPhase::DOWN, 0, 0)); |
| |
| #if 0 |
| for (const auto& event : events) |
| FXL_LOG(INFO) << "Client got: " << event; // Handy debugging. |
| #endif |
| }); |
| } |
| |
| TEST_F(FocusAvoidanceTest, ViewHierarchyByScenic) { |
| // Create the tokens for the Presenter to share with each client. |
| zx::eventpair vh_1, v_1, vh_2, v_2; |
| CreateTokenPair(&vh_1, &v_1); |
| CreateTokenPair(&vh_2, &v_2); |
| |
| // "Presenter" sets up a scene with room for two Views. |
| uint32_t compositor_id = 0; |
| SessionWrapper presenter(scenic()); |
| presenter.RunNow( |
| [this, &compositor_id, vh_1 = std::move(vh_1), vh_2 = std::move(vh_2)]( |
| scenic::Session* session, scenic::EntityNode* root_node) mutable { |
| // Minimal scene. |
| scenic::Compositor compositor(session); |
| compositor_id = compositor.id(); |
| |
| scenic::Scene scene(session); |
| scenic::Camera camera(scene); |
| scenic::Renderer renderer(session); |
| renderer.SetCamera(camera); |
| |
| scenic::Layer layer(session); |
| layer.SetSize(test_display_width_px(), test_display_height_px()); |
| layer.SetRenderer(renderer); |
| |
| scenic::LayerStack layer_stack(session); |
| layer_stack.AddLayer(layer); |
| compositor.SetLayerStack(layer_stack); |
| |
| // Add local root node to the scene. Add per-view translation for each |
| // View, hang the ViewHolders. |
| scene.AddChild(*root_node); |
| scenic::EntityNode translate_1(session), translate_2(session); |
| scenic::ViewHolder holder_1(session, std::move(vh_1), "view holder 1"), |
| holder_2(session, std::move(vh_2), "view holder 2"); |
| |
| root_node->AddChild(translate_1); |
| translate_1.SetTranslation(0, 0, 1); |
| translate_1.Attach(holder_1); |
| |
| root_node->AddChild(translate_2); |
| translate_2.SetTranslation(4, 4, 2); |
| translate_2.Attach(holder_2); |
| |
| // View 2's parent (Presenter) sets "no-focus" property for view 2. |
| { |
| fuchsia::ui::gfx::ViewProperties properties; |
| properties.focus_change = false; |
| holder_2.SetViewProperties(std::move(properties)); |
| } |
| |
| RequestToPresent(session); |
| }); |
| |
| // Client 1 vends a View to the global scene. |
| SessionWrapper client_1(scenic()); |
| client_1.RunNow( |
| [this, v_1 = std::move(v_1)](scenic::Session* session, |
| scenic::EntityNode* root_node) mutable { |
| scenic::View view(session, std::move(v_1), "view 1"); |
| view.AddChild(*root_node); |
| |
| scenic::ShapeNode shape(session); |
| shape.SetTranslation(2, 2, 0); // Center the shape within the View. |
| root_node->AddPart(shape); |
| |
| scenic::Rectangle rec(session, 5, 5); // Simple; no real GPU work. |
| shape.SetShape(rec); |
| |
| scenic::Material material(session); |
| shape.SetMaterial(material); |
| |
| RequestToPresent(session); |
| }); |
| |
| // Client 2 vends a View to the global scene. |
| SessionWrapper client_2(scenic()); |
| client_2.RunNow( |
| [this, v_2 = std::move(v_2)](scenic::Session* session, |
| scenic::EntityNode* root_node) mutable { |
| scenic::View view(session, std::move(v_2), "view 2"); |
| view.AddChild(*root_node); |
| |
| scenic::ShapeNode shape(session); |
| shape.SetTranslation(2, 2, 0); // Center the shape within the View. |
| root_node->AddPart(shape); |
| |
| scenic::Rectangle rec(session, 5, 5); // Simple; no real GPU work. |
| shape.SetShape(rec); |
| |
| scenic::Material material(session); |
| shape.SetMaterial(material); |
| |
| RequestToPresent(session); |
| }); |
| |
| // Multi-agent scene is now set up, send in the input. |
| presenter.RunNow([this, compositor_id](scenic::Session* session, |
| scenic::EntityNode* root_node) { |
| PointerCommandGenerator pointer_1(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, |
| PointerEventType::TOUCH); |
| // A touch sequence that starts in the upper left corner of the display. |
| session->Enqueue(pointer_1.Add(0, 0)); |
| session->Enqueue(pointer_1.Down(0, 0)); |
| |
| PointerCommandGenerator pointer_2(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 2, |
| PointerEventType::TOUCH); |
| // A touch sequence that starts in the middle of the display. |
| session->Enqueue(pointer_2.Add(4, 4)); |
| session->Enqueue(pointer_2.Down(4, 4)); |
| |
| RunLoopUntilIdle(); |
| |
| #if 0 |
| FXL_LOG(INFO) << DumpScenes(); // Handy debugging. |
| #endif |
| }); |
| |
| client_1.ExamineEvents([this](const std::vector<InputEvent>& events) { |
| EXPECT_EQ(events.size(), 5u) << "Should receive exactly 5 input events."; |
| |
| // ADD |
| EXPECT_TRUE(events[0].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[0].pointer(), 1u, PointerEventPhase::ADD, 0, 0)); |
| |
| // FOCUS |
| EXPECT_TRUE(events[1].is_focus()); |
| EXPECT_TRUE(events[1].focus().focused); |
| |
| // DOWN |
| EXPECT_TRUE(events[2].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[2].pointer(), 1u, PointerEventPhase::DOWN, 0, 0)); |
| |
| // ADD |
| EXPECT_TRUE(events[3].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[3].pointer(), 2u, PointerEventPhase::ADD, 4, 4)); |
| |
| // No unfocus event here! |
| |
| // DOWN |
| EXPECT_TRUE(events[4].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[4].pointer(), 2u, PointerEventPhase::DOWN, 4, 4)); |
| }); |
| |
| client_2.ExamineEvents([this](const std::vector<InputEvent>& events) { |
| EXPECT_EQ(events.size(), 2u) << "Should receive exactly 2 input events."; |
| |
| // ADD |
| EXPECT_TRUE(events[0].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[0].pointer(), 2u, PointerEventPhase::ADD, 0, 0)); |
| |
| // No focus event here! |
| |
| // DOWN |
| EXPECT_TRUE(events[1].is_pointer()); |
| EXPECT_TRUE( |
| PointerMatches(events[1].pointer(), 2u, PointerEventPhase::DOWN, 0, 0)); |
| |
| #if 0 |
| for (const auto& event : events) |
| FXL_LOG(INFO) << "Client got: " << event; // Handy debugging. |
| #endif |
| }); |
| } |
| |
| } // namespace lib_ui_input_tests |