blob: 915b804862cc79e30682d099cbd76fafd3a6cf62 [file] [log] [blame]
// 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