| // 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/utility/arc_curve_path_util.h" |
| |
| #include <optional> |
| |
| #include "base/check_op.h" |
| #include "base/logging.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "third_party/skia/include/core/SkPathBuilder.h" |
| #include "third_party/skia/include/core/SkPoint.h" |
| #include "ui/gfx/geometry/size.h" |
| |
| namespace ash::util { |
| |
| namespace { |
| |
| // Aliases --------------------------------------------------------------------- |
| |
| using CornerLocation = ArcCurveCorner::CornerLocation; |
| |
| // Helpers --------------------------------------------------------------------- |
| |
| // Returns the corner point specified by `location` on the bounding rectangle |
| // of `size`. |
| SkPoint GetCornerPoint(const gfx::Size& size, CornerLocation location) { |
| bool at_left = false; |
| switch (location) { |
| case CornerLocation::kBottomLeft: |
| case CornerLocation::kTopLeft: |
| at_left = true; |
| break; |
| case CornerLocation::kBottomRight: |
| case CornerLocation::kTopRight: |
| at_left = false; |
| break; |
| } |
| |
| bool at_bottom = false; |
| switch (location) { |
| case CornerLocation::kBottomLeft: |
| case CornerLocation::kBottomRight: |
| at_bottom = true; |
| break; |
| case CornerLocation::kTopLeft: |
| case CornerLocation::kTopRight: |
| at_bottom = false; |
| break; |
| } |
| |
| return SkPoint::Make(at_left ? 0 : size.width(), |
| at_bottom ? size.height() : 0); |
| } |
| |
| // Returns the next corner counterclockwise from `corner`. |
| CornerLocation GetNextCorner(CornerLocation corner) { |
| return corner == CornerLocation::kMax |
| ? CornerLocation::kMin |
| : static_cast<CornerLocation>(static_cast<size_t>(corner) + 1); |
| } |
| |
| // Returns the previous corner counterclockwise from `corner`. |
| CornerLocation GetPrevCorner(CornerLocation corner) { |
| return corner == CornerLocation::kMin |
| ? CornerLocation::kMax |
| : static_cast<CornerLocation>(static_cast<size_t>(corner) - 1); |
| } |
| |
| bool IsSizeAndCornerRadiusValid(const gfx::Size& size, |
| const std::optional<size_t>& corner_radius) { |
| if (size.IsEmpty()) { |
| LOG(ERROR) << "GetArcCurveRectPath() is called with an empty size: " |
| << size.ToString(); |
| return false; |
| } |
| |
| if (corner_radius && |
| (2 * corner_radius.value() > |
| static_cast<size_t>(std::min(size.width(), size.height())))) { |
| LOG(ERROR) << "GetArcCurveRectPath() is called with a size that is too " |
| "small for rounded corners; the size: " |
| << size.ToString() << ", corner radius: " << *corner_radius; |
| |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| // ArcCurveCorner -------------------------------------------------------------- |
| |
| ArcCurveCorner::ArcCurveCorner(CornerLocation location, |
| const gfx::Size& size, |
| float concave_radius, |
| float convex_radius) |
| : location(location), |
| size(size), |
| concave_radius(concave_radius), |
| convex_radius(convex_radius) { |
| CHECK_LE(convex_radius * 2 + concave_radius, size.width()); |
| CHECK_LE(convex_radius * 2 + concave_radius, size.height()); |
| } |
| |
| // Utils ----------------------------------------------------------------------- |
| |
| SkPath GetArcCurveRectPath(const gfx::Size& size, const size_t corner_radius) { |
| if (!IsSizeAndCornerRadiusValid(size, corner_radius)) { |
| return SkPath(); |
| } |
| |
| const auto width = size.width(); |
| const auto height = size.height(); |
| |
| const auto bottom_left = SkPoint::Make(0.f, height); |
| const auto bottom_right = SkPoint::Make(width, height); |
| const auto top_right = SkPoint::Make(width, 0.f); |
| const auto top_left = SkPoint::Make(0.f, 0.f); |
| |
| // One-radius offsets that can be added to or subtracted from coordinates to |
| // indicate a unidirectional move, e.g., when calculating the endpoint of an |
| // arc. |
| const auto horizontal_offset = SkPoint::Make(corner_radius, 0.f); |
| const auto vertical_offset = SkPoint::Make(0.f, corner_radius); |
| |
| return SkPathBuilder() |
| // Start just after the curve of the top-left rounded corner. |
| .moveTo(0.f, corner_radius) |
| .arcTo(bottom_left, bottom_left + horizontal_offset, corner_radius) |
| .arcTo(bottom_right, bottom_right - vertical_offset, corner_radius) |
| .arcTo(top_right, top_right - horizontal_offset, corner_radius) |
| .arcTo(top_left, top_left + vertical_offset, corner_radius) |
| .close() |
| .detach(); |
| } |
| |
| SkPath GetArcCurveRectPath(const gfx::Size& size, |
| const ArcCurveCorner& arc_curve_corner, |
| const std::optional<size_t>& corner_radius) { |
| if (!IsSizeAndCornerRadiusValid(size, corner_radius)) { |
| return SkPath(); |
| } |
| |
| if (const gfx::Size& arc_corner_size = arc_curve_corner.size; |
| size.height() < arc_corner_size.height() || |
| size.width() < arc_corner_size.width()) { |
| LOG(ERROR) << "GetArcCurveRectPath() is called with a size that is too " |
| "small for the arc curve corner; the size: " |
| << size.ToString() << ", arc_curve_corner size: " |
| << arc_curve_corner.size.ToString(); |
| |
| return SkPath(); |
| } |
| |
| // Iterate all corners counterclockwise, starting from the top left corner. |
| // Therefore, the total iteration count should be 4. |
| SkPathBuilder builder; |
| CornerLocation current_corner = CornerLocation::kMin; |
| for (size_t iteration_index = 0; iteration_index < 4; ++iteration_index) { |
| const SkPoint current_corner_point = GetCornerPoint(size, current_corner); |
| |
| // Calculate the normalized vector from the previous corner counterclockwise |
| // to `current_corner`. For example, if `current_corner` is the top left |
| // one, this vector should be (-1, 0). |
| SkVector prev_to_current_normalized_offset = |
| current_corner_point - |
| GetCornerPoint(size, GetPrevCorner(current_corner)); |
| SkVector::Normalize(&prev_to_current_normalized_offset); |
| |
| const bool is_arc_curve = current_corner == arc_curve_corner.location; |
| |
| // Calculate the starting point of the path for `current_corner`. |
| const SkVector start_offset = |
| SkVector::Make(prev_to_current_normalized_offset.x() * |
| (is_arc_curve ? arc_curve_corner.size.width() |
| : corner_radius.value_or(0)), |
| prev_to_current_normalized_offset.y() * |
| (is_arc_curve ? arc_curve_corner.size.height() |
| : corner_radius.value_or(0))); |
| const SkPoint corner_path_start = current_corner_point - start_offset; |
| |
| if (builder.snapshot().isEmpty()) { |
| builder.moveTo(corner_path_start); |
| } else { |
| builder.lineTo(corner_path_start); |
| } |
| |
| // Calculate the normalized vector from `current_corner` to the next corner |
| // counterclockwise. For example, if `current_corner` is the top left one, |
| // this vector should be (0, 1). |
| SkVector current_to_next_normalized_offset = |
| GetCornerPoint(size, GetNextCorner(current_corner)) - |
| current_corner_point; |
| SkVector::Normalize(¤t_to_next_normalized_offset); |
| |
| const SkVector offset_sum = |
| prev_to_current_normalized_offset + current_to_next_normalized_offset; |
| |
| // Calculate the remaining offset after excluding the spacing required by |
| // the convex radius and the concave radius. |
| const float radius_sum_spacing = |
| 2 * arc_curve_corner.convex_radius + arc_curve_corner.concave_radius; |
| const auto extra_spacing_offset = |
| SkVector::Make(arc_curve_corner.size.width(), |
| arc_curve_corner.size.height()) - |
| SkVector::Make(radius_sum_spacing, radius_sum_spacing); |
| |
| if (is_arc_curve) { |
| // Draw the first convex curve. |
| SkPoint arc1 = corner_path_start + prev_to_current_normalized_offset * |
| arc_curve_corner.convex_radius; |
| SkPoint arc2 = |
| corner_path_start + offset_sum * arc_curve_corner.convex_radius; |
| builder.arcTo(arc1, arc2, arc_curve_corner.convex_radius); |
| |
| // Draw the extra spacing. |
| arc2 = arc2 + SkVector::Make(extra_spacing_offset.x() * |
| current_to_next_normalized_offset.x(), |
| extra_spacing_offset.y() * |
| current_to_next_normalized_offset.y()); |
| builder.lineTo(arc2); |
| |
| // Draw the concave curve. |
| SkPoint last_point = arc2; |
| arc1 = last_point + current_to_next_normalized_offset * |
| arc_curve_corner.concave_radius; |
| arc2 = last_point + offset_sum * arc_curve_corner.concave_radius; |
| builder.arcTo(arc1, arc2, arc_curve_corner.concave_radius); |
| |
| // Draw the extra spacing. |
| last_point = arc2; |
| arc2 = |
| last_point + |
| SkVector::Make( |
| extra_spacing_offset.x() * prev_to_current_normalized_offset.x(), |
| extra_spacing_offset.y() * prev_to_current_normalized_offset.y()); |
| builder.lineTo(arc2); |
| |
| // Draw the second convex curve. |
| last_point = arc2; |
| arc1 = last_point + |
| prev_to_current_normalized_offset * arc_curve_corner.convex_radius; |
| arc2 = last_point + offset_sum * arc_curve_corner.convex_radius; |
| builder.arcTo(arc1, arc2, arc_curve_corner.convex_radius); |
| } else if (corner_radius) { |
| const SkPoint arc1 = |
| corner_path_start + |
| prev_to_current_normalized_offset * corner_radius.value(); |
| const SkPoint arc2 = |
| corner_path_start + offset_sum * corner_radius.value(); |
| builder.arcTo(arc1, arc2, corner_radius.value()); |
| } |
| |
| if (current_corner == CornerLocation::kMax) { |
| builder.close(); |
| } else { |
| current_corner = GetNextCorner(current_corner); |
| } |
| } |
| |
| return builder.detach(); |
| } |
| |
| } // namespace ash::util |