Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Examples/Sources/PathsExample/PathsApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ struct PathsApp: App {
HStack {
ZStack {
RoundedRectangle(cornerRadius: 12)
.inset(by: 2)
.stroke(.red, style: .init(width: 4))
.fill(.gray)

HStack {
Expand Down
6 changes: 5 additions & 1 deletion Sources/SwiftCrossUI/Views/Shapes/Capsule.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
/// A rounded rectangle whose corner radius is equal to half the length of its
/// shortest side.
public struct Capsule: Shape {
public struct Capsule: InsettableShape {
/// Creates a ``Capsule`` instance.
public nonisolated init() {}

public nonisolated func path(in bounds: Path.Rect) -> Path {
let radius = min(bounds.width, bounds.height) / 2.0
return RoundedRectangle(cornerRadius: radius).path(in: bounds)
}

public nonisolated func inset(by amount: Double) -> some InsettableShape {
InsettableShapeImpl(inset: amount, base: self)
}
}
6 changes: 5 additions & 1 deletion Sources/SwiftCrossUI/Views/Shapes/Circle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
///
/// Circles have equal widths and heights; the `Circle` shape will take on the
/// minimum of its proposed width and height.
public struct Circle: Shape {
public struct Circle: InsettableShape {
/// The ideal diameter of a ``Circle``.
nonisolated static let idealDiameter = 10.0

Expand All @@ -24,4 +24,8 @@ public struct Circle: Shape {

return ViewSize(diameter, diameter)
}

public nonisolated func inset(by amount: Double) -> some InsettableShape {
InsettableShapeImpl(inset: amount, base: self)
}
}
6 changes: 5 additions & 1 deletion Sources/SwiftCrossUI/Views/Shapes/Ellipse.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// An ellipse.
public struct Ellipse: Shape {
public struct Ellipse: InsettableShape {
/// Creates an ``Ellipse`` instance.
public nonisolated init() {}

Expand All @@ -18,4 +18,8 @@ public struct Ellipse: Shape {
)
)
}

public nonisolated func inset(by amount: Double) -> some InsettableShape {
InsettableShapeImpl(inset: amount, base: self)
}
}
46 changes: 46 additions & 0 deletions Sources/SwiftCrossUI/Views/Shapes/InsettableShape.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/// A shape type that is able to inset itself to produce another shape.
public protocol InsettableShape: Shape {
/// The type of the inset shape.
associatedtype InsetShape: InsettableShape

/// Returns `self` inset by `amount`.
nonisolated func inset(by amount: Double) -> InsetShape
}

/// The `InsetShape` implementation used by ``Rectangle``, ``Ellipse``, ``Circle``, and ``Capsule``.
///
/// This implementation only works for convex shapes where insetting the shape is equivalent to
/// making the shape smaller.
struct InsettableShapeImpl<Base: Shape>: InsettableShape {
var inset: Double
var base: Base

nonisolated func path(in bounds: Path.Rect) -> Path {
base.path(
in: .init(
x: bounds.x + inset,
y: bounds.y + inset,
width: bounds.width - 2 * inset,
height: bounds.height - 2 * inset
)
)
}

nonisolated func size(fitting proposal: ProposedViewSize) -> ViewSize {
let innerProposal = ProposedViewSize(
proposal.width.map { max(0, $0 - 2 * inset) },
proposal.height.map { max(0, $0 - 2 * inset) }
)

let innerSize = base.size(fitting: innerProposal)

return ViewSize(
innerSize.width + 2 * inset,
innerSize.height + 2 * inset
)
}

func inset(by amount: Double) -> InsettableShapeImpl<Base> {
.init(inset: inset + amount, base: base)
}
}
6 changes: 5 additions & 1 deletion Sources/SwiftCrossUI/Views/Shapes/Rectangle.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/// A rectangle.
public struct Rectangle: Shape {
public struct Rectangle: InsettableShape {
/// Creates a ``Rectangle`` instance.
public nonisolated init() {}

public nonisolated func path(in bounds: Path.Rect) -> Path {
Path().addRectangle(bounds)
}

public nonisolated func inset(by amount: Double) -> some InsettableShape {
InsettableShapeImpl(inset: amount, base: self)
}
}
39 changes: 39 additions & 0 deletions Sources/SwiftCrossUI/Views/Shapes/RoundedRectangle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,42 @@ extension RoundedRectangle: Shape {
.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)
}
}
Loading