diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index 320796c802..dcfd77413d 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -127,7 +127,6 @@ ../../../flutter/impeller/aiks/aiks_blend_unittests.cc ../../../flutter/impeller/aiks/aiks_blur_unittests.cc ../../../flutter/impeller/aiks/aiks_gradient_unittests.cc -../../../flutter/impeller/aiks/aiks_path_unittests.cc ../../../flutter/impeller/aiks/aiks_unittests.cc ../../../flutter/impeller/aiks/aiks_unittests.h ../../../flutter/impeller/aiks/canvas_unittests.cc diff --git a/engine/src/flutter/display_list/dl_color.h b/engine/src/flutter/display_list/dl_color.h index 239b49042e..0338169077 100644 --- a/engine/src/flutter/display_list/dl_color.h +++ b/engine/src/flutter/display_list/dl_color.h @@ -14,6 +14,15 @@ struct DlColor { constexpr DlColor() : argb_(0xFF000000) {} constexpr explicit DlColor(uint32_t argb) : argb_(argb) {} + /// @brief Construct a 32 bit color from a floating point A, R, G, and B color + /// channel. + constexpr explicit DlColor(SkScalar a, SkScalar r, SkScalar g, SkScalar b) + : argb_(static_cast(std::round(a * 255)) << 24 | // + static_cast(std::round(r * 255)) << 16 | // + static_cast(std::round(g * 255)) << 8 | // + static_cast(std::round(b * 255)) << 0 // + ) {} + static constexpr uint8_t toAlpha(SkScalar opacity) { return toC(opacity); } static constexpr SkScalar toOpacity(uint8_t alpha) { return toF(alpha); } diff --git a/engine/src/flutter/impeller/aiks/BUILD.gn b/engine/src/flutter/impeller/aiks/BUILD.gn index 35a97ac681..31da5054b4 100644 --- a/engine/src/flutter/impeller/aiks/BUILD.gn +++ b/engine/src/flutter/impeller/aiks/BUILD.gn @@ -78,7 +78,6 @@ template("aiks_unittests_component") { "aiks_blend_unittests.cc", "aiks_blur_unittests.cc", "aiks_gradient_unittests.cc", - "aiks_path_unittests.cc", "aiks_unittests.cc", "aiks_unittests.h", ] diff --git a/engine/src/flutter/impeller/aiks/aiks_path_unittests.cc b/engine/src/flutter/impeller/aiks/aiks_path_unittests.cc deleted file mode 100644 index e2aa87fc74..0000000000 --- a/engine/src/flutter/impeller/aiks/aiks_path_unittests.cc +++ /dev/null @@ -1,460 +0,0 @@ -// Copyright 2013 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 "flutter/impeller/aiks/aiks_unittests.h" - -#include "impeller/aiks/canvas.h" -#include "impeller/geometry/path_builder.h" -#include "impeller/playground/widgets.h" -#include "third_party/imgui/imgui.h" - -//////////////////////////////////////////////////////////////////////////////// -// This is for tests of Canvas that are interested the results of rendering -// paths. -//////////////////////////////////////////////////////////////////////////////// - -namespace impeller { -namespace testing { - -TEST_P(AiksTest, CanRenderStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 20.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddLine({200, 100}, {800, 100}).TakePath(), - paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderCurvedStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 25.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderThickCurvedStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 100.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderThinCurvedStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - // Impeller doesn't support hairlines yet, but size this guarantees - // the smallest possible stroke width. - paint.stroke_width = 0.01; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderStrokePathThatEndsAtSharpTurn) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 200; - - Rect rect = Rect::MakeXYWH(100, 100, 200, 200); - PathBuilder builder; - builder.AddArc(rect, Degrees(0), Degrees(90), false); - - canvas.DrawPath(builder.TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderStrokePathWithCubicLine) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 20; - - PathBuilder builder; - builder.AddCubicCurve({0, 200}, {50, 400}, {350, 0}, {400, 200}); - - canvas.DrawPath(builder.TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderQuadraticStrokeWithInstantTurn) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_cap = Cap::kRound; - paint.stroke_width = 50; - - // Should draw a diagonal pill shape. If flat on either end, the stroke is - // rendering wrong. - PathBuilder builder; - builder.MoveTo({250, 250}); - builder.QuadraticCurveTo({100, 100}, {250, 250}); - - canvas.DrawPath(builder.TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderDifferencePaths) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - - PathBuilder builder; - - PathBuilder::RoundingRadii radii; - radii.top_left = {50, 25}; - radii.top_right = {25, 50}; - radii.bottom_right = {50, 25}; - radii.bottom_left = {25, 50}; - - builder.AddRoundedRect(Rect::MakeXYWH(100, 100, 200, 200), radii); - builder.AddCircle({200, 200}, 50); - auto path = builder.TakePath(FillType::kOdd); - - canvas.DrawImage( - std::make_shared(CreateTextureForFixture("boston.jpg")), {10, 10}, - Paint{}); - canvas.DrawPath(path, paint); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -// Regression test for https://github.com/flutter/flutter/issues/134816. -// -// It should be possible to draw 3 lines, and not have an implicit close path. -TEST_P(AiksTest, CanDrawAnOpenPath) { - Canvas canvas; - - // Starting at (50, 50), draw lines from: - // 1. (50, height) - // 2. (width, height) - // 3. (width, 50) - PathBuilder builder; - builder.MoveTo({50, 50}); - builder.LineTo({50, 100}); - builder.LineTo({100, 100}); - builder.LineTo({100, 50}); - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - - canvas.DrawPath(builder.TakePath(), paint); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanDrawAnOpenPathThatIsntARect) { - Canvas canvas; - - // Draw a stroked path that is explicitly closed to verify - // It doesn't become a rectangle. - PathBuilder builder; - builder.MoveTo({50, 50}); - builder.LineTo({520, 120}); - builder.LineTo({300, 310}); - builder.LineTo({100, 50}); - builder.Close(); - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - - canvas.DrawPath(builder.TakePath(), paint); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, SolidStrokesRenderCorrectly) { - // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 - auto callback = [&](AiksContext& renderer) -> std::optional { - static Color color = Color::Black().WithAlpha(0.5); - static float scale = 3; - static bool add_circle_clip = true; - - if (AiksTest::ImGuiBegin("Controls", nullptr, - ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::ColorEdit4("Color", reinterpret_cast(&color)); - ImGui::SliderFloat("Scale", &scale, 0, 6); - ImGui::Checkbox("Circle clip", &add_circle_clip); - ImGui::End(); - } - - Canvas canvas; - canvas.Scale(GetContentScale()); - Paint paint; - - paint.color = Color::White(); - canvas.DrawPaint(paint); - - paint.color = color; - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - - Path path = PathBuilder{} - .MoveTo({20, 20}) - .QuadraticCurveTo({60, 20}, {60, 60}) - .Close() - .MoveTo({60, 20}) - .QuadraticCurveTo({60, 60}, {20, 60}) - .TakePath(); - - canvas.Scale(Vector2(scale, scale)); - - if (add_circle_clip) { - static PlaygroundPoint circle_clip_point_a(Point(60, 300), 20, - Color::Red()); - static PlaygroundPoint circle_clip_point_b(Point(600, 300), 20, - Color::Red()); - auto [handle_a, handle_b] = - DrawPlaygroundLine(circle_clip_point_a, circle_clip_point_b); - - auto screen_to_canvas = canvas.GetCurrentTransform().Invert(); - Point point_a = screen_to_canvas * handle_a * GetContentScale(); - Point point_b = screen_to_canvas * handle_b * GetContentScale(); - - Point middle = (point_a + point_b) / 2; - auto radius = point_a.GetDistance(middle); - canvas.ClipPath(PathBuilder{}.AddCircle(middle, radius).TakePath()); - } - - for (auto join : {Join::kBevel, Join::kRound, Join::kMiter}) { - paint.stroke_join = join; - for (auto cap : {Cap::kButt, Cap::kSquare, Cap::kRound}) { - paint.stroke_cap = cap; - canvas.DrawPath(path, paint); - canvas.Translate({80, 0}); - } - canvas.Translate({-240, 60}); - } - - return canvas.EndRecordingAsPicture(); - }; - - ASSERT_TRUE(OpenPlaygroundHere(callback)); -} - -TEST_P(AiksTest, DrawLinesRenderCorrectly) { - Canvas canvas; - canvas.Scale(GetContentScale()); - Paint paint; - paint.color = Color::Blue(); - paint.stroke_width = 10; - - auto draw = [&canvas](Paint& paint) { - for (auto cap : {Cap::kButt, Cap::kSquare, Cap::kRound}) { - paint.stroke_cap = cap; - Point origin = {100, 100}; - Point p0 = {50, 0}; - Point p1 = {150, 0}; - canvas.DrawLine({150, 100}, {250, 100}, paint); - for (int d = 15; d < 90; d += 15) { - Matrix m = Matrix::MakeRotationZ(Degrees(d)); - canvas.DrawLine(origin + m * p0, origin + m * p1, paint); - } - canvas.DrawLine({100, 150}, {100, 250}, paint); - canvas.DrawCircle({origin}, 35, paint); - - canvas.DrawLine({250, 250}, {250, 250}, paint); - - canvas.Translate({250, 0}); - } - canvas.Translate({-750, 250}); - }; - - std::vector colors = { - Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, - Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, - Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, - Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, - Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; - std::vector stops = { - 0.0, - (1.0 / 6.0) * 1, - (1.0 / 6.0) * 2, - (1.0 / 6.0) * 3, - (1.0 / 6.0) * 4, - (1.0 / 6.0) * 5, - 1.0, - }; - - auto texture = CreateTextureForFixture("airplane.jpg", - /*enable_mipmapping=*/true); - - draw(paint); - - paint.color_source = ColorSource::MakeRadialGradient( - {100, 100}, 200, std::move(colors), std::move(stops), - Entity::TileMode::kMirror, {}); - draw(paint); - - paint.color_source = ColorSource::MakeImage( - texture, Entity::TileMode::kRepeat, Entity::TileMode::kRepeat, {}, - Matrix::MakeTranslation({-150, 75})); - draw(paint); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, DrawRectStrokesRenderCorrectly) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - - canvas.Translate({100, 100}); - canvas.DrawPath( - PathBuilder{}.AddRect(Rect::MakeSize(Size{100, 100})).TakePath(), - {paint}); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, DrawRectStrokesWithBevelJoinRenderCorrectly) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - paint.stroke_join = Join::kBevel; - - canvas.Translate({100, 100}); - canvas.DrawPath( - PathBuilder{}.AddRect(Rect::MakeSize(Size{100, 100})).TakePath(), - {paint}); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanDrawMultiContourConvexPath) { - PathBuilder builder = {}; - for (auto i = 0; i < 10; i++) { - if (i % 2 == 0) { - builder.AddCircle(Point(100 + 50 * i, 100 + 50 * i), 100); - } else { - builder.MoveTo({100.f + 50.f * i - 100, 100.f + 50.f * i}); - builder.LineTo({100.f + 50.f * i, 100.f + 50.f * i - 100}); - builder.LineTo({100.f + 50.f * i - 100, 100.f + 50.f * i - 100}); - builder.Close(); - } - } - builder.SetConvexity(Convexity::kConvex); - - Canvas canvas; - canvas.DrawPath(builder.TakePath(), {.color = Color::Red().WithAlpha(0.4)}); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, ArcWithZeroSweepAndBlur) { - Canvas canvas; - canvas.Scale(GetContentScale()); - - Paint paint; - paint.color = Color::Red(); - std::vector colors = {Color{1.0, 0.0, 0.0, 1.0}, - Color{0.0, 0.0, 0.0, 1.0}}; - std::vector stops = {0.0, 1.0}; - paint.color_source = ColorSource::MakeSweepGradient( - {100, 100}, Degrees(45), Degrees(135), std::move(colors), - std::move(stops), Entity::TileMode::kMirror, {}); - paint.mask_blur_descriptor = Paint::MaskBlurDescriptor{ - .style = FilterContents::BlurStyle::kNormal, - .sigma = Sigma(20), - }; - - PathBuilder builder; - builder.AddArc(Rect::MakeXYWH(10, 10, 100, 100), Degrees(0), Degrees(0), - false); - canvas.DrawPath(builder.TakePath(), paint); - - // Check that this empty picture can be created without crashing. - canvas.EndRecordingAsPicture(); -} - -TEST_P(AiksTest, CanRenderClips) { - Canvas canvas; - Paint paint; - paint.color = Color::Fuchsia(); - canvas.ClipPath( - PathBuilder{}.AddRect(Rect::MakeXYWH(0, 0, 500, 500)).TakePath()); - canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderOverlappingMultiContourPath) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - - PathBuilder::RoundingRadii radii; - radii.top_left = {50, 50}; - radii.top_right = {50, 50}; - radii.bottom_right = {50, 50}; - radii.bottom_left = {50, 50}; - - const Scalar kTriangleHeight = 100; - canvas.Translate(Vector2(200, 200)); - // Form a path similar to the Material drop slider value indicator. Both - // shapes should render identically side-by-side. - { - auto path = - PathBuilder{} - .MoveTo({0, kTriangleHeight}) - .LineTo({-kTriangleHeight / 2.0f, 0}) - .LineTo({kTriangleHeight / 2.0f, 0}) - .Close() - .AddRoundedRect( - Rect::MakeXYWH(-kTriangleHeight / 2.0f, -kTriangleHeight / 2.0f, - kTriangleHeight, kTriangleHeight), - radii) - .TakePath(); - - canvas.DrawPath(path, paint); - } - canvas.Translate(Vector2(100, 0)); - { - auto path = - PathBuilder{} - .MoveTo({0, kTriangleHeight}) - .LineTo({-kTriangleHeight / 2.0f, 0}) - .LineTo({0, -10}) - .LineTo({kTriangleHeight / 2.0f, 0}) - .Close() - .AddRoundedRect( - Rect::MakeXYWH(-kTriangleHeight / 2.0f, -kTriangleHeight / 2.0f, - kTriangleHeight, kTriangleHeight), - radii) - .TakePath(); - - canvas.DrawPath(path, paint); - } - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -} // namespace testing -} // namespace impeller diff --git a/engine/src/flutter/impeller/aiks/aiks_unittests.cc b/engine/src/flutter/impeller/aiks/aiks_unittests.cc index 73833f3a77..9d6a730161 100644 --- a/engine/src/flutter/impeller/aiks/aiks_unittests.cc +++ b/engine/src/flutter/impeller/aiks/aiks_unittests.cc @@ -2570,5 +2570,4 @@ TEST_P(AiksTest, SetContentsWithRegion) { // █ - aiks_blend_unittests.cc // █ - aiks_blur_unittests.cc // █ - aiks_gradient_unittests.cc -// █ - aiks_path_unittests.cc // █████████████████████████████████████████████████████████████████████████████ diff --git a/engine/src/flutter/impeller/aiks/canvas.cc b/engine/src/flutter/impeller/aiks/canvas.cc index c007af65a8..917df48bd3 100644 --- a/engine/src/flutter/impeller/aiks/canvas.cc +++ b/engine/src/flutter/impeller/aiks/canvas.cc @@ -476,7 +476,12 @@ void Canvas::DrawRect(const Rect& rect, const Paint& paint) { } void Canvas::DrawOval(const Rect& rect, const Paint& paint) { - if (rect.IsSquare()) { + // TODO(jonahwilliams): This additional condition avoids an assert in the + // stroke circle geometry generator. I need to verify the condition that this + // assert prevents. + if (rect.IsSquare() && (paint.style == Paint::Style::kFill || + (paint.style == Paint::Style::kStroke && + paint.stroke_width < rect.GetWidth()))) { // Circles have slightly less overhead and can do stroking DrawCircle(rect.GetCenter(), rect.GetWidth() * 0.5f, paint); return; diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc index 4a54beb7d4..c481a2d59f 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "display_list/dl_sampling_options.h" +#include "display_list/dl_tile_mode.h" +#include "display_list/effects/dl_color_source.h" +#include "display_list/effects/dl_mask_filter.h" #include "flutter/impeller/aiks/aiks_unittests.h" #include "flutter/display_list/dl_blend_mode.h" @@ -10,6 +14,13 @@ #include "flutter/display_list/dl_paint.h" #include "flutter/display_list/effects/dl_color_filter.h" #include "flutter/testing/testing.h" +#include "impeller/display_list/dl_image_impeller.h" +#include "impeller/playground/widgets.h" + +#include "include/core/SkMatrix.h" +#include "include/core/SkPath.h" +#include "include/core/SkPathTypes.h" +#include "include/core/SkRRect.h" namespace impeller { namespace testing { @@ -42,5 +53,458 @@ TEST_P(AiksTest, RotateColorFilteredPath) { ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); } + +TEST_P(AiksTest, CanRenderStrokes) { + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setStrokeWidth(20); + paint.setDrawStyle(DlDrawStyle::kStroke); + + builder.DrawPath(SkPath::Line({200, 100}, {800, 100}), paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderCurvedStrokes) { + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setStrokeWidth(25); + paint.setDrawStyle(DlDrawStyle::kStroke); + + builder.DrawPath(SkPath::Circle(500, 500, 250), paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderThickCurvedStrokes) { + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setStrokeWidth(100); + paint.setDrawStyle(DlDrawStyle::kStroke); + + builder.DrawPath(SkPath::Circle(100, 100, 50), paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderThinCurvedStrokes) { + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setStrokeWidth(0.01); + paint.setDrawStyle(DlDrawStyle::kStroke); + + builder.DrawPath(SkPath::Circle(100, 100, 50), paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderStrokePathThatEndsAtSharpTurn) { + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setStrokeWidth(200); + paint.setDrawStyle(DlDrawStyle::kStroke); + + SkPath path; + path.arcTo(SkRect::MakeXYWH(100, 100, 200, 200), 0, 90, false); + + builder.DrawPath(path, paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderStrokePathWithCubicLine) { + DisplayListBuilder builder; + + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setStrokeWidth(20); + paint.setDrawStyle(DlDrawStyle::kStroke); + + SkPath path; + path.moveTo(0, 200); + path.cubicTo(50, 400, 350, 0, 400, 200); + + builder.DrawPath(path, paint); + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderQuadraticStrokeWithInstantTurn) { + DisplayListBuilder builder; + + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setStrokeWidth(50); + paint.setDrawStyle(DlDrawStyle::kStroke); + paint.setStrokeCap(DlStrokeCap::kRound); + + // Should draw a diagonal pill shape. If flat on either end, the stroke is + // rendering wrong. + SkPath path; + path.moveTo(250, 250); + path.quadTo(100, 100, 250, 250); + + builder.DrawPath(path, paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderDifferencePaths) { + DisplayListBuilder builder; + + DlPaint paint; + paint.setColor(DlColor::kRed()); + + SkPoint radii[4] = {{50, 25}, {25, 50}, {50, 25}, {25, 50}}; + SkPath path; + SkRRect rrect; + rrect.setRectRadii(SkRect::MakeXYWH(100, 100, 200, 200), radii); + path.addRRect(rrect); + path.addCircle(200, 200, 50); + path.setFillType(SkPathFillType::kEvenOdd); + + builder.DrawImage( + DlImageImpeller::Make(CreateTextureForFixture("boston.jpg")), {10, 10}, + {}); + builder.DrawPath(path, paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +// Regression test for https://github.com/flutter/flutter/issues/134816. +// +// It should be possible to draw 3 lines, and not have an implicit close path. +TEST_P(AiksTest, CanDrawAnOpenPath) { + DisplayListBuilder builder; + + // Starting at (50, 50), draw lines from: + // 1. (50, height) + // 2. (width, height) + // 3. (width, 50) + SkPath path; + path.moveTo(50, 50); + path.lineTo(50, 100); + path.lineTo(100, 100); + path.lineTo(100, 50); + + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setDrawStyle(DlDrawStyle::kStroke); + paint.setStrokeWidth(10); + + builder.DrawPath(path, paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanDrawAnOpenPathThatIsntARect) { + DisplayListBuilder builder; + + // Draw a stroked path that is explicitly closed to verify + // It doesn't become a rectangle. + SkPath path; + // PathBuilder builder; + path.moveTo(50, 50); + path.lineTo(520, 120); + path.lineTo(300, 310); + path.lineTo(100, 50); + path.close(); + + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setDrawStyle(DlDrawStyle::kStroke); + paint.setStrokeWidth(10); + + builder.DrawPath(path, paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, SolidStrokesRenderCorrectly) { + // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 + auto callback = [&]() -> sk_sp { + static Color color = Color::Black().WithAlpha(0.5); + static float scale = 3; + static bool add_circle_clip = true; + + if (AiksTest::ImGuiBegin("Controls", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::ColorEdit4("Color", reinterpret_cast(&color)); + ImGui::SliderFloat("Scale", &scale, 0, 6); + ImGui::Checkbox("Circle clip", &add_circle_clip); + ImGui::End(); + } + + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + DlPaint paint; + + paint.setColor(DlColor::kWhite()); + builder.DrawPaint(paint); + + paint.setColor(DlColor(color.alpha, color.red, color.green, color.blue)); + paint.setDrawStyle(DlDrawStyle::kStroke); + paint.setStrokeWidth(10); + + SkPath path; + path.moveTo({20, 20}); + path.quadTo({60, 20}, {60, 60}); + path.close(); + path.moveTo({60, 20}); + path.quadTo({60, 60}, {20, 60}); + + builder.Scale(scale, scale); + + if (add_circle_clip) { + static PlaygroundPoint circle_clip_point_a(Point(60, 300), 20, + Color::Red()); + static PlaygroundPoint circle_clip_point_b(Point(600, 300), 20, + Color::Red()); + auto [handle_a, handle_b] = + DrawPlaygroundLine(circle_clip_point_a, circle_clip_point_b); + + SkMatrix screen_to_canvas = SkMatrix::I(); + if (!builder.GetTransform().invert(&screen_to_canvas)) { + return nullptr; + } + + SkPoint point_a = + screen_to_canvas.mapPoint(SkPoint::Make(handle_a.x, handle_a.y)); + SkPoint point_b = + screen_to_canvas.mapPoint(SkPoint::Make(handle_b.x, handle_b.y)); + + SkPoint middle = point_a + point_b; + middle.scale(GetContentScale().x / 2); + + auto radius = SkPoint::Distance(point_a, middle); + + builder.ClipPath(SkPath::Circle(middle.x(), middle.y(), radius)); + } + + for (auto join : + {DlStrokeJoin::kBevel, DlStrokeJoin::kRound, DlStrokeJoin::kMiter}) { + paint.setStrokeJoin(join); + for (auto cap : + {DlStrokeCap::kButt, DlStrokeCap::kSquare, DlStrokeCap::kRound}) { + paint.setStrokeCap(cap); + builder.DrawPath(path, paint); + builder.Translate(80, 0); + } + builder.Translate(-240, 60); + } + + return builder.Build(); + }; + + ASSERT_TRUE(OpenPlaygroundHere(callback)); +} + +TEST_P(AiksTest, DrawLinesRenderCorrectly) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + + DlPaint paint; + paint.setColor(DlColor::kBlue()); + paint.setStrokeWidth(10); + + auto draw = [&builder](DlPaint& paint) { + for (auto cap : + {DlStrokeCap::kButt, DlStrokeCap::kSquare, DlStrokeCap::kRound}) { + paint.setStrokeCap(cap); + SkPoint origin = {100, 100}; + builder.DrawLine({150, 100}, {250, 100}, paint); + for (int d = 15; d < 90; d += 15) { + Matrix m = Matrix::MakeRotationZ(Degrees(d)); + Point origin = {100, 100}; + Point p0 = {50, 0}; + Point p1 = {150, 0}; + auto a = origin + m * p0; + auto b = origin + m * p1; + + builder.DrawLine(SkPoint::Make(a.x, a.y), SkPoint::Make(b.x, b.y), + paint); + } + builder.DrawLine({100, 150}, {100, 250}, paint); + builder.DrawCircle({origin}, 35, paint); + + builder.DrawLine({250, 250}, {250, 250}, paint); + + builder.Translate(250, 0); + } + builder.Translate(-750, 250); + }; + + std::vector colors = { + DlColor{1, 0x1f / 255.0, 0.0, 0x5c / 255.0}, + DlColor{1, 0x5b / 255.0, 0.0, 0x60 / 255.0}, + DlColor{1, 0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0}, + DlColor{1, 0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0}, + DlColor{1, 0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0}, + DlColor{1, 0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0}, + DlColor{1, 0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0}}; + std::vector stops = { + 0.0, + (1.0 / 6.0) * 1, + (1.0 / 6.0) * 2, + (1.0 / 6.0) * 3, + (1.0 / 6.0) * 4, + (1.0 / 6.0) * 5, + 1.0, + }; + + auto texture = DlImageImpeller::Make( + CreateTextureForFixture("airplane.jpg", + /*enable_mipmapping=*/true)); + + draw(paint); + + paint.setColorSource(DlColorSource::MakeRadial({100, 100}, 200, stops.size(), + colors.data(), stops.data(), + DlTileMode::kMirror)); + draw(paint); + + SkMatrix matrix = SkMatrix::Translate(-150, 75); + paint.setColorSource(std::make_shared( + texture, DlTileMode::kRepeat, DlTileMode::kRepeat, + DlImageSampling::kMipmapLinear, &matrix)); + draw(paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, DrawRectStrokesRenderCorrectly) { + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setDrawStyle(DlDrawStyle::kStroke); + paint.setStrokeWidth(10); + + builder.Translate(100, 100); + builder.DrawPath(SkPath::Rect(SkRect::MakeSize(SkSize{100, 100})), {paint}); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, DrawRectStrokesWithBevelJoinRenderCorrectly) { + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setDrawStyle(DlDrawStyle::kStroke); + paint.setStrokeWidth(10); + paint.setStrokeJoin(DlStrokeJoin::kBevel); + + builder.Translate(100, 100); + builder.DrawPath(SkPath::Rect(SkRect::MakeSize(SkSize{100, 100})), paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanDrawMultiContourConvexPath) { + SkPath path; + for (auto i = 0; i < 10; i++) { + if (i % 2 == 0) { + path.addCircle(100 + 50 * i, 100 + 50 * i, 100); + path.close(); + } else { + path.moveTo({100.f + 50.f * i - 100, 100.f + 50.f * i}); + path.lineTo({100.f + 50.f * i, 100.f + 50.f * i - 100}); + path.lineTo({100.f + 50.f * i - 100, 100.f + 50.f * i - 100}); + path.close(); + } + } + + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kRed().withAlpha(102)); + builder.DrawPath(path, paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, ArcWithZeroSweepAndBlur) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + + DlPaint paint; + paint.setColor(DlColor::kRed()); + + std::vector colors = {DlColor{1.0, 0.0, 0.0, 1.0}, + DlColor{0.0, 0.0, 0.0, 1.0}}; + std::vector stops = {0.0, 1.0}; + + paint.setColorSource( + DlColorSource::MakeSweep({100, 100}, 45, 135, stops.size(), colors.data(), + stops.data(), DlTileMode::kMirror)); + paint.setMaskFilter(DlBlurMaskFilter::Make(DlBlurStyle::kNormal, 20)); + + SkPath path; + path.addArc(SkRect::MakeXYWH(10, 10, 100, 100), 0, 0); + builder.DrawPath(path, paint); + + // Check that this empty picture can be created without crashing. + builder.Build(); +} + +TEST_P(AiksTest, CanRenderClips) { + DisplayListBuilder builder; + DlPaint paint; + paint.setColor(DlColor::kFuchsia()); + + builder.ClipPath(SkPath::Rect(SkRect::MakeXYWH(0, 0, 500, 500))); + builder.DrawPath(SkPath::Circle(500, 500, 250), paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderOverlappingMultiContourPath) { + DisplayListBuilder builder; + + DlPaint paint; + paint.setColor(DlColor::kRed()); + + SkPoint radii[4] = {{50, 50}, {50, 50}, {50, 50}, {50, 50}}; + + const Scalar kTriangleHeight = 100; + SkRRect rrect; + rrect.setRectRadii( + SkRect::MakeXYWH(-kTriangleHeight / 2.0f, -kTriangleHeight / 2.0f, + kTriangleHeight, kTriangleHeight), + radii // + ); + + builder.Translate(200, 200); + // Form a path similar to the Material drop slider value indicator. Both + // shapes should render identically side-by-side. + { + SkPath path; + path.moveTo(0, kTriangleHeight); + path.lineTo(-kTriangleHeight / 2.0f, 0); + path.lineTo(kTriangleHeight / 2.0f, 0); + path.close(); + path.addRRect(rrect); + + builder.DrawPath(path, paint); + } + builder.Translate(100, 0); + + { + SkPath path; + path.moveTo(0, kTriangleHeight); + path.lineTo(-kTriangleHeight / 2.0f, 0); + path.lineTo(0, -10); + path.lineTo(kTriangleHeight / 2.0f, 0); + path.close(); + path.addRRect(rrect); + + builder.DrawPath(path, paint); + } + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + } // namespace testing } // namespace impeller