blob: 68aa5e073ff0e54df719c855dddad149a7c403c0 [file] [log] [blame]
// 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 "src/ui/scenic/lib/input/input_system.h"
#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <lib/fostr/fidl/fuchsia/ui/input/formatting.h>
#include <lib/syslog/cpp/macros.h>
#include <zircon/status.h>
#include "src/ui/lib/glm_workaround/glm_workaround.h"
#include "src/ui/scenic/lib/gfx/resources/compositor/layer.h"
#include "src/ui/scenic/lib/gfx/resources/compositor/layer_stack.h"
#include "src/ui/scenic/lib/input/helper.h"
#include "src/ui/scenic/lib/input/internal_pointer_event.h"
#include "src/ui/scenic/lib/utils/helpers.h"
namespace scenic_impl {
namespace input {
using AccessibilityPointerEvent = fuchsia::ui::input::accessibility::PointerEvent;
using FocusChangeStatus = scenic_impl::gfx::ViewTree::FocusChangeStatus;
using fuchsia::ui::input::InputEvent;
using fuchsia::ui::input::PointerEvent;
using fuchsia::ui::input::PointerEventType;
namespace {
uint64_t NextTraceId() {
static uint64_t next_trace_id = 1;
return next_trace_id++;
}
// Creates a hit ray at z = -1000, pointing in the z-direction.
escher::ray4 CreateZRay(glm::vec2 coords) {
return {
// Origin as homogeneous point.
.origin = {coords.x, coords.y, -1000, 1},
.direction = {0, 0, 1, 0},
};
}
bool IsOutsideViewport(const Viewport& viewport, const glm::vec2& position_in_viewport) {
FX_DCHECK(!std::isunordered(position_in_viewport.x, viewport.extents.min.x) &&
!std::isunordered(position_in_viewport.x, viewport.extents.max.x) &&
!std::isunordered(position_in_viewport.y, viewport.extents.min.y) &&
!std::isunordered(position_in_viewport.y, viewport.extents.max.y));
return position_in_viewport.x < viewport.extents.min.x ||
position_in_viewport.y < viewport.extents.min.y ||
position_in_viewport.x > viewport.extents.max.x ||
position_in_viewport.y > viewport.extents.max.y;
}
// Helper function to build an AccessibilityPointerEvent when there is a
// registered accessibility listener.
AccessibilityPointerEvent BuildAccessibilityPointerEvent(const InternalPointerEvent& internal_event,
const glm::vec2& ndc_point,
const glm::vec2& local_point,
uint64_t viewref_koid) {
AccessibilityPointerEvent event;
event.set_event_time(internal_event.timestamp);
event.set_device_id(internal_event.device_id);
event.set_pointer_id(internal_event.pointer_id);
event.set_type(fuchsia::ui::input::PointerEventType::TOUCH);
event.set_phase(InternalPhaseToGfxPhase(internal_event.phase));
event.set_ndc_point({ndc_point.x, ndc_point.y});
event.set_viewref_koid(viewref_koid);
if (viewref_koid != ZX_KOID_INVALID) {
event.set_local_point({local_point.x, local_point.y});
}
return event;
}
bool IsDescendantAndConnected(const gfx::ViewTree& view_tree, zx_koid_t descendant_koid,
zx_koid_t ancestor_koid) {
if (!view_tree.IsTracked(descendant_koid) || !view_tree.IsTracked(ancestor_koid))
return false;
return view_tree.IsDescendant(descendant_koid, ancestor_koid) &&
view_tree.IsConnectedToScene(ancestor_koid);
}
std::optional<glm::mat4> GetWorldFromViewTransform(zx_koid_t view_ref_koid,
const gfx::ViewTree& view_tree) {
return view_tree.GlobalTransformOf(view_ref_koid);
}
std::optional<glm::mat4> GetViewFromWorldTransform(zx_koid_t view_ref_koid,
const gfx::ViewTree& view_tree) {
const auto world_from_view_transform = GetWorldFromViewTransform(view_ref_koid, view_tree);
if (!world_from_view_transform)
return std::nullopt;
const glm::mat4 view_from_world_transform = glm::inverse(world_from_view_transform.value());
return view_from_world_transform;
}
std::optional<glm::mat4> GetDestinationViewFromSourceViewTransform(zx_koid_t source,
zx_koid_t destination,
const gfx::ViewTree& view_tree) {
std::optional<glm::mat4> world_from_source_transform =
GetWorldFromViewTransform(source, view_tree);
if (!world_from_source_transform)
return std::nullopt;
std::optional<glm::mat4> destination_from_world_transform =
GetViewFromWorldTransform(destination, view_tree);
if (!destination_from_world_transform)
return std::nullopt;
return destination_from_world_transform.value() * world_from_source_transform.value();
}
escher::ray4 CreateWorldSpaceRay(const InternalPointerEvent& event,
const gfx::ViewTree& view_tree) {
const std::optional<glm::mat4> world_from_context_transform =
GetWorldFromViewTransform(event.context, view_tree);
FX_DCHECK(world_from_context_transform)
<< "Failed to create world space ray. Either the |event.context| ViewRef is invalid, we're "
"out of sync with the ViewTree, or the ViewTree callback returned std::nullopt.";
const glm::mat4 world_from_viewport_transform =
world_from_context_transform.value() * event.viewport.context_from_viewport_transform;
const escher::ray4 viewport_space_ray = CreateZRay(event.position_in_viewport);
return world_from_viewport_transform * viewport_space_ray;
}
// Takes an InternalPointerEvent and returns a point in (Vulkan) Normalized Device Coordinates,
// in relation to the viewport. Intended for magnification
// TODO(fxbug.dev/50549): Only here to allow the legacy a11y flow. Remove along with the legacy a11y
// code.
glm::vec2 GetViewportNDCPoint(const InternalPointerEvent& internal_event) {
const float width = internal_event.viewport.extents.max.x - internal_event.viewport.extents.min.x;
const float height =
internal_event.viewport.extents.max.y - internal_event.viewport.extents.min.y;
return {
width > 0 ? 2.f * internal_event.position_in_viewport.x / width - 1 : 0,
height > 0 ? 2.f * internal_event.position_in_viewport.y / height - 1 : 0,
};
}
} // namespace
const char* InputSystem::kName = "InputSystem";
InputSystem::InputSystem(SystemContext context, fxl::WeakPtr<gfx::SceneGraph> scene_graph)
: System(std::move(context)), scene_graph_(scene_graph) {
FX_CHECK(scene_graph);
pointer_event_registry_ = std::make_unique<A11yPointerEventRegistry>(
this->context(),
/*on_register=*/
[this] {
FX_CHECK(!pointer_event_buffer_)
<< "on_disconnect must be called before registering a new listener";
// In case a11y is turned on mid execution make sure to send active pointer event streams to
// their final location and do not send them to the a11y listener.
pointer_event_buffer_ = std::make_unique<PointerEventBuffer>(
/* DispatchEventFunction */
[this](PointerEventBuffer::DeferredPointerEvent views_and_event) {
DispatchDeferredPointerEvent(std::move(views_and_event));
},
/* ReportAccessibilityEventFunction */
[this](fuchsia::ui::input::accessibility::PointerEvent pointer) {
accessibility_pointer_event_listener()->OnEvent(std::move(pointer));
});
FX_LOGS(INFO) << "PointerEventBuffer created";
for (const auto& kv : touch_targets_) {
// Force a reject in all active pointer IDs. When a new stream arrives,
// they will automatically be sent for the a11y listener decide
// what to do with them as the status will change to WAITING_RESPONSE.
pointer_event_buffer_->SetActiveStreamInfo(
/*pointer_id=*/kv.first, PointerEventBuffer::PointerIdStreamStatus::REJECTED);
}
// Registers an event handler for this listener. This callback captures a pointer to the
// event buffer that we own, so we need to clear it before we destroy it (see below).
accessibility_pointer_event_listener().events().OnStreamHandled =
[buffer = pointer_event_buffer_.get()](
uint32_t device_id, uint32_t pointer_id,
fuchsia::ui::input::accessibility::EventHandling handled) {
buffer->UpdateStream(pointer_id, handled);
};
},
/*on_disconnect=*/
[this] {
FX_CHECK(pointer_event_buffer_) << "can not disconnect before registering";
// The listener disconnected. Release held events, delete the buffer.
accessibility_pointer_event_listener().events().OnStreamHandled = nullptr;
pointer_event_buffer_.reset();
FX_LOGS(INFO) << "PointerEventBuffer destroyed";
});
ime_service_ = this->context()->app_context()->svc()->Connect<fuchsia::ui::input::ImeService>();
ime_service_.set_error_handler(
[](zx_status_t status) { FX_LOGS(WARNING) << "Scenic lost connection to TextSync"; });
this->context()->app_context()->outgoing()->AddPublicService(injector_registry_.GetHandler(this));
this->context()->app_context()->outgoing()->AddPublicService(
pointer_capture_registry_.GetHandler(this));
FX_LOGS(INFO) << "Scenic input system initialized.";
}
CommandDispatcherUniquePtr InputSystem::CreateCommandDispatcher(
scheduling::SessionId session_id, std::shared_ptr<EventReporter> event_reporter,
std::shared_ptr<ErrorReporter> error_reporter) {
return CommandDispatcherUniquePtr(
new InputCommandDispatcher(session_id, std::move(event_reporter), scene_graph_, this),
// Custom deleter.
[](CommandDispatcher* cd) { delete cd; });
}
A11yPointerEventRegistry::A11yPointerEventRegistry(SystemContext* context,
fit::function<void()> on_register,
fit::function<void()> on_disconnect)
: on_register_(std::move(on_register)), on_disconnect_(std::move(on_disconnect)) {
FX_DCHECK(on_register_);
FX_DCHECK(on_disconnect_);
context->app_context()->outgoing()->AddPublicService(
accessibility_pointer_event_registry_.GetHandler(this));
}
void A11yPointerEventRegistry::Register(
fidl::InterfaceHandle<fuchsia::ui::input::accessibility::PointerEventListener>
pointer_event_listener,
RegisterCallback callback) {
if (!accessibility_pointer_event_listener()) {
accessibility_pointer_event_listener_.Bind(std::move(pointer_event_listener));
accessibility_pointer_event_listener_.set_error_handler(
[this](zx_status_t) { on_disconnect_(); });
on_register_();
callback(/*success=*/true);
} else {
// An accessibility listener is already registered.
callback(/*success=*/false);
}
}
void InputSystem::Register(fuchsia::ui::pointerinjector::Config config,
fidl::InterfaceRequest<fuchsia::ui::pointerinjector::Device> injector,
RegisterCallback callback) {
if (!Injector::IsValidConfig(config)) {
// Errors printed inside IsValidConfig. Just return here.
return;
}
// Check connectivity here, since injector doesn't have access to it.
const zx_koid_t context_koid = utils::ExtractKoid(config.context().view());
const zx_koid_t target_koid = utils::ExtractKoid(config.target().view());
if (context_koid == ZX_KOID_INVALID || target_koid == ZX_KOID_INVALID) {
FX_LOGS(ERROR) << "InjectorRegistry::Register : Argument |config.context| or |config.target| "
"was invalid.";
return;
}
if (!IsDescendantAndConnected(scene_graph_->view_tree(), target_koid, context_koid)) {
FX_LOGS(ERROR) << "InjectorRegistry::Register : Argument |config.context| must be connected to "
"the Scene, and |config.target| must be a descendant of |config.context|";
return;
}
// TODO(fxbug.dev/50348): Add a callback to kill the channel immediately if connectivity breaks.
const InjectorId id = ++last_injector_id_;
InjectorSettings settings{.dispatch_policy = config.dispatch_policy(),
.device_id = config.device_id(),
.device_type = config.device_type(),
.context_koid = context_koid,
.target_koid = target_koid};
Viewport viewport{
.extents = {config.viewport().extents()},
.context_from_viewport_transform =
ColumnMajorMat3VectorToMat4(config.viewport().viewport_to_context_transform()),
};
fit::function<void(const InternalPointerEvent&, StreamId)> inject_func;
switch (settings.dispatch_policy) {
case fuchsia::ui::pointerinjector::DispatchPolicy::EXCLUSIVE_TARGET:
inject_func = [this](const InternalPointerEvent& event, StreamId stream_id) {
InjectTouchEventExclusive(event);
};
break;
case fuchsia::ui::pointerinjector::DispatchPolicy::TOP_HIT_AND_ANCESTORS_IN_TARGET:
inject_func = [this](const InternalPointerEvent& event, StreamId stream_id) {
InjectTouchEventHitTested(event, stream_id, /*parallel_dispatch*/ false);
};
break;
default:
FX_CHECK(false) << "Should never be reached.";
break;
}
const auto [it, success] = injectors_.try_emplace(
id, std::move(settings), std::move(viewport), std::move(injector),
/*is_descendant_and_connected*/
[this](zx_koid_t descendant, zx_koid_t ancestor) {
return IsDescendantAndConnected(scene_graph_->view_tree(), descendant, ancestor);
},
std::move(inject_func),
/*on_channel_closed*/
[this, id] { injectors_.erase(id); });
FX_CHECK(success) << "Injector already exists.";
callback();
}
void InputSystem::RegisterListener(
fidl::InterfaceHandle<fuchsia::ui::input::PointerCaptureListener> listener_handle,
fuchsia::ui::views::ViewRef view_ref, RegisterListenerCallback success_callback) {
if (pointer_capture_listener_) {
// Already have a listener, decline registration.
success_callback(false);
return;
}
fuchsia::ui::input::PointerCaptureListenerPtr new_listener;
new_listener.Bind(std::move(listener_handle));
// Remove listener if the interface closes.
new_listener.set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Pointer capture listener interface closed with error: "
<< zx_status_get_string(status);
pointer_capture_listener_ = std::nullopt;
});
pointer_capture_listener_ = PointerCaptureListener{.listener_ptr = std::move(new_listener),
.view_ref = std::move(view_ref)};
success_callback(true);
}
void InputSystem::HitTest(const gfx::ViewTree& view_tree, const InternalPointerEvent& event,
gfx::HitAccumulator<gfx::ViewHit>& accumulator,
bool semantic_hit_test) const {
if (IsOutsideViewport(event.viewport, event.position_in_viewport)) {
return;
}
escher::ray4 world_ray = CreateWorldSpaceRay(event, view_tree);
view_tree.HitTestFrom(event.target, world_ray, &accumulator, semantic_hit_test);
}
void InputSystem::DispatchPointerCommand(const fuchsia::ui::input::SendPointerInputCmd& command,
scheduling::SessionId session_id, bool parallel_dispatch) {
TRACE_DURATION("input", "dispatch_command", "command", "PointerCmd");
if (command.pointer_event.phase == fuchsia::ui::input::PointerEventPhase::HOVER) {
FX_LOGS(WARNING) << "Injected pointer event had unexpected HOVER event.";
return;
}
if (!scene_graph_) {
FX_LOGS(INFO) << "SceneGraph wasn't set up before injecting legacy input. Dropping event.";
return;
}
// Compositor and layer stack required for dispatch.
const GlobalId compositor_id(session_id, command.compositor_id);
gfx::CompositorWeakPtr compositor = scene_graph_->GetCompositor(compositor_id);
if (!compositor) {
FX_LOGS(INFO) << "Compositor wasn't set up before injecting legacy input. Dropping event.";
return; // It's legal to race against GFX's compositor setup.
}
gfx::LayerStackPtr layer_stack = compositor->layer_stack();
if (!layer_stack) {
FX_LOGS(INFO) << "Layer stack wasn't set up before injecting legacy input. Dropping event.";
return; // It's legal to race against GFX's layer stack setup.
}
const auto layers = layer_stack->layers();
if (layers.empty()) {
FX_LOGS(INFO) << "Layer wasn't set up before injecting legacy input. Dropping event.";
return;
}
const gfx::ViewTree& view_tree = scene_graph_->view_tree();
// Assume we only have one layer.
const gfx::LayerPtr first_layer = *layers.begin();
const std::optional<glm::mat4> world_from_screen_transform =
first_layer->GetWorldFromScreenTransform();
if (!world_from_screen_transform) {
FX_LOGS(INFO) << "Wasn't able to get a WorldFromScreenTransform when injecting legacy input. "
"Dropping event. Is the camera or renderer uninitialized?";
return;
}
const zx_koid_t scene_koid = first_layer->scene()->view_ref_koid();
const std::optional<glm::mat4> context_from_world_transform =
GetViewFromWorldTransform(scene_koid, view_tree);
FX_DCHECK(context_from_world_transform);
const uint32_t screen_width = first_layer->width();
const uint32_t screen_height = first_layer->height();
if (screen_width == 0 || screen_height == 0) {
FX_LOGS(WARNING) << "Attempted to inject legacy input while Layer had 0 area";
return;
}
const glm::mat4 context_from_screen_transform =
context_from_world_transform.value() * world_from_screen_transform.value();
InternalPointerEvent internal_event =
GfxPointerEventToInternalEvent(command.pointer_event, scene_koid, screen_width, screen_height,
context_from_screen_transform);
switch (command.pointer_event.type) {
case PointerEventType::TOUCH: {
// Get stream id. Create one if this is a new stream.
const uint64_t stream_key =
((uint64_t)internal_event.device_id << 32) | (uint64_t)internal_event.pointer_id;
if (!gfx_legacy_streams_.count(stream_key)) {
if (internal_event.phase != Phase::ADD) {
FX_LOGS(WARNING) << "Attempted to start a stream without an initial ADD.";
return;
}
gfx_legacy_streams_.emplace(stream_key, NewStreamId());
} else if (internal_event.phase == Phase::ADD) {
FX_LOGS(WARNING) << "Attempted to ADD twice for the same stream.";
return;
}
const auto stream_id = gfx_legacy_streams_[stream_key];
// Remove from ongoing streams on stream end.
if (internal_event.phase == Phase::REMOVE || internal_event.phase == Phase::CANCEL) {
gfx_legacy_streams_.erase(stream_key);
}
TRACE_DURATION("input", "dispatch_command", "command", "TouchCmd");
TRACE_FLOW_END(
"input", "dispatch_event_to_scenic",
PointerTraceHACK(command.pointer_event.radius_major, command.pointer_event.radius_minor));
InjectTouchEventHitTested(internal_event, stream_id, parallel_dispatch);
break;
}
case PointerEventType::MOUSE: {
TRACE_DURATION("input", "dispatch_command", "command", "MouseCmd");
if (internal_event.phase == Phase::ADD || internal_event.phase == Phase::REMOVE) {
FX_LOGS(WARNING) << "Oops, mouse device (id=" << internal_event.device_id
<< ") had an unexpected event: " << internal_event.phase;
return;
}
InjectMouseEventHitTested(internal_event);
break;
}
default:
FX_LOGS(INFO) << "Stylus not supported by legacy input injection API.";
break;
}
}
void InputSystem::InjectTouchEventExclusive(const InternalPointerEvent& event) {
if (!scene_graph_)
return;
ReportPointerEventToView(event, event.target, fuchsia::ui::input::PointerEventType::TOUCH,
scene_graph_->view_tree());
}
// The touch state machine comprises ADD/DOWN/MOVE*/UP/REMOVE. Some notes:
// - We assume one touchscreen device, and use the device-assigned finger ID.
// - Touch ADD associates the following ADD/DOWN/MOVE*/UP/REMOVE event sequence
// with the set of clients available at that time. To enable gesture
// disambiguation, we perform parallel dispatch to all clients.
// - Touch DOWN triggers a focus change, honoring the "may receive focus" property.
// - Touch REMOVE drops the association between event stream and client.
void InputSystem::InjectTouchEventHitTested(const InternalPointerEvent& event, StreamId stream_id,
bool parallel_dispatch) {
FX_DCHECK(scene_graph_);
const gfx::ViewTree& view_tree = scene_graph_->view_tree();
const uint32_t pointer_id = event.pointer_id;
const Phase pointer_phase = event.phase;
// The a11y listener is only enabled if the root view is the context. This will later be handled
// implicitly by scene graph structure when gesture disambiguation is implemented.
// TODO(fxbug.dev/52134): Remove when gesture disambiguation makes it obsolete.
const bool a11y_enabled =
IsA11yListenerEnabled() && IsOwnedByRootSession(view_tree, event.context);
if (pointer_phase == Phase::ADD) {
gfx::ViewHitAccumulator accumulator;
HitTest(view_tree, event, accumulator, /*semantic_hit_test*/ false);
const auto& hits = accumulator.hits();
// Find input targets. Honor the "input masking" view property.
std::vector<zx_koid_t> hit_views;
for (const gfx::ViewHit& hit : hits) {
hit_views.push_back(hit.view_ref_koid);
}
FX_VLOGS(1) << "View hits: ";
for (auto view_ref_koid : hit_views) {
FX_VLOGS(1) << "[ViewRefKoid=" << view_ref_koid << "]";
}
// Save targets for consistent delivery of touch events.
touch_targets_[pointer_id] = hit_views;
// If there is an accessibility pointer event listener enabled, an ADD event means that a new
// pointer id stream started. Perform it unconditionally, even if the view stack is empty.
if (a11y_enabled) {
pointer_event_buffer_->AddStream(pointer_id);
}
} else if (pointer_phase == Phase::DOWN) {
// If accessibility listener is on, focus change events must be sent only if
// the stream is rejected. This way, this operation is deferred.
if (!a11y_enabled) {
if (!touch_targets_[pointer_id].empty()) {
// Request that focus be transferred to the top view.
RequestFocusChange(touch_targets_[pointer_id].front());
} else if (focus_chain_root() != ZX_KOID_INVALID) {
// The touch event stream has no designated receiver.
// Request that focus be transferred to the root view, so that (1) the currently focused
// view becomes unfocused, and (2) the focus chain remains under control of the root view.
RequestFocusChange(focus_chain_root());
}
}
}
// Input delivery must be parallel; needed for gesture disambiguation.
std::vector<zx_koid_t> deferred_event_receivers;
for (zx_koid_t view_ref_koid : touch_targets_[pointer_id]) {
if (a11y_enabled) {
deferred_event_receivers.emplace_back(view_ref_koid);
} else {
ReportPointerEventToView(event, view_ref_koid, fuchsia::ui::input::PointerEventType::TOUCH,
view_tree);
}
if (!parallel_dispatch) {
break; // TODO(fxbug.dev/24258): Remove when gesture disambiguation is ready.
}
}
FX_DCHECK(a11y_enabled || deferred_event_receivers.empty())
<< "When a11y pointer forwarding is off, never defer events.";
if (a11y_enabled) {
// We handle both latched (!deferred_event_receivers.empty()) and unlatched
// (deferred_event_receivers.empty()) touch events, for two reasons.
//
// (1) We must notify accessibility about events regardless of latch, so that it has full
// information about a gesture stream. E.g., the gesture could start traversal in empty space
// before MOVE-ing onto a rect; accessibility needs both the gesture and the rect.
//
// (2) We must trigger a potential focus change request, even if no view receives the triggering
// DOWN event, so that (a) the focused view receives an unfocus event, and (b) the focus chain
// gets updated and dispatched accordingly.
//
// NOTE: Do not rely on the latched view stack for "top hit" information; elevation can change
// dynamically (it's only guaranteed correct for DOWN). Instead, perform an independent query
// for "top hit".
zx_koid_t view_ref_koid = ZX_KOID_INVALID;
{
// Find top-hit target and send it to accessibility.
gfx::TopHitAccumulator top_hit;
HitTest(view_tree, event, top_hit, /*semantic_hit_test*/ true);
if (top_hit.hit()) {
view_ref_koid = top_hit.hit()->view_ref_koid;
}
}
glm::vec2 top_hit_view_local;
if (view_ref_koid != ZX_KOID_INVALID) {
std::optional<glm::mat4> view_from_context = GetDestinationViewFromSourceViewTransform(
/*source*/ event.context, /*destination*/ view_ref_koid, view_tree);
FX_DCHECK(view_from_context)
<< "Failed to create world space ray. Either the |event.context| ViewRef is invalid, "
"we're out of sync with the ViewTree, or the ViewTree callback returned std::nullopt.";
const glm::mat4 view_from_viewport =
view_from_context.value() * event.viewport.context_from_viewport_transform;
top_hit_view_local = TransformPointerCoords(event.position_in_viewport, view_from_viewport);
}
const glm::vec2 ndc = GetViewportNDCPoint(event);
AccessibilityPointerEvent packet =
BuildAccessibilityPointerEvent(event, ndc, top_hit_view_local, view_ref_koid);
pointer_event_buffer_->AddEvent(
pointer_id,
{.event = std::move(event),
.parallel_event_receivers = std::move(deferred_event_receivers)},
std::move(packet));
} else {
// TODO(fxbug.dev/48150): Delete when we delete the PointerCapture functionality.
ReportPointerEventToPointerCaptureListener(event, view_tree);
}
if (pointer_phase == Phase::REMOVE || pointer_phase == Phase::CANCEL) {
touch_targets_.erase(pointer_id);
}
}
// The mouse state machine is simpler, comprising MOVE*-DOWN/MOVE*/UP-MOVE*. Its
// behavior is similar to touch events, but with some differences.
// - There can be multiple mouse devices, so we track each device individually.
// - Mouse DOWN associates the following DOWN/MOVE*/UP event sequence with one
// particular client: the top-hit View. Mouse events aren't associated with
// gestures, so there is no parallel dispatch.
// - Mouse DOWN triggers a focus change, honoring the "may receive focus" property.
// - Mouse UP drops the association between event stream and client.
// - For an unlatched MOVE event, we perform a hit test, and send the
// top-most client this MOVE event. Focus does not change for unlatched
// MOVEs.
// - The hit test must account for the mouse cursor itself, which today is
// owned by the root presenter. The nodes associated with visible mouse
// cursors(!) do not roll up to any View (as expected), but may appear in the
// hit test; our dispatch needs to account for such behavior.
// TODO(fxbug.dev/24288): Enhance trackpad support.
void InputSystem::InjectMouseEventHitTested(const InternalPointerEvent& event) {
FX_DCHECK(scene_graph_);
const gfx::ViewTree& view_tree = scene_graph_->view_tree();
const uint32_t device_id = event.device_id;
const Phase pointer_phase = event.phase;
if (pointer_phase == Phase::DOWN) {
// Find top-hit target and associated properties.
// NOTE: We may hit various mouse cursors (owned by root presenter), but |TopHitAccumulator|
// will keep going until we find a hit with a valid owning View.
gfx::TopHitAccumulator top_hit;
HitTest(view_tree, event, top_hit, /*semantic_hit_test*/ false);
std::vector</*view_ref_koids*/ zx_koid_t> hit_views;
if (top_hit.hit()) {
hit_views.push_back(top_hit.hit()->view_ref_koid);
}
FX_VLOGS(1) << "View hits: ";
for (auto view_ref_koid : hit_views) {
FX_VLOGS(1) << "[ViewRefKoid=" << view_ref_koid << "]";
}
if (!hit_views.empty()) {
// Request that focus be transferred to the top view.
RequestFocusChange(hit_views.front());
} else if (focus_chain_root() != ZX_KOID_INVALID) {
// The mouse event stream has no designated receiver.
// Request that focus be transferred to the root view, so that (1) the currently focused
// view becomes unfocused, and (2) the focus chain remains under control of the root view.
RequestFocusChange(focus_chain_root());
}
// Save target for consistent delivery of mouse events.
mouse_targets_[device_id] = hit_views;
}
if (mouse_targets_.count(device_id) > 0 && // Tracking this device, and
mouse_targets_[device_id].size() > 0) { // target view exists.
const zx_koid_t top_view_koid = mouse_targets_[device_id].front();
ReportPointerEventToView(event, top_view_koid, fuchsia::ui::input::PointerEventType::MOUSE,
view_tree);
}
if (pointer_phase == Phase::UP || pointer_phase == Phase::CANCEL) {
mouse_targets_.erase(device_id);
}
// Deal with unlatched MOVE events.
if (pointer_phase == Phase::CHANGE && mouse_targets_.count(device_id) == 0) {
// Find top-hit target and send it this move event.
// NOTE: We may hit various mouse cursors (owned by root presenter), but |TopHitAccumulator|
// will keep going until we find a hit with a valid owning View.
gfx::TopHitAccumulator top_hit;
HitTest(view_tree, event, top_hit, /*semantic_hit_test*/ false);
if (top_hit.hit()) {
const zx_koid_t top_view_koid = top_hit.hit()->view_ref_koid;
ReportPointerEventToView(event, top_view_koid, fuchsia::ui::input::PointerEventType::MOUSE,
view_tree);
}
}
}
void InputSystem::DispatchDeferredPointerEvent(
PointerEventBuffer::DeferredPointerEvent views_and_event) {
if (!scene_graph_)
return;
// If this parallel dispatch of events corresponds to a DOWN event, this
// triggers a possible deferred focus change event.
if (views_and_event.event.phase == Phase::DOWN) {
if (!views_and_event.parallel_event_receivers.empty()) {
// Request that focus be transferred to the top view.
const zx_koid_t view_koid = views_and_event.parallel_event_receivers.front();
FX_DCHECK(view_koid != ZX_KOID_INVALID) << "invariant";
RequestFocusChange(view_koid);
} else if (focus_chain_root() != ZX_KOID_INVALID) {
// The touch event stream has no designated receiver.
// Request that focus be transferred to the root view, so that (1) the currently focused
// view becomes unfocused, and (2) the focus chain remains under control of the root view.
RequestFocusChange(focus_chain_root());
}
}
const gfx::ViewTree& view_tree = scene_graph_->view_tree();
for (zx_koid_t view_ref_koid : views_and_event.parallel_event_receivers) {
ReportPointerEventToView(views_and_event.event, view_ref_koid,
fuchsia::ui::input::PointerEventType::TOUCH, view_tree);
}
{ // TODO(fxbug.dev/48150): Delete when we delete the PointerCapture functionality.
ReportPointerEventToPointerCaptureListener(views_and_event.event, view_tree);
}
}
zx_koid_t InputSystem::focus() const {
if (!scene_graph_)
return ZX_KOID_INVALID; // No scene graph, no view tree, no focus chain.
const auto& chain = scene_graph_->view_tree().focus_chain();
if (chain.empty())
return ZX_KOID_INVALID; // Scene not present, or scene not connected to compositor.
const zx_koid_t focused_view = chain.back();
FX_DCHECK(focused_view != ZX_KOID_INVALID) << "invariant";
return focused_view;
}
zx_koid_t InputSystem::focus_chain_root() const {
if (!scene_graph_)
return ZX_KOID_INVALID; // No scene graph, no view tree, no focus chain.
const auto& chain = scene_graph_->view_tree().focus_chain();
if (chain.empty())
return ZX_KOID_INVALID; // Scene not present, or scene not connected to compositor.
const zx_koid_t root_view = chain.front();
FX_DCHECK(root_view != ZX_KOID_INVALID) << "invariant";
return root_view;
}
void InputSystem::RequestFocusChange(zx_koid_t view) {
FX_DCHECK(view != ZX_KOID_INVALID) << "precondition";
if (!scene_graph_)
return; // No scene graph, no view tree, no focus chain.
if (scene_graph_->view_tree().focus_chain().empty())
return; // Scene not present, or scene not connected to compositor.
// Input system acts on authority of top-most view.
const zx_koid_t requestor = scene_graph_->view_tree().focus_chain().front();
auto status = scene_graph_->RequestFocusChange(requestor, view);
FX_VLOGS(1) << "Scenic RequestFocusChange. Authority: " << requestor << ", request: " << view
<< ", status: " << static_cast<int>(status);
FX_DCHECK(status == FocusChangeStatus::kAccept ||
status == FocusChangeStatus::kErrorRequestCannotReceiveFocus)
<< "User has authority to request focus change, but the only valid rejection is when the "
"requested view may not receive focus. Error code: "
<< static_cast<int>(status);
}
bool InputSystem::IsOwnedByRootSession(const gfx::ViewTree& view_tree, zx_koid_t koid) const {
const zx_koid_t root_koid = focus_chain_root();
return root_koid != ZX_KOID_INVALID &&
view_tree.SessionIdOf(koid) == view_tree.SessionIdOf(root_koid);
}
// TODO(fxbug.dev/48150): Delete when we delete the PointerCapture functionality.
void InputSystem::ReportPointerEventToPointerCaptureListener(const InternalPointerEvent& event,
const gfx::ViewTree& view_tree) const {
if (!pointer_capture_listener_)
return;
const PointerCaptureListener& listener = pointer_capture_listener_.value();
const zx_koid_t view_ref_koid = utils::ExtractKoid(listener.view_ref);
std::optional<glm::mat4> view_from_context_transform = GetDestinationViewFromSourceViewTransform(
/*source*/ event.context, /*destination*/ view_ref_koid, view_tree);
if (!view_from_context_transform)
return;
fuchsia::ui::input::PointerEvent gfx_event = InternalPointerEventToGfxPointerEvent(
event, view_from_context_transform.value(), fuchsia::ui::input::PointerEventType::TOUCH,
/*trace_id*/ 0);
// TODO(fxbug.dev/42145): Implement flow control.
listener.listener_ptr->OnPointerEvent(gfx_event, [] {});
}
void InputSystem::ReportPointerEventToView(const InternalPointerEvent& event,
zx_koid_t view_ref_koid,
fuchsia::ui::input::PointerEventType type,
const gfx::ViewTree& view_tree) const {
TRACE_DURATION("input", "dispatch_event_to_client", "event_type", "pointer");
EventReporterWeakPtr event_reporter = view_tree.EventReporterOf(view_ref_koid);
if (!event_reporter)
return;
std::optional<glm::mat4> view_from_context_transform = GetDestinationViewFromSourceViewTransform(
/*source*/ event.context, /*destination*/ view_ref_koid, view_tree);
if (!view_from_context_transform)
return;
const uint64_t trace_id = NextTraceId();
TRACE_FLOW_BEGIN("input", "dispatch_event_to_client", trace_id);
InputEvent input_event;
input_event.set_pointer(InternalPointerEventToGfxPointerEvent(
event, view_from_context_transform.value(), type, trace_id));
FX_VLOGS(1) << "Event dispatch to view=" << view_ref_koid << ": " << input_event;
event_reporter->EnqueueEvent(std::move(input_event));
}
} // namespace input
} // namespace scenic_impl