| // 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 <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: |
| // A never ending sequence of corners around a rectangle in a given |
| // `direction` starting at `start`. |
| CornersSequence(RoundedRectCutoutPathBuilder::Corner start, |
| Direction direction) |
| : direction_(direction) { |
| size_t i = 0; |
| for (; i < kOrderedCorners.size(); i++) { |
| if (kOrderedCorners[i] == start) { |
| break; |
| } |
| } |
| current_ = i; |
| } |
| |
| // Returns the current corner and advances in `direction_`. |
| RoundedRectCutoutPathBuilder::Corner Next() { |
| RoundedRectCutoutPathBuilder::Corner corner = kOrderedCorners[current_]; |
| current_ = NextIndex(current_, direction_); |
| return corner; |
| } |
| |
| // Returns the current corner then updates the current corner in the opposite |
| // of `direction_`. |
| 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; |
| } |
| |
| // Returns the corner that is opposite of the current corner. |
| RoundedRectCutoutPathBuilder::Corner OppositeCurrent() const { |
| // Since this is a rectangle, the opposite corner is always 2 steps away. |
| return kOrderedCorners.at((current_ + 2) % 4); |
| } |
| |
| private: |
| // Corners in counterclockwise order. |
| 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_; |
| // Tracks the index of the current corner. |
| int current_; |
| }; |
| |
| // Returns the offset vector from the kUpperLeft to `corner` for `rect`. |
| // |
| // NOTE: Since the vector is relative to `rect`, it is independent of the origin |
| // of `rect`. |
| 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}; |
| } |
| } |
| |
| // Returns the `corner` point of the `rect`. |
| SkPoint CornerFromRect(RoundedRectCutoutPathBuilder::Corner corner, |
| const SkRect& rect) { |
| SkPoint top_left(rect.left(), rect.top()); |
| return top_left + OffsetFromUpperLeft(corner, rect); |
| } |
| |
| // Move `rect` so that the position of `rect_corner` in `rect` matches the |
| // position of `view_corner` in `view`. |
| 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()); |
| } |
| |
| // Modifies `rect` so that the `corner` of `view` and `rect` match. |
| // e.g. If `corner` is `kBottomRight`, `rect` is moved so that the bottom right |
| // corner of `view` and `rect` match. |
| void MoveRectToCorner(RoundedRectCutoutPathBuilder::Corner corner, |
| const SkRect& view, |
| SkRect& rect) { |
| MatchCorners(corner, view, corner, rect); |
| } |
| |
| // Generates 3 rectangles (small corner, inner corner, small corner) that |
| // represent the bounds of the arcs for the cutout at `corner`. The cutout is |
| // of `cutout_size`, inscribed into `view`. Small corners have |
| // `outer_corner_radius` and the inner corner has `inner_corner_radius`. |
| // |
| // e.g. Corner::kLowerRight |
| // |
| // SS |
| // SS |
| // LLLLOO |
| // LLLLOO |
| // LLLLOO |
| // RROOOOOO |
| // RROOOOOO |
| // |
| // R = 1st outer corner, L = inner corner, S = 2nd outer corner, O = Remainder |
| // of cutout (not in rects). |
| std::array<SkRect, 3> PlaceRects(RoundedRectCutoutPathBuilder::Corner corner, |
| const SkRect& view, |
| const SkSize& cutout_size, |
| float outer_corner_radius, |
| float inner_corner_radius) { |
| // rects[0] is the first outer corner, rects[1] is the inner corner, rects[2] |
| // is the last outer corner. |
| 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); |
| // Place `cutout` in the appropriate corner of `view`. |
| MoveRectToCorner(corner, view, cutout); |
| |
| CornersSequence sequence(corner, Direction::kClockwise); |
| // For the cutout, corners are drawn in the opposite direction from how we |
| // iterate around the rectangle (because the cutouts are convex). We happen |
| // to draw all the corners except the corner where the cutout is located. So |
| // start there but skip it. |
| sequence.Next(); |
| |
| MatchCorners(sequence.Next(), cutout, corner, rects[0]); |
| MoveRectToCorner(sequence.Next(), cutout, rects[1]); |
| MatchCorners(sequence.Next(), cutout, corner, rects[2]); |
| |
| // This draws nonsensical curves if the corners overlap. |
| CHECK(!rects[1].intersect(rects[0])); |
| CHECK(!rects[1].intersect(rects[2])); |
| return rects; |
| } |
| |
| // Add a rounded path to `builder` for a `corner` of `rect`. The path is drawn |
| // counter clockwise from the corner before `corner` to the opposite corner. |
| // e.g. if `corner` is kUpperRight, the path is drawn from kLowerRight to |
| // kUpperLeft. |
| void AddRoundedCorner(RoundedRectCutoutPathBuilder::Corner corner, |
| const SkRect& rect, |
| SkPathBuilder& builder, |
| Direction direction = Direction::kCounterClockwise) { |
| CornersSequence sequence(corner, direction); |
| // The large rounded corner starts at the corner before the corner where |
| // it is drawn. So, backup one and discard it since we'll hit it again. |
| 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); |
| } |
| |
| // Adds the required paths to `builder` to draw a the cutout of |
| // `cutout_size` within `view` with `outer_corner_radius` for the first two |
| // corners and `inner_corner_radius` for the interior corner. A line will be |
| // drawn to the first conic will be drawn from the location of `builder`. |
| // The path will end at the end of the last conic. |
| SkRect AddCutoutPaths(RoundedRectCutoutPathBuilder::Corner corner, |
| SkPathBuilder& builder, |
| const SkRect& view, |
| const SkSize& cutout_size, |
| int outer_corner_radius, |
| int inner_corner_radius) { |
| // Create rectangles that enclose each of the curves. |
| std::array<SkRect, 3> rects = PlaceRects( |
| corner, view, cutout_size, outer_corner_radius, inner_corner_radius); |
| |
| // Draw the first outer corner. |
| AddRoundedCorner(corner, rects[0], builder); |
| |
| // The inner corner is drawn opposite from the current corner and in the |
| // clockwise direction because it is convex. |
| CornersSequence sequence(corner, Direction::kCounterClockwise); |
| AddRoundedCorner(sequence.OppositeCurrent(), rects[1], builder, |
| Direction::kClockwise); |
| |
| // Draw the other outer corner. |
| AddRoundedCorner(corner, rects[2], builder); |
| |
| // A rectangle enclosing all the rectangles to conservatively check for |
| // overlap. |
| SkRect union_rect = rects[0]; |
| union_rect.join(rects[1]); |
| union_rect.join(rects[2]); |
| return union_rect; |
| } |
| |
| } // namespace |
| |
| 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; |
| // Start at the top center of the rectangle. |
| builder.moveTo(view.width() / 2.f, view.top()); |
| |
| // Save cutout bounds to check for overlap. |
| std::vector<SkRect> drawn_cutouts; |
| drawn_cutouts.reserve(4); |
| |
| // Build paths counter clockwise around view. |
| CornersSequence around(RoundedRectCutoutPathBuilder::Corner::kUpperLeft, |
| Direction::kCounterClockwise); |
| RoundedRectCutoutPathBuilder::Corner cur_corner = around.Next(); |
| do { |
| auto iter = cutouts_.find(cur_corner); |
| if (iter != cutouts_.end()) { |
| // Adds the paths for the cutout. |
| 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) { |
| // If corner radius is 0, it's a point so just draw a line. |
| SkPoint point = CornerFromRect(cur_corner, view); |
| builder.lineTo(point); |
| } else { |
| // Draw the rounded corners for the larger view. |
| 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); |
| |
| // Verify that none of the cutouts intersect or the drawn shape will not work. |
| // This is checking all pairs so it's O(n^2) but n<4. |
| 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"; |
| } |
| } |
| |
| // `close()` will draw a line from the last point to the start (top middle of |
| // the shape). |
| builder.close(); |
| return builder.detach(); |
| } |
| |
| } // namespace ash |