[DisplayList] DlPath supports generic path dispatching (#164753)
There are different ways to iterate over an SkPath or an impeller::Path
and various points in the engine source tree we have boilerplate
duplicates of this code to transfer the contents of the DlPath wrapper
object into some platform-specific path. This PR adds a
dispatch/receiver mechanism to read back the contents of a DlPath -
independent of whether it is backed by an SkPath or an impeller::Path -
in a simpler form that avoids potential mistakes in the various
conversion methods.
See DlPathReceiver and DlPath::Dispatch in the dl_path.h file
diff --git a/engine/src/flutter/display_list/geometry/dl_path.cc b/engine/src/flutter/display_list/geometry/dl_path.cc
index 471470f..51bd8cc 100644
--- a/engine/src/flutter/display_list/geometry/dl_path.cc
+++ b/engine/src/flutter/display_list/geometry/dl_path.cc
@@ -8,6 +8,29 @@
#include "flutter/impeller/geometry/path_builder.h"
#include "impeller/geometry/path.h"
+namespace {
+inline constexpr flutter::DlPathFillType ToDlFillType(SkPathFillType sk_type) {
+ switch (sk_type) {
+ case SkPathFillType::kEvenOdd:
+ return impeller::FillType::kOdd;
+ case SkPathFillType::kWinding:
+ return impeller::FillType::kNonZero;
+ case SkPathFillType::kInverseEvenOdd:
+ case SkPathFillType::kInverseWinding:
+ FML_UNREACHABLE();
+ }
+}
+
+inline constexpr SkPathFillType ToSkFillType(flutter::DlPathFillType dl_type) {
+ switch (dl_type) {
+ case impeller::FillType::kOdd:
+ return SkPathFillType::kEvenOdd;
+ case impeller::FillType::kNonZero:
+ return SkPathFillType::kWinding;
+ }
+}
+} // namespace
+
namespace flutter {
using Path = impeller::Path;
@@ -120,6 +143,22 @@
return path.value();
}
+void DlPath::Dispatch(DlPathReceiver& receiver) const {
+ if (data_->sk_path_original) {
+ auto& sk_path = data_->sk_path;
+ FML_DCHECK(sk_path.has_value());
+ if (sk_path.has_value()) {
+ DispatchFromSkiaPath(sk_path.value(), receiver);
+ }
+ } else {
+ auto& path = data_->path;
+ FML_DCHECK(path.has_value());
+ if (path.has_value()) {
+ DispatchFromImpellerPath(path.value(), receiver);
+ }
+ }
+}
+
void DlPath::WillRenderSkPath() const {
if (data_->render_count >= kMaxVolatileUses) {
auto& sk_path = data_->sk_path;
@@ -252,26 +291,94 @@
return DlPath(path);
}
-SkPath DlPath::ConvertToSkiaPath(const Path& path, const DlPoint& shift) {
- SkPath sk_path;
- sk_path.setFillType(ToSkFillType(path.GetFillType()));
+static void ReduceConic(DlPathReceiver& receiver,
+ const DlPoint& p1,
+ const DlPoint& cp,
+ const DlPoint& p2,
+ DlScalar weight) {
+ // We might eventually have conic conversion math that deals with
+ // degenerate conics gracefully (or have all receivers just handle
+ // them directly). But, until then, we will just convert them to a
+ // pair of quads and accept the results as "close enough".
+ if (p1 != cp) {
+ if (cp != p2) {
+ std::array<DlPoint, 5> points;
+ impeller::ConicPathComponent conic(p1, cp, p2, weight);
+ conic.SubdivideToQuadraticPoints(points);
+ receiver.QuadTo(points[1], points[2]);
+ receiver.QuadTo(points[3], points[4]);
+ } else {
+ receiver.LineTo(cp);
+ }
+ } else if (cp != p2) {
+ receiver.LineTo(p2);
+ }
+}
+
+namespace {
+class SkiaPathReceiver final : public DlPathReceiver {
+ public:
+ void SetPathInfo(DlPathFillType fill_type, bool is_convex) override {
+ sk_path_.setFillType(ToSkFillType(fill_type));
+ }
+ void MoveTo(const DlPoint& p2) override { sk_path_.moveTo(ToSkPoint(p2)); }
+ void LineTo(const DlPoint& p2) override { sk_path_.lineTo(ToSkPoint(p2)); }
+ void QuadTo(const DlPoint& cp, const DlPoint& p2) override {
+ sk_path_.quadTo(ToSkPoint(cp), ToSkPoint(p2));
+ }
+ bool ConicTo(const DlPoint& cp, const DlPoint& p2, DlScalar weight) override {
+ sk_path_.conicTo(ToSkPoint(cp), ToSkPoint(p2), weight);
+ return true;
+ }
+ void CubicTo(const DlPoint& cp1,
+ const DlPoint& cp2,
+ const DlPoint& p2) override {
+ sk_path_.cubicTo(ToSkPoint(cp1), ToSkPoint(cp2), ToSkPoint(p2));
+ }
+ void Close() override { sk_path_.close(); }
+
+ SkPath TakePath() { return sk_path_; }
+
+ private:
+ SkPath sk_path_;
+};
+} // namespace
+
+SkPath DlPath::ConvertToSkiaPath(const Path& path) {
+ SkiaPathReceiver receiver;
+
+ DispatchFromImpellerPath(path, receiver);
+
+ return receiver.TakePath();
+}
+
+void DlPath::DispatchFromImpellerPath(const impeller::Path& path,
+ DlPathReceiver& receiver) {
bool subpath_needs_close = false;
std::optional<DlPoint> pending_moveto;
- auto resolve_moveto = [&pending_moveto, &sk_path]() {
+ auto resolve_moveto = [&receiver, &pending_moveto]() {
if (pending_moveto.has_value()) {
- sk_path.moveTo(ToSkPoint(pending_moveto.value()));
+ receiver.MoveTo(pending_moveto.value());
pending_moveto.reset();
}
};
+ // The Impeller Point Count is way overestimated due to duplicate
+ // points between elements.
+ receiver.RecommendSizes(path.GetComponentCount(), path.GetPointCount());
+ std::optional<DlRect> bounds = path.GetBoundingBox();
+ if (bounds.has_value()) {
+ receiver.RecommendBounds(bounds.value());
+ }
+ receiver.SetPathInfo(path.GetFillType(), path.IsConvex());
for (auto it = path.begin(), end = path.end(); it != end; ++it) {
switch (it.type()) {
case ComponentType::kContour: {
const impeller::ContourComponent* contour = it.contour();
FML_DCHECK(contour != nullptr);
if (subpath_needs_close) {
- sk_path.close();
+ receiver.Close();
}
pending_moveto = contour->destination;
subpath_needs_close = contour->IsClosed();
@@ -281,45 +388,96 @@
const impeller::LinearPathComponent* linear = it.linear();
FML_DCHECK(linear != nullptr);
resolve_moveto();
- sk_path.lineTo(ToSkPoint(linear->p2));
+ receiver.LineTo(linear->p2);
break;
}
case ComponentType::kQuadratic: {
const impeller::QuadraticPathComponent* quadratic = it.quadratic();
FML_DCHECK(quadratic != nullptr);
resolve_moveto();
- sk_path.quadTo(ToSkPoint(quadratic->cp), ToSkPoint(quadratic->p2));
+ receiver.QuadTo(quadratic->cp, quadratic->p2);
break;
}
case ComponentType::kConic: {
const impeller::ConicPathComponent* conic = it.conic();
FML_DCHECK(conic != nullptr);
resolve_moveto();
- sk_path.conicTo(ToSkPoint(conic->cp), ToSkPoint(conic->p2),
- conic->weight.x);
+ if (!receiver.ConicTo(conic->cp, conic->p2, conic->weight.x)) {
+ ReduceConic(receiver, conic->p1, conic->cp, conic->p2,
+ conic->weight.x);
+ }
break;
}
case ComponentType::kCubic: {
const impeller::CubicPathComponent* cubic = it.cubic();
FML_DCHECK(cubic != nullptr);
resolve_moveto();
- sk_path.cubicTo(ToSkPoint(cubic->cp1), ToSkPoint(cubic->cp2),
- ToSkPoint(cubic->p2));
+ receiver.CubicTo(cubic->cp1, cubic->cp2, cubic->p2);
break;
}
}
}
if (subpath_needs_close) {
- sk_path.close();
+ receiver.Close();
}
-
- return sk_path;
}
-Path DlPath::ConvertToImpellerPath(const SkPath& path, const DlPoint& shift) {
- if (path.isEmpty() || !shift.IsFinite()) {
+namespace {
+class ImpellerPathReceiver final : public DlPathReceiver {
+ public:
+ void RecommendSizes(size_t verb_count, size_t point_count) override {
+ // Reserve a path size with some arbitrarily additional padding.
+ builder_.Reserve(point_count + 8, verb_count + 8);
+ }
+ void RecommendBounds(const DlRect& bounds) override {
+ builder_.SetBounds(bounds);
+ }
+ void SetPathInfo(DlPathFillType fill_type, bool is_convex) override {
+ this->fill_type_ = fill_type;
+ builder_.SetConvexity(is_convex ? Convexity::kConvex //
+ : Convexity::kUnknown);
+ }
+ void MoveTo(const DlPoint& p2) override { builder_.MoveTo(p2); }
+ void LineTo(const DlPoint& p2) override { builder_.LineTo(p2); }
+ void QuadTo(const DlPoint& cp, const DlPoint& p2) override {
+ builder_.QuadraticCurveTo(cp, p2);
+ }
+ // For legacy compatibility we do not override ConicTo to let the dispatcher
+ // convert conics to quads until we update Impeller for full support of
+ // rational quadratics
+ void CubicTo(const DlPoint& cp1,
+ const DlPoint& cp2,
+ const DlPoint& p2) override {
+ builder_.CubicCurveTo(cp1, cp2, p2);
+ }
+ void Close() override { builder_.Close(); }
+
+ impeller::Path TakePath() { return builder_.TakePath(fill_type_); }
+
+ private:
+ PathBuilder builder_;
+ DlPathFillType fill_type_;
+};
+} // namespace
+
+Path DlPath::ConvertToImpellerPath(const SkPath& path) {
+ if (path.isEmpty()) {
return Path{};
}
+
+ ImpellerPathReceiver receiver;
+
+ DispatchFromSkiaPath(path, receiver);
+
+ return receiver.TakePath();
+}
+
+void DlPath::DispatchFromSkiaPath(const SkPath& path,
+ DlPathReceiver& receiver) {
+ if (path.isEmpty()) {
+ return;
+ }
+
auto iterator = SkPath::Iter(path, false);
struct PathData {
@@ -328,67 +486,47 @@
};
};
- PathBuilder builder;
PathData data;
- // Reserve a path size with some arbitrarily additional padding.
- builder.Reserve(path.countPoints() + 8, path.countVerbs() + 8);
+
+ receiver.RecommendSizes(path.countVerbs(), path.countPoints());
+ receiver.RecommendBounds(ToDlRect(path.getBounds()));
+ receiver.SetPathInfo(ToDlFillType(path.getFillType()), path.isConvex());
auto verb = SkPath::Verb::kDone_Verb;
do {
verb = iterator.next(data.points);
switch (verb) {
case SkPath::kMove_Verb:
- builder.MoveTo(ToDlPoint(data.points[0]));
+ receiver.MoveTo(ToDlPoint(data.points[0]));
break;
case SkPath::kLine_Verb:
- builder.LineTo(ToDlPoint(data.points[1]));
+ receiver.LineTo(ToDlPoint(data.points[1]));
break;
case SkPath::kQuad_Verb:
- builder.QuadraticCurveTo(ToDlPoint(data.points[1]),
- ToDlPoint(data.points[2]));
+ receiver.QuadTo(ToDlPoint(data.points[1]), ToDlPoint(data.points[2]));
break;
case SkPath::kConic_Verb:
- // We might eventually have conic conversion math that deals with
- // degenerate conics gracefully (or just handle them directly),
- // but until then, we will detect and ignore them.
- if (data.points[0] != data.points[1]) {
- if (data.points[1] != data.points[2]) {
- std::array<DlPoint, 5> points;
- impeller::ConicPathComponent conic(
- ToDlPoint(data.points[0]), ToDlPoint(data.points[1]),
- ToDlPoint(data.points[2]), iterator.conicWeight());
- conic.SubdivideToQuadraticPoints(points);
- builder.QuadraticCurveTo(points[1], points[2]);
- builder.QuadraticCurveTo(points[3], points[4]);
- } else {
- builder.LineTo(ToDlPoint(data.points[1]));
- }
- } else if (data.points[1] != data.points[2]) {
- builder.LineTo(ToDlPoint(data.points[2]));
+ if (!receiver.ConicTo(ToDlPoint(data.points[1]),
+ ToDlPoint(data.points[2]),
+ iterator.conicWeight())) {
+ ReduceConic(receiver, //
+ ToDlPoint(data.points[0]), //
+ ToDlPoint(data.points[1]), //
+ ToDlPoint(data.points[2]), //
+ iterator.conicWeight());
}
break;
case SkPath::kCubic_Verb:
- builder.CubicCurveTo(ToDlPoint(data.points[1]),
- ToDlPoint(data.points[2]),
- ToDlPoint(data.points[3]));
+ receiver.CubicTo(ToDlPoint(data.points[1]), //
+ ToDlPoint(data.points[2]), //
+ ToDlPoint(data.points[3]));
break;
case SkPath::kClose_Verb:
- builder.Close();
+ receiver.Close();
break;
case SkPath::kDone_Verb:
break;
}
} while (verb != SkPath::Verb::kDone_Verb);
-
- DlRect bounds = ToDlRect(path.getBounds());
- if (!shift.IsZero()) {
- builder.Shift(shift);
- bounds = bounds.Shift(shift);
- }
-
- builder.SetConvexity(path.isConvex() ? Convexity::kConvex
- : Convexity::kUnknown);
- builder.SetBounds(bounds);
- return builder.TakePath(ToDlFillType(path.getFillType()));
}
} // namespace flutter
diff --git a/engine/src/flutter/display_list/geometry/dl_path.h b/engine/src/flutter/display_list/geometry/dl_path.h
index b00011e..9dc9115 100644
--- a/engine/src/flutter/display_list/geometry/dl_path.h
+++ b/engine/src/flutter/display_list/geometry/dl_path.h
@@ -5,6 +5,8 @@
#ifndef FLUTTER_DISPLAY_LIST_GEOMETRY_DL_PATH_H_
#define FLUTTER_DISPLAY_LIST_GEOMETRY_DL_PATH_H_
+#include <functional>
+
#include "flutter/display_list/geometry/dl_geometry_types.h"
#include "flutter/impeller/geometry/path.h"
#include "flutter/impeller/geometry/path_builder.h"
@@ -15,6 +17,38 @@
using DlPathFillType = impeller::FillType;
using DlPathBuilder = impeller::PathBuilder;
+/// @brief Collection of functions to receive path segments from the
+/// underlying path representation via the DlPath::Dispatch method.
+///
+/// The conic_to function is optional. If the receiver understands rational
+/// quadratic Bezier curve forms then it should accept the curve parameters
+/// and return true, otherwise it can return false and the dispatcher will
+/// provide the path segment in a different form via the other methods.
+///
+/// The dispatcher might not call the recommend_size or recommend_bounds
+/// functions if the original path does not contain such information.
+///
+/// The dispatcher will always call the path_info function, though the
+/// is_convex parameter may be conservatively reported as false if the
+/// original path does not contain such info.
+class DlPathReceiver {
+ public:
+ virtual ~DlPathReceiver() = default;
+ virtual void RecommendSizes(size_t verb_count, size_t point_count) {};
+ virtual void RecommendBounds(const DlRect& bounds) {};
+ virtual void SetPathInfo(DlPathFillType fill_type, bool is_convex) = 0;
+ virtual void MoveTo(const DlPoint& p2) = 0;
+ virtual void LineTo(const DlPoint& p2) = 0;
+ virtual void QuadTo(const DlPoint& cp, const DlPoint& p2) = 0;
+ virtual bool ConicTo(const DlPoint& cp, const DlPoint& p2, DlScalar weight) {
+ return false;
+ };
+ virtual void CubicTo(const DlPoint& cp1,
+ const DlPoint& cp2,
+ const DlPoint& p2) = 0;
+ virtual void Close() = 0;
+};
+
class DlPath {
public:
static constexpr uint32_t kMaxVolatileUses = 2;
@@ -69,6 +103,8 @@
const SkPath& GetSkPath() const;
const impeller::Path& GetPath() const;
+ void Dispatch(DlPathReceiver& receiver) const;
+
/// Intent to render an SkPath multiple times will make the path
/// non-volatile to enable caching in Skia. Calling this method
/// before every rendering call that uses the SkPath will count
@@ -116,34 +152,17 @@
const bool sk_path_original;
};
- inline constexpr static DlPathFillType ToDlFillType(SkPathFillType sk_type) {
- switch (sk_type) {
- case SkPathFillType::kEvenOdd:
- return impeller::FillType::kOdd;
- case SkPathFillType::kWinding:
- return impeller::FillType::kNonZero;
- case SkPathFillType::kInverseEvenOdd:
- case SkPathFillType::kInverseWinding:
- FML_UNREACHABLE();
- }
- }
-
- inline constexpr static SkPathFillType ToSkFillType(DlPathFillType dl_type) {
- switch (dl_type) {
- case impeller::FillType::kOdd:
- return SkPathFillType::kEvenOdd;
- case impeller::FillType::kNonZero:
- return SkPathFillType::kWinding;
- }
- }
-
std::shared_ptr<Data> data_;
- static SkPath ConvertToSkiaPath(const impeller::Path& path,
- const DlPoint& shift = DlPoint());
+ static void DispatchFromSkiaPath(const SkPath& path,
+ DlPathReceiver& receiver);
- static impeller::Path ConvertToImpellerPath(const SkPath& path,
- const DlPoint& shift = DlPoint());
+ static void DispatchFromImpellerPath(const impeller::Path& path,
+ DlPathReceiver& receiver);
+
+ static SkPath ConvertToSkiaPath(const impeller::Path& path);
+
+ static impeller::Path ConvertToImpellerPath(const SkPath& path);
};
} // namespace flutter
diff --git a/engine/src/flutter/display_list/geometry/dl_path_unittests.cc b/engine/src/flutter/display_list/geometry/dl_path_unittests.cc
index c481897..7008e6c 100644
--- a/engine/src/flutter/display_list/geometry/dl_path_unittests.cc
+++ b/engine/src/flutter/display_list/geometry/dl_path_unittests.cc
@@ -3,6 +3,8 @@
// found in the LICENSE file.
#include "flutter/display_list/geometry/dl_path.h"
+
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "flutter/third_party/skia/include/core/SkRRect.h"
@@ -553,6 +555,337 @@
EXPECT_EQ(DlPath(path_builder, DlPathFillType::kNonZero), DlPath(sk_path));
}
+namespace {
+class DlPathReceiverMock : public DlPathReceiver {
+ public:
+ MOCK_METHOD(void,
+ RecommendSizes,
+ (size_t verb_count, size_t point_count),
+ (override));
+ MOCK_METHOD(void, RecommendBounds, (const DlRect& bounds), (override));
+ MOCK_METHOD(void,
+ SetPathInfo,
+ (DlPathFillType fill_type, bool is_convex),
+ (override));
+ MOCK_METHOD(void, MoveTo, (const DlPoint& p2), (override));
+ MOCK_METHOD(void, LineTo, (const DlPoint& p2), (override));
+ MOCK_METHOD(void, QuadTo, (const DlPoint& cp, const DlPoint& p2), (override));
+ MOCK_METHOD(bool,
+ ConicTo,
+ (const DlPoint& cp, const DlPoint& p2, DlScalar weight),
+ (override));
+ MOCK_METHOD(void,
+ CubicTo,
+ (const DlPoint& cp1, const DlPoint& cp2, const DlPoint& p2),
+ (override));
+ MOCK_METHOD(void, Close, (), (override));
+};
+
+using ::testing::AtMost;
+using ::testing::Return;
+} // namespace
+
+TEST(DisplayListPath, DispatchSkiaPathEvenOdd) {
+ SkPath path;
+
+ path.setFillType(SkPathFillType::kEvenOdd);
+ path.moveTo(100, 200);
+ path.lineTo(101, 201);
+ path.quadTo(110, 202, 102, 210);
+ path.conicTo(150, 240, 250, 140, 0.5);
+ path.cubicTo(300, 300, 350, 300, 300, 350);
+ path.close();
+
+ ::testing::StrictMock<DlPathReceiverMock> mock_receiver;
+
+ // Recommendations must happen before any of the path segments is dispatched
+ ::testing::ExpectationSet all_recommendations;
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendSizes(6u, 9u)) //
+ .Times(AtMost(1));
+ all_recommendations +=
+ EXPECT_CALL(mock_receiver,
+ RecommendBounds(DlRect::MakeLTRB(100, 140, 350, 350)))
+ .Times(AtMost(1));
+ EXPECT_CALL(mock_receiver, SetPathInfo(DlPathFillType::kOdd, false));
+
+ {
+ ::testing::InSequence sequence;
+
+ EXPECT_CALL(mock_receiver, MoveTo(DlPoint(100, 200)))
+ .After(all_recommendations);
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(101, 201)));
+ EXPECT_CALL(mock_receiver, QuadTo(DlPoint(110, 202), DlPoint(102, 210)));
+ EXPECT_CALL(mock_receiver,
+ ConicTo(DlPoint(150, 240), DlPoint(250, 140), 0.5f))
+ .WillOnce(Return(true));
+ EXPECT_CALL(mock_receiver, CubicTo(DlPoint(300, 300), DlPoint(350, 300),
+ DlPoint(300, 350)));
+ // Closing LineTo added implicitly to return to first point
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 200)));
+ EXPECT_CALL(mock_receiver, Close());
+ }
+
+ DlPath(path).Dispatch(mock_receiver);
+}
+
+TEST(DisplayListPath, DispatchSkiaPathNonZero) {
+ SkPath path;
+
+ path.setFillType(SkPathFillType::kWinding);
+ path.moveTo(100, 200);
+ path.lineTo(101, 201);
+ path.quadTo(110, 202, 102, 210);
+ path.conicTo(150, 240, 250, 140, 0.5);
+ path.cubicTo(300, 300, 350, 300, 300, 350);
+ path.close();
+
+ ::testing::StrictMock<DlPathReceiverMock> mock_receiver;
+
+ // Recommendations must happen before any of the path segments is dispatched
+ ::testing::ExpectationSet all_recommendations;
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendSizes(6u, 9u)) //
+ .Times(AtMost(1));
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver,
+ RecommendBounds(DlRect::MakeLTRB(100, 140, 350, 350)))
+ .Times(AtMost(1));
+ EXPECT_CALL(mock_receiver, SetPathInfo(DlPathFillType::kNonZero, false));
+
+ {
+ ::testing::InSequence sequence;
+
+ EXPECT_CALL(mock_receiver, MoveTo(DlPoint(100, 200)))
+ .After(all_recommendations);
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(101, 201)));
+ EXPECT_CALL(mock_receiver, QuadTo(DlPoint(110, 202), DlPoint(102, 210)));
+ EXPECT_CALL(mock_receiver,
+ ConicTo(DlPoint(150, 240), DlPoint(250, 140), 0.5f))
+ .WillOnce(Return(true));
+ EXPECT_CALL(mock_receiver, CubicTo(DlPoint(300, 300), DlPoint(350, 300),
+ DlPoint(300, 350)));
+ // Closing LineTo added implicitly to return to first point
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 200)));
+ EXPECT_CALL(mock_receiver, Close());
+ }
+
+ DlPath(path).Dispatch(mock_receiver);
+}
+
+TEST(DisplayListPath, DispatchSkiaPathConvex) {
+ SkPath path;
+
+ path.setFillType(SkPathFillType::kWinding);
+ // Keep it simple - a triangle is obviously convex
+ path.moveTo(100, 200);
+ path.lineTo(200, 200);
+ path.lineTo(100, 300);
+ path.close();
+
+ ::testing::StrictMock<DlPathReceiverMock> mock_receiver;
+
+ // Recommendations must happen before any of the path segments is dispatched
+ ::testing::ExpectationSet all_recommendations;
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendSizes(4u, 3u)) //
+ .Times(AtMost(1));
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver,
+ RecommendBounds(DlRect::MakeLTRB(100, 200, 200, 300)))
+ .Times(AtMost(1));
+ EXPECT_CALL(mock_receiver, SetPathInfo(DlPathFillType::kNonZero, true));
+
+ {
+ ::testing::InSequence sequence;
+
+ EXPECT_CALL(mock_receiver, MoveTo(DlPoint(100, 200)))
+ .After(all_recommendations);
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(200, 200)));
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 300)));
+ // Closing LineTo added implicitly to return to first point
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 200)));
+ EXPECT_CALL(mock_receiver, Close());
+ }
+
+ DlPath(path).Dispatch(mock_receiver);
+}
+
+TEST(DisplayListPath, DispatchImpellerPathEvenOdd) {
+ DlPathBuilder path_builder;
+
+ path_builder.MoveTo(DlPoint(100, 200));
+ path_builder.LineTo(DlPoint(101, 201));
+ path_builder.QuadraticCurveTo(DlPoint(110, 202), DlPoint(102, 210));
+ path_builder.ConicCurveTo(DlPoint(150, 240), DlPoint(250, 140), 0.5);
+ path_builder.CubicCurveTo(DlPoint(300, 300), DlPoint(350, 300),
+ DlPoint(300, 350));
+ path_builder.Close();
+
+ DlPath path(path_builder, DlPathFillType::kOdd);
+ // Impeller computes tight bounds so it is difficult to hard-code the
+ // answer for the bounds of the above path...
+ auto bounds = path.GetBounds();
+
+ ::testing::StrictMock<DlPathReceiverMock> mock_receiver;
+
+ // Recommendations must happen before any of the path segments is dispatched
+ ::testing::ExpectationSet all_recommendations;
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendSizes(7u, 19u)) //
+ .Times(AtMost(1));
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendBounds(bounds)) //
+ .Times(AtMost(1));
+ EXPECT_CALL(mock_receiver, SetPathInfo(DlPathFillType::kOdd, false));
+
+ {
+ ::testing::InSequence sequence;
+
+ EXPECT_CALL(mock_receiver, MoveTo(DlPoint(100, 200)))
+ .After(all_recommendations);
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(101, 201)));
+ EXPECT_CALL(mock_receiver, QuadTo(DlPoint(110, 202), DlPoint(102, 210)));
+ EXPECT_CALL(mock_receiver,
+ ConicTo(DlPoint(150, 240), DlPoint(250, 140), 0.5f))
+ .WillOnce(Return(true));
+ EXPECT_CALL(mock_receiver, CubicTo(DlPoint(300, 300), DlPoint(350, 300),
+ DlPoint(300, 350)));
+ // Closing LineTo added implicitly to return to first point
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 200)));
+ EXPECT_CALL(mock_receiver, Close());
+ }
+
+ path.Dispatch(mock_receiver);
+}
+
+TEST(DisplayListPath, DispatchImpellerPathNonZero) {
+ DlPathBuilder path_builder;
+
+ path_builder.MoveTo(DlPoint(100, 200));
+ path_builder.LineTo(DlPoint(101, 201));
+ path_builder.QuadraticCurveTo(DlPoint(110, 202), DlPoint(102, 210));
+ path_builder.ConicCurveTo(DlPoint(150, 240), DlPoint(250, 140), 0.5);
+ path_builder.CubicCurveTo(DlPoint(300, 300), DlPoint(350, 300),
+ DlPoint(300, 350));
+ path_builder.Close();
+
+ DlPath path(path_builder, DlPathFillType::kNonZero);
+ // Impeller computes tight bounds so it is difficult to hard-code the
+ // answer for the bounds of the above path...
+ auto bounds = path.GetBounds();
+
+ ::testing::StrictMock<DlPathReceiverMock> mock_receiver;
+
+ // Recommendations must happen before any of the path segments is dispatched
+ ::testing::ExpectationSet all_recommendations;
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendSizes(7u, 19u)) //
+ .Times(AtMost(1));
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendBounds(bounds)) //
+ .Times(AtMost(1));
+ EXPECT_CALL(mock_receiver, SetPathInfo(DlPathFillType::kNonZero, false));
+
+ {
+ ::testing::InSequence sequence;
+
+ EXPECT_CALL(mock_receiver, MoveTo(DlPoint(100, 200)))
+ .After(all_recommendations);
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(101, 201)));
+ EXPECT_CALL(mock_receiver, QuadTo(DlPoint(110, 202), DlPoint(102, 210)));
+ EXPECT_CALL(mock_receiver,
+ ConicTo(DlPoint(150, 240), DlPoint(250, 140), 0.5f))
+ .WillOnce(Return(true));
+ EXPECT_CALL(mock_receiver, CubicTo(DlPoint(300, 300), DlPoint(350, 300),
+ DlPoint(300, 350)));
+ // Closing LineTo added implicitly to return to first point
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 200)));
+ EXPECT_CALL(mock_receiver, Close());
+ }
+
+ path.Dispatch(mock_receiver);
+}
+
+TEST(DisplayListPath, DispatchImpellerPathConvexUnspecified) {
+ DlPathBuilder path_builder;
+
+ // Keep it simple - a triangle is obviously convex
+ path_builder.MoveTo(DlPoint(100, 200));
+ path_builder.LineTo(DlPoint(200, 200));
+ path_builder.LineTo(DlPoint(100, 300));
+ path_builder.Close();
+
+ DlPath path(path_builder, DlPathFillType::kNonZero);
+
+ ::testing::StrictMock<DlPathReceiverMock> mock_receiver;
+
+ // Recommendations must happen before any of the path segments is dispatched
+ ::testing::ExpectationSet all_recommendations;
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendSizes(5u, 10u)) //
+ .Times(AtMost(1));
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver,
+ RecommendBounds(DlRect::MakeLTRB(100, 200, 200, 300)))
+ .Times(AtMost(1));
+ EXPECT_CALL(mock_receiver, SetPathInfo(DlPathFillType::kNonZero, false));
+
+ {
+ ::testing::InSequence sequence;
+
+ EXPECT_CALL(mock_receiver, MoveTo(DlPoint(100, 200)))
+ .After(all_recommendations);
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(200, 200)));
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 300)));
+ // Closing LineTo added implicitly to return to first point
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 200)));
+ EXPECT_CALL(mock_receiver, Close());
+ }
+
+ path.Dispatch(mock_receiver);
+}
+
+TEST(DisplayListPath, DispatchImpellerPathConvexSpecified) {
+ DlPathBuilder path_builder;
+
+ // Keep it simple - a triangle is obviously convex
+ path_builder.MoveTo(DlPoint(100, 200));
+ path_builder.LineTo(DlPoint(200, 200));
+ path_builder.LineTo(DlPoint(100, 300));
+ path_builder.Close();
+ path_builder.SetConvexity(impeller::Convexity::kConvex);
+
+ DlPath path(path_builder, DlPathFillType::kNonZero);
+
+ ::testing::StrictMock<DlPathReceiverMock> mock_receiver;
+
+ // Recommendations must happen before any of the path segments is dispatched
+ ::testing::ExpectationSet all_recommendations;
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver, RecommendSizes(5u, 10u)) //
+ .Times(AtMost(1));
+ all_recommendations += //
+ EXPECT_CALL(mock_receiver,
+ RecommendBounds(DlRect::MakeLTRB(100, 200, 200, 300)))
+ .Times(AtMost(1));
+ EXPECT_CALL(mock_receiver, SetPathInfo(DlPathFillType::kNonZero, true));
+
+ {
+ ::testing::InSequence sequence;
+
+ EXPECT_CALL(mock_receiver, MoveTo(DlPoint(100, 200)))
+ .After(all_recommendations);
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(200, 200)));
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 300)));
+ // Closing LineTo added implicitly to return to first point
+ EXPECT_CALL(mock_receiver, LineTo(DlPoint(100, 200)));
+ EXPECT_CALL(mock_receiver, Close());
+ }
+
+ path.Dispatch(mock_receiver);
+}
+
#ifndef NDEBUG
// Tests that verify we don't try to use inverse path modes as they aren't
// supported by either Flutter public APIs or Impeller
diff --git a/engine/src/flutter/impeller/geometry/path.cc b/engine/src/flutter/impeller/geometry/path.cc
index 385fe0b..35539c5 100644
--- a/engine/src/flutter/impeller/geometry/path.cc
+++ b/engine/src/flutter/impeller/geometry/path.cc
@@ -103,6 +103,10 @@
return count;
}
+size_t Path::GetPointCount() const {
+ return data_->points.size();
+}
+
FillType Path::GetFillType() const {
return data_->fill;
}
diff --git a/engine/src/flutter/impeller/geometry/path.h b/engine/src/flutter/impeller/geometry/path.h
index 978e9f3..ce00424 100644
--- a/engine/src/flutter/impeller/geometry/path.h
+++ b/engine/src/flutter/impeller/geometry/path.h
@@ -183,6 +183,8 @@
size_t GetComponentCount(std::optional<ComponentType> type = {}) const;
+ size_t GetPointCount() const;
+
FillType GetFillType() const;
bool IsConvex() const;
diff --git a/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc b/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc
index 5cc4f40..400d714 100644
--- a/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc
+++ b/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc
@@ -2023,6 +2023,54 @@
FML_CHECK(fml::jni::CheckException(env));
}
+namespace {
+class AndroidPathReceiver final : public DlPathReceiver {
+ public:
+ explicit AndroidPathReceiver(JNIEnv* env)
+ : env_(env),
+ android_path_(env->NewObject(path_class->obj(), path_constructor)) {}
+
+ void SetPathInfo(DlPathFillType type, bool is_convex) override {
+ // Need to convert the fill type to the Android enum and
+ // call setFillType on the path...
+ // see https://github.com/flutter/flutter/issues/164808
+ }
+ void MoveTo(const DlPoint& p2) override {
+ env_->CallVoidMethod(android_path_, path_move_to_method, p2.x, p2.y);
+ }
+ void LineTo(const DlPoint& p2) override {
+ env_->CallVoidMethod(android_path_, path_line_to_method, p2.x, p2.y);
+ }
+ void QuadTo(const DlPoint& cp, const DlPoint& p2) override {
+ env_->CallVoidMethod(android_path_, path_quad_to_method, //
+ cp.x, cp.y, p2.x, p2.y);
+ }
+ bool ConicTo(const DlPoint& cp, const DlPoint& p2, DlScalar weight) override {
+ if (!path_conic_to_method) {
+ return false;
+ }
+ env_->CallVoidMethod(android_path_, path_conic_to_method, //
+ cp.x, cp.y, p2.x, p2.y, weight);
+ return true;
+ };
+ void CubicTo(const DlPoint& cp1,
+ const DlPoint& cp2,
+ const DlPoint& p2) override {
+ env_->CallVoidMethod(android_path_, path_cubic_to_method, //
+ cp1.x, cp1.y, cp2.x, cp2.y, p2.x, p2.y);
+ }
+ void Close() override {
+ env_->CallVoidMethod(android_path_, path_close_method);
+ }
+
+ jobject TakePath() { return android_path_; }
+
+ private:
+ JNIEnv* env_;
+ jobject android_path_;
+};
+} // namespace
+
void PlatformViewAndroidJNIImpl::onDisplayPlatformView2(
int32_t view_id,
int32_t x,
@@ -2132,79 +2180,14 @@
FML_DCHECK(!dlPath.IsRoundRect());
// Define and populate an Android Path with data from the DlPath
- jobject androidPath =
- env->NewObject(path_class->obj(), path_constructor);
+ AndroidPathReceiver receiver(env);
- bool subpath_needs_close = false;
- std::optional<flutter::DlPoint> pending_moveto;
-
- auto resolve_moveto = [&env, &pending_moveto, &androidPath]() {
- if (pending_moveto.has_value()) {
- env->CallVoidMethod(androidPath, path_move_to_method,
- pending_moveto->x, pending_moveto->y);
- pending_moveto.reset();
- }
- };
-
- auto& path = dlPath.GetPath();
- for (auto it = path.begin(), end = path.end(); it != end; ++it) {
- switch (it.type()) {
- case impeller::Path::ComponentType::kContour: {
- const impeller::ContourComponent* contour = it.contour();
- FML_DCHECK(contour != nullptr);
- if (subpath_needs_close) {
- env->CallVoidMethod(androidPath, path_close_method);
- }
- pending_moveto = contour->destination;
- subpath_needs_close = contour->IsClosed();
- break;
- }
- case impeller::Path::ComponentType::kLinear: {
- const impeller::LinearPathComponent* linear = it.linear();
- FML_DCHECK(linear != nullptr);
- resolve_moveto();
- env->CallVoidMethod(androidPath, path_line_to_method,
- linear->p2.x, linear->p2.y);
- break;
- }
- case impeller::Path::ComponentType::kQuadratic: {
- const impeller::QuadraticPathComponent* quadratic =
- it.quadratic();
- FML_DCHECK(quadratic != nullptr);
- resolve_moveto();
- env->CallVoidMethod(androidPath, path_quad_to_method,
- quadratic->cp.x, quadratic->cp.y,
- quadratic->p2.x, quadratic->p2.y);
- break;
- }
- case impeller::Path::ComponentType::kConic: {
- const impeller::ConicPathComponent* conic = it.conic();
- FML_DCHECK(conic != nullptr);
- resolve_moveto();
- FML_DCHECK(path_conic_to_method != nullptr);
- env->CallVoidMethod(androidPath, path_conic_to_method,
- conic->cp.x, conic->cp.y, //
- conic->p2.x, conic->p2.y, conic->weight);
- break;
- }
- case impeller::Path::ComponentType::kCubic: {
- const impeller::CubicPathComponent* cubic = it.cubic();
- FML_DCHECK(cubic != nullptr);
- resolve_moveto();
- env->CallVoidMethod(androidPath, path_cubic_to_method,
- cubic->cp1.x, cubic->cp1.y, //
- cubic->cp2.x, cubic->cp2.y, //
- cubic->p2.x, cubic->p2.y);
- break;
- }
- }
- }
- if (subpath_needs_close) {
- env->CallVoidMethod(androidPath, path_close_method);
- }
+ dlPath.Dispatch(receiver);
env->CallVoidMethod(mutatorsStack,
- g_mutators_stack_push_clippath_method, androidPath);
+ g_mutators_stack_push_clippath_method,
+ receiver.TakePath());
+ break;
}
// TODO(cyanglaz): Implement other mutators.
// https://github.com/flutter/flutter/issues/58426
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
index b4137fa..efac6ae 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
@@ -43,6 +43,37 @@
transform.m44 = matrix.m[15];
return transform;
}
+
+class CGPathReceiver final : public flutter::DlPathReceiver {
+ public:
+ void SetPathInfo(flutter::DlPathFillType type, bool is_convex) override {
+ // CGPaths do not have an inherit fill type, we would need to remember
+ // the fill type and employ it when we use the path.
+ // see https://github.com/flutter/flutter/issues/164826
+ }
+ void MoveTo(const flutter::DlPoint& p2) override { //
+ CGPathMoveToPoint(path_ref_, nil, p2.x, p2.y);
+ }
+ void LineTo(const flutter::DlPoint& p2) override {
+ CGPathAddLineToPoint(path_ref_, nil, p2.x, p2.y);
+ }
+ void QuadTo(const flutter::DlPoint& cp, const flutter::DlPoint& p2) override {
+ CGPathAddQuadCurveToPoint(path_ref_, nil, cp.x, cp.y, p2.x, p2.y);
+ }
+ // bool conic_to(...) { CGPath has no equivalent to the conic curve type }
+ void CubicTo(const flutter::DlPoint& cp1,
+ const flutter::DlPoint& cp2,
+ const flutter::DlPoint& p2) override {
+ CGPathAddCurveToPoint(path_ref_, nil, //
+ cp1.x, cp1.y, cp2.x, cp2.y, p2.x, p2.y);
+ }
+ void Close() override { CGPathCloseSubpath(path_ref_); }
+
+ CGMutablePathRef TakePath() { return path_ref_; }
+
+ private:
+ CGMutablePathRef path_ref_ = CGPathCreateMutable();
+};
} // namespace
@interface PlatformViewFilter ()
@@ -382,77 +413,15 @@
- (void)clipPath:(const flutter::DlPath&)dlPath matrix:(const flutter::DlMatrix&)matrix {
containsNonRectPath_ = YES;
- CGMutablePathRef pathRef = CGPathCreateMutable();
- bool subpath_needs_close = false;
- std::optional<flutter::DlPoint> pending_moveto;
- auto resolve_moveto = [&pending_moveto, &pathRef]() {
- if (pending_moveto.has_value()) {
- CGPathMoveToPoint(pathRef, nil, pending_moveto->x, pending_moveto->y);
- pending_moveto.reset();
- }
- };
+ CGPathReceiver receiver;
- auto& path = dlPath.GetPath();
- for (auto it = path.begin(), end = path.end(); it != end; ++it) {
- switch (it.type()) {
- case impeller::Path::ComponentType::kContour: {
- const impeller::ContourComponent* contour = it.contour();
- FML_DCHECK(contour != nullptr);
- if (subpath_needs_close) {
- CGPathCloseSubpath(pathRef);
- }
- pending_moveto = contour->destination;
- subpath_needs_close = contour->IsClosed();
- break;
- }
- case impeller::Path::ComponentType::kLinear: {
- const impeller::LinearPathComponent* linear = it.linear();
- FML_DCHECK(linear != nullptr);
- resolve_moveto();
- CGPathAddLineToPoint(pathRef, nil, linear->p2.x, linear->p2.y);
- break;
- }
- case impeller::Path::ComponentType::kQuadratic: {
- const impeller::QuadraticPathComponent* quadratic = it.quadratic();
- FML_DCHECK(quadratic != nullptr);
- resolve_moveto();
- CGPathAddQuadCurveToPoint(pathRef, nil, //
- quadratic->cp.x, quadratic->cp.y, //
- quadratic->p2.x, quadratic->p2.y);
- break;
- }
- case impeller::Path::ComponentType::kConic: {
- const impeller::ConicPathComponent* conic = it.conic();
- FML_DCHECK(conic != nullptr);
- resolve_moveto();
- // Conic is not available in quartz, we use quad to approximate.
- // TODO(cyanglaz): Better approximate the conic path.
- // https://github.com/flutter/flutter/issues/35062
- CGPathAddQuadCurveToPoint(pathRef, nil, //
- conic->cp.x, conic->cp.y, //
- conic->p2.x, conic->p2.y);
- break;
- }
- case impeller::Path::ComponentType::kCubic: {
- const impeller::CubicPathComponent* cubic = it.cubic();
- FML_DCHECK(cubic != nullptr);
- resolve_moveto();
- CGPathAddCurveToPoint(pathRef, nil, //
- cubic->cp1.x, cubic->cp1.y, //
- cubic->cp2.x, cubic->cp2.y, //
- cubic->p2.x, cubic->p2.y);
- break;
- }
- }
- }
- if (subpath_needs_close) {
- CGPathCloseSubpath(pathRef);
- }
+ dlPath.Dispatch(receiver);
+
// The `matrix` is based on the physical pixels, convert it to UIKit points.
CATransform3D matrixInPoints =
CATransform3DConcat(GetCATransform3DFromDlMatrix(matrix), _reverseScreenScale);
- paths_.push_back([self getTransformedPath:pathRef matrix:matrixInPoints]);
+ paths_.push_back([self getTransformedPath:receiver.TakePath() matrix:matrixInPoints]);
}
- (CGAffineTransform)affineWithMatrix:(CATransform3D)matrix {