| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- /// A rounded rectangle.
- ///
- /// This is not necessarily four line segments and four circular arcs. If
- /// possible, this shape uses smoother curves to make the transition between the
- /// edges and corners less abrupt.
- public struct RoundedRectangle {
- public var cornerRadius: Double
- /// Creates a ``RoundedRectangle`` instance.
- ///
- /// - Precondition: `cornerRadius` must be finite and positive.
- ///
- /// - Parameter cornerRadius: The corner radius for this rounded rectangle.
- public init(cornerRadius: Double) {
- assert(
- cornerRadius >= 0.0 && cornerRadius.isFinite,
- "Corner radius must be a positive finite value"
- )
- self.cornerRadius = cornerRadius
- }
- /// This shape tries to mimic an order 5 superellipse, extending the sides with line segments.
- /// Since paths don't support quintic curves, I'm using an approximation consisting of
- /// two cubic curves and a line segment. This constant is the list of control points for
- /// the cubic curves. See https://www.desmos.com/calculator/chwx3ddx6u .
- ///
- /// Preconditions:
- /// - points.0 is the same as if a line segment and a circular arc were used
- /// - points.6.y == 0.0
- fileprivate static let points = (
- SIMD2(0.292893218813, 0.292893218813),
- SIMD2(0.517, 0.0687864376269),
- SIMD2(0.87, 0.0337),
- SIMD2(1.13130356636, 0.0139677719414),
- SIMD2(1.1973, 0.0089),
- SIMD2(1.5038, 0.0002),
- SIMD2(1.7, 0.0)
- )
- /// This corresponds to r_{min} in the above Desmos link. This is the minimum ratio of
- /// cornerRadius to half the side length at which the superellipse is not applicable. Above this,
- /// line segments and circular arcs are used.
- fileprivate static let rMin = 0.441968022436
- }
- extension RoundedRectangle: Shape {
- public func path(in bounds: Path.Rect) -> Path {
- // just to avoid `RoundedRectangle.` qualifiers
- let rMin = RoundedRectangle.rMin
- let points = RoundedRectangle.points
- let effectiveRadius = min(cornerRadius, bounds.width / 2.0, bounds.height / 2.0)
- let xRatio = effectiveRadius / (bounds.width / 2.0)
- let yRatio = effectiveRadius / (bounds.height / 2.0)
- // MARK: Early exits
- // These code paths are guaranteed to not use the approximations of the quintic curves.
- // Optimization: just a circle
- if bounds.width == bounds.height && bounds.width <= cornerRadius * 2.0 {
- return Circle().path(in: bounds)
- }
- // Optimization: just a rectangle
- if effectiveRadius == 0.0 {
- return Rectangle().path(in: bounds)
- }
- // Optimization: corner radius is too large to use quintic curves
- if xRatio >= rMin && yRatio >= rMin {
- return Path()
- .move(to: SIMD2(x: bounds.x + effectiveRadius, y: bounds.y))
- .addLine(to: SIMD2(x: bounds.maxX - effectiveRadius, y: bounds.y))
- .addArc(
- center: SIMD2(x: bounds.maxX - effectiveRadius, y: bounds.y + effectiveRadius),
- radius: effectiveRadius,
- startAngle: .pi * 1.5,
- endAngle: 0.0,
- clockwise: true
- )
- .addLine(to: SIMD2(x: bounds.maxX, y: bounds.maxY - effectiveRadius))
- .addArc(
- center: SIMD2(
- x: bounds.maxX - effectiveRadius,
- y: bounds.maxY - effectiveRadius
- ),
- radius: effectiveRadius,
- startAngle: 0.0,
- endAngle: .pi * 0.5,
- clockwise: true
- )
- .addLine(to: SIMD2(x: bounds.x + effectiveRadius, y: bounds.maxY))
- .addArc(
- center: SIMD2(x: bounds.x + effectiveRadius, y: bounds.maxY - effectiveRadius),
- radius: effectiveRadius,
- startAngle: .pi * 0.5,
- endAngle: .pi,
- clockwise: true
- )
- .addLine(to: SIMD2(x: bounds.x, y: bounds.y + effectiveRadius))
- .addArc(
- center: SIMD2(x: bounds.x + effectiveRadius, y: bounds.y + effectiveRadius),
- radius: effectiveRadius,
- startAngle: .pi,
- endAngle: .pi * 1.5,
- clockwise: true
- )
- }
- return Path()
- // MARK: Top edge, right side
- .move(to: SIMD2(x: bounds.center.x, y: bounds.y))
- .if(xRatio >= rMin) {
- $0
- .addLine(to: SIMD2(x: bounds.maxX - effectiveRadius, y: bounds.y))
- .addArc(
- center: SIMD2(
- x: bounds.maxX - effectiveRadius,
- y: bounds.y + effectiveRadius
- ),
- radius: effectiveRadius,
- startAngle: .pi * 1.5,
- endAngle: .pi * 1.75,
- clockwise: true
- )
- } else: {
- $0
- .addLine(
- to: SIMD2(
- x: bounds.maxX - points.6.x * effectiveRadius,
- y: bounds.y + points.6.y * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.maxX - points.5.x * effectiveRadius,
- y: bounds.y + points.5.y * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.maxX - points.4.x * effectiveRadius,
- y: bounds.y + points.4.y * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.maxX - points.3.x * effectiveRadius,
- y: bounds.y + points.3.y * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.maxX - points.2.x * effectiveRadius,
- y: bounds.y + points.2.y * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.maxX - points.1.x * effectiveRadius,
- y: bounds.y + points.1.y * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.maxX - points.0.x * effectiveRadius,
- y: bounds.y + points.0.y * effectiveRadius
- )
- )
- }
- // MARK: Right edge
- .if(yRatio >= rMin) {
- $0
- .addArc(
- center: SIMD2(
- x: bounds.maxX - effectiveRadius,
- y: bounds.y + effectiveRadius
- ),
- radius: effectiveRadius,
- startAngle: .pi * 1.75,
- endAngle: 0.0,
- clockwise: true
- )
- .addLine(to: SIMD2(x: bounds.maxX, y: bounds.maxY - effectiveRadius))
- .addArc(
- center: SIMD2(
- x: bounds.maxX - effectiveRadius,
- y: bounds.maxY - effectiveRadius
- ),
- radius: effectiveRadius,
- startAngle: 0.0,
- endAngle: .pi * 0.25,
- clockwise: true
- )
- } else: {
- $0
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.maxX - points.1.y * effectiveRadius,
- y: bounds.y + points.1.x * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.maxX - points.2.y * effectiveRadius,
- y: bounds.y + points.2.x * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.maxX - points.3.y * effectiveRadius,
- y: bounds.y + points.3.x * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.maxX - points.4.y * effectiveRadius,
- y: bounds.y + points.4.x * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.maxX - points.5.y * effectiveRadius,
- y: bounds.y + points.5.x * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.maxX - points.6.y * effectiveRadius,
- y: bounds.y + points.6.x * effectiveRadius
- )
- )
- .addLine(
- to: SIMD2(
- x: bounds.maxX - points.6.y * effectiveRadius,
- y: bounds.maxY - points.6.x * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.maxX - points.5.y * effectiveRadius,
- y: bounds.maxY - points.5.x * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.maxX - points.4.y * effectiveRadius,
- y: bounds.maxY - points.4.x * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.maxX - points.3.y * effectiveRadius,
- y: bounds.maxY - points.3.x * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.maxX - points.2.y * effectiveRadius,
- y: bounds.maxY - points.2.x * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.maxX - points.1.y * effectiveRadius,
- y: bounds.maxY - points.1.x * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.maxX - points.0.y * effectiveRadius,
- y: bounds.maxY - points.0.x * effectiveRadius
- )
- )
- }
- // MARK: Bottom edge
- .if(xRatio >= rMin) {
- $0
- .addArc(
- center: SIMD2(
- x: bounds.maxX - effectiveRadius,
- y: bounds.maxY - effectiveRadius
- ),
- radius: effectiveRadius,
- startAngle: .pi * 0.25,
- endAngle: .pi * 0.5,
- clockwise: true
- )
- .addLine(to: SIMD2(x: bounds.x + effectiveRadius, y: bounds.maxY))
- .addArc(
- center: SIMD2(
- x: bounds.x + effectiveRadius,
- y: bounds.maxY - effectiveRadius
- ),
- radius: effectiveRadius,
- startAngle: .pi * 0.5,
- endAngle: .pi * 0.75,
- clockwise: true
- )
- } else: {
- $0
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.maxX - points.1.x * effectiveRadius,
- y: bounds.maxY - points.1.y * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.maxX - points.2.x * effectiveRadius,
- y: bounds.maxY - points.2.y * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.maxX - points.3.x * effectiveRadius,
- y: bounds.maxY - points.3.y * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.maxX - points.4.x * effectiveRadius,
- y: bounds.maxY - points.4.y * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.maxX - points.5.x * effectiveRadius,
- y: bounds.maxY - points.5.y * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.maxX - points.6.x * effectiveRadius,
- y: bounds.maxY - points.6.y * effectiveRadius
- )
- )
- .addLine(
- to: SIMD2(
- x: bounds.x + points.6.x * effectiveRadius,
- y: bounds.maxY - points.6.y * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.x + points.5.x * effectiveRadius,
- y: bounds.maxY - points.5.y * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.x + points.4.x * effectiveRadius,
- y: bounds.maxY - points.4.y * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.x + points.3.x * effectiveRadius,
- y: bounds.maxY - points.3.y * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.x + points.2.x * effectiveRadius,
- y: bounds.maxY - points.2.y * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.x + points.1.x * effectiveRadius,
- y: bounds.maxY - points.1.y * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.x + points.0.x * effectiveRadius,
- y: bounds.maxY - points.0.y * effectiveRadius
- )
- )
- }
- // MARK: Left edge
- .if(yRatio >= rMin) {
- $0
- .addArc(
- center: SIMD2(
- x: bounds.x + effectiveRadius,
- y: bounds.maxY - effectiveRadius
- ),
- radius: effectiveRadius,
- startAngle: .pi * 0.75,
- endAngle: .pi,
- clockwise: true
- )
- .addLine(to: SIMD2(x: bounds.x, y: bounds.y + effectiveRadius))
- .addArc(
- center: SIMD2(x: bounds.x + effectiveRadius, y: bounds.y + effectiveRadius),
- radius: effectiveRadius,
- startAngle: .pi,
- endAngle: .pi * 1.25,
- clockwise: true
- )
- } else: {
- $0
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.x + points.1.y * effectiveRadius,
- y: bounds.maxY - points.1.x * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.x + points.2.y * effectiveRadius,
- y: bounds.maxY - points.2.x * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.x + points.3.y * effectiveRadius,
- y: bounds.maxY - points.3.x * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.x + points.4.y * effectiveRadius,
- y: bounds.maxY - points.4.x * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.x + points.5.y * effectiveRadius,
- y: bounds.maxY - points.5.x * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.x + points.6.y * effectiveRadius,
- y: bounds.maxY - points.6.x * effectiveRadius
- )
- )
- .addLine(
- to: SIMD2(
- x: bounds.x + points.6.y * effectiveRadius,
- y: bounds.y + points.6.x * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.x + points.5.y * effectiveRadius,
- y: bounds.y + points.5.x * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.x + points.4.y * effectiveRadius,
- y: bounds.y + points.4.x * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.x + points.3.y * effectiveRadius,
- y: bounds.y + points.3.x * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.x + points.2.y * effectiveRadius,
- y: bounds.y + points.2.x * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.x + points.1.y * effectiveRadius,
- y: bounds.y + points.1.x * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.x + points.0.y * effectiveRadius,
- y: bounds.y + points.0.x * effectiveRadius
- )
- )
- }
- // MARK: Top edge, left side
- .if(xRatio >= rMin) {
- $0
- .addArc(
- center: SIMD2(x: bounds.x + effectiveRadius, y: bounds.y + effectiveRadius),
- radius: effectiveRadius,
- startAngle: .pi * 1.25,
- endAngle: .pi * 1.5,
- clockwise: true
- )
- } else: {
- $0
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.x + points.1.x * effectiveRadius,
- y: bounds.y + points.1.y * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.x + points.2.x * effectiveRadius,
- y: bounds.y + points.2.y * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.x + points.3.x * effectiveRadius,
- y: bounds.y + points.3.y * effectiveRadius
- )
- )
- .addCubicCurve(
- control1: SIMD2(
- x: bounds.x + points.4.x * effectiveRadius,
- y: bounds.y + points.4.y * effectiveRadius
- ),
- control2: SIMD2(
- x: bounds.x + points.5.x * effectiveRadius,
- y: bounds.y + points.5.y * effectiveRadius
- ),
- to: SIMD2(
- x: bounds.x + points.6.x * effectiveRadius,
- y: bounds.y + points.6.y * effectiveRadius
- )
- )
- }
- .addLine(to: SIMD2(x: bounds.center.x, y: bounds.y))
- }
- }
- // MARK: InsettableShape
- extension RoundedRectangle: InsettableShape {
- public func inset(by amount: Double) -> some InsettableShape {
- InsetShapeImpl(initialCornerRadius: cornerRadius, insetAmount: amount)
- }
- struct InsetShapeImpl {
- var initialCornerRadius: Double
- var insetAmount: Double
- }
- }
- extension RoundedRectangle.InsetShapeImpl: InsettableShape {
- private var actualCornerRadius: Double { max(0, initialCornerRadius - insetAmount) }
- func path(in bounds: Path.Rect) -> Path {
- RoundedRectangle(cornerRadius: actualCornerRadius)
- .path(
- in: .init(
- x: bounds.x + insetAmount,
- y: bounds.y + insetAmount,
- width: bounds.width - 2 * insetAmount,
- height: bounds.height - 2 * insetAmount
- )
- )
- }
- func size(fitting proposal: ProposedViewSize) -> ViewSize {
- let proposedWidth = proposal.width ?? 10
- let proposedHeight = proposal.height ?? 10
- return ViewSize(max(proposedWidth, insetAmount * 2), max(proposedHeight, insetAmount * 2))
- }
- func inset(by amount: Double) -> Self {
- Self(initialCornerRadius: initialCornerRadius, insetAmount: insetAmount + amount)
- }
- }
|