[ui] Coerce DOWN pointer to inside the view. (flutter/engine#27632)

Floating point error can cause edge pointer coordinates to fall
slightly outside of the view's logical bounding box, causing a failure
of gesture recognition in Dart code. This patch coerces the DOWN pointer
to inside the logical view boundary.

https://github.com/flutter/flutter/issues/86882
This commit is contained in:
Jaeheon Yi 2021-07-22 14:15:38 -07:00 committed by GitHub
parent d39722bd71
commit 0821f23495
3 changed files with 218 additions and 24 deletions

View File

@ -9,7 +9,9 @@
#include <fuchsia/ui/gfx/cpp/fidl.h>
#include <algorithm>
#include <cstring>
#include <limits>
#include <sstream>
#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<float, float> new_view_size = {
const std::array<float, 2> 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<float, float> logical_size = *view_logical_size_;
const std::array<float, 2> 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<float, 2> 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<float>::epsilon();
const float max_y_inclusive = max_y - std::numeric_limits<float>::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<uint64_t>(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);

View File

@ -14,6 +14,7 @@
#include <lib/sys/cpp/service_directory.h>
#include <lib/ui/scenic/cpp/id.h>
#include <array>
#include <map>
#include <set>
#include <unordered_map>
@ -181,20 +182,24 @@ class PlatformView final : public flutter::PlatformView,
OnShaderWarmup on_shader_warmup,
std::unique_ptr<flutter::PlatformMessage> message);
// Utility function for coordinate massaging.
std::array<float, 2> 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<FocusDelegate> 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<std::pair<float, float>> view_logical_size_;
std::optional<std::array<float, 2>> view_logical_size_;
std::optional<std::array<float, 2>> view_logical_origin_;
std::optional<float> view_pixel_ratio_;
fidl::Binding<fuchsia::ui::scenic::SessionListener> session_listener_binding_;

View File

@ -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<flutter::PointerDataPacket> packet) {}
std::unique_ptr<flutter::PointerDataPacket> packet) {
pointer_packets_.push_back(std::move(packet));
}
// |flutter::PlatformView::Delegate|
void OnPlatformViewDispatchKeyDataPacket(
std::unique_ptr<flutter::KeyDataPacket> 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<std::unique_ptr<flutter::PointerDataPacket>>&
pointer_packets() const {
return pointer_packets_;
}
std::vector<std::unique_ptr<flutter::PointerDataPacket>>
TakePointerDataPackets() {
auto tmp = std::move(pointer_packets_);
pointer_packets_.clear();
return tmp;
}
private:
std::unique_ptr<flutter::Surface> surface_;
std::unique_ptr<flutter::PlatformMessage> message_;
flutter::ViewportMetrics metrics_;
std::vector<std::unique_ptr<flutter::PointerDataPacket>> 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<flutter::PointerData>& output, // NOLINT
std::unique_ptr<flutter::PointerDataPacket> 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<fuchsia::ui::scenic::Event> 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<std::unique_ptr<flutter::PointerDataPacket>> packets =
delegate.TakePointerDataPackets();
std::vector<flutter::PointerData> 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