Skip to content

Commit 18e38da

Browse files
committed
improved custom angle support on negative ranges and added tests
1 parent faf4d1a commit 18e38da

File tree

3 files changed

+131
-15
lines changed

3 files changed

+131
-15
lines changed

Sources/SwiftCrossUI/Values/Gradient.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ public struct Gradient {
55

66
/// Creates a gradient from an array of color stops.
77
init(stops: [Gradient.Stop]) {
8+
guard
9+
let first = stops.first
10+
else {
11+
let invisible = Color.black.opacity(0)
12+
self.stops = [
13+
Stop(color: invisible, location: 0),
14+
Stop(color: invisible, location: 1),
15+
]
16+
return
17+
}
18+
19+
#if DEBUG
20+
if stops != stops.sorted(by: { $0.location < $1.location }) {
21+
logger.warning("Gradient stop locations must be ordered")
22+
}
23+
#endif
24+
25+
if stops.count == 1 {
26+
self.stops = [
27+
Stop(color: first.color, location: 0),
28+
Stop(color: first.color, location: 1),
29+
]
30+
return
31+
}
832
self.stops = stops
933
}
1034

@@ -60,3 +84,5 @@ public struct Gradient {
6084
public var location: Double
6185
}
6286
}
87+
88+
extension Gradient.Stop: Equatable {}

Sources/SwiftCrossUI/Views/Gradients/AngularGradient.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ extension AngularGradient {
132132
let range = (endAngle - startAngle).degrees
133133

134134
if range < 0 {
135-
stops = stops.reversed().map {
135+
stops = stops.map {
136136
Gradient.Stop(color: $0.color, location: 1 - $0.location)
137137
}
138138
}
@@ -145,10 +145,6 @@ extension AngularGradient {
145145
Gradient.Stop(color: $0.color, location: $0.location * dividableRange)
146146
}
147147

148-
stops.append(
149-
Gradient.Stop(color: stops.last!.color, location: 1)
150-
)
151-
152-
return stops
148+
return stops.sorted(by: { $0.location < $1.location })
153149
}
154150
}

Tests/SwiftCrossUITests/GradientTests.swift

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Testing
22
@testable import SwiftCrossUI
33

44
@Suite("Test Gradients")
5+
@MainActor
56
struct GradientTests {
67
@Test("Automatic equal distribution of color")
78
func testAutomaticColorDistribution() async throws {
@@ -13,14 +14,9 @@ struct GradientTests {
1314

1415
func checkExpectations(gradient: Gradient) {
1516
let count = Double(gradient.stops.count) - 1
16-
print(count)
17+
1718
for (i, stop) in gradient.stops.enumerated() {
18-
// Multiplied by 1 million before converting to Int
19-
// to avoid floating point calculation/rounding errors
20-
#expect(
21-
Int(stop.location * 1_000_000) ==
22-
Int((Double(i) / count) * 1_000_000)
23-
)
19+
#expect(stop.location ~= (Double(i) / count))
2420
}
2521
}
2622
}
@@ -40,8 +36,6 @@ struct GradientTests {
4036
func testSingleColorArrayCreates2Stops() async throws {
4137
let gradient = Gradient(colors: [.red])
4238

43-
print(gradient.stops)
44-
4539
#expect(gradient.stops.count == 2)
4640
#expect(gradient.stops.first!.color == .red)
4741
#expect(gradient.stops.first!.location == 0)
@@ -66,4 +60,104 @@ struct GradientTests {
6660
#expect(colors[i] == stop.color)
6761
}
6862
}
63+
64+
@Test("AngularGradient: Unspecified end angle returns original stops")
65+
func nilEndAngleReturnsOriginalStops() async throws {
66+
let gradient = AngularGradient(
67+
stops: [
68+
.init(color: .red, location: 0),
69+
.init(color: .blue, location: 1)
70+
],
71+
center: .center,
72+
angle: .degrees(45)
73+
)
74+
75+
let result = gradient.adjustedStops
76+
77+
#expect(gradient.endAngle == nil)
78+
#expect(result == gradient.gradient.stops)
79+
}
80+
81+
@Test("AngularGradient: Positive range scales correctly")
82+
func positiveRangeScalesCorrectly() async throws {
83+
let gradient = AngularGradient(
84+
stops: [
85+
.init(color: .red, location: 0),
86+
.init(color: .blue, location: 0.5),
87+
.init(color: .green, location: 1)
88+
],
89+
center: .center,
90+
startAngle: .degrees(0),
91+
endAngle: .degrees(180)
92+
)
93+
94+
let result = gradient.adjustedStops
95+
96+
#expect(result[0].location == 0)
97+
#expect(result[1].location ~= 0.25)
98+
#expect(result[2].location ~= 0.5)
99+
}
100+
101+
@Test("AngularGradient: Negative range inverts locations")
102+
func negativeRangeReversesAndInverts() async throws {
103+
let gradient = AngularGradient(
104+
stops: [
105+
.init(color: .red, location: 0),
106+
.init(color: .blue, location: 0.5),
107+
.init(color: .green, location: 1)
108+
],
109+
center: .center,
110+
startAngle: .degrees(180),
111+
endAngle: .degrees(0)
112+
)
113+
114+
let result = gradient.adjustedStops
115+
116+
#expect(result[0].color == .green)
117+
#expect(result[0].location ~= 0)
118+
#expect(result[1].location ~= 0.25)
119+
#expect(result[2].location ~= 0.5)
120+
}
121+
122+
@Test("AngularGradient: Full circle range")
123+
func fullCircleRange() async throws {
124+
let gradient = AngularGradient(
125+
stops: [
126+
.init(color: .red, location: 0),
127+
.init(color: .blue, location: 1)
128+
],
129+
center: .center,
130+
startAngle: .degrees(0),
131+
endAngle: .degrees(360)
132+
)
133+
134+
let result = gradient.adjustedStops
135+
136+
#expect(result[0].location == 0)
137+
#expect(result[1].location ~= 1.0)
138+
}
139+
140+
@Test("Final Color matches last Original")
141+
func finalColorMatchesLastOriginal() async throws {
142+
let gradient = AngularGradient(
143+
stops: [
144+
.init(color: .red, location: 0),
145+
.init(color: .blue, location: 1)
146+
],
147+
center: .center,
148+
startAngle: .degrees(0),
149+
endAngle: .degrees(180)
150+
)
151+
152+
let result = gradient.adjustedStops
153+
154+
#expect(result.last?.color == .blue)
155+
}
156+
}
157+
158+
fileprivate extension Double {
159+
static func ~= (lhs: Self, rhs: Self) -> Bool {
160+
Int(lhs * 1_000_000_000) ==
161+
Int(rhs * 1_000_000_000)
162+
}
69163
}

0 commit comments

Comments
 (0)