blob: f09fa4a40bfc5a94222a95cba6911502d18744f5 [file] [log] [blame]
// 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 "display_list/display_list_blend_mode.h"
#include "display_list/display_list_color.h"
#include "display_list/display_list_color_filter.h"
#include "display_list/display_list_image_filter.h"
#include "display_list/display_list_paint.h"
#include "display_list/display_list_tile_mode.h"
#include "flutter/display_list/display_list_builder.h"
#include "flutter/display_list/display_list_mask_filter.h"
#include "flutter/display_list/types.h"
#include "flutter/testing/testing.h"
#include "impeller/display_list/display_list_image_impeller.h"
#include "impeller/display_list/display_list_playground.h"
#include "impeller/geometry/point.h"
#include "impeller/playground/widgets.h"
#include "include/core/SkRRect.h"
#include "third_party/imgui/imgui.h"
#include "third_party/skia/include/core/SkClipOp.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPathBuilder.h"
namespace impeller {
namespace testing {
using DisplayListTest = DisplayListPlayground;
INSTANTIATE_PLAYGROUND_SUITE(DisplayListTest);
TEST_P(DisplayListTest, CanDrawRect) {
flutter::DisplayListBuilder builder;
builder.setColor(SK_ColorBLUE);
builder.drawRect(SkRect::MakeXYWH(10, 10, 100, 100));
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawTextBlob) {
flutter::DisplayListBuilder builder;
builder.setColor(SK_ColorBLUE);
builder.drawTextBlob(SkTextBlob::MakeFromString("Hello", CreateTestFont()),
100, 100);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawImage) {
auto texture = CreateTextureForFixture("embarcadero.jpg");
flutter::DisplayListBuilder builder;
builder.drawImage(DlImageImpeller::Make(texture), SkPoint::Make(100, 100),
flutter::DlImageSampling::kNearestNeighbor, true);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawCapsAndJoins) {
flutter::DisplayListBuilder builder;
builder.setStyle(flutter::DlDrawStyle::kStroke);
builder.setStrokeWidth(30);
builder.setColor(SK_ColorRED);
auto path =
SkPathBuilder{}.moveTo(-50, 0).lineTo(0, -50).lineTo(50, 0).snapshot();
builder.translate(100, 100);
{
builder.setStrokeCap(flutter::DlStrokeCap::kButt);
builder.setStrokeJoin(flutter::DlStrokeJoin::kMiter);
builder.setStrokeMiter(4);
builder.drawPath(path);
}
{
builder.save();
builder.translate(0, 100);
// The joint in the path is 45 degrees. A miter length of 1 convert to a
// bevel in this case.
builder.setStrokeMiter(1);
builder.drawPath(path);
builder.restore();
}
builder.translate(150, 0);
{
builder.setStrokeCap(flutter::DlStrokeCap::kSquare);
builder.setStrokeJoin(flutter::DlStrokeJoin::kBevel);
builder.drawPath(path);
}
builder.translate(150, 0);
{
builder.setStrokeCap(flutter::DlStrokeCap::kRound);
builder.setStrokeJoin(flutter::DlStrokeJoin::kRound);
builder.drawPath(path);
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawArc) {
bool first_frame = true;
auto callback = [&]() {
if (first_frame) {
first_frame = false;
ImGui::SetNextWindowSize({400, 100});
ImGui::SetNextWindowPos({300, 550});
}
static float start_angle = 45;
static float sweep_angle = 270;
static bool use_center = true;
ImGui::Begin("Controls");
ImGui::SliderFloat("Start angle", &start_angle, -360, 360);
ImGui::SliderFloat("Sweep angle", &sweep_angle, -360, 360);
ImGui::Checkbox("Use center", &use_center);
ImGui::End();
auto [p1, p2] = IMPELLER_PLAYGROUND_LINE(
Point(200, 200), Point(400, 400), 20, Color::White(), Color::White());
flutter::DisplayListBuilder builder;
Vector2 scale = GetContentScale();
builder.scale(scale.x, scale.y);
builder.setStyle(flutter::DlDrawStyle::kStroke);
builder.setStrokeCap(flutter::DlStrokeCap::kRound);
builder.setStrokeJoin(flutter::DlStrokeJoin::kMiter);
builder.setStrokeMiter(10);
auto rect = SkRect::MakeLTRB(p1.x, p1.y, p2.x, p2.y);
builder.setColor(SK_ColorGREEN);
builder.setStrokeWidth(2);
builder.drawRect(rect);
builder.setColor(SK_ColorRED);
builder.setStrokeWidth(10);
builder.drawArc(rect, start_angle, sweep_angle, use_center);
return builder.Build();
};
ASSERT_TRUE(OpenPlaygroundHere(callback));
}
TEST_P(DisplayListTest, StrokedPathsDrawCorrectly) {
flutter::DisplayListBuilder builder;
builder.setColor(SK_ColorRED);
builder.setStyle(flutter::DlDrawStyle::kStroke);
builder.setStrokeWidth(10);
// Rectangle
builder.translate(100, 100);
builder.drawRect(SkRect::MakeSize({100, 100}));
// Rounded rectangle
builder.translate(150, 0);
builder.drawRRect(SkRRect::MakeRectXY(SkRect::MakeSize({100, 50}), 10, 10));
// Double rounded rectangle
builder.translate(150, 0);
builder.drawDRRect(
SkRRect::MakeRectXY(SkRect::MakeSize({100, 50}), 10, 10),
SkRRect::MakeRectXY(SkRect::MakeXYWH(10, 10, 80, 30), 10, 10));
// Contour with duplicate join points
{
builder.translate(150, 0);
SkPath path;
path.lineTo({100, 0});
path.lineTo({100, 0});
path.lineTo({100, 100});
builder.drawPath(path);
}
// Contour with duplicate end points
{
builder.setStrokeCap(flutter::DlStrokeCap::kRound);
builder.translate(150, 0);
SkPath path;
path.moveTo(0, 0);
path.lineTo({0, 0});
path.lineTo({50, 50});
path.lineTo({100, 0});
path.lineTo({100, 0});
builder.drawPath(path);
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawWithOddPathWinding) {
flutter::DisplayListBuilder builder;
builder.setColor(SK_ColorRED);
builder.setStyle(flutter::DlDrawStyle::kFill);
builder.translate(300, 300);
SkPath path;
path.setFillType(SkPathFillType::kEvenOdd);
path.addCircle(0, 0, 100);
path.addCircle(0, 0, 50);
builder.drawPath(path);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawWithMaskBlur) {
auto texture = CreateTextureForFixture("embarcadero.jpg");
flutter::DisplayListBuilder builder;
// Mask blurred image.
{
auto filter = flutter::DlBlurMaskFilter(kNormal_SkBlurStyle, 10.0f);
builder.setMaskFilter(&filter);
builder.drawImage(DlImageImpeller::Make(texture), SkPoint::Make(100, 100),
flutter::DlImageSampling::kNearestNeighbor, true);
}
// Mask blurred filled path.
{
builder.setColor(SK_ColorYELLOW);
auto filter = flutter::DlBlurMaskFilter(kOuter_SkBlurStyle, 10.0f);
builder.setMaskFilter(&filter);
builder.drawArc(SkRect::MakeXYWH(410, 110, 100, 100), 45, 270, true);
}
// Mask blurred text.
{
auto filter = flutter::DlBlurMaskFilter(kSolid_SkBlurStyle, 10.0f);
builder.setMaskFilter(&filter);
builder.drawTextBlob(
SkTextBlob::MakeFromString("Testing", CreateTestFont()), 220, 170);
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawWithBlendColorFilter) {
auto texture = CreateTextureForFixture("embarcadero.jpg");
flutter::DisplayListBuilder builder;
// Pipeline blended image.
{
auto filter = flutter::DlBlendColorFilter(SK_ColorYELLOW,
flutter::DlBlendMode::kModulate);
builder.setColorFilter(&filter);
builder.drawImage(DlImageImpeller::Make(texture), SkPoint::Make(100, 100),
flutter::DlImageSampling::kNearestNeighbor, true);
}
// Advanced blended image.
{
auto filter =
flutter::DlBlendColorFilter(SK_ColorRED, flutter::DlBlendMode::kScreen);
builder.setColorFilter(&filter);
builder.drawImage(DlImageImpeller::Make(texture), SkPoint::Make(250, 250),
flutter::DlImageSampling::kNearestNeighbor, true);
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawWithImageBlurFilter) {
auto texture = CreateTextureForFixture("embarcadero.jpg");
bool first_frame = true;
auto callback = [&]() {
if (first_frame) {
first_frame = false;
ImGui::SetNextWindowSize({400, 100});
ImGui::SetNextWindowPos({300, 550});
}
static float sigma[] = {10, 10};
ImGui::Begin("Controls");
ImGui::SliderFloat2("Sigma", sigma, 0, 100);
ImGui::End();
flutter::DisplayListBuilder builder;
auto filter = flutter::DlBlurImageFilter(sigma[0], sigma[1],
flutter::DlTileMode::kClamp);
builder.setImageFilter(&filter);
builder.drawImage(DlImageImpeller::Make(texture), SkPoint::Make(200, 200),
flutter::DlImageSampling::kNearestNeighbor, true);
return builder.Build();
};
ASSERT_TRUE(OpenPlaygroundHere(callback));
}
TEST_P(DisplayListTest, CanDrawBackdropFilter) {
auto texture = CreateTextureForFixture("embarcadero.jpg");
bool first_frame = true;
auto callback = [&]() {
if (first_frame) {
first_frame = false;
ImGui::SetNextWindowPos({10, 10});
}
static float sigma[] = {10, 10};
static float ctm_scale = 1;
static bool use_bounds = true;
static bool draw_circle = true;
static bool add_clip = true;
ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
ImGui::SliderFloat2("Sigma", sigma, 0, 100);
ImGui::SliderFloat("Scale", &ctm_scale, 0, 10);
ImGui::NewLine();
ImGui::TextWrapped(
"If everything is working correctly, none of the options below should "
"impact the filter's appearance.");
ImGui::Checkbox("Use SaveLayer bounds", &use_bounds);
ImGui::Checkbox("Draw child element", &draw_circle);
ImGui::Checkbox("Add pre-clip", &add_clip);
ImGui::End();
flutter::DisplayListBuilder builder;
Vector2 scale = ctm_scale * GetContentScale();
builder.scale(scale.x, scale.y);
auto filter = flutter::DlBlurImageFilter(sigma[0], sigma[1],
flutter::DlTileMode::kClamp);
std::optional<SkRect> bounds;
if (use_bounds) {
auto [p1, p2] = IMPELLER_PLAYGROUND_LINE(
Point(350, 150), Point(800, 600), 20, Color::White(), Color::White());
bounds = SkRect::MakeLTRB(p1.x, p1.y, p2.x, p2.y);
}
// Insert a clip to test that the backdrop filter handles stencil depths > 0
// correctly.
if (add_clip) {
builder.clipRect(SkRect::MakeLTRB(0, 0, 99999, 99999),
SkClipOp::kIntersect, true);
}
builder.drawImage(DlImageImpeller::Make(texture), SkPoint::Make(200, 200),
flutter::DlImageSampling::kNearestNeighbor, true);
builder.saveLayer(bounds.has_value() ? &bounds.value() : nullptr, nullptr,
&filter);
if (draw_circle) {
auto circle_center =
IMPELLER_PLAYGROUND_POINT(Point(500, 400), 20, Color::Red());
builder.setStyle(flutter::DlDrawStyle::kStroke);
builder.setStrokeCap(flutter::DlStrokeCap::kButt);
builder.setStrokeJoin(flutter::DlStrokeJoin::kBevel);
builder.setStrokeWidth(10);
builder.setColor(flutter::DlColor::kRed().withAlpha(100));
builder.drawCircle({circle_center.x, circle_center.y}, 100);
}
return builder.Build();
};
ASSERT_TRUE(OpenPlaygroundHere(callback));
}
TEST_P(DisplayListTest, CanDrawNinePatchImage) {
// Image is drawn with corners to scale and center pieces stretched to fit.
auto texture = CreateTextureForFixture("embarcadero.jpg");
flutter::DisplayListBuilder builder;
auto size = texture->GetSize();
builder.drawImageNine(
DlImageImpeller::Make(texture),
SkIRect::MakeLTRB(size.width / 4, size.height / 4, size.width * 3 / 4,
size.height * 3 / 4),
SkRect::MakeLTRB(0, 0, size.width * 2, size.height * 2),
flutter::DlFilterMode::kNearest, true);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawNinePatchImageCenterWidthBiggerThanDest) {
// Edge case, the width of the corners does not leave any room for the
// center slice. The center (across the vertical axis) is folded out of the
// resulting image.
auto texture = CreateTextureForFixture("embarcadero.jpg");
flutter::DisplayListBuilder builder;
auto size = texture->GetSize();
builder.drawImageNine(
DlImageImpeller::Make(texture),
SkIRect::MakeLTRB(size.width / 4, size.height / 4, size.width * 3 / 4,
size.height * 3 / 4),
SkRect::MakeLTRB(0, 0, size.width / 2, size.height),
flutter::DlFilterMode::kNearest, true);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawNinePatchImageCenterHeightBiggerThanDest) {
// Edge case, the height of the corners does not leave any room for the
// center slice. The center (across the horizontal axis) is folded out of the
// resulting image.
auto texture = CreateTextureForFixture("embarcadero.jpg");
flutter::DisplayListBuilder builder;
auto size = texture->GetSize();
builder.drawImageNine(
DlImageImpeller::Make(texture),
SkIRect::MakeLTRB(size.width / 4, size.height / 4, size.width * 3 / 4,
size.height * 3 / 4),
SkRect::MakeLTRB(0, 0, size.width, size.height / 2),
flutter::DlFilterMode::kNearest, true);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawNinePatchImageCenterBiggerThanDest) {
// Edge case, the width and height of the corners does not leave any
// room for the center slices. Only the corners are displayed.
auto texture = CreateTextureForFixture("embarcadero.jpg");
flutter::DisplayListBuilder builder;
auto size = texture->GetSize();
builder.drawImageNine(
DlImageImpeller::Make(texture),
SkIRect::MakeLTRB(size.width / 4, size.height / 4, size.width * 3 / 4,
size.height * 3 / 4),
SkRect::MakeLTRB(0, 0, size.width / 2, size.height / 2),
flutter::DlFilterMode::kNearest, true);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawNinePatchImageCornersScaledDown) {
// Edge case, there is not enough room for the corners to be drawn
// without scaling them down.
auto texture = CreateTextureForFixture("embarcadero.jpg");
flutter::DisplayListBuilder builder;
auto size = texture->GetSize();
builder.drawImageNine(
DlImageImpeller::Make(texture),
SkIRect::MakeLTRB(size.width / 4, size.height / 4, size.width * 3 / 4,
size.height * 3 / 4),
SkRect::MakeLTRB(0, 0, size.width / 4, size.height / 4),
flutter::DlFilterMode::kNearest, true);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawPoints) {
flutter::DisplayListBuilder builder;
SkPoint points[7] = {
{0, 0}, //
{100, 100}, //
{100, 0}, //
{0, 100}, //
{0, 0}, //
{48, 48}, //
{52, 52}, //
};
std::vector<flutter::DlStrokeCap> caps = {
flutter::DlStrokeCap::kButt,
flutter::DlStrokeCap::kRound,
flutter::DlStrokeCap::kSquare,
};
flutter::DlPaint paint =
flutter::DlPaint() //
.setColor(flutter::DlColor::kYellow().withAlpha(127)) //
.setStrokeWidth(20);
builder.translate(50, 50);
for (auto cap : caps) {
paint.setStrokeCap(cap);
builder.save();
builder.drawPoints(SkCanvas::kPoints_PointMode, 7, points, paint);
builder.translate(150, 0);
builder.drawPoints(SkCanvas::kLines_PointMode, 5, points, paint);
builder.translate(150, 0);
builder.drawPoints(SkCanvas::kPolygon_PointMode, 5, points, paint);
builder.restore();
builder.translate(0, 150);
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawZeroLengthLine) {
flutter::DisplayListBuilder builder;
std::vector<flutter::DlStrokeCap> caps = {
flutter::DlStrokeCap::kButt,
flutter::DlStrokeCap::kRound,
flutter::DlStrokeCap::kSquare,
};
flutter::DlPaint paint =
flutter::DlPaint() //
.setColor(flutter::DlColor::kYellow().withAlpha(127)) //
.setDrawStyle(flutter::DlDrawStyle::kStroke) //
.setStrokeCap(flutter::DlStrokeCap::kButt) //
.setStrokeWidth(20);
SkPath path = SkPath().addPoly({{150, 50}, {150, 50}}, false);
for (auto cap : caps) {
paint.setStrokeCap(cap);
builder.drawLine({50, 50}, {50, 50}, paint);
builder.drawPath(path, paint);
builder.translate(0, 150);
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawShadow) {
flutter::DisplayListBuilder builder;
std::array<SkPath, 3> paths = {
SkPath{}.addRect(SkRect::MakeXYWH(0, 0, 200, 100)),
SkPath{}.addRRect(
SkRRect::MakeRectXY(SkRect::MakeXYWH(0, 0, 200, 100), 30, 30)),
SkPath{}.addCircle(100, 50, 50),
};
builder.setColor(flutter::DlColor::kWhite());
builder.drawPaint();
builder.setColor(flutter::DlColor::kCyan());
builder.translate(100, 100);
for (size_t x = 0; x < paths.size(); x++) {
builder.save();
for (size_t y = 0; y < 5; y++) {
builder.drawShadow(paths[x], flutter::DlColor::kBlack(), 3 + y * 5, false,
1);
builder.drawPath(paths[x]);
builder.translate(0, 200);
}
builder.restore();
builder.translate(300, 0);
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
// Draw a hexagon using triangle fan
TEST_P(DisplayListTest, CanConvertTriangleFanToTriangles) {
constexpr Scalar hexagon_radius = 125;
auto hex_start = Point(200.0, -hexagon_radius + 200.0);
auto center_to_flat = 1.73 / 2 * hexagon_radius;
// clang-format off
std::vector<SkPoint> vertices = {
SkPoint::Make(hex_start.x, hex_start.y),
SkPoint::Make(hex_start.x + center_to_flat, hex_start.y + 0.5 * hexagon_radius),
SkPoint::Make(hex_start.x + center_to_flat, hex_start.y + 1.5 * hexagon_radius),
SkPoint::Make(hex_start.x + center_to_flat, hex_start.y + 1.5 * hexagon_radius),
SkPoint::Make(hex_start.x, hex_start.y + 2 * hexagon_radius),
SkPoint::Make(hex_start.x, hex_start.y + 2 * hexagon_radius),
SkPoint::Make(hex_start.x - center_to_flat, hex_start.y + 1.5 * hexagon_radius),
SkPoint::Make(hex_start.x - center_to_flat, hex_start.y + 1.5 * hexagon_radius),
SkPoint::Make(hex_start.x - center_to_flat, hex_start.y + 0.5 * hexagon_radius)
};
// clang-format on
auto paint = flutter::DlPaint().setColor(flutter::DlColor::kDarkGrey());
auto dl_vertices = flutter::DlVertices::Make(
flutter::DlVertexMode::kTriangleFan, vertices.size(), vertices.data(),
nullptr, nullptr);
flutter::DisplayListBuilder builder;
builder.drawVertices(dl_vertices, flutter::DlBlendMode::kSrcOver, paint);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(DisplayListTest, CanDrawZeroWidthLine) {
flutter::DisplayListBuilder builder;
std::vector<flutter::DlStrokeCap> caps = {
flutter::DlStrokeCap::kButt,
flutter::DlStrokeCap::kRound,
flutter::DlStrokeCap::kSquare,
};
flutter::DlPaint paint = //
flutter::DlPaint() //
.setColor(flutter::DlColor::kWhite()) //
.setDrawStyle(flutter::DlDrawStyle::kStroke) //
.setStrokeWidth(0);
flutter::DlPaint outline_paint = //
flutter::DlPaint() //
.setColor(flutter::DlColor::kYellow()) //
.setDrawStyle(flutter::DlDrawStyle::kStroke) //
.setStrokeCap(flutter::DlStrokeCap::kSquare) //
.setStrokeWidth(1);
SkPath path = SkPath().addPoly({{150, 50}, {160, 50}}, false);
for (auto cap : caps) {
paint.setStrokeCap(cap);
builder.drawLine({50, 50}, {60, 50}, paint);
builder.drawRect({45, 45, 65, 55}, outline_paint);
builder.drawLine({100, 50}, {100, 50}, paint);
if (cap != flutter::DlStrokeCap::kButt) {
builder.drawRect({95, 45, 105, 55}, outline_paint);
}
builder.drawPath(path, paint);
builder.drawRect(path.getBounds().makeOutset(5, 5), outline_paint);
builder.translate(0, 150);
}
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
} // namespace testing
} // namespace impeller