diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc index 0a0de41da2b..0a1240541fb 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc @@ -9,7 +9,9 @@ #include +#include #include +#include #include #include "flutter/fml/logging.h" @@ -272,13 +274,13 @@ void PlatformView::OnScenicEvent( case fuchsia::ui::gfx::Event::Tag::kViewPropertiesChanged: { const fuchsia::ui::gfx::BoundingBox& bounding_box = event.gfx().view_properties_changed().properties.bounding_box; - const std::pair new_view_size = { + const std::array new_view_size = { std::max(bounding_box.max.x - bounding_box.min.x, 0.0f), std::max(bounding_box.max.y - bounding_box.min.y, 0.0f)}; - if (new_view_size.first <= 0.f || new_view_size.second <= 0.f) { + if (new_view_size[0] <= 0.f || new_view_size[1] <= 0.f) { FML_DLOG(ERROR) << "Got an invalid view size from Scenic; ignoring: " - << new_view_size.first << " " << new_view_size.second; + << new_view_size[0] << " " << new_view_size[1]; break; } @@ -288,11 +290,12 @@ void PlatformView::OnScenicEvent( *view_logical_size_ == new_view_size) { FML_DLOG(ERROR) << "Got an identical view size from Scenic; ignoring: " - << new_view_size.first << " " << new_view_size.second; + << new_view_size[0] << " " << new_view_size[1]; break; } view_logical_size_ = new_view_size; + view_logical_origin_ = {bounding_box.min.x, bounding_box.min.y}; metrics_changed = true; break; } @@ -398,21 +401,21 @@ void PlatformView::OnScenicEvent( if (view_pixel_ratio_.has_value() && view_logical_size_.has_value() && metrics_changed) { const float pixel_ratio = *view_pixel_ratio_; - const std::pair logical_size = *view_logical_size_; + const std::array logical_size = *view_logical_size_; SetViewportMetrics({ - pixel_ratio, // device_pixel_ratio - logical_size.first * pixel_ratio, // physical_width - logical_size.second * pixel_ratio, // physical_height - 0.0f, // physical_padding_top - 0.0f, // physical_padding_right - 0.0f, // physical_padding_bottom - 0.0f, // physical_padding_left - 0.0f, // physical_view_inset_top - 0.0f, // physical_view_inset_right - 0.0f, // physical_view_inset_bottom - 0.0f, // physical_view_inset_left - 0.0f, // p_physical_system_gesture_inset_top - 0.0f, // p_physical_system_gesture_inset_right + pixel_ratio, // device_pixel_ratio + logical_size[0] * pixel_ratio, // physical_width + logical_size[1] * pixel_ratio, // physical_height + 0.0f, // physical_padding_top + 0.0f, // physical_padding_right + 0.0f, // physical_padding_bottom + 0.0f, // physical_padding_left + 0.0f, // physical_view_inset_top + 0.0f, // physical_view_inset_right + 0.0f, // physical_view_inset_bottom + 0.0f, // physical_view_inset_left + 0.0f, // p_physical_system_gesture_inset_top + 0.0f, // p_physical_system_gesture_inset_right 0.0f, // p_physical_system_gesture_inset_bottom 0.0f, // p_physical_system_gesture_inset_left }); @@ -538,6 +541,35 @@ static trace_flow_id_t PointerTraceHACK(float fa, float fb) { return (((uint64_t)ia) << 32) | ib; } +// For certain scenarios that must avoid floating-point drift, compute a +// coordinate that falls within the logical view bounding box. +std::array PlatformView::ClampToViewSpace(const float x, + const float y) const { + if (!view_logical_size_.has_value() || !view_logical_origin_.has_value()) { + return {x, y}; // If we can't do anything, return the original values. + } + + const auto origin = view_logical_origin_.value(); + const auto size = view_logical_size_.value(); + const float min_x = origin[0]; + const float max_x = origin[0] + size[0]; + const float min_y = origin[1]; + const float max_y = origin[1] + size[1]; + if (min_x <= x && x < max_x && min_y <= y && y < max_y) { + return {x, y}; // No clamping to perform. + } + + // View boundary is [min_x, max_x) x [min_y, max_y). Note that min is + // inclusive, but max is exclusive - so we subtract epsilon. + const float max_x_inclusive = max_x - std::numeric_limits::epsilon(); + const float max_y_inclusive = max_y - std::numeric_limits::epsilon(); + const float& clamped_x = std::clamp(x, min_x, max_x_inclusive); + const float& clamped_y = std::clamp(y, min_y, max_y_inclusive); + FML_LOG(INFO) << "Clamped (" << x << ", " << y << ") to (" << clamped_x + << ", " << clamped_y << ")."; + return {clamped_x, clamped_y}; +} + bool PlatformView::OnHandlePointerEvent( const fuchsia::ui::input::PointerEvent& pointer) { TRACE_EVENT0("flutter", "PlatformView::OnHandlePointerEvent"); @@ -563,9 +595,15 @@ bool PlatformView::OnHandlePointerEvent( pointer_data.buttons = static_cast(pointer.buttons); switch (pointer_data.change) { - case flutter::PointerData::Change::kDown: + case flutter::PointerData::Change::kDown: { + // Make the pointer start in the view space, despite numerical drift. + auto clamped_pointer = ClampToViewSpace(pointer.x, pointer.y); + pointer_data.physical_x = clamped_pointer[0] * pixel_ratio; + pointer_data.physical_y = clamped_pointer[1] * pixel_ratio; + down_pointers_.insert(pointer_data.device); break; + } case flutter::PointerData::Change::kCancel: case flutter::PointerData::Change::kUp: down_pointers_.erase(pointer_data.device); diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h index f0b3b5a8dca..75d1f3ae1d0 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -181,20 +182,24 @@ class PlatformView final : public flutter::PlatformView, OnShaderWarmup on_shader_warmup, std::unique_ptr message); + // Utility function for coordinate massaging. + std::array ClampToViewSpace(const float x, const float y) const; + const std::string debug_label_; // TODO(MI4-2490): remove once ViewRefControl is passed to Scenic and kept // alive there const fuchsia::ui::views::ViewRef view_ref_; std::shared_ptr focus_delegate_; - // Logical size and logical->physical ratio. These are optional to provide - // an "unset" state during program startup, before Scenic has sent any - // metrics-related events to provide initial values for these. + // Logical size and origin, and logical->physical ratio. These are optional + // to provide an "unset" state during program startup, before Scenic has sent + // any metrics-related events to provide initial values for these. // // The engine internally uses a default size of (0.f 0.f) with a default 1.f // ratio, so there is no need to emit events until Scenic has actually sent a // valid size and ratio. - std::optional> view_logical_size_; + std::optional> view_logical_size_; + std::optional> view_logical_origin_; std::optional view_pixel_ratio_; fidl::Binding session_listener_binding_; diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view_unittest.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view_unittest.cc index 17abc72195d..31e95ed20e8 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view_unittest.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view_unittest.cc @@ -19,6 +19,7 @@ #include "flutter/flow/embedded_views.h" #include "flutter/lib/ui/window/platform_message.h" +#include "flutter/lib/ui/window/pointer_data.h" #include "flutter/lib/ui/window/viewport_metrics.h" #include "flutter/shell/common/context_options.h" #include "flutter/shell/platform/fuchsia/flutter/platform_view.h" @@ -71,6 +72,7 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { metrics_ = flutter::ViewportMetrics{}; semantics_features_ = 0; semantics_enabled_ = false; + pointer_packets_.clear(); } // |flutter::PlatformView::Delegate| @@ -95,7 +97,9 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { } // |flutter::PlatformView::Delegate| void OnPlatformViewDispatchPointerDataPacket( - std::unique_ptr packet) {} + std::unique_ptr packet) { + pointer_packets_.push_back(std::move(packet)); + } // |flutter::PlatformView::Delegate| void OnPlatformViewDispatchKeyDataPacket( std::unique_ptr packet, @@ -143,11 +147,22 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { const flutter::ViewportMetrics& metrics() const { return metrics_; } int32_t semantics_features() const { return semantics_features_; } bool semantics_enabled() const { return semantics_enabled_; } + const std::vector>& + pointer_packets() const { + return pointer_packets_; + } + std::vector> + TakePointerDataPackets() { + auto tmp = std::move(pointer_packets_); + pointer_packets_.clear(); + return tmp; + } private: std::unique_ptr surface_; std::unique_ptr message_; flutter::ViewportMetrics metrics_; + std::vector> pointer_packets_; int32_t semantics_features_ = 0; bool semantics_enabled_ = false; }; @@ -297,6 +312,23 @@ class PlatformViewBuilder { fml::TimeDelta vsync_offset_{fml::TimeDelta::Zero()}; }; +// Stolen from pointer_data_packet_converter_unittests.cc. +void UnpackPointerPacket(std::vector& output, // NOLINT + std::unique_ptr packet) { + size_t kBytesPerPointerData = + flutter::kPointerDataFieldCount * flutter::kBytesPerField; + auto buffer = packet->data(); + size_t buffer_length = buffer.size(); + + for (size_t i = 0; i < buffer_length / kBytesPerPointerData; i++) { + flutter::PointerData pointer_data; + memcpy(&pointer_data, &buffer[i * kBytesPerPointerData], + sizeof(flutter::PointerData)); + output.push_back(pointer_data); + } + packet.reset(); +} + } // namespace class PlatformViewTests : public ::testing::Test { @@ -1344,4 +1376,123 @@ TEST_F(PlatformViewTests, OnShaderWarmup) { EXPECT_EQ(expected_result_string, response->result_string); } +TEST_F(PlatformViewTests, DownPointerNumericNudge) { + using ScenicEvent = fuchsia::ui::scenic::Event; + using GfxEvent = fuchsia::ui::gfx::Event; + using GfxViewPropertiesChangedEvent = + fuchsia::ui::gfx::ViewPropertiesChangedEvent; + using GfxViewProperties = fuchsia::ui::gfx::ViewProperties; + using GfxBoundingBox = fuchsia::ui::gfx::BoundingBox; + using GfxVec3 = fuchsia::ui::gfx::vec3; + using fuchsia::ui::input::InputEvent; + using fuchsia::ui::input::PointerEvent; + + const float kSmallDiscrepancy = -0.00003f; + + fuchsia::ui::scenic::SessionListenerPtr session_listener; + sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners("test_runners", nullptr, nullptr, nullptr, + nullptr); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetSessionListenerRequest(session_listener.NewRequest()) + .Build(); + RunLoopUntilIdle(); + EXPECT_EQ(delegate.pointer_packets().size(), 0u); + + std::vector events; + events.emplace_back(ScenicEvent::WithGfx( + GfxEvent::WithViewPropertiesChanged(GfxViewPropertiesChangedEvent{ + .view_id = 0, + .properties = + GfxViewProperties{ + .bounding_box = + GfxBoundingBox{ + .min = + GfxVec3{ + .x = 0.f, + .y = 0.f, + .z = 0.f, + }, + .max = + GfxVec3{ + .x = 100.f, + .y = 100.f, + .z = 100.f, + }, + }, + }, + }))); + events.emplace_back( + ScenicEvent::WithGfx(GfxEvent::WithMetrics(fuchsia::ui::gfx::MetricsEvent{ + .node_id = 0, + .metrics = + fuchsia::ui::gfx::Metrics{ + .scale_x = 1.f, + .scale_y = 1.f, + .scale_z = 1.f, + }, + }))); + events.emplace_back( + ScenicEvent::WithInput(InputEvent::WithPointer(PointerEvent{ + .event_time = 1111, + .device_id = 2222, + .pointer_id = 3333, + .type = fuchsia::ui::input::PointerEventType::TOUCH, + .phase = fuchsia::ui::input::PointerEventPhase::ADD, + .x = 50.f, + .y = kSmallDiscrepancy, // floating point inaccuracy + .radius_major = 0.f, + .radius_minor = 0.f, + .buttons = 0u, + }))); + events.emplace_back( + ScenicEvent::WithInput(InputEvent::WithPointer(PointerEvent{ + .event_time = 1111, + .device_id = 2222, + .pointer_id = 3333, + .type = fuchsia::ui::input::PointerEventType::TOUCH, + .phase = fuchsia::ui::input::PointerEventPhase::DOWN, + .x = 50.f, + .y = kSmallDiscrepancy, // floating point inaccuracy + .radius_major = 0.f, + .radius_minor = 0.f, + .buttons = 0u, + }))); + events.emplace_back( + ScenicEvent::WithInput(InputEvent::WithPointer(PointerEvent{ + .event_time = 1111, + .device_id = 2222, + .pointer_id = 3333, + .type = fuchsia::ui::input::PointerEventType::TOUCH, + .phase = fuchsia::ui::input::PointerEventPhase::MOVE, + .x = 50.f, + .y = kSmallDiscrepancy, // floating point inaccuracy + .radius_major = 0.f, + .radius_minor = 0.f, + .buttons = 0u, + }))); + session_listener->OnScenicEvent(std::move(events)); + RunLoopUntilIdle(); + ASSERT_EQ(delegate.pointer_packets().size(), 3u); + + // Embedder issues pointer data in a bytestream format, PointerDataPacket. + // Use this handy utility to recover data as a C struct, PointerData. + std::vector> packets = + delegate.TakePointerDataPackets(); + std::vector add, down, move; + UnpackPointerPacket(add, std::move(packets[0])); + UnpackPointerPacket(down, std::move(packets[1])); + UnpackPointerPacket(move, std::move(packets[2])); + + EXPECT_EQ(add[0].physical_x, 50.f); + EXPECT_EQ(add[0].physical_y, kSmallDiscrepancy); + EXPECT_EQ(down[0].physical_x, 50.f); + EXPECT_EQ(down[0].physical_y, 0.f); // clamping happened + EXPECT_EQ(move[0].physical_x, 50.f); + EXPECT_EQ(move[0].physical_y, kSmallDiscrepancy); +} + } // namespace flutter_runner::testing