// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/style/rounded_rect_cutout_path_builder.h"
#include <ostream>
#include "base/test/gtest_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/gfx/geometry/size_f.h"
// Pretty print SkRect for failure logs.
void PrintTo(const SkRect& size, std::ostream* os) {
*os << "(" << size.x() << ", " << size.y() << ") [" << size.width() << ", "
<< size.height() << "]";
}
namespace ash {
namespace {
constexpr gfx::SizeF kViewSize(114.f, 432.f);
TEST(RoundedRectCutoutPathBuilderTest, RectanglePoints) {
RoundedRectCutoutPathBuilder builder(kViewSize);
builder.CornerRadius(0);
SkPath path = builder.Build();
// A radius of 0 should be a rectangle so we have 4 points and the starting
// point.
EXPECT_EQ(path.countPoints(), 4 + 1);
}
TEST(RoundedRectCutoutPathBuilderTest, RoundedCorners) {
RoundedRectCutoutPathBuilder builder(kViewSize);
builder.CornerRadius(16);
SkPath path = builder.Build();
// A rounded rect has 12 points (3 for each ronded corner, one for the control
// point in conic and two for the start and end) and the starting
// point.
EXPECT_EQ(path.countPoints(), 12 + 1);
}
TEST(RoundedRectCutoutPathBuilderTest, OneCutout) {
RoundedRectCutoutPathBuilder builder(kViewSize);
builder.AddCutout(RoundedRectCutoutPathBuilder::Corner::kLowerRight,
gfx::SizeF(30, 30));
builder.CornerRadius(0);
SkPath path = builder.Build();
// Cutouts have 3 rounded corners each. Each rounded corner has 3 points (so 9
// total). There are 3 other corners and the starting point. 13 total.
EXPECT_EQ(path.countPoints(), 9 + 3 + 1);
}
TEST(RoundedRectCutoutPathBuilderTest, TwoCutouts) {
RoundedRectCutoutPathBuilder builder(kViewSize);
builder
.AddCutout(RoundedRectCutoutPathBuilder::Corner::kLowerRight,
gfx::SizeF(30, 30))
.AddCutout(RoundedRectCutoutPathBuilder::Corner::kUpperRight,
gfx::SizeF(40, 20))
.CornerRadius(0);
SkPath path = builder.Build();
// 2 cutouts, 2 normal corners, and the starting point.
EXPECT_EQ(path.countPoints(), 9 + 9 + 2 + 1);
// Ensure the path is complete.
EXPECT_TRUE(path.isLastContourClosed());
// The bounds should be equal to the View that the path will clip.
EXPECT_THAT(
path.getBounds(),
testing::Eq(SkRect::MakeSize({kViewSize.width(), kViewSize.height()})));
}
TEST(RoundedRectCutoutPathBuilderTest, RemoveCutout) {
RoundedRectCutoutPathBuilder builder(kViewSize);
builder.CornerRadius(0);
// Add cutout.
builder.AddCutout(RoundedRectCutoutPathBuilder::Corner::kUpperLeft,
gfx::SizeF(40, 40));
// Remove the cutout.
builder.AddCutout(RoundedRectCutoutPathBuilder::Corner::kUpperLeft,
gfx::SizeF());
// The resulting path should be a rectangle.
SkPath path = builder.Build();
EXPECT_EQ(path.countPoints(), 5);
SkRect bounds;
EXPECT_TRUE(path.isRect(&bounds));
EXPECT_THAT(
bounds,
testing::Eq(SkRect::MakeSize({kViewSize.width(), kViewSize.height()})));
}
TEST(RoundedRectCutoutPathBuilderTest, ExtraLargeCutout) {
RoundedRectCutoutPathBuilder builder(gfx::SizeF{100.0f, 100.0f});
// Add cutout that is more than half the height.
builder.AddCutout(RoundedRectCutoutPathBuilder::Corner::kUpperLeft,
gfx::SizeF(55.0f, 55.0f));
SkPath path = builder.Build();
// 3 * 3 points for the rounded corners. 9 points in the cutout. 1 starting
// point.
EXPECT_EQ(path.countPoints(), 9 + 9 + 1);
SkRect bounds = path.getBounds();
EXPECT_THAT(bounds, testing::Eq(SkRect::MakeSize({100.0f, 100.0f})));
}
TEST(RoundedRectCutoutPathBuilderDeathTest, MaximumCutout) {
RoundedRectCutoutPathBuilder builder(gfx::SizeF{100.0f, 100.0f});
builder.CornerRadius(4);
builder.CutoutOuterCornerRadius(8);
// cutout + outer corner + corner radius is allowed to equal the bounds.
// 4 + 8 + 88 = 100.
builder.AddCutout(RoundedRectCutoutPathBuilder::Corner::kUpperLeft,
gfx::SizeF(88.0f, 55.0f));
SkPath path = builder.Build();
EXPECT_FALSE(path.isEmpty());
}
TEST(RoundedRectCutoutPathBuilderDeathTest, CutoutTooLarge) {
RoundedRectCutoutPathBuilder builder(gfx::SizeF{100.0f, 100.0f});
builder.CornerRadius(4);
builder.CutoutOuterCornerRadius(8);
// When cutout + outer corner + corner radius is larger than the
// bounds, we expect a crash. 4 + 8 + 89 = 101.
builder.AddCutout(RoundedRectCutoutPathBuilder::Corner::kUpperLeft,
gfx::SizeF(89.0f, 55.0f));
EXPECT_CHECK_DEATH_WITH(
{
// This should crash because the cutout is larger than the bounds.
SkPath path = builder.Build();
},
"must be less than or equal to bounds");
}
TEST(RoundedRectCutoutPathBuilderDeathTest, CutoutsIntersect) {
RoundedRectCutoutPathBuilder builder(gfx::SizeF{100.0f, 100.0f});
builder.CornerRadius(8);
// Technically, this can be drawn but looks strange. So this crashes because
// we require that there is at least `outer_corner_radius * 2` between
// cutouts.
builder.AddCutout(RoundedRectCutoutPathBuilder::Corner::kUpperLeft,
gfx::SizeF(70.0f, 70.0f));
builder.AddCutout(RoundedRectCutoutPathBuilder::Corner::kUpperRight,
gfx::SizeF(30.0f, 70.0f));
EXPECT_CHECK_DEATH_WITH(
{
// Cutouts overlap so this should crash.
SkPath path = builder.Build();
},
"cutouts intersect");
}
} // namespace
} // namespace ash