| // 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/accessibility/formatting.h> |
| #include <lib/fostr/fidl/fuchsia/ui/input/formatting.h> |
| #include <lib/fostr/fidl/fuchsia/ui/pointerinjector/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/constants.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 fuchsia::ui::input::InputEvent; |
| using fuchsia::ui::input::PointerEvent; |
| using fuchsia::ui::input::PointerEventType; |
| |
| namespace { |
| |
| 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; |
| } |
| |
| // 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, |
| }; |
| } |
| |
| void ChattyGfxLog(const fuchsia::ui::input::InputEvent& event) { |
| static uint32_t chatty = 0; |
| if (chatty++ < ChattyMax()) { |
| FX_LOGS(INFO) << "Ptr-GFX[" << chatty << "/" << ChattyMax() << "]: " << event; |
| } |
| } |
| |
| void ChattyCaptureLog(const fuchsia::ui::input::PointerEvent& event) { |
| static uint32_t chatty = 0; |
| if (chatty++ < ChattyMax()) { |
| FX_LOGS(INFO) << "Ptr-Capture[" << chatty << "/" << ChattyMax() << "]: " << event; |
| } |
| } |
| |
| void ChattyA11yLog(const fuchsia::ui::input::accessibility::PointerEvent& event) { |
| static uint32_t chatty = 0; |
| if (chatty++ < ChattyMax()) { |
| FX_LOGS(INFO) << "Ptr-A11y[" << chatty << "/" << ChattyMax() << "]: " << event; |
| } |
| } |
| |
| } // namespace |
| |
| const char* InputSystem::kName = "InputSystem"; |
| |
| InputSystem::InputSystem(SystemContext context, fxl::WeakPtr<gfx::SceneGraph> scene_graph, |
| fit::function<void(zx_koid_t)> request_focus) |
| : System(std::move(context)), |
| scene_graph_(scene_graph), |
| request_focus_(std::move(request_focus)) { |
| FX_CHECK(scene_graph); |
| |
| a11y_pointer_event_registry_ = std::make_unique<A11yPointerEventRegistry>( |
| this->context()->app_context(), |
| /*on_register=*/ |
| [this] { |
| FX_CHECK(!a11y_legacy_contender_) |
| << "on_disconnect must be called before registering a new listener"; |
| |
| a11y_legacy_contender_ = std::make_unique<A11yLegacyContender>( |
| /*respond*/ |
| [this](StreamId stream_id, GestureResponse response) { |
| RecordGestureDisambiguationResponse(stream_id, a11y_contender_id_, {response}); |
| }, |
| /*deliver_to_client*/ |
| [this](const InternalPointerEvent& event) { |
| auto a11y_event = CreateAccessibilityEvent(event); |
| ChattyA11yLog(a11y_event); |
| accessibility_pointer_event_listener()->OnEvent(std::move(a11y_event)); |
| }); |
| FX_LOGS(INFO) << "A11yLegacyContender created."; |
| contenders_.emplace(a11y_contender_id_, a11y_legacy_contender_.get()); |
| |
| accessibility_pointer_event_listener().events().OnStreamHandled = |
| [this](uint32_t device_id, uint32_t pointer_id, |
| fuchsia::ui::input::accessibility::EventHandling handled) { |
| FX_DCHECK(a11y_legacy_contender_); |
| a11y_legacy_contender_->OnStreamHandled(pointer_id, handled); |
| }; |
| }, |
| /*on_disconnect=*/ |
| [this] { |
| FX_CHECK(a11y_legacy_contender_) << "can not disconnect before registering"; |
| // The listener disconnected. Release held events, delete the buffer. |
| accessibility_pointer_event_listener().events().OnStreamHandled = nullptr; |
| contenders_.erase(a11y_contender_id_); |
| a11y_legacy_contender_.reset(); |
| FX_LOGS(INFO) << "A11yLegacyContender destroyed"; |
| }); |
| |
| pointerinjector_registry_ = std::make_unique<PointerinjectorRegistry>( |
| this->context()->app_context(), |
| /*inject_touch_exclusive*/ |
| [this](const InternalPointerEvent& event, StreamId stream_id) { |
| InjectTouchEventExclusive(event); |
| }, |
| /*inject_touch_hit_tested*/ |
| [this](const InternalPointerEvent& event, StreamId stream_id) { |
| InjectTouchEventHitTested(event, stream_id); |
| }); |
| |
| 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, this), |
| // Custom deleter. |
| [](CommandDispatcher* cd) { delete cd; }); |
| } |
| |
| fuchsia::ui::input::accessibility::PointerEvent InputSystem::CreateAccessibilityEvent( |
| const InternalPointerEvent& event) { |
| zx_koid_t view_ref_koid = ZX_KOID_INVALID; |
| { |
| // Find top-hit target and send it to accessibility. |
| const std::vector<zx_koid_t> hits = HitTest(event, /*semantic_hit_test*/ true); |
| if (!hits.empty()) { |
| view_ref_koid = hits.front(); |
| } |
| } |
| |
| 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); |
| FX_DCHECK(view_from_context) |
| << "could only happen if the view_tree_view_tree_snapshot_ was updated " |
| "between the event arriving and now"; |
| |
| 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); |
| |
| return BuildAccessibilityPointerEvent(event, ndc, top_hit_view_local, view_ref_koid); |
| } |
| |
| ContenderId InputSystem::AddGfxLegacyContender(StreamId stream_id, zx_koid_t view_ref_koid) { |
| FX_DCHECK(view_ref_koid != ZX_KOID_INVALID); |
| |
| const ContenderId contender_id = next_contender_id_++; |
| gfx_legacy_contenders_.try_emplace( |
| contender_id, |
| /*respond*/ |
| [this, stream_id, contender_id](GestureResponse response) { |
| RecordGestureDisambiguationResponse(stream_id, contender_id, {response}); |
| }, |
| /*deliver_events_to_client*/ |
| [this, view_ref_koid](const std::vector<InternalPointerEvent>& events) { |
| for (const auto& event : events) { |
| ReportPointerEventToPointerCaptureListener(event); |
| ReportPointerEventToGfxLegacyView(event, view_ref_koid, |
| fuchsia::ui::input::PointerEventType::TOUCH); |
| |
| // Update focus if necessary. |
| // TODO(fxbug.dev/59858): Figure out how to handle focus with real GD clients. |
| if (event.phase == Phase::kAdd) { |
| if (view_tree_snapshot_->view_tree.count(view_ref_koid) != 0) { |
| if (view_tree_snapshot_->view_tree.at(view_ref_koid).is_focusable) { |
| request_focus_(view_ref_koid); |
| } |
| } else { |
| // Focus root. |
| request_focus_(ZX_KOID_INVALID); |
| } |
| } |
| } |
| }, |
| /*self_destruct*/ |
| [this, contender_id] { |
| contenders_.erase(contender_id); |
| gfx_legacy_contenders_.erase(contender_id); |
| }); |
| contenders_.emplace(contender_id, &gfx_legacy_contenders_.at(contender_id)); |
| return contender_id; |
| } |
| |
| 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(INFO) << "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); |
| } |
| |
| std::vector<zx_koid_t> InputSystem::HitTest(const InternalPointerEvent& event, |
| bool semantic_hit_test) const { |
| if (IsOutsideViewport(event.viewport, event.position_in_viewport)) { |
| return {}; |
| } |
| |
| const std::optional<glm::mat4> world_from_context_transform = |
| GetWorldFromViewTransform(event.context); |
| if (!world_from_context_transform) { |
| return {}; |
| } |
| |
| const auto world_from_viewport_transform = |
| world_from_context_transform.value() * event.viewport.context_from_viewport_transform; |
| const auto world_space_point = |
| TransformPointerCoords(event.position_in_viewport, world_from_viewport_transform); |
| return view_tree_snapshot_->HitTest(event.target, world_space_point, semantic_hit_test); |
| } |
| |
| void InputSystem::DispatchPointerCommand(const fuchsia::ui::input::SendPointerInputCmd& command, |
| scheduling::SessionId session_id) { |
| 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; |
| } |
| |
| // 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 root_koid = view_tree_snapshot_->root; |
| if (root_koid == ZX_KOID_INVALID) { |
| FX_LOGS(WARNING) << "Attempted to inject legacy input before scene setup"; |
| return; |
| } |
| |
| const std::optional<glm::mat4> context_from_world_transform = |
| GetViewFromWorldTransform(root_koid); |
| 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, root_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 std::pair<uint32_t, uint32_t> stream_key{internal_event.device_id, |
| internal_event.pointer_id}; |
| if (!gfx_legacy_streams_.count(stream_key)) { |
| if (internal_event.phase != Phase::kAdd) { |
| 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::kAdd) { |
| 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::kRemove || internal_event.phase == Phase::kCancel) { |
| 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); |
| break; |
| } |
| case PointerEventType::MOUSE: { |
| TRACE_DURATION("input", "dispatch_command", "command", "MouseCmd"); |
| if (internal_event.phase == Phase::kAdd || internal_event.phase == Phase::kRemove) { |
| 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) { |
| ReportPointerEventToGfxLegacyView(event, event.target, |
| fuchsia::ui::input::PointerEventType::TOUCH); |
| } |
| |
| // 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) { |
| // New stream. Collect contenders and set up a new arena. |
| if (event.phase == Phase::kAdd) { |
| std::vector<ContenderId> contenders = CollectContenders(stream_id, event); |
| if (!contenders.empty()) { |
| gesture_arenas_.emplace(stream_id, GestureArena{std::move(contenders)}); |
| } else { |
| // No node was hit. Transfer focus to root. |
| request_focus_(ZX_KOID_INVALID); |
| } |
| } |
| |
| // No arena means the contest is over and no one won. |
| if (!gesture_arenas_.count(stream_id)) { |
| return; |
| } |
| |
| UpdateGestureContest(event, stream_id); |
| } |
| |
| static bool IsRootOrDirectChildOfRoot(zx_koid_t koid, const view_tree::Snapshot& snapshot) { |
| if (snapshot.root == koid) { |
| return true; |
| } |
| if (snapshot.view_tree.count(koid) == 0) { |
| return false; |
| } |
| |
| return snapshot.view_tree.at(koid).parent == snapshot.root; |
| } |
| |
| std::vector<ContenderId> InputSystem::CollectContenders(StreamId stream_id, |
| const InternalPointerEvent& event) { |
| FX_DCHECK(event.phase == Phase::kAdd); |
| std::vector<ContenderId> contenders; |
| |
| // Add an A11yLegacyContender if the injection context is the root of the ViewTree. |
| // TODO(fxbug.dev/50549): Remove when a11y is a native GD client. |
| if (a11y_legacy_contender_ && IsRootOrDirectChildOfRoot(event.context, *view_tree_snapshot_)) { |
| contenders.push_back(a11y_contender_id_); |
| } |
| |
| { // Add a GfxLegacyContender based on the closest hit. |
| // TODO(fxbug.dev/64206): Remove when we no longer have any legacy clients. |
| const std::vector<zx_koid_t> hits = HitTest(event, /*semantic_hit_test*/ false); |
| if (!hits.empty()) { |
| const zx_koid_t hit_view_koid = hits.front(); |
| FX_VLOGS(1) << "View hit: [ViewRefKoid=" << hit_view_koid << "]"; |
| |
| const ContenderId contender_id = AddGfxLegacyContender(stream_id, hit_view_koid); |
| contenders.push_back(contender_id); |
| } |
| } |
| return contenders; |
| } |
| |
| void InputSystem::UpdateGestureContest(const InternalPointerEvent& event, StreamId stream_id) { |
| auto arena_it = gesture_arenas_.find(stream_id); |
| if (arena_it == gesture_arenas_.end()) |
| return; // Contest already ended, with no winner. |
| auto& arena = arena_it->second; |
| |
| const bool is_end_of_stream = event.phase == Phase::kRemove || event.phase == Phase::kCancel; |
| arena.UpdateStream(/*length*/ 1, is_end_of_stream); |
| |
| // Update remaining contenders. |
| // Copy the vector to avoid problems if the arena is destroyed inside of UpdateStream(). |
| const std::vector<ContenderId> contenders = arena.contenders(); |
| for (const auto contender_id : contenders) { |
| auto contender_it = contenders_.find(contender_id); |
| if (contender_it != contenders_.end()) { |
| contender_it->second->UpdateStream(stream_id, event, is_end_of_stream); |
| } |
| } |
| |
| DestroyArenaIfComplete(stream_id); |
| } |
| |
| void InputSystem::RecordGestureDisambiguationResponse( |
| StreamId stream_id, ContenderId contender_id, const std::vector<GestureResponse>& responses) { |
| auto arena_it = gesture_arenas_.find(stream_id); |
| if (arena_it == gesture_arenas_.end() || !arena_it->second.contains(contender_id)) { |
| FX_LOGS(ERROR) << "Failed to record GestureResponse"; |
| return; |
| } |
| auto& arena = arena_it->second; |
| |
| // No need to record after the contest has ended. |
| if (!arena.contest_has_ended()) { |
| // Update the arena. |
| const ContestResults result = arena.RecordResponse(contender_id, responses); |
| for (auto loser_id : result.losers) { |
| auto contender = contenders_.find(loser_id); |
| if (contender != contenders_.end()) { |
| contenders_.at(loser_id)->EndContest(stream_id, /*awarded_win*/ false); |
| } |
| } |
| if (result.winner) { |
| auto contender_it = contenders_.find(result.winner.value()); |
| if (contender_it != contenders_.end()) { |
| contender_it->second->EndContest(stream_id, /*awarded_win*/ true); |
| } |
| FX_DCHECK(arena.contenders().size() == 1u); |
| } |
| } |
| |
| DestroyArenaIfComplete(stream_id); |
| } |
| |
| void InputSystem::DestroyArenaIfComplete(StreamId stream_id) { |
| const auto arena_it = gesture_arenas_.find(stream_id); |
| if (arena_it == gesture_arenas_.end()) { |
| return; |
| } |
| |
| const auto& arena = arena_it->second; |
| |
| if (arena.contenders().empty()) { |
| // If no one won the contest then it will appear as if nothing was hit. Transfer focus to root. |
| // TODO(fxbug.dev/59858): This probably needs to change when we figure out the exact semantics |
| // we want. |
| request_focus_(ZX_KOID_INVALID); |
| gesture_arenas_.erase(stream_id); |
| } else if (arena.contest_has_ended() && arena.stream_has_ended()) { |
| // If both the contest and the stream is over, destroy the arena. |
| // This branch will always be reached eventually. |
| gesture_arenas_.erase(stream_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) { |
| const uint32_t device_id = event.device_id; |
| const Phase pointer_phase = event.phase; |
| |
| if (pointer_phase == Phase::kDown) { |
| // Find top-hit target and associated properties. |
| const std::vector<zx_koid_t> hit_views = HitTest(event, /*semantic_hit_test*/ false); |
| |
| 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. |
| request_focus_(hit_views.front()); |
| } else { |
| // 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. |
| request_focus_(ZX_KOID_INVALID); |
| } |
| |
| // 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(); |
| ReportPointerEventToGfxLegacyView(event, top_view_koid, |
| fuchsia::ui::input::PointerEventType::MOUSE); |
| } |
| |
| if (pointer_phase == Phase::kUp || pointer_phase == Phase::kCancel) { |
| mouse_targets_.erase(device_id); |
| } |
| |
| // Deal with unlatched MOVE events. |
| if (pointer_phase == Phase::kChange && mouse_targets_.count(device_id) == 0) { |
| // Find top-hit target and send it this move event. |
| const std::vector<zx_koid_t> hits = HitTest(event, /*semantic_hit_test*/ false); |
| if (!hits.empty()) { |
| const zx_koid_t top_view_koid = hits.front(); |
| ReportPointerEventToGfxLegacyView(event, top_view_koid, |
| fuchsia::ui::input::PointerEventType::MOUSE); |
| } |
| } |
| } |
| |
| // TODO(fxbug.dev/48150): Delete when we delete the PointerCapture functionality. |
| void InputSystem::ReportPointerEventToPointerCaptureListener( |
| const InternalPointerEvent& event) 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); |
| 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); |
| |
| ChattyCaptureLog(gfx_event); |
| |
| // TODO(fxbug.dev/42145): Implement flow control. |
| listener.listener_ptr->OnPointerEvent(gfx_event, [] {}); |
| } |
| |
| void InputSystem::ReportPointerEventToGfxLegacyView( |
| const InternalPointerEvent& event, zx_koid_t view_ref_koid, |
| fuchsia::ui::input::PointerEventType type) const { |
| TRACE_DURATION("input", "dispatch_event_to_client", "event_type", "pointer"); |
| if (!scene_graph_) |
| return; |
| |
| EventReporterWeakPtr event_reporter = scene_graph_->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); |
| if (!view_from_context_transform) |
| return; |
| |
| const uint64_t trace_id = TRACE_NONCE(); |
| 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; |
| ChattyGfxLog(input_event); |
| event_reporter->EnqueueEvent(std::move(input_event)); |
| } |
| |
| std::optional<glm::mat4> InputSystem::GetViewFromWorldTransform(zx_koid_t view_ref_koid) const { |
| if (view_tree_snapshot_->view_tree.count(view_ref_koid) == 0) { |
| return std::nullopt; |
| } |
| |
| return view_tree_snapshot_->view_tree.at(view_ref_koid).local_from_world_transform; |
| } |
| |
| std::optional<glm::mat4> InputSystem::GetWorldFromViewTransform(zx_koid_t view_ref_koid) const { |
| const std::optional<glm::mat4> view_from_world_transform = |
| GetViewFromWorldTransform(view_ref_koid); |
| if (!view_from_world_transform.has_value()) { |
| return std::nullopt; |
| } |
| |
| return glm::inverse(view_from_world_transform.value()); |
| } |
| |
| std::optional<glm::mat4> InputSystem::GetDestinationViewFromSourceViewTransform( |
| zx_koid_t source, zx_koid_t destination) const { |
| std::optional<glm::mat4> world_from_source_transform = GetWorldFromViewTransform(source); |
| std::optional<glm::mat4> destination_from_world_transform = |
| GetViewFromWorldTransform(destination); |
| |
| if (!world_from_source_transform.has_value() || !destination_from_world_transform.has_value()) { |
| return std::nullopt; |
| } |
| |
| return destination_from_world_transform.value() * world_from_source_transform.value(); |
| } |
| |
| } // namespace input |
| } // namespace scenic_impl |