Skip to content

Commit e2164a7

Browse files
committed
A bunch more shape stuff
1 parent e5cd76c commit e2164a7

File tree

9 files changed

+345
-91
lines changed

9 files changed

+345
-91
lines changed

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,16 @@ public protocol AppBackend {
544544
/// - path: The path to be rendered.
545545
/// - container: The container widget that the path will render in. It has no other
546546
/// children.
547-
/// - environment: The environment values, including color.
548-
func renderPath(_ path: Path, container: Widget, environment: EnvironmentValues)
547+
/// - strokeColor: The color to draw the path's stroke.
548+
/// - fillColor: The color to shade the path's fill.
549+
/// - overrideStrokeStyle: If present, a value to override the path's stroke style.
550+
func renderPath(
551+
_ path: Path,
552+
container: Widget,
553+
strokeColor: Color,
554+
fillColor: Color,
555+
overrideStrokeStyle: StrokeStyle?
556+
)
549557
}
550558

551559
extension AppBackend {
@@ -870,7 +878,13 @@ extension AppBackend {
870878
func updatePath(_ path: Path, _ source: SwiftCrossUI.Path, pointsChanged: Bool) {
871879
todo()
872880
}
873-
func renderPath(_ path: Path, container: Widget, environment: EnvironmentValues) {
881+
func renderPath(
882+
_ path: Path,
883+
container: Widget,
884+
strokeColor: Color,
885+
fillColor: Color,
886+
overrideStrokeStyle: StrokeStyle?
887+
) {
874888
todo()
875889
}
876890
}

Sources/SwiftCrossUI/Path.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ public enum StrokeJoin {
2020
}
2121

2222
public struct StrokeStyle {
23-
public var width: Double = 1.0
24-
public var cap: StrokeCap = .butt
25-
public var join: StrokeJoin = .miter(limit: 10.0)
26-
27-
public static let none = StrokeStyle(width: 0.0)
23+
public var width: Double
24+
public var cap: StrokeCap
25+
public var join: StrokeJoin
26+
27+
public init(width: Double, cap: StrokeCap = .butt, join: StrokeJoin = .miter(limit: 10.0)) {
28+
self.width = width
29+
self.cap = cap
30+
self.join = join
31+
}
2832
}
2933

3034
/// An enum describing how a path is shaded.
@@ -181,6 +185,8 @@ public struct Path {
181185
public var width: Double { size.x }
182186
public var height: Double { size.y }
183187

188+
public var center: SIMD2<Double> { size * 0.5 + origin }
189+
184190
public init(x: Double, y: Double, width: Double, height: Double) {
185191
origin = SIMD2(x: x, y: y)
186192
size = SIMD2(x: width, y: height)
@@ -203,7 +209,7 @@ public struct Path {
203209
clockwise: Bool
204210
)
205211
case transform(AffineTransform)
206-
// case subpath([Action], FillRule)
212+
case subpath([Action], FillRule)
207213
}
208214

209215
/// A list of every action that has been performed on this path.
@@ -214,7 +220,7 @@ public struct Path {
214220
/// to each action.
215221
public private(set) var actions: [Action] = []
216222
public private(set) var fillRule: FillRule = .evenOdd
217-
public private(set) var strokeStyle: StrokeStyle = .none
223+
public private(set) var strokeStyle = StrokeStyle(width: 1.0)
218224

219225
public init() {}
220226

@@ -274,11 +280,16 @@ public struct Path {
274280
return self
275281
}
276282

277-
public consuming func transform(_ transform: AffineTransform) -> Path {
283+
public consuming func applyTransform(_ transform: AffineTransform) -> Path {
278284
actions.append(.transform(transform))
279285
return self
280286
}
281287

288+
public consuming func addSubpath(_ subpath: Path) -> Path {
289+
actions.append(.subpath(subpath.actions, subpath.fillRule))
290+
return self
291+
}
292+
282293
public consuming func stroke(style: StrokeStyle) -> Path {
283294
strokeStyle = style
284295
return self
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
public struct Capsule: Shape {
2+
public init() {}
3+
4+
public func path(in bounds: Path.Rect) -> Path {
5+
if bounds.width > bounds.height {
6+
let radius = bounds.height / 2.0
7+
8+
return Path()
9+
.move(to: SIMD2(x: bounds.width + bounds.x - radius, y: bounds.y))
10+
.addArc(
11+
center: SIMD2(x: bounds.width + bounds.x - radius, y: bounds.center.y),
12+
radius: radius,
13+
startAngle: .pi * 1.5,
14+
endAngle: .pi * 0.5,
15+
clockwise: true
16+
)
17+
.addLine(to: SIMD2(x: radius + bounds.x, y: bounds.height + bounds.y))
18+
.addArc(
19+
center: SIMD2(x: radius + bounds.x, y: bounds.center.y),
20+
radius: radius,
21+
startAngle: .pi * 0.5,
22+
endAngle: .pi * 1.5,
23+
clockwise: true
24+
)
25+
.addLine(to: SIMD2(x: bounds.width + bounds.x - radius, y: bounds.y))
26+
} else if bounds.width < bounds.height {
27+
let radius = bounds.width / 2.0
28+
29+
return Path()
30+
.move(to: SIMD2(x: bounds.x, y: bounds.height + bounds.y - radius))
31+
.addArc(
32+
center: SIMD2(x: bounds.center.x, y: bounds.height + bounds.y - radius),
33+
radius: radius,
34+
startAngle: .pi,
35+
endAngle: 0.0,
36+
clockwise: false
37+
)
38+
.addLine(to: SIMD2(x: bounds.width + bounds.x, y: radius + bounds.y))
39+
.addArc(
40+
center: SIMD2(x: bounds.center.x, y: radius + bounds.y),
41+
radius: radius,
42+
startAngle: 0.0,
43+
endAngle: .pi,
44+
clockwise: false
45+
)
46+
.addLine(to: SIMD2(x: bounds.x, y: bounds.height + bounds.y - radius))
47+
} else {
48+
return Circle().path(in: bounds)
49+
}
50+
}
51+
}

Sources/SwiftCrossUI/Views/Shapes/Circle.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@ public struct Circle: Shape {
22
public init() {}
33

44
public func path(in bounds: Path.Rect) -> Path {
5-
Path().addCircle(
6-
center: SIMD2(x: bounds.x + bounds.width / 2.0, y: bounds.y + bounds.height / 2.0),
7-
radius: min(bounds.width, bounds.height) / 2.0
8-
)
5+
Path()
6+
.addCircle(center: bounds.center, radius: min(bounds.width, bounds.height) / 2.0)
97
}
108

11-
public func size(fitting proposal: SIMD2<Int>) -> SIMD2<Int> {
12-
let minDim = min(proposal.x, proposal.y)
13-
return SIMD2(x: minDim, y: minDim)
9+
public func size(fitting proposal: SIMD2<Int>) -> ViewSize {
10+
let diameter = min(proposal.x, proposal.y)
11+
12+
return ViewSize(
13+
size: SIMD2(x: diameter, y: diameter),
14+
idealSize: SIMD2(x: 10, y: 10),
15+
idealWidthForProposedHeight: proposal.y,
16+
idealHeightForProposedWidth: proposal.x,
17+
minimumWidth: 1,
18+
minimumHeight: 1,
19+
maximumWidth: nil,
20+
maximumHeight: nil
21+
)
1422
}
1523
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
public struct Ellipse: Shape {
2+
public init() {}
3+
4+
public func path(in bounds: Path.Rect) -> Path {
5+
Path()
6+
.addCircle(center: .zero, radius: bounds.width / 2.0)
7+
.applyTransform(
8+
AffineTransform(
9+
linearTransform: SIMD4(
10+
x: 1.0,
11+
y: 0.0,
12+
z: 0.0,
13+
w: bounds.height / bounds.width
14+
),
15+
translation: bounds.center
16+
)
17+
)
18+
}
19+
}

Sources/SwiftCrossUI/Views/Shapes/Shape.swift

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,30 @@ where Content == EmptyView {
44
func path(in bounds: Path.Rect) -> Path
55
/// Determine the ideal size of this shape given the proposed bounds.
66
///
7-
/// The default implementation returns the proposal unmodified.
8-
func size(fitting proposal: SIMD2<Int>) -> SIMD2<Int>
7+
/// The default implementation accepts the proposal and imposes no practical limit on
8+
/// the shape's size.
9+
/// - Returns: Information about the shape's size. The ``ViewSize/size`` property is what
10+
/// frame the shape will actually be rendered with if the current layout pass is not
11+
/// a dry run, while the other properties are used to inform the layout engine how big
12+
/// or small the shape can be. The ``ViewSize/idealSize`` property should not vary with
13+
/// the `proposal`, and should only depend on the view's contents. Pass `nil` for the
14+
/// maximum width/height if the shape has no maximum size (and therefore may occupy
15+
/// the entire screen).
16+
func size(fitting proposal: SIMD2<Int>) -> ViewSize
917
}
1018

1119
extension Shape {
1220
public var body: EmptyView { return EmptyView() }
1321

14-
public func size(fitting proposal: SIMD2<Int>) -> SIMD2<Int> {
15-
return proposal
22+
public func size(fitting proposal: SIMD2<Int>) -> ViewSize {
23+
return ViewSize(
24+
size: proposal,
25+
idealSize: SIMD2(x: 10, y: 10),
26+
minimumWidth: 1,
27+
minimumHeight: 1,
28+
maximumWidth: nil,
29+
maximumHeight: nil
30+
)
1631
}
1732

1833
public func children<Backend: AppBackend>(
@@ -41,28 +56,27 @@ extension Shape {
4156
let size = size(fitting: proposedSize)
4257

4358
let path = path(
44-
in: Path.Rect(x: 0.0, y: 0.0, width: Double(size.x), height: Double(size.y)))
59+
in: Path.Rect(x: 0.0, y: 0.0, width: Double(size.size.x), height: Double(size.size.y))
60+
)
61+
4562
let pointsChanged = storage.oldPath?.actions != path.actions
4663
storage.oldPath = path
4764

4865
let backendPath = storage.backendPath as! Backend.Path
4966
backend.updatePath(backendPath, path, pointsChanged: pointsChanged)
5067

5168
if !dryRun {
52-
backend.setSize(of: widget, to: size)
53-
backend.renderPath(backendPath, container: widget, environment: environment)
69+
backend.setSize(of: widget, to: size.size)
70+
backend.renderPath(
71+
backendPath,
72+
container: widget,
73+
strokeColor: .clear,
74+
fillColor: environment.suggestedForegroundColor,
75+
overrideStrokeStyle: nil
76+
)
5477
}
5578

56-
return ViewUpdateResult.leafView(
57-
size: ViewSize(
58-
size: size,
59-
idealSize: SIMD2(x: 10, y: 10),
60-
minimumWidth: 0,
61-
minimumHeight: 0,
62-
maximumWidth: nil,
63-
maximumHeight: nil
64-
)
65-
)
79+
return ViewUpdateResult.leafView(size: size)
6680
}
6781
}
6882

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
public protocol StyledShape: Shape {
2+
var strokeColor: Color? { get }
3+
var fillColor: Color? { get }
4+
var strokeStyle: StrokeStyle? { get }
5+
}
6+
7+
struct StyledShapeImpl<Base: Shape>: StyledShape {
8+
var base: Base
9+
var strokeColor: Color?
10+
var fillColor: Color?
11+
var strokeStyle: StrokeStyle?
12+
13+
init(
14+
base: Base, strokeColor: Color? = nil, fillColor: Color? = nil,
15+
strokeStyle: StrokeStyle? = nil
16+
) {
17+
self.base = base
18+
19+
if let styledBase = base as? any StyledShape {
20+
self.strokeColor = strokeColor ?? styledBase.strokeColor
21+
self.fillColor = fillColor ?? styledBase.fillColor
22+
self.strokeStyle = strokeStyle ?? styledBase.strokeStyle
23+
} else {
24+
self.strokeColor = strokeColor
25+
self.fillColor = fillColor
26+
self.strokeStyle = strokeStyle
27+
}
28+
}
29+
30+
func path(in bounds: Path.Rect) -> Path {
31+
return base.path(in: bounds)
32+
}
33+
34+
func size(fitting proposal: SIMD2<Int>) -> ViewSize {
35+
return base.size(fitting: proposal)
36+
}
37+
}
38+
39+
extension Shape {
40+
public func fill(_ color: Color) -> some StyledShape {
41+
StyledShapeImpl(base: self, fillColor: color)
42+
}
43+
44+
public func stroke(_ color: Color, style: StrokeStyle? = nil) -> some StyledShape {
45+
StyledShapeImpl(base: self, strokeColor: color, strokeStyle: style)
46+
}
47+
}
48+
49+
extension StyledShape {
50+
public func update<Backend: AppBackend>(
51+
_ widget: Backend.Widget, children: any ViewGraphNodeChildren, proposedSize: SIMD2<Int>,
52+
environment: EnvironmentValues, backend: Backend, dryRun: Bool
53+
) -> ViewUpdateResult {
54+
let storage = children as! ShapeStorage
55+
let size = size(fitting: proposedSize)
56+
57+
let path = path(
58+
in: Path.Rect(x: 0.0, y: 0.0, width: Double(size.size.x), height: Double(size.size.y))
59+
)
60+
61+
let pointsChanged = storage.oldPath?.actions != path.actions
62+
storage.oldPath = path
63+
64+
let backendPath = storage.backendPath as! Backend.Path
65+
backend.updatePath(backendPath, path, pointsChanged: pointsChanged)
66+
67+
if !dryRun {
68+
backend.setSize(of: widget, to: size.size)
69+
backend.renderPath(
70+
backendPath,
71+
container: widget,
72+
strokeColor: strokeColor ?? .clear,
73+
fillColor: fillColor ?? .clear,
74+
overrideStrokeStyle: strokeStyle
75+
)
76+
}
77+
78+
return ViewUpdateResult.leafView(size: size)
79+
}
80+
}

Sources/UIKitBackend/UIColor+Color.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,9 @@ extension Color {
2727
var uiColor: UIColor {
2828
UIColor(color: self)
2929
}
30+
31+
var cgColor: CGColor {
32+
CGColor(
33+
red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(alpha))
34+
}
3035
}

0 commit comments

Comments
 (0)