| // Copyright 2022 The Flutter 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/embedder/mouse_delegate.h" |
| |
| #include <lib/syslog/global.h> |
| #include <lib/trace/event.h> |
| #include <zircon/rights.h> |
| #include <zircon/status.h> |
| #include <zircon/types.h> |
| |
| #include "src/embedder/fuchsia_logger.h" |
| #include "src/embedder/pointer_utility.h" |
| |
| namespace embedder { |
| namespace { |
| |
| /// Checks the validity of a MouseEvent's pointer sample |
| /// and returns if one exists |
| bool HasValidatedMouseSample(const fup::MouseEvent& event) { |
| if (!event.has_pointer_sample()) { |
| return false; |
| } |
| const auto& sample = event.pointer_sample(); |
| FX_DCHECK(sample.has_device_id()); |
| FX_DCHECK(sample.has_position_in_viewport()); |
| FX_DCHECK(!sample.has_pressed_buttons() || !sample.pressed_buttons().empty()); |
| |
| return true; |
| } |
| |
| /// Computes the phase of the pointer event based on the previous state. |
| /// See FlutterPointerPhase definition for information on the phases and |
| /// how they are computed |
| FlutterPointerPhase ComputePhase(bool any_button_down, std::unordered_set<uint32_t>& mouse_down, |
| uint32_t id) { |
| if (!mouse_down.count(id) && !any_button_down) { |
| return FlutterPointerPhase::kHover; |
| } else if (!mouse_down.count(id) && any_button_down) { |
| mouse_down.insert(id); |
| return FlutterPointerPhase::kDown; |
| } else if (mouse_down.count(id) && any_button_down) { |
| return FlutterPointerPhase::kMove; |
| } else if (mouse_down.count(id) && !any_button_down) { |
| mouse_down.erase(id); |
| return FlutterPointerPhase::kUp; |
| } |
| return FlutterPointerPhase::kCancel; |
| } |
| |
| /// It returns a "draft" because the coordinates are logical. Later, view pixel |
| /// ratio is applied to obtain physical coordinates. |
| /// |
| /// Phase data is computed before this call; it involves state tracking based on |
| /// button-down state. |
| /// |
| /// Button data, if available, gets packed into the |buttons| field, in flutter |
| /// button order (kMousePrimaryButton, etc). The device-assigned button IDs are |
| /// provided in priority order in MouseEvent.device_info (at the start of channel |
| /// connection), and maps from device button ID (given in fup::MouseEvent) to |
| /// flutter button ID (flutter::PointerData). |
| /// |
| /// Scroll data, if available, gets packed into the |scroll_delta_x| or |
| /// |scroll_delta_y| fields, and the |signal_kind| field is set to kScroll. |
| /// The PointerDataPacketConverter reads this field to synthesize events to match |
| /// Flutter's expected pointer stream. |
| /// TODO(fxbug.dev/87073): PointerDataPacketConverter should synthesize a |
| /// discrete scroll event on kDown or kUp, to match engine expectations. |
| /// |
| /// Flutter gestures expect a gesture to start within the logical view space, and |
| /// is not tolerant of floating point drift. This function coerces just the DOWN |
| /// event's coordinate to start within the logical view. |
| FlutterPointerEvent CreateMouseDraft(const fup::MouseEvent& event, const FlutterPointerPhase phase, |
| const fup::ViewParameters& view_parameters, |
| const fup::MouseDeviceInfo& device_info) { |
| FX_DCHECK(HasValidatedMouseSample(event)); |
| const auto& sample = event.pointer_sample(); |
| |
| FlutterPointerEvent ptr; |
| pointer_utility::ResetFlutterPointerEvent(ptr); |
| ptr.timestamp = pointer_utility::ConvertEventTimeToMicroseconds(event.timestamp()); |
| ptr.phase = phase; |
| ptr.device_kind = FlutterPointerDeviceKind::kFlutterPointerDeviceKindMouse; |
| ptr.device = sample.device_id(); |
| |
| // View parameters can change mid-interaction; apply transform on the fly. |
| auto logical = pointer_utility::ViewportToViewCoordinates( |
| sample.position_in_viewport(), view_parameters.viewport_to_view_transform); |
| ptr.x = logical[0]; // Not yet physical; adjusted in PlatformView. |
| ptr.y = logical[1]; // Not yet physical; adjusted in PlatformView. |
| |
| // Ensure gesture recognition: DOWN starts in the logical view space. |
| if (ptr.phase == FlutterPointerPhase::kDown) { |
| auto [x, y] = pointer_utility::ClampToViewSpace(ptr.x, ptr.y, view_parameters); |
| ptr.x = x; |
| ptr.y = y; |
| } |
| |
| if (sample.has_pressed_buttons()) { |
| int64_t flutter_buttons = 0; |
| const auto& pressed = sample.pressed_buttons(); |
| for (size_t idx = 0; idx < pressed.size(); ++idx) { |
| const uint8_t button_id = pressed[idx]; |
| FX_DCHECK(device_info.has_buttons()); |
| // Priority 0 maps to kPrimaryButton, and so on. |
| for (uint8_t prio = 0; prio < device_info.buttons().size(); ++prio) { |
| if (button_id == device_info.buttons()[prio]) { |
| flutter_buttons |= (1 << prio); |
| } |
| } |
| } |
| FX_DCHECK(flutter_buttons != 0); |
| ptr.buttons = flutter_buttons; |
| } |
| // Fuchsia previously only provided scroll data in "ticks", not physical |
| // pixels. On legacy platforms, since Flutter expects scroll data in physical |
| // pixels, to compensate for lack of guidance, we make up a "reasonable |
| // amount". |
| // TODO(fxbug.dev/103443): Remove the tick based scrolling after the |
| // transition. |
| const int kScrollOffsetMultiplier = 20; |
| |
| double dy = 0; |
| double dx = 0; |
| bool is_scroll = false; |
| |
| if (sample.has_scroll_v_physical_pixel()) { |
| dy = -sample.scroll_v_physical_pixel(); |
| is_scroll = true; |
| } else if (sample.has_scroll_v()) { |
| dy = -sample.scroll_v() * kScrollOffsetMultiplier; // logical amount, not yet physical; |
| // adjusted in Platform View. |
| is_scroll = true; |
| } |
| |
| if (sample.has_scroll_h_physical_pixel()) { |
| dx = sample.scroll_h_physical_pixel(); |
| is_scroll = true; |
| } else if (sample.has_scroll_h()) { |
| dx = sample.scroll_h() * kScrollOffsetMultiplier; // logical amount |
| is_scroll = true; |
| } |
| |
| if (is_scroll) { |
| ptr.signal_kind = FlutterPointerSignalKind::kFlutterPointerSignalKindScroll; |
| ptr.scroll_delta_y = dy; |
| ptr.scroll_delta_x = dx; |
| } |
| |
| return ptr; |
| } |
| } // namespace |
| |
| MouseDelegate::MouseDelegate(fuchsia::ui::pointer::MouseSourceHandle mouse_source) |
| : mouse_source_(mouse_source.Bind()) { |
| if (mouse_source_) { |
| mouse_source_.set_error_handler([](zx_status_t status) { |
| FX_LOGF(ERROR, kLogTag, "MouseSource channel error: %s", zx_status_get_string(status)); |
| }); |
| } |
| } |
| |
| // Core logic of this class. |
| // Aim to keep state management in this function. |
| void MouseDelegate::WatchLoop(std::function<void(std::vector<FlutterPointerEvent>)> callback) { |
| if (mouse_responder_) { |
| FX_LOG(ERROR, embedder::kLogTag, "MouseDelegate WatchLoop must only be called once"); |
| return; |
| } |
| |
| mouse_responder_ = [this, callback](std::vector<fup::MouseEvent> events) { |
| std::vector<FlutterPointerEvent> to_client; |
| for (fup::MouseEvent& event : events) { |
| pointer_utility::IssueInputTraceEvent(event); |
| if (event.has_device_info()) { |
| const auto& id = event.device_info().id(); |
| mouse_device_info_[id] = std::move(*event.mutable_device_info()); |
| } |
| if (event.has_view_parameters()) { |
| mouse_view_parameters_ = std::move(event.view_parameters()); |
| } |
| if (HasValidatedMouseSample(event)) { |
| const auto& sample = event.pointer_sample(); |
| const auto& id = sample.device_id(); |
| const bool any_button_down = sample.has_pressed_buttons(); |
| FX_DCHECK(mouse_view_parameters_.has_value()); |
| FX_DCHECK(mouse_device_info_.count(id) > 0); |
| |
| const auto phase = ComputePhase(any_button_down, mouse_down_, id); |
| FlutterPointerEvent data = |
| CreateMouseDraft(event, phase, mouse_view_parameters_.value(), mouse_device_info_[id]); |
| to_client.emplace_back(std::move(data)); |
| } |
| } |
| callback(std::move(to_client)); |
| mouse_source_->Watch(/*copy*/ mouse_responder_); |
| }; |
| if (mouse_source_) { |
| mouse_source_->Watch(/*copy*/ mouse_responder_); |
| } |
| } |
| |
| } // namespace embedder |