#include "ash/style/rounded_rect_cutout_path_builder.h"
#include <array>
#include <utility>
#include "base/check_op.h"
#include "base/containers/fixed_flat_map.h"
#include "base/logging.h"
#include "third_party/skia/include/core/SkPathBuilder.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkSize.h"
namespace ash {
namespace {
enum class Direction { kCounterClockwise, kClockwise };
class CornersSequence {
public:
CornersSequence(RoundedRectCutoutPathBuilder::Corner start,
Direction direction)
: direction_(direction) {
size_t i = 0;
for (; i < kOrderedCorners.size(); i++) {
if (kOrderedCorners[i] == start) {
break;
}
}
current_ = i;
}
RoundedRectCutoutPathBuilder::Corner Next() {
RoundedRectCutoutPathBuilder::Corner corner = kOrderedCorners[current_];
current_ = NextIndex(current_, direction_);
return corner;
}
RoundedRectCutoutPathBuilder::Corner Back() {
RoundedRectCutoutPathBuilder::Corner corner = kOrderedCorners[current_];
Direction reversed_direction = direction_ == Direction::kCounterClockwise
? Direction::kClockwise
: Direction::kCounterClockwise;
current_ = NextIndex(current_, reversed_direction);
return corner;
}
RoundedRectCutoutPathBuilder::Corner OppositeCurrent() const {
return kOrderedCorners.at((current_ + 2) % 4);
}
private:
static constexpr std::array<RoundedRectCutoutPathBuilder::Corner, 4>
kOrderedCorners = {RoundedRectCutoutPathBuilder::Corner::kUpperLeft,
RoundedRectCutoutPathBuilder::Corner::kLowerLeft,
RoundedRectCutoutPathBuilder::Corner::kLowerRight,
RoundedRectCutoutPathBuilder::Corner::kUpperRight};
int NextIndex(int current, Direction direction) const {
int index = current + (direction == Direction::kCounterClockwise
? 1
: kOrderedCorners.size() - 1);
return index % kOrderedCorners.size();
}
Direction direction_;
int current_;
};
SkVector OffsetFromUpperLeft(RoundedRectCutoutPathBuilder::Corner corner,
const SkRect& rect) {
switch (corner) {
case RoundedRectCutoutPathBuilder::Corner::kUpperLeft:
return {0.f, 0.f};
case RoundedRectCutoutPathBuilder::Corner::kLowerLeft:
return {0.f, rect.height()};
case RoundedRectCutoutPathBuilder::Corner::kLowerRight:
return {rect.width(), rect.height()};
case RoundedRectCutoutPathBuilder::Corner::kUpperRight:
return {rect.width(), 0.f};
}
}
SkPoint CornerFromRect(RoundedRectCutoutPathBuilder::Corner corner,
const SkRect& rect) {
SkPoint top_left(rect.left(), rect.top());
return top_left + OffsetFromUpperLeft(corner, rect);
}
void MatchCorners(RoundedRectCutoutPathBuilder::Corner view_corner,
const SkRect& view,
RoundedRectCutoutPathBuilder::Corner rect_corner,
SkRect& rect) {
SkPoint new_location = CornerFromRect(view_corner, view) -
OffsetFromUpperLeft(rect_corner, rect);
rect.offsetTo(new_location.x(), new_location.y());
}
void MoveRectToCorner(RoundedRectCutoutPathBuilder::Corner corner,
const SkRect& view,
SkRect& rect) {
MatchCorners(corner, view, corner, rect);
}
std::array<SkRect, 3> PlaceRects(RoundedRectCutoutPathBuilder::Corner corner,
const SkRect& view,
const SkSize& cutout_size,
float outer_corner_radius,
float inner_corner_radius) {
std::array<SkRect, 3> rects = {
SkRect::MakeWH(outer_corner_radius, outer_corner_radius),
SkRect::MakeWH(inner_corner_radius, inner_corner_radius),
SkRect::MakeWH(outer_corner_radius, outer_corner_radius)};
SkRect cutout = SkRect::MakeSize(cutout_size);
MoveRectToCorner(corner, view, cutout);
CornersSequence sequence(corner, Direction::kClockwise);
sequence.Next();
MatchCorners(sequence.Next(), cutout, corner, rects[0]);
MoveRectToCorner(sequence.Next(), cutout, rects[1]);
MatchCorners(sequence.Next(), cutout, corner, rects[2]);
CHECK(!rects[1].intersect(rects[0]));
CHECK(!rects[1].intersect(rects[2]));
return rects;
}
void AddRoundedCorner(RoundedRectCutoutPathBuilder::Corner corner,
const SkRect& rect,
SkPathBuilder& builder,
Direction direction = Direction::kCounterClockwise) {
CornersSequence sequence(corner, direction);
sequence.Back();
SkPoint start = CornerFromRect(sequence.Next(), rect);
SkPoint control_point = CornerFromRect(sequence.Next(), rect);
SkPoint end = CornerFromRect(sequence.Next(), rect);
builder.lineTo(start);
builder.conicTo(control_point, end, SK_ScalarRoot2Over2);
}
SkRect AddCutoutPaths(RoundedRectCutoutPathBuilder::Corner corner,
SkPathBuilder& builder,
const SkRect& view,
const SkSize& cutout_size,
int outer_corner_radius,
int inner_corner_radius) {
std::array<SkRect, 3> rects = PlaceRects(
corner, view, cutout_size, outer_corner_radius, inner_corner_radius);
AddRoundedCorner(corner, rects[0], builder);
CornersSequence sequence(corner, Direction::kCounterClockwise);
AddRoundedCorner(sequence.OppositeCurrent(), rects[1], builder,
Direction::kClockwise);
AddRoundedCorner(corner, rects[2], builder);
SkRect union_rect = rects[0];
union_rect.join(rects[1]);
union_rect.join(rects[2]);
return union_rect;
}
}
RoundedRectCutoutPathBuilder::RoundedRectCutoutPathBuilder(gfx::SizeF bounds)
: bounds_(bounds) {
CHECK_GE(bounds.width(), corner_radius_ * 2.f)
<< "Width must be at least twice as large as corner radius";
CHECK_GE(bounds.height(), corner_radius_ * 2.f)
<< "Height must be at least twice as large as corner radius";
}
RoundedRectCutoutPathBuilder::RoundedRectCutoutPathBuilder(
const RoundedRectCutoutPathBuilder&) = default;
RoundedRectCutoutPathBuilder& RoundedRectCutoutPathBuilder::operator=(
const RoundedRectCutoutPathBuilder&) = default;
RoundedRectCutoutPathBuilder::~RoundedRectCutoutPathBuilder() = default;
RoundedRectCutoutPathBuilder& RoundedRectCutoutPathBuilder::CornerRadius(
int radius) {
corner_radius_ = radius;
return *this;
}
RoundedRectCutoutPathBuilder& RoundedRectCutoutPathBuilder::AddCutout(
RoundedRectCutoutPathBuilder::Corner corner,
gfx::SizeF size) {
if (size.IsZero()) {
cutouts_.erase(corner);
return *this;
}
cutouts_[corner] = size;
return *this;
}
RoundedRectCutoutPathBuilder&
RoundedRectCutoutPathBuilder::CutoutInnerCornerRadius(int radius) {
cutout_inner_corner_radius_ = radius;
return *this;
}
RoundedRectCutoutPathBuilder&
RoundedRectCutoutPathBuilder::CutoutOuterCornerRadius(int radius) {
cutout_outer_corner_radius_ = radius;
return *this;
}
SkPath RoundedRectCutoutPathBuilder::Build() {
SkRect view = SkRect::MakeWH(bounds_.width(), bounds_.height());
CHECK_GE(view.width(), corner_radius_ * 2.f)
<< "Width must be at least twice as large as corner radius";
CHECK_GE(view.height(), corner_radius_ * 2.f)
<< "Height must be at least twice as large as corner radius";
SkPathBuilder builder;
builder.moveTo(view.width() / 2.f, view.top());
std::vector<SkRect> drawn_cutouts;
drawn_cutouts.reserve(4);
CornersSequence around(RoundedRectCutoutPathBuilder::Corner::kUpperLeft,
Direction::kCounterClockwise);
RoundedRectCutoutPathBuilder::Corner cur_corner = around.Next();
do {
auto iter = cutouts_.find(cur_corner);
if (iter != cutouts_.end()) {
gfx::SizeF size = iter->second;
CHECK_GE(bounds_.width(),
(size.width() + cutout_outer_corner_radius_ + corner_radius_))
<< "Cutout width + outer corner radius + corner radius must be less "
"than or equal to bounds width";
CHECK_GE(bounds_.height(),
(size.height() + cutout_outer_corner_radius_ + corner_radius_))
<< "Cutout height + outer corner radius + corner radius must be less "
"than or equal to bounds height";
SkRect rect = AddCutoutPaths(
cur_corner, builder, view, SkSize::Make(size.width(), size.height()),
cutout_outer_corner_radius_, cutout_inner_corner_radius_);
drawn_cutouts.push_back(rect);
} else {
if (corner_radius_ == 0) {
SkPoint point = CornerFromRect(cur_corner, view);
builder.lineTo(point);
} else {
SkRect rect = SkRect::MakeWH(corner_radius_, corner_radius_);
MoveRectToCorner(cur_corner, view, rect);
AddRoundedCorner(cur_corner, rect, builder);
}
}
cur_corner = around.Next();
} while (cur_corner != RoundedRectCutoutPathBuilder::Corner::kUpperLeft);
for (size_t i = 0; i < drawn_cutouts.size(); i++) {
const SkRect& cutout = drawn_cutouts[i];
for (size_t j = i + 1; j < drawn_cutouts.size(); j++) {
CHECK(!cutout.intersects(drawn_cutouts[j]))
<< "At least two cutouts intersect and the path is invalid";
}
}
builder.close();
return builder.detach();
}
}