Skip to content

Commit a9c4ca4

Browse files
committed
Rect: add additional Rect interfaces
This adds the `standardized`, `integeral`, `isEmpty` properties, the `insetBy` method and fixes an oversight in the `applying` method.
1 parent 1068368 commit a9c4ca4

File tree

2 files changed

+122
-3
lines changed

2 files changed

+122
-3
lines changed

Sources/SwiftWin32/CG/Rect.swift

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright © 2019 Saleem Abdulrasool <[email protected]>
22
// SPDX-License-Identifier: BSD-3-Clause
33

4+
@_implementationOnly import func CRT.floor
5+
@_implementationOnly import func CRT.ceil
6+
47
/// A structure that contains the location and dimensions of a rectangle.
58
public struct Rect {
69
// MARK - Creating Rectangle Values
@@ -90,9 +93,33 @@ public struct Rect {
9093

9194
// MARK - Creating Derived Rectangles
9295

96+
/// Returns a rectangle with a positive width and height.
97+
public var standardized: Rect {
98+
guard !self.isNull else { return .null }
99+
100+
guard self.size.width < 0 || self.size.height < 0 else { return self }
101+
return Rect(x: self.origin.x + (self.width < 0 ? self.width : 0),
102+
y: self.origin.y + (self.height < 0 ? self.height : 0),
103+
width: abs(self.width), height: abs(self.height))
104+
}
105+
106+
/// Returns the smallest rectangle that results from converting the source
107+
/// rectangle values to integers.
108+
public var integral: Rect {
109+
guard !self.isNull else { return .null }
110+
111+
let standardized = self.standardized
112+
let origin: Point = Point(x: floor(standardized.minX),
113+
y: floor(standardized.minY))
114+
let size: Size = Size(width: ceil(standardized.maxX) - origin.x,
115+
height: ceil(standardized.maxY) - origin.y)
116+
return Rect(origin: origin, size: size)
117+
}
118+
93119
/// Applies an affine transform to a rectangle.
94120
public func applying(_ transform: AffineTransform) -> Rect {
95-
if transform.isIdentity { return self }
121+
guard !self.isNull else { return .null }
122+
if transform.isIdentity { return self.standardized }
96123

97124
let points: [Point] = [
98125
Point(x: minX, y: minY), // top left
@@ -111,16 +138,39 @@ public struct Rect {
111138
size: Size(width: maxX - minX, height: maxY - minY))
112139
}
113140

141+
/// Returns a rectangle that is smaller or larger than the source rectangle,
142+
/// with the same center point.
143+
public func insetBy(dx: Double, dy: Double) -> Rect {
144+
let standardized = self.standardized
145+
let origin: Point =
146+
Point(x: standardized.minX + dx, y: standardized.minY + dy)
147+
let size: Size = Size(width: standardized.width - 2 * dx,
148+
height: standardized.height - 2 * dy)
149+
guard size.width > 0, size.height > 0 else { return .null }
150+
return Rect(origin: origin, size: size)
151+
}
152+
114153
/// Returns a rectangle with an origin that is offset from that of the source
115154
/// rectangle.
116155
public func offsetBy(dx: Double, dy: Double) -> Rect {
117156
guard !self.isNull else { return self }
118-
return Rect(x: self.origin.x + dx, y: self.origin.y + dy,
119-
width: self.size.width, height: self.size.height)
157+
158+
let standardized = self.standardized
159+
return Rect(x: standardized.origin.x + dx,
160+
y: standardized.origin.y + dy,
161+
width: standardized.size.width,
162+
height: standardized.size.height)
120163
}
121164

122165
// MARK - Checking Characteristics
123166

167+
168+
/// Returns whether a rectangle has zero width or height, or is a null
169+
/// rectangle.
170+
public var isEmpty: Bool {
171+
return self.size.height == 0 || self.size.width == 0 || self.isNull
172+
}
173+
124174
/// Returns whether the rectangle is equal to the null rectangle.
125175
public var isNull: Bool {
126176
return self == .null

Tests/CoreGraphicsTests/CoreGraphicsTests.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ final class CoreGraphicsTests: XCTestCase {
4040
}
4141

4242
func testRectApplyAffineTransform() {
43+
let null: Rect = .null
44+
XCTAssertEqual(null.applying(AffineTransform(rotationAngle: .pi)),
45+
Rect.null)
46+
47+
let nonStandardized: Rect =
48+
Rect(origin: .zero, size: Size(width: -32, height: -32))
49+
XCTAssertEqual(nonStandardized.applying(.identity),
50+
Rect(x: -32.0, y: -32.0, width: 32.0, height: 32.0))
51+
52+
let nonStandardizedOblong: Rect =
53+
Rect(origin: Point(x: -16, y: -8), size: Size(width: -16, height: -8))
54+
XCTAssertEqual(nonStandardizedOblong.applying(AffineTransform(rotationAngle: .pi / 2)),
55+
Rect(x: 7.999999999999998, y: -32, width: 8, height: 16))
56+
4357
var rect: Rect =
4458
Rect(origin: Point(x: -6, y: -7),
4559
size: Size(width: 12, height: 14))
@@ -95,6 +109,58 @@ final class CoreGraphicsTests: XCTestCase {
95109
.offsetBy(dx: 4.0, dy: 4.0)
96110
XCTAssertEqual(r2.origin, Point(x: 8.0, y: 8.0))
97111
XCTAssertEqual(r2.size, Size(width: 4.0, height: 4.0))
112+
113+
let nonStandardized: Rect =
114+
Rect(origin: .zero, size: Size(width: -32, height: -32))
115+
XCTAssertEqual(nonStandardized.offsetBy(dx: 16.0, dy: 16.0),
116+
Rect(x: -16.0, y: -16.0, width: 32.0, height: 32.0))
117+
}
118+
119+
func testRectStandardizing() {
120+
let null: Rect = .null
121+
XCTAssertEqual(null.standardized, Rect.null)
122+
123+
let normal: Rect = Rect(x: 0, y: 0, width: 32, height: 32)
124+
XCTAssertEqual(normal.standardized, normal)
125+
126+
let negativeHeight: Rect = Rect(x: 0, y: 0, width: -32, height: 32)
127+
XCTAssertEqual(negativeHeight.standardized,
128+
Rect(x: -32, y: 0, width: 32, height: 32))
129+
130+
let negativeWidth: Rect = Rect(x: 0, y: 0, width: 32, height: -32)
131+
XCTAssertEqual(negativeWidth.standardized,
132+
Rect(x: 0, y: -32, width: 32, height: 32))
133+
134+
let negativeHeightAndWidth: Rect = Rect(x: 0, y: 0, width: -32, height: -32)
135+
XCTAssertEqual(negativeHeightAndWidth.standardized,
136+
Rect(x: -32, y: -32, width: 32, height: 32))
137+
138+
let positiveOrigin: Rect = Rect(x: 32, y: 32, width: -32, height: -32)
139+
XCTAssertEqual(positiveOrigin.standardized,
140+
Rect(origin: .zero, size: Size(width: 32, height: 32)))
141+
142+
let negativeOrigin: Rect = Rect(x: -32, y: -32, width: -32, height: -32)
143+
XCTAssertEqual(negativeOrigin.standardized,
144+
Rect(x: -64, y: -64, width: 32, height: 32))
145+
}
146+
147+
func testRectIntegral() {
148+
let null: Rect = .null
149+
XCTAssertEqual(null.integral, Rect.null)
150+
}
151+
152+
func testRectInsetBy() {
153+
let null: Rect = .null
154+
XCTAssertEqual(null.insetBy(dx: 0.0, dy: 0.0), Rect.null)
155+
156+
let normal: Rect = Rect(x: 4.0, y: 4.0, width: 16.0, height: 8.0)
157+
XCTAssertEqual(normal.insetBy(dx: 2.0, dy: 2.0),
158+
Rect(x: 6.0, y: 6.0, width: 12.0, height: 4.0))
159+
160+
let nonStandardized: Rect =
161+
Rect(origin: .zero, size: Size(width: -32, height: -32))
162+
XCTAssertEqual(nonStandardized.insetBy(dx: 4.0, dy: 4.0),
163+
Rect(x: -28.0, y: -28.0, width: 24.0, height: 24.0))
98164
}
99165

100166
static var allTests = [
@@ -104,5 +170,8 @@ final class CoreGraphicsTests: XCTestCase {
104170
("testRectApplyAffineTransform", testRectApplyAffineTransform),
105171
("testRectOffsetByNullRect", testRectOffsetByNullRect),
106172
("testRectOffsetBy", testRectOffsetBy),
173+
("testRectStandardizing", testRectStandardizing),
174+
("testRectIntegral", testRectIntegral),
175+
("testRectInsetBy", testRectInsetBy),
107176
]
108177
}

0 commit comments

Comments
 (0)