blob: 3c8f8251df752490851effe97e30676d4b9c0859 [file]
// Copyright 2021 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 <fuchsia/ui/pointer/cpp/fidl.h>
#include <fuchsia/ui/pointerinjector/cpp/fidl.h>
#include <lib/async-loop/testing/cpp/real_loop.h>
#include <lib/sys/component/cpp/testing/realm_builder.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/resources.h>
#include <lib/ui/scenic/cpp/session.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <zircon/status.h>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <zxtest/zxtest.h>
#include "src/ui/scenic/tests/utils/scenic_realm_builder.h"
#include "src/ui/scenic/tests/utils/utils.h"
// These tests exercise the integration between GFX and the InputSystem, including the View-to-View
// transform logic between the injection point and the receiver.
// Setup:
// - Injection done in context View Space, with fuchsia.ui.pointerinjector
// - Target(s) specified by View (using view ref koids)
// - Dispatch done to fuchsia.ui.pointer.TouchSource in receiver View Space.
// Macro for calling EXPECT on fuchsia::ui::pointer::PointerSample.
// Is a macro to ensure we get the correct line number for the error.
#define EXPECT_EQ_POINTER(pointer_sample, viewport_to_view_transform, expected_phase, expected_x, \
expected_y) \
{ \
EXPECT_EQ(pointer_sample.phase(), expected_phase); \
const Mat3 transform_matrix = ArrayToMat3(viewport_to_view_transform); \
const std::array<float, 2> transformed_pointer = \
TransformPointerCoords(pointer_sample.position_in_viewport(), transform_matrix); \
EXPECT_TRUE(CmpFloatingValues(transformed_pointer[0], expected_x)); \
EXPECT_TRUE(CmpFloatingValues(transformed_pointer[1], expected_y)); \
}
namespace integration_tests {
using fuchsia::ui::pointer::TouchInteractionStatus;
using fuchsia::ui::pointerinjector::DispatchPolicy;
using fupi_EventPhase = fuchsia::ui::pointerinjector::EventPhase;
using fuchsia::ui::pointer::EventPhase;
using fuchsia::ui::pointer::TouchEvent;
using fuchsia::ui::pointer::TouchResponse;
using fuchsia::ui::pointer::TouchResponseType;
using fuchsia::ui::views::ViewRef;
using RealmRoot = component_testing::RealmRoot;
namespace {
std::array<float, 2> TransformPointerCoords(std::array<float, 2> pointer, const Mat3& transform) {
const Vec3 homogenous_pointer = {pointer[0], pointer[1], 1};
Vec3 transformed_pointer = transform * homogenous_pointer;
FX_CHECK(transformed_pointer[2] != 0);
const Vec3& homogenized = transformed_pointer / transformed_pointer[2];
return {homogenized[0], homogenized[1]};
}
struct SessionWithTouchSource {
std::unique_ptr<scenic::Session> session;
fuchsia::ui::pointer::TouchSourcePtr touch_source_ptr;
};
SessionWithTouchSource CreateSessionWithTouchSource(fuchsia::ui::scenic::Scenic* scenic) {
SessionWithTouchSource session_with_touch_source;
fuchsia::ui::scenic::SessionEndpoints endpoints;
fuchsia::ui::scenic::SessionPtr session_ptr;
fuchsia::ui::scenic::SessionListenerHandle listener_handle;
auto listener_request = listener_handle.NewRequest();
endpoints.set_session(session_ptr.NewRequest());
endpoints.set_session_listener(std::move(listener_handle));
endpoints.set_touch_source(session_with_touch_source.touch_source_ptr.NewRequest());
scenic->CreateSessionT(std::move(endpoints), [] {});
session_with_touch_source.session =
std::make_unique<scenic::Session>(std::move(session_ptr), std::move(listener_request));
return session_with_touch_source;
}
// Sets up the root of a scene.
// Present() must be called separately by the creator, since this does not have access to the
// looper.
struct RootSession {
RootSession(fuchsia::ui::scenic::Scenic* scenic)
: session_with_touch_source(CreateSessionWithTouchSource(scenic)),
session(session_with_touch_source.session.get()),
compositor(session),
layer_stack(session),
layer(session),
renderer(session),
scene(session),
camera(scene) {
compositor.SetLayerStack(layer_stack);
layer_stack.AddLayer(layer);
layer.SetSize(/*width*/ 9, /*height*/ 9); // 9x9 "display".
layer.SetRenderer(renderer);
renderer.SetCamera(camera);
}
SessionWithTouchSource session_with_touch_source;
scenic::Session* session = nullptr;
scenic::DisplayCompositor compositor;
scenic::LayerStack layer_stack;
scenic::Layer layer;
scenic::Renderer renderer;
scenic::Scene scene;
scenic::Camera camera;
std::unique_ptr<scenic::ViewHolder> view_holder;
};
} // namespace
class GfxTouchIntegrationTest : public zxtest::Test, public loop_fixture::RealLoop {
protected:
static constexpr fuchsia::ui::gfx::ViewProperties k5x5x1 = {.bounding_box = {.max = {5, 5, 1}}};
static constexpr uint32_t kDeviceId = 1111;
static constexpr uint32_t kPointerId = 2222;
// clang-format off
static constexpr std::array<float, 9> kIdentityMatrix = {
1, 0, 0, // column one
0, 1, 0, // column two
0, 0, 1, // column three
};
// clang-format on
fuchsia::ui::scenic::Scenic* scenic() { return scenic_.get(); }
void SetUp() override {
// Build the realm topology and route the protocols required by this test fixture from the
// scenic subrealm.
realm_ = std::make_unique<RealmRoot>(
ScenicRealmBuilder()
.AddRealmProtocol(fuchsia::ui::scenic::Scenic::Name_)
.AddRealmProtocol(fuchsia::ui::pointerinjector::Registry::Name_)
.Build());
scenic_ = realm_->Connect<fuchsia::ui::scenic::Scenic>();
scenic_.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to Scenic: %s", zx_status_get_string(status));
});
registry_ = realm_->Connect<fuchsia::ui::pointerinjector::Registry>();
registry_.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to pointerinjector Registry: %s", zx_status_get_string(status));
});
// Set up root view.
root_session_ = std::make_unique<RootSession>(scenic());
root_session_->session->set_error_handler([](auto) { FAIL("Root session terminated."); });
BlockingPresent(*root_session_->session);
}
void BlockingPresent(scenic::Session& session) {
bool presented = false;
session.set_on_frame_presented_handler([&presented](auto) { presented = true; });
session.Present2(0, 0, [](auto) {});
RunLoopUntil([&presented] { return presented; });
session.set_on_frame_presented_handler([](auto) {});
}
SessionWithTouchSource CreateChildView(fuchsia::ui::views::ViewToken view_token,
fuchsia::ui::views::ViewRefControl control_ref,
fuchsia::ui::views::ViewRef view_ref,
std::string debug_name) {
auto session_with_touch_source = CreateSessionWithTouchSource(scenic());
scenic::Session* session = session_with_touch_source.session.get();
scenic::View view(session, std::move(view_token), std::move(control_ref), std::move(view_ref),
debug_name);
scenic::ShapeNode shape(session);
scenic::Rectangle rec(session, 5, 5);
shape.SetTranslation(2.5f, 2.5f, 0); // Center the shape within the View.
view.AddChild(shape);
shape.SetShape(rec);
BlockingPresent(*session);
return session_with_touch_source;
}
void Inject(float x, float y, fupi_EventPhase phase) {
FX_CHECK(injector_);
fuchsia::ui::pointerinjector::Event event;
event.set_timestamp(0);
{
fuchsia::ui::pointerinjector::PointerSample pointer_sample;
pointer_sample.set_pointer_id(kPointerId);
pointer_sample.set_phase(phase);
pointer_sample.set_position_in_viewport({x, y});
fuchsia::ui::pointerinjector::Data data;
data.set_pointer_sample(std::move(pointer_sample));
event.set_data(std::move(data));
}
std::vector<fuchsia::ui::pointerinjector::Event> events;
events.emplace_back(std::move(event));
bool hanging_get_returned = false;
injector_->Inject(std::move(events), [&hanging_get_returned] { hanging_get_returned = true; });
RunLoopUntil(
[this, &hanging_get_returned] { return hanging_get_returned || injector_channel_closed_; });
}
void RegisterInjector(fuchsia::ui::views::ViewRef context_view_ref,
fuchsia::ui::views::ViewRef target_view_ref,
DispatchPolicy dispatch_policy = DispatchPolicy::EXCLUSIVE_TARGET,
std::array<float, 9> viewport_to_context_transform = kIdentityMatrix) {
fuchsia::ui::pointerinjector::Config config;
config.set_device_id(kDeviceId);
config.set_device_type(fuchsia::ui::pointerinjector::DeviceType::TOUCH);
config.set_dispatch_policy(dispatch_policy);
{
{
fuchsia::ui::pointerinjector::Context context;
context.set_view(std::move(context_view_ref));
config.set_context(std::move(context));
}
{
fuchsia::ui::pointerinjector::Target target;
target.set_view(std::move(target_view_ref));
config.set_target(std::move(target));
}
{
fuchsia::ui::pointerinjector::Viewport viewport;
viewport.set_extents(FullScreenExtents());
viewport.set_viewport_to_context_transform(viewport_to_context_transform);
config.set_viewport(std::move(viewport));
}
}
injector_.set_error_handler([this](zx_status_t) { injector_channel_closed_ = true; });
bool register_callback_fired = false;
registry_->Register(std::move(config), injector_.NewRequest(),
[&register_callback_fired] { register_callback_fired = true; });
RunLoopUntil([&register_callback_fired] { return register_callback_fired; });
ASSERT_FALSE(injector_channel_closed_);
}
// Starts a recursive TouchSource::Watch() loop that collects all received events into
// |out_events|.
void StartWatchLoop(fuchsia::ui::pointer::TouchSourcePtr& touch_source,
std::vector<TouchEvent>& out_events,
TouchResponseType response_type = TouchResponseType::MAYBE) {
const size_t index = watch_loops_.size();
watch_loops_.emplace_back();
watch_loops_.at(index) = [this, &touch_source, &out_events, response_type,
index](std::vector<TouchEvent> events) {
std::vector<TouchResponse> responses;
for (auto& event : events) {
if (event.has_pointer_sample()) {
TouchResponse response;
response.set_response_type(response_type);
responses.emplace_back(std::move(response));
} else {
responses.emplace_back();
}
}
std::move(events.begin(), events.end(), std::back_inserter(out_events));
touch_source->Watch(std::move(responses), [this, index](std::vector<TouchEvent> events) {
watch_loops_.at(index)(std::move(events));
});
};
touch_source->Watch({}, watch_loops_.at(index));
}
std::array<std::array<float, 2>, 2> FullScreenExtents() const { return {{{0, 0}, {9, 9}}}; }
std::unique_ptr<RootSession> root_session_;
bool injector_channel_closed_ = false;
private:
fuchsia::ui::scenic::ScenicPtr scenic_;
fuchsia::ui::pointerinjector::RegistryPtr registry_;
fuchsia::ui::pointerinjector::DevicePtr injector_;
// Holds watch loops so they stay alive through the duration of the test.
std::vector<std::function<void(std::vector<TouchEvent>)>> watch_loops_;
std::unique_ptr<RealmRoot> realm_;
};
// Test for checking that the pointerinjector channel is closed when context and target relationship
// in the scene graph becomes invalid for injection.
TEST_F(GfxTouchIntegrationTest, InjectorChannel_ShouldClose_WhenSceneBreaks) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
// Set up scene with two ViewHolders, one a child of the other.
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "child1_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder holder_2(root_session_->session, std::move(vh2), "holder_2");
view.AddChild(holder_2);
BlockingPresent(*root_session_->session);
auto [child_control_ref, child_view_ref] = scenic::ViewRefPair::New();
auto [child_session, child_touch_source] = CreateChildView(
std::move(v2), std::move(child_control_ref), fidl::Clone(child_view_ref), "child_view");
RegisterInjector(std::move(root_view_ref), std::move(child_view_ref));
// Break the scene graph relation that the pointerinjector relies on. Observe the channel close.
view.DetachChild(holder_2);
BlockingPresent(*root_session_->session);
}
// In this test we set up the context and the target. We apply a scale, rotation and translation
// transform to both of their view holder nodes, and then inject pointer events to confirm that
// the coordinates received by the listener are correctly transformed.
// Only the transformation of the target, relative to the context, should have any effect on
// the output.
// The viewport-to-context transform here is the identity. That is, the size of the 9x9 viewport
// matches the size of the 5x5 context view.
//
// Below are ASCII diagrams showing the transformation *difference* between target and context.
// Note that the dashes represent the context view and notated X,Y coordinate system is the
// context's coordinate system. The target view's coordinate system has its origin at corner '1'.
//
// Scene pre-transformation
// 1,2,3,4 denote the corners of the target view:
// X ->
// Y 1 O O O O 2
// | O O O O O O
// v O O O O O O
// O O O O O O
// O O O O O O
// 4 O O O O 3
//
// After scale:
// X ->
// Y 1 - O - O - O O 2
// | - - - - - - -
// V - - - - - - -
// O - O - O - O O O
// - - - - - - -
// - - - - - - -
// O O O O O O
//
//
// O O O O O O
//
//
// O O O O O O
//
//
// 4 O O O O 3
//
// After rotation:
// X ->
// Y 4 O O O O 1 - - - - - -
// | - - - - - -
// V O O O O O O - - - - - -
// - - - - - -
// O O O O O O - - - - - -
// - - - - - -
// O O O O O O
//
// O O O O O O
//
// 3 O O O O 2
//
// After translation:
// X ->
// Y 4 O O O O A 1 - - - C1
// | - - - - - -
// V O O O O O - O - - - -
// - - - - - -
// O O O O O - O - - - -
// R - - - - C2
// O O O O O O
//
// O O O O O O
//
// 3 O O O O 2
//
TEST_F(GfxTouchIntegrationTest, InjectedInput_ShouldBeCorrectlyTransformed) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
// 90 degrees counter clockwise rotation around Z-axis (Z-axis points into screen, so appears as
// clockwise rotation).
const auto rotation_quaternion = angleAxis(static_cast<float>(M_PI) / 2.f, /*Vec3*/ {0, 0, 1});
// Set up scene with two ViewHolders, one a child of the other.
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
{
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "child1_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
root_session_->scene.AddChild(holder_1);
holder_1.SetViewProperties(k5x5x1);
// Scale, rotate and translate the context to verify that it has no effect on the outcome.
holder_1.SetScale(2, 3, 1);
holder_1.SetRotation(rotation_quaternion[0], rotation_quaternion[1], rotation_quaternion[2],
rotation_quaternion[3]);
holder_1.SetTranslation(1, 0, 0);
scenic::ViewHolder holder_2(root_session_->session, std::move(vh2), "holder_2");
view.AddChild(holder_2);
holder_2.SetViewProperties(k5x5x1);
// Scale, rotate and translate target.
// Scale X by 2 and Y by 3.
holder_2.SetScale(2, 3, 1);
holder_2.SetRotation(rotation_quaternion[0], rotation_quaternion[1], rotation_quaternion[2],
rotation_quaternion[3]);
// Translate by 1 in the X direction.
holder_2.SetTranslation(1, 0, 0);
BlockingPresent(*root_session_->session);
}
auto [child_control_ref, child_view_ref] = scenic::ViewRefPair::New();
auto [child_session, child_touch_source] = CreateChildView(
std::move(v2), std::move(child_control_ref), fidl::Clone(child_view_ref), "child_view");
child_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child_events;
StartWatchLoop(child_touch_source, child_events);
// Scene is now set up, send in the input. One event for where each corner of the view was
// pre-transformation.
RegisterInjector(std::move(root_view_ref), std::move(child_view_ref));
Inject(0, 0, fupi_EventPhase::ADD); // A
Inject(5, 0, fupi_EventPhase::CHANGE); // C1
Inject(5, 5, fupi_EventPhase::CHANGE); // C2
Inject(0, 5, fupi_EventPhase::REMOVE); // R
RunLoopUntil([&child_events] { return child_events.size() == 4u; }); // Succeeds or times out.
// ASSERT for existance checks to avoid UB data reads.
ASSERT_TRUE(child_events[0].has_timestamp());
ASSERT_TRUE(child_events[0].has_trace_flow_id());
ASSERT_TRUE(child_events[0].has_device_info());
ASSERT_TRUE(child_events[0].has_view_parameters());
ASSERT_TRUE(child_events[0].has_pointer_sample());
ASSERT_TRUE(child_events[0].has_interaction_result());
ASSERT_TRUE(child_events[1].has_timestamp());
ASSERT_TRUE(child_events[1].has_trace_flow_id());
EXPECT_FALSE(child_events[1].has_device_info());
EXPECT_FALSE(child_events[1].has_view_parameters());
ASSERT_TRUE(child_events[1].has_pointer_sample());
EXPECT_FALSE(child_events[1].has_interaction_result());
ASSERT_TRUE(child_events[2].has_timestamp());
ASSERT_TRUE(child_events[2].has_trace_flow_id());
EXPECT_FALSE(child_events[2].has_device_info());
EXPECT_FALSE(child_events[2].has_view_parameters());
ASSERT_TRUE(child_events[2].has_pointer_sample());
EXPECT_FALSE(child_events[2].has_interaction_result());
ASSERT_TRUE(child_events[3].has_timestamp());
ASSERT_TRUE(child_events[3].has_trace_flow_id());
EXPECT_FALSE(child_events[3].has_device_info());
EXPECT_FALSE(child_events[3].has_view_parameters());
ASSERT_TRUE(child_events[3].has_pointer_sample());
EXPECT_FALSE(child_events[3].has_interaction_result());
{ // Check layout validity.CreateChildView
EXPECT_EQ(child_events[0].device_info().id(), kDeviceId);
const auto& interaction_result = child_events[0].interaction_result();
EXPECT_EQ(interaction_result.interaction.device_id, kDeviceId);
EXPECT_EQ(interaction_result.interaction.pointer_id, kPointerId);
EXPECT_EQ(interaction_result.status, TouchInteractionStatus::GRANTED);
const auto& view_parameters = child_events[0].view_parameters();
EXPECT_TRUE(CmpFloatingValues(view_parameters.view.min[0], 0.f));
EXPECT_TRUE(CmpFloatingValues(view_parameters.view.min[1], 0.f));
EXPECT_TRUE(CmpFloatingValues(view_parameters.view.max[0], 5.f));
EXPECT_TRUE(CmpFloatingValues(view_parameters.view.max[1], 5.f));
EXPECT_TRUE(CmpFloatingValues(view_parameters.viewport.min[0], 0.f));
EXPECT_TRUE(CmpFloatingValues(view_parameters.viewport.min[1], 0.f));
EXPECT_TRUE(CmpFloatingValues(view_parameters.viewport.max[0], 9.f));
EXPECT_TRUE(CmpFloatingValues(view_parameters.viewport.max[1], 9.f));
}
const uint32_t interaction_id = child_events[0].interaction_result().interaction.interaction_id;
{
const auto& pointer = child_events[0].pointer_sample();
ASSERT_TRUE(pointer.has_interaction());
EXPECT_TRUE(pointer.has_phase());
EXPECT_TRUE(pointer.has_position_in_viewport());
const auto& pointer_interaction_id = pointer.interaction();
EXPECT_EQ(pointer_interaction_id.device_id, kDeviceId);
EXPECT_EQ(pointer_interaction_id.pointer_id, kPointerId);
EXPECT_EQ(pointer_interaction_id.interaction_id, interaction_id);
}
{
const auto& pointer = child_events[1].pointer_sample();
ASSERT_TRUE(pointer.has_interaction());
EXPECT_TRUE(pointer.has_phase());
EXPECT_TRUE(pointer.has_position_in_viewport());
const auto& pointer_interaction_id = pointer.interaction();
EXPECT_EQ(pointer_interaction_id.device_id, kDeviceId);
EXPECT_EQ(pointer_interaction_id.pointer_id, kPointerId);
EXPECT_EQ(pointer_interaction_id.interaction_id, interaction_id);
}
{
const auto& pointer = child_events[2].pointer_sample();
ASSERT_TRUE(pointer.has_interaction());
EXPECT_TRUE(pointer.has_phase());
EXPECT_TRUE(pointer.has_position_in_viewport());
const auto& pointer_interaction_id = pointer.interaction();
EXPECT_EQ(pointer_interaction_id.device_id, kDeviceId);
EXPECT_EQ(pointer_interaction_id.pointer_id, kPointerId);
EXPECT_EQ(pointer_interaction_id.interaction_id, interaction_id);
}
{
const auto& pointer = child_events[3].pointer_sample();
ASSERT_TRUE(pointer.has_interaction());
EXPECT_TRUE(pointer.has_phase());
EXPECT_TRUE(pointer.has_position_in_viewport());
const auto& pointer_interaction_id = pointer.interaction();
EXPECT_EQ(pointer_interaction_id.device_id, kDeviceId);
EXPECT_EQ(pointer_interaction_id.pointer_id, kPointerId);
EXPECT_EQ(pointer_interaction_id.interaction_id, interaction_id);
}
// Check pointer samples.
{
const auto& viewport_to_view_transform =
child_events[0].view_parameters().viewport_to_view_transform;
EXPECT_EQ_POINTER(child_events[0].pointer_sample(), viewport_to_view_transform, EventPhase::ADD,
0.f / 2.f, (0.f + 1.f) / 3.f);
EXPECT_EQ_POINTER(child_events[1].pointer_sample(), viewport_to_view_transform,
EventPhase::CHANGE, 0.f / 2.f, (-5.f + 1.f) / 3.f);
EXPECT_EQ_POINTER(child_events[2].pointer_sample(), viewport_to_view_transform,
EventPhase::CHANGE, 5.f / 2.f, (-5.f + 1.f) / 3.f);
EXPECT_EQ_POINTER(child_events[3].pointer_sample(), viewport_to_view_transform,
EventPhase::REMOVE, 5.f / 2.f, (0.f + 1.f) / 3.f);
}
}
// In this test the context and the target have identical coordinate systems, but the viewport
// no longer matches the context's coordinate system.
//
// Below is an ASCII diagram showing the resulting setup.
// O represents the views, - the viewport.
// X ->
// Y O O O O O O
// |
// V A - - - - C1- - - -
// O - O - O - O - O - O
// - - - - - - - - - -
// - - - - - - - - - -
// O - O - O - O - O - O
// R - - - - C2- - - -
// - - - - - - - - - -
// O - O - O - O - O - O
// - - - - - - - - - -
// - - - - - - - - - -
// O O O O O O
//
//
// O O O O O O
//
TEST_F(GfxTouchIntegrationTest, InjectedInput_ShouldBeCorrectlyViewportTransformed) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
// Set up a scene with two ViewHolders, one a child of the other.
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
{
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "child1_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
holder_1.SetViewProperties(k5x5x1);
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder holder_2(root_session_->session, std::move(vh2), "holder_2");
holder_2.SetViewProperties(k5x5x1);
view.AddChild(holder_2);
BlockingPresent(*root_session_->session);
}
auto [child_control_ref, child_view_ref] = scenic::ViewRefPair::New();
auto [child_session, child_touch_source] = CreateChildView(
std::move(v2), std::move(child_control_ref), fidl::Clone(child_view_ref), "child_view");
child_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child_events;
StartWatchLoop(child_touch_source, child_events);
// Scene is now set up, send in the input. One event for where each corner of the view was
// pre-transformation.
// Transform to scale the viewport by 1/2 in the x-direction, 1/3 in the y-direction,
// and then translate by (1, 2).
// clang-format off
static constexpr std::array<float, 9> kViewportToContextTransform = {
1.f/2.f, 0, 0, // first column
0, 1.f/3.f, 0, // second column
1, 2, 1, // third column
};
// clang-format on
RegisterInjector(std::move(root_view_ref), std::move(child_view_ref),
DispatchPolicy::EXCLUSIVE_TARGET, kViewportToContextTransform);
Inject(0, 0, fupi_EventPhase::ADD); // A
Inject(5, 0, fupi_EventPhase::CHANGE); // C1
Inject(5, 5, fupi_EventPhase::CHANGE); // C2
Inject(0, 5, fupi_EventPhase::REMOVE); // R
RunLoopUntil([&child_events] { return child_events.size() == 4u; }); // Succeeds or times out.
// Check pointer samples.
{
const auto& viewport_to_view_transform =
child_events[0].view_parameters().viewport_to_view_transform;
EXPECT_EQ_POINTER(child_events[0].pointer_sample(), viewport_to_view_transform, EventPhase::ADD,
0.f / 2.f + 1, 0.f / 3.f + 2);
EXPECT_EQ_POINTER(child_events[1].pointer_sample(), viewport_to_view_transform,
EventPhase::CHANGE, 5.f / 2.f + 1, 0.f / 3.f + 2);
EXPECT_EQ_POINTER(child_events[2].pointer_sample(), viewport_to_view_transform,
EventPhase::CHANGE, 5.f / 2.f + 1, 5.f / 3.f + 2);
EXPECT_EQ_POINTER(child_events[3].pointer_sample(), viewport_to_view_transform,
EventPhase::REMOVE, 0.f / 2.f + 1, 5.f / 3.f + 2);
}
}
// In this test the context and the target have identical coordinate systems except for a 90 degree
// rotation. Check that all corners still generate hits. This confirms that small floating point
// errors don't cause misses.
//
// Scene pre-transformation
// 1,2,3,4 denote the corners of the target view:
// X ->
// Y 1 O O O O 2
// | O O O O O O
// v O O O O O O
// O O O O O O
// O O O O O O
// 4 O O O O 3
//
// Post-rotation
// X ->
// Y 4 O O O O 1
// | O O O O O O
// v O O O O O O
// O O O O O O
// O O O O O O
// 3 O O O O 2
TEST_F(GfxTouchIntegrationTest, InjectedInput_OnRotatedChild_ShouldHitEdges) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
// Set up a scene with two ViewHolders, one a child of the other.
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
{
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "child1_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
holder_1.SetViewProperties(k5x5x1);
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder holder_2(root_session_->session, std::move(vh2), "holder_2");
holder_2.SetViewProperties(k5x5x1);
// Rotate 90 degrees counter clockwise around Z-axis (Z-axis points into screen, so appears as
// clockwise rotation).
holder_2.SetAnchor(2.5f, 2.5f, 0);
const auto rotation_quaternion = angleAxis(static_cast<float>(M_PI) / 2.f, {0, 0, 1});
holder_2.SetRotation(rotation_quaternion[0], rotation_quaternion[1], rotation_quaternion[2],
rotation_quaternion[3]);
view.AddChild(holder_2);
BlockingPresent(*root_session_->session);
}
auto [child_control_ref, child_view_ref] = scenic::ViewRefPair::New();
auto [child_session, child_touch_source] = CreateChildView(
std::move(v2), std::move(child_control_ref), fidl::Clone(child_view_ref), "child_view");
child_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child_events;
StartWatchLoop(child_touch_source, child_events);
// Scene is now set up, send in the input. One interaction for each corner.
RegisterInjector(std::move(root_view_ref), std::move(child_view_ref),
DispatchPolicy::TOP_HIT_AND_ANCESTORS_IN_TARGET);
Inject(0, 0, fupi_EventPhase::ADD);
Inject(0, 0, fupi_EventPhase::REMOVE);
Inject(0, 5, fupi_EventPhase::ADD);
Inject(0, 5, fupi_EventPhase::REMOVE);
Inject(5, 5, fupi_EventPhase::ADD);
Inject(5, 5, fupi_EventPhase::REMOVE);
Inject(5, 0, fupi_EventPhase::ADD);
Inject(5, 0, fupi_EventPhase::REMOVE);
RunLoopUntil([&child_events] { return child_events.size() == 8u; }); // Succeeds or times out.
{ // Target should receive all events rotated 90 degrees.
const auto& viewport_to_view_transform =
child_events[0].view_parameters().viewport_to_view_transform;
EXPECT_EQ_POINTER(child_events[0].pointer_sample(), viewport_to_view_transform, EventPhase::ADD,
0.f, 5.f);
EXPECT_EQ_POINTER(child_events[1].pointer_sample(), viewport_to_view_transform,
EventPhase::REMOVE, 0.f, 5.f);
EXPECT_EQ_POINTER(child_events[2].pointer_sample(), viewport_to_view_transform, EventPhase::ADD,
5.f, 5.f);
EXPECT_EQ_POINTER(child_events[3].pointer_sample(), viewport_to_view_transform,
EventPhase::REMOVE, 5.f, 5.f);
EXPECT_EQ_POINTER(child_events[4].pointer_sample(), viewport_to_view_transform, EventPhase::ADD,
5.f, 0.f);
EXPECT_EQ_POINTER(child_events[5].pointer_sample(), viewport_to_view_transform,
EventPhase::REMOVE, 5.f, 0.f);
EXPECT_EQ_POINTER(child_events[6].pointer_sample(), viewport_to_view_transform, EventPhase::ADD,
0.f, 0.f);
EXPECT_EQ_POINTER(child_events[7].pointer_sample(), viewport_to_view_transform,
EventPhase::REMOVE, 0.f, 0.f);
}
}
// In this test we set up the context and the target. We apply clip space transform to the camera
// and then inject pointer events to confirm that the coordinates received by the listener are
// not impacted by the clip space transform.
TEST_F(GfxTouchIntegrationTest, ClipSpaceTransformedScene_ShouldHaveNoImpactOnOutput) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
// Set the clip space transform on the camera.
// Camera zooms in by 3x, and the camera is moved to (24,54) in the scene's coordinate space.
root_session_->camera.SetClipSpaceTransform(/*x offset*/ 24, /*y offset*/ 54, /*scale*/ 3);
// Set up a scene with two ViewHolders, one a child of the other.
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
{
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "child1_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
holder_1.SetViewProperties(k5x5x1);
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder holder_2(root_session_->session, std::move(vh2), "holder_2");
holder_2.SetViewProperties(k5x5x1);
view.AddChild(holder_2);
BlockingPresent(*root_session_->session);
}
auto [child_control_ref, child_view_ref] = scenic::ViewRefPair::New();
auto [child_session, child_touch_source] = CreateChildView(
std::move(v2), std::move(child_control_ref), fidl::Clone(child_view_ref), "child_view");
child_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child_events;
StartWatchLoop(child_touch_source, child_events);
// Scene is now set up, send in the input. One event for where each corner of the view was
// pre-transformation.
RegisterInjector(std::move(root_view_ref), std::move(child_view_ref));
Inject(0, 0, fupi_EventPhase::ADD);
Inject(5, 0, fupi_EventPhase::CHANGE);
Inject(5, 5, fupi_EventPhase::CHANGE);
Inject(0, 5, fupi_EventPhase::REMOVE);
RunLoopUntil([&child_events] { return child_events.size() == 4u; }); // Succeeds or times out.
// Target should receive identical events to injected, since their coordinate spaces are the same.
{
const auto& viewport_to_view_transform =
child_events[0].view_parameters().viewport_to_view_transform;
EXPECT_EQ_POINTER(child_events[0].pointer_sample(), viewport_to_view_transform, EventPhase::ADD,
0.f, 0.f);
EXPECT_EQ_POINTER(child_events[1].pointer_sample(), viewport_to_view_transform,
EventPhase::CHANGE, 5.f, 0.f);
EXPECT_EQ_POINTER(child_events[2].pointer_sample(), viewport_to_view_transform,
EventPhase::CHANGE, 5.f, 5.f);
EXPECT_EQ_POINTER(child_events[3].pointer_sample(), viewport_to_view_transform,
EventPhase::REMOVE, 0.f, 5.f);
}
}
// Basic scene (no transformations) where the Viewport is smaller than the Views.
// We then inject two streams: The first has an ADD outside the Viewport, which counts as a miss and
// should not be seen by anyone. The second stream has the ADD inside the Viewport and subsequent
// events outside, and this full stream should be seen by the target.
TEST_F(GfxTouchIntegrationTest, InjectionOutsideViewport_ShouldLimitOnADD) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
// Set up a scene with two ViewHolders, one a child of the other. Make the Views bigger than the\
// Viewport.
static constexpr fuchsia::ui::gfx::ViewProperties k100x100x1 = {
.bounding_box = {.max = {100, 100, 1}}};
// Set up a scene with two ViewHolders, one a child of the other.
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
{
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "child1_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
holder_1.SetViewProperties(k100x100x1);
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder holder_2(root_session_->session, std::move(vh2), "holder_2");
holder_2.SetViewProperties(k100x100x1);
view.AddChild(holder_2);
BlockingPresent(*root_session_->session);
}
auto [child_control_ref, child_view_ref] = scenic::ViewRefPair::New();
auto [child_session, child_touch_source] = CreateChildView(
std::move(v2), std::move(child_control_ref), fidl::Clone(child_view_ref), "child_view");
child_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child_events;
StartWatchLoop(child_touch_source, child_events);
// Scene is now set up, send in the input. The initial input is outside the viewport and
// the stream should therefore not be seen by anyone.
RegisterInjector(std::move(root_view_ref), std::move(child_view_ref),
DispatchPolicy::TOP_HIT_AND_ANCESTORS_IN_TARGET);
Inject(10, 10, fupi_EventPhase::ADD); // Outside viewport.
// Rest inside viewport, but should not be delivered.
Inject(5, 0, fupi_EventPhase::CHANGE);
Inject(5, 5, fupi_EventPhase::CHANGE);
Inject(0, 5, fupi_EventPhase::REMOVE);
// Send in input starting in the viewport and moving outside.
Inject(1, 1, fupi_EventPhase::ADD); // Inside viewport.
// Rest outside viewport, but should still be delivered.
Inject(50, 0, fupi_EventPhase::CHANGE);
Inject(50, 50, fupi_EventPhase::CHANGE);
Inject(0, 50, fupi_EventPhase::REMOVE);
RunLoopUntil([&child_events] { return child_events.size() >= 4u; }); // Succeeds or times out.
EXPECT_EQ(child_events.size(), 4u);
{
const auto& viewport_to_view_transform =
child_events[0].view_parameters().viewport_to_view_transform;
EXPECT_EQ_POINTER(child_events[0].pointer_sample(), viewport_to_view_transform, EventPhase::ADD,
1.f, 1.f);
EXPECT_EQ_POINTER(child_events[1].pointer_sample(), viewport_to_view_transform,
EventPhase::CHANGE, 50.f, 0.f);
EXPECT_EQ_POINTER(child_events[2].pointer_sample(), viewport_to_view_transform,
EventPhase::CHANGE, 50.f, 50.f);
EXPECT_EQ_POINTER(child_events[3].pointer_sample(), viewport_to_view_transform,
EventPhase::REMOVE, 0.f, 50.f);
}
}
TEST_F(GfxTouchIntegrationTest, Exclusive_TargetDisconnectedMidStream_ShouldCancelStream) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
// Set up a scene with two ViewHolders, one a child of the other.
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "root_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
holder_1.SetViewProperties(k5x5x1);
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder holder_2(root_session_->session, std::move(vh2), "holder_2");
holder_2.SetViewProperties(k5x5x1);
view.AddChild(holder_2);
BlockingPresent(*root_session_->session);
auto [child_control_ref, child_view_ref] = scenic::ViewRefPair::New();
auto [child_session, child_touch_source] = CreateChildView(
std::move(v2), std::move(child_control_ref), fidl::Clone(child_view_ref), "child_view");
child_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child_events;
StartWatchLoop(child_touch_source, child_events);
// Send in the first events of a stream.
RegisterInjector(std::move(root_view_ref), std::move(child_view_ref));
Inject(0, 0, fupi_EventPhase::ADD);
Inject(5, 0, fupi_EventPhase::CHANGE);
RunLoopUntil([&child_events] { return child_events.size() >= 2u; }); // Succeeds or times out.
EXPECT_EQ(child_events.size(), 2u);
// Disrupt the scene graph.
view.DetachChild(holder_2);
BlockingPresent(*root_session_->session);
// Next event should deliver a cancel event to the child (and close the injector since it's the
// target)
Inject(5, 5, fupi_EventPhase::CHANGE);
EXPECT_TRUE(injector_channel_closed_);
RunLoopUntil([&child_events] { return child_events.size() >= 3u; }); // Succeeds or times out.
{
ASSERT_EQ(child_events.size(), 3u);
ASSERT_TRUE(child_events.back().has_pointer_sample());
const auto& sample = child_events.back().pointer_sample();
ASSERT_TRUE(sample.has_phase());
EXPECT_EQ(sample.phase(), EventPhase::CANCEL);
}
}
TEST_F(GfxTouchIntegrationTest, Exclusive_TargetDyingMidStream_ShouldKillChannel) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
// Set up a scene with two ViewHolders, one a child of the other.
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
{
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "root_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
holder_1.SetViewProperties(k5x5x1);
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder holder_2(root_session_->session, std::move(vh2), "holder_2");
holder_2.SetViewProperties(k5x5x1);
view.AddChild(holder_2);
BlockingPresent(*root_session_->session);
}
auto [child_control_ref, child_view_ref] = scenic::ViewRefPair::New();
auto [child_session, child_touch_source] = CreateChildView(
std::move(v2), std::move(child_control_ref), fidl::Clone(child_view_ref), "child_view");
child_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child_events;
StartWatchLoop(child_touch_source, child_events);
// Send in the first events of a stream.
RegisterInjector(std::move(root_view_ref), std::move(child_view_ref));
Inject(0, 0, fupi_EventPhase::ADD);
Inject(5, 0, fupi_EventPhase::CHANGE);
RunLoopUntil([&child_events] { return child_events.size() >= 2u; }); // Succeeds or times out.
EXPECT_EQ(child_events.size(), 2u);
// Kill the child.
bool child_view_died = false;
root_session_->session->set_event_handler(
[&child_view_died](std::vector<fuchsia::ui::scenic::Event> events) {
for (const auto& event : events) {
if (event.is_gfx() && event.gfx().is_view_disconnected())
child_view_died = true;
}
});
child_touch_source.Unbind();
child_session.reset();
RunLoopUntil([&child_view_died] { return child_view_died; });
// TODO(fxbug.dev/81683): We perform one more present to avoid flakes. Session death causes view
// disconnected signals to be sent out-of-sync with the normal Present flow.
BlockingPresent(*root_session_->session);
// Next injection should close the channel.
Inject(0, 5, fupi_EventPhase::CHANGE);
EXPECT_TRUE(injector_channel_closed_);
}
// Sets up a scene with three views: Root -> Child1 -> Child2.
// Injects in HitTest mode, all events delivered to Child1 and Child2.
// Disconnects Child2 and observes loss from Child2.
TEST_F(GfxTouchIntegrationTest, HitTested_ViewDisconnectedMidContest_ShouldLoseContest) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
{
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "root_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
holder_1.SetViewProperties(k5x5x1);
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder child1_holder(root_session_->session, std::move(vh2), "child1_holder");
child1_holder.SetViewProperties(k5x5x1);
view.AddChild(child1_holder);
BlockingPresent(*root_session_->session);
}
auto [child1_control_ref, child1_view_ref] = scenic::ViewRefPair::New();
auto [child1_session, child1_touch_source] = CreateSessionWithTouchSource(scenic());
auto [v3, vh3] = scenic::ViewTokenPair::New();
scenic::View view(child1_session.get(), std::move(v2), std::move(child1_control_ref),
fidl::Clone(child1_view_ref), "child1_view");
scenic::ViewHolder child2_holder(child1_session.get(), std::move(vh3), "child2_holder");
child2_holder.SetViewProperties(k5x5x1);
view.AddChild(child2_holder);
BlockingPresent(*child1_session);
child1_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
auto [child2_control_ref, child2_view_ref] = scenic::ViewRefPair::New();
auto [child2_session, child2_touch_source] = CreateChildView(
std::move(v3), std::move(child2_control_ref), fidl::Clone(child2_view_ref), "child2_view");
child2_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child1_events;
StartWatchLoop(child1_touch_source, child1_events);
std::vector<TouchEvent> child2_events;
StartWatchLoop(child2_touch_source, child2_events);
// Send in the first events of a stream.
RegisterInjector(std::move(root_view_ref), std::move(child1_view_ref),
DispatchPolicy::TOP_HIT_AND_ANCESTORS_IN_TARGET);
Inject(0, 0, fupi_EventPhase::ADD);
Inject(5, 0, fupi_EventPhase::CHANGE);
RunLoopUntil([&child2_events] { return child2_events.size() == 2u; }); // Succeeds or times out.
EXPECT_EQ(child2_events.size(), 2u);
// Detach view Child2 from the scene graph.
view.DetachChild(child2_holder);
BlockingPresent(*child1_session);
// Next event should deliver cause child 2 to lose the contest and child 1 to win.
Inject(5, 5, fupi_EventPhase::CHANGE);
Inject(0, 5, fupi_EventPhase::CHANGE);
RunLoopUntil([&child2_events] { return child2_events.size() == 3u; }); // Succeeds or times out.
RunLoopUntil([&child1_events] { return child1_events.size() == 5u; }); // Succeeds or times out.
ASSERT_EQ(child2_events.size(), 3u);
ASSERT_TRUE(child2_events.back().has_interaction_result());
EXPECT_EQ(child2_events.back().interaction_result().status, TouchInteractionStatus::DENIED);
EXPECT_EQ(child1_events.size(), 5u);
EXPECT_TRUE(std::any_of(child1_events.begin(), child1_events.end(), [](const TouchEvent& event) {
return event.has_interaction_result() &&
event.interaction_result().status == TouchInteractionStatus::GRANTED;
}));
}
// Sets up a scene with three views: Root -> Child1 -> Child2.
// Injects in HitTest mode, all events delivered to Child1 and Child2.
// Child2 wins the contest.
// Disconnects Child2 and observes cancel event delivered to Child2.
TEST_F(GfxTouchIntegrationTest, HitTested_ViewDisconnectedAfterWinning_ShouldCancelStream) {
auto [v1, vh1] = scenic::ViewTokenPair::New();
auto [v2, vh2] = scenic::ViewTokenPair::New();
auto [root_control_ref, root_view_ref] = scenic::ViewRefPair::New();
{
scenic::View view(root_session_->session, std::move(v1), std::move(root_control_ref),
fidl::Clone(root_view_ref), "root_view");
scenic::ViewHolder holder_1(root_session_->session, std::move(vh1), "holder_1");
holder_1.SetViewProperties(k5x5x1);
root_session_->scene.AddChild(holder_1);
scenic::ViewHolder child1_holder(root_session_->session, std::move(vh2), "child1_holder");
child1_holder.SetViewProperties(k5x5x1);
view.AddChild(child1_holder);
BlockingPresent(*root_session_->session);
}
auto [child1_control_ref, child1_view_ref] = scenic::ViewRefPair::New();
auto [child1_session, child1_touch_source] = CreateSessionWithTouchSource(scenic());
auto [v3, vh3] = scenic::ViewTokenPair::New();
scenic::View view(child1_session.get(), std::move(v2), std::move(child1_control_ref),
fidl::Clone(child1_view_ref), "child1_view");
scenic::ViewHolder child2_holder(child1_session.get(), std::move(vh3), "child2_holder");
child2_holder.SetViewProperties(k5x5x1);
view.AddChild(child2_holder);
BlockingPresent(*child1_session);
child1_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
auto [child2_control_ref, child2_view_ref] = scenic::ViewRefPair::New();
auto [child2_session, child2_touch_source] = CreateChildView(
std::move(v3), std::move(child2_control_ref), fidl::Clone(child2_view_ref), "child2_view");
child2_touch_source.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Touch source closed with status: " << zx_status_get_string(status);
});
std::vector<TouchEvent> child1_events;
StartWatchLoop(child1_touch_source, child1_events, TouchResponseType::NO);
std::vector<TouchEvent> child2_events;
StartWatchLoop(child2_touch_source, child2_events);
// Send in the first events of a stream.
RegisterInjector(std::move(root_view_ref), std::move(child1_view_ref),
DispatchPolicy::TOP_HIT_AND_ANCESTORS_IN_TARGET);
Inject(0, 0, fupi_EventPhase::ADD);
Inject(5, 0, fupi_EventPhase::CHANGE);
// Child2 should win the contest.
RunLoopUntil([&child2_events] { return child2_events.size() >= 3u; }); // Succeeds or times out.
ASSERT_EQ(child2_events.size(), 3u);
EXPECT_TRUE(std::any_of(child2_events.begin(), child2_events.end(), [](const TouchEvent& event) {
return event.has_interaction_result() &&
event.interaction_result().status == TouchInteractionStatus::GRANTED;
}));
// Detach view Child2 from the scene graph.
view.DetachChild(child2_holder);
BlockingPresent(*child1_session);
// Next event should deliver CANCEL to Child2.
Inject(5, 5, fupi_EventPhase::CHANGE);
RunLoopUntil([&child2_events] { return child2_events.size() >= 4u; }); // Succeeds or times out.
ASSERT_EQ(child2_events.size(), 4u);
ASSERT_TRUE(child2_events.back().has_pointer_sample());
ASSERT_TRUE(child2_events.back().pointer_sample().has_phase());
EXPECT_EQ(child2_events.back().pointer_sample().phase(), EventPhase::CANCEL);
// Future injections should be ignored.
child1_events.clear();
child2_events.clear();
Inject(0, 5, fupi_EventPhase::CHANGE);
EXPECT_TRUE(child1_events.empty());
EXPECT_TRUE(child2_events.empty());
}
} // namespace integration_tests