diff --git a/Sources/OpenSwiftUI/View/Control/Slider/Slider.swift b/Sources/OpenSwiftUI/View/Control/Slider/Slider.swift index c39db015..2d25f576 100644 --- a/Sources/OpenSwiftUI/View/Control/Slider/Slider.swift +++ b/Sources/OpenSwiftUI/View/Control/Slider/Slider.swift @@ -650,7 +650,7 @@ private struct Clamping: Projection where Value: BinaryFloatingPoint { } func set(base: inout Value, newValue: Double) { - base = clamp(Value(newValue), min: 0, max: 1) + base = Value(newValue).clamp(min: 0, max: 1) } } diff --git a/Sources/OpenSwiftUICore/Animation/TODO/Animatable.swift b/Sources/OpenSwiftUICore/Animation/TODO/Animatable.swift index 3e2745cc..bff6fe4a 100644 --- a/Sources/OpenSwiftUICore/Animation/TODO/Animatable.swift +++ b/Sources/OpenSwiftUICore/Animation/TODO/Animatable.swift @@ -5,8 +5,6 @@ // Audited for iOS 15.5 // Status: Blocked by Graph -public import Foundation - // MARK: - Animatable /// A type that describes how to animate a property of a view. @@ -49,36 +47,3 @@ extension Animatable where AnimatableData == EmptyAnimatableData { // TODO } } - -// MARK: - Animatable + CoreGraphics - -extension CGPoint: Animatable { - public var animatableData: AnimatablePair { - @inlinable - get { .init(x, y) } - @inlinable - set { (x, y) = newValue[] } - } -} - -extension CGSize: Animatable { - public var animatableData: AnimatablePair { - @inlinable - get { .init(width, height) } - @inlinable - set { (width, height) = newValue[] } - } -} - -extension CGRect: Animatable { - public var animatableData: AnimatablePair { - @inlinable - get { - .init(origin.animatableData, size.animatableData) - } - @inlinable - set { - (origin.animatableData, size.animatableData) = newValue[] - } - } -} diff --git a/Sources/OpenSwiftUICore/Data/Codable/CodableProxy.swift b/Sources/OpenSwiftUICore/Data/Codable/CodableProxy.swift index 6cea1711..2216e9df 100644 --- a/Sources/OpenSwiftUICore/Data/Codable/CodableProxy.swift +++ b/Sources/OpenSwiftUICore/Data/Codable/CodableProxy.swift @@ -1,18 +1,24 @@ // // CodableProxy.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 -// Status: Complete +// Audited for iOS 18.0 +// Status: WIP -protocol CodableProxy: Codable { - associatedtype Base - var base: Base { get } -} - -protocol CodableByProxy { +package protocol CodableByProxy { associatedtype CodingProxy: Codable var codingProxy: CodingProxy { get } static func unwrap(codingProxy: CodingProxy) -> Self } + +package protocol CodableProxy: Codable { + associatedtype Base + var base: Base { get } +} + +extension CodableByProxy where Self == CodingProxy.Base, CodingProxy: CodableProxy { + package static func unwrap(codingProxy: CodingProxy) -> Self { + codingProxy.base + } +} diff --git a/Sources/OpenSwiftUICore/Data/Update.swift b/Sources/OpenSwiftUICore/Data/Update.swift index dd887fa7..6b343a96 100644 --- a/Sources/OpenSwiftUICore/Data/Update.swift +++ b/Sources/OpenSwiftUICore/Data/Update.swift @@ -4,8 +4,8 @@ // // Audited for iOS 18.0 // Status: Blocked by Signpost -// ID: EA173074DA35FA471DC70643259B7E74 (RELEASE_2021) -// ID: 61534957AEEC2EDC447ABDC13B4D426F (RELEASE_2024) +// ID: EA173074DA35FA471DC70643259B7E74 (SwiftUI) +// ID: 61534957AEEC2EDC447ABDC13B4D426F (SwiftUICore) import OpenSwiftUI_SPI import OpenGraphShims diff --git a/Sources/OpenSwiftUICore/Extension/CGAffineTransform+Extension.swift b/Sources/OpenSwiftUICore/Extension/CGAffineTransform+Extension.swift new file mode 100644 index 00000000..829211ec --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/CGAffineTransform+Extension.swift @@ -0,0 +1,77 @@ +// +// CGAffineTransform+Extension.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +#if canImport(Darwin) + +package import CoreGraphics + +extension CGAffineTransform { + package init(rotation: Angle) { + let sin = sin(rotation.radians) + let cos = cos(rotation.radians) + self.init(a: cos, b: sin, c: -sin, d: cos, tx: 0, ty: 0) + } + + package var isTranslation: Bool { + return a == 1 && b == 0 && c == 0 && d == 1 + } + + package var isRectilinear: Bool { + return (b == 0 && c == 0) || (a == 0 && d == 0) + } + + package var isUniform: Bool { + guard isRectilinear else { + return false + } + return a == d && b == c + } + + package func rotated(by angle: Angle) -> CGAffineTransform { + CGAffineTransform(rotation: angle).concatenating(self) + } + + package var scale: CGFloat { + let m = a * a + b * b + let n = c * c + d * d + + if m == 1.0 && n == 1.0 { + return 1.0 + } else { + return (sqrt(m) + sqrt(n)) / 2 + } + } +} + +extension CGAffineTransform: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.cgFloatField(1, a, defaultValue: 1) + encoder.cgFloatField(2, b, defaultValue: 0) + encoder.cgFloatField(3, c, defaultValue: 0) + encoder.cgFloatField(4, d, defaultValue: 1) + encoder.cgFloatField(5, tx, defaultValue: 0) + encoder.cgFloatField(6, ty, defaultValue: 0) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var transform = CGAffineTransform.identity + while let field = try decoder.nextField() { + switch field.tag { + case 1: transform.a = try decoder.cgFloatField(field) + case 2: transform.b = try decoder.cgFloatField(field) + case 3: transform.c = try decoder.cgFloatField(field) + case 4: transform.d = try decoder.cgFloatField(field) + case 5: transform.tx = try decoder.cgFloatField(field) + case 6: transform.ty = try decoder.cgFloatField(field) + default: try decoder.skipField(field) + } + } + self = transform + } +} + +#endif diff --git a/Sources/OpenSwiftUICore/Extension/CGPoint+Extension.swift b/Sources/OpenSwiftUICore/Extension/CGPoint+Extension.swift new file mode 100644 index 00000000..92ff4fee --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/CGPoint+Extension.swift @@ -0,0 +1,151 @@ +// +// CGPoint+Extension.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation + +@_spi(ForOpenSwiftUIOnly) public typealias PlatformPoint = CGPoint + +extension CGPoint { + @inlinable + package static var infinity: CGPoint { + .init(x: CGFloat.infinity, y: CGFloat.infinity) + } + + @inlinable + package init(_ size: CGSize) { + self.init(x: size.width, y: size.height) + } + + @inlinable + package var isFinite: Bool { + x.isFinite && y.isFinite + } + + @inlinable + package func offsetBy(dx: CGFloat, dy: CGFloat) -> CGPoint { + CGPoint(x: x + dx, y: y + dy) + } + + @inlinable + package func offsetBy(dx: CGFloat) -> CGPoint { + offsetBy(dx: dx, dy: 0) + } + + @inlinable + package func offsetBy(dy: CGFloat) -> CGPoint { + offsetBy(dx: 0, dy: dy) + } + + @inlinable + package func offset(by offset: CGSize) -> CGPoint { + offsetBy(dx: offset.width, dy: offset.height) + } + + @inlinable + package func scaledBy(x: CGFloat, y: CGFloat) -> CGPoint { + CGPoint(x: self.x * x, y: self.y * y) + } + + @inlinable + package func scaledBy(x: CGFloat) -> CGPoint { + scaledBy(x: x, y: 1) + } + + @inlinable + package func scaledBy(y: CGFloat) -> CGPoint { + scaledBy(x: 1, y: y) + } + + @inlinable + package func scaled(by scale: CGFloat) -> CGPoint { + scaledBy(x: scale, y: scale) + } + + @inlinable + package var isNaN: Bool { + x.isNaN || y.isNaN + } + + @inlinable + package var flushingNaNs: CGPoint { + CGPoint(x: !x.isNaN ? x : 0, y: !y.isNaN ? y : 0) + } + + @inlinable + package func approximates(_ other: CGPoint, epsilon: CGFloat) -> Bool { + x.approximates(other.x, epsilon: epsilon) + && y.approximates(other.y, epsilon: epsilon) + } + + @inlinable + package mutating func clamp(size: CGSize) { + x.clamp(to: 0...size.width) + y.clamp(to: 0...size.height) + } + + @inlinable + package func clamped(size: CGSize) -> CGPoint { + var point = self + point.clamp(size: size) + return point + } + + @inlinable + package mutating func clamp(rect: CGRect) { + x.clamp(to: rect.x...rect.size.width) + y.clamp(to: rect.y...rect.size.height) + } + + @inlinable + package func clamped(rect: CGRect) -> CGPoint { + var point = self + point.clamp(rect: rect) + return point + } +} + +extension CGPoint { + @inlinable + package subscript(d: Axis) -> CGFloat { + get { d == .horizontal ? x : y } + set { if d == .horizontal { x = newValue } else { y = newValue } } + } + + @inlinable + package init(_ l1: CGFloat, in first: Axis, by l2: CGFloat) { + self = first == .horizontal ? CGPoint(x: l1, y: l2) : CGPoint(x: l2, y: l1) + } +} + +extension CGPoint: Animatable { + public var animatableData: AnimatablePair { + @inlinable + get { .init(x, y) } + @inlinable + set { (x, y) = (newValue.first, newValue.second) } + } +} + +extension CGPoint: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) { + encoder.cgFloatField(1, x) + encoder.cgFloatField(2, y) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var x: CGFloat = .zero + var y: CGFloat = .zero + while let field = try decoder.nextField() { + switch field.tag { + case 1: x = try decoder.cgFloatField(field) + case 2: y = try decoder.cgFloatField(field) + default: try decoder.skipField(field) + } + } + self.init(x: x, y: y) + } +} diff --git a/Sources/OpenSwiftUICore/Extension/CGPoint+Math.swift b/Sources/OpenSwiftUICore/Extension/CGPoint+Math.swift new file mode 100644 index 00000000..a7283828 --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/CGPoint+Math.swift @@ -0,0 +1,81 @@ +// +// CGPoint+Math.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation +#if canImport(Darwin) +import CoreGraphics +#endif + +#if canImport(Darwin) + +extension CGPoint { + package func unapplying(_ m: CGAffineTransform) -> CGPoint { + if m.isTranslation { + self - CGSize(width: m.tx, height: m.ty) + } else { + applying(m.inverted()) + } + } +} +#endif + +package func distance(_ p0: CGPoint, _ p1: CGPoint) -> CGFloat { + let dx = p1.x - p0.x + let dy = p1.y - p0.y + return sqrt(dx * dx + dy * dy) +} + +extension CGPoint { + package func clamp(min minValue: CGPoint, max maxValue: CGPoint) -> CGPoint { + CGPoint( + x: x.clamp(min: minValue.x, max: maxValue.x), + y: y.clamp(min: minValue.y, max: maxValue.y) + ) + } +} + +extension CGPoint { + @inlinable + package static func + (lhs: CGPoint, rhs: CGSize) -> CGPoint { + lhs.offset(by: rhs) + } + + @inlinable + package static func - (lhs: CGPoint, rhs: CGSize) -> CGPoint { + lhs + -rhs + } + + @inlinable + package static func += (lhs: inout CGPoint, rhs: CGSize) { + lhs = lhs + rhs + } + + @inlinable + package static func -= (lhs: inout CGPoint, rhs: CGSize) { + lhs = lhs - rhs + } + + @inlinable + package static prefix func - (lhs: CGPoint) -> CGPoint { + CGPoint(x: -lhs.x, y: -lhs.y) + } + + @inlinable + package static func - (lhs: CGPoint, rhs: CGPoint) -> CGSize { + CGSize(width: lhs.x - rhs.x, height: lhs.y - rhs.y) + } + + @inlinable + package static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + lhs.scaled(by: rhs) + } + + @inlinable + package static func *= (lhs: inout CGPoint, rhs: CGFloat) { + lhs = lhs * rhs + } +} diff --git a/Sources/OpenSwiftUICore/Extension/CGRect+Extension.swift b/Sources/OpenSwiftUICore/Extension/CGRect+Extension.swift new file mode 100644 index 00000000..70d61a79 --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/CGRect+Extension.swift @@ -0,0 +1,267 @@ +// +// CGRect+Extension.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation +#if canImport(Darwin) +public import CoreGraphics +#endif + +// MARK: CGRect + Extensions + +extension CGRect { + @inlinable + package var x: CGFloat { + get { origin.x } + set { origin.x = newValue } + } + + @inlinable + package var y: CGFloat { + get { origin.y } + set { origin.y = newValue } + } + + @inlinable + package var center: CGPoint { + get { CGPoint(x: x + width / 2, y: y + height / 2) } + set { x = newValue.x - width / 2; y = newValue.y - height / 2 } + } + + @inlinable + package init(size: CGSize) { + self.init(x: 0, y: 0, width: size.width, height: size.height) + } + + @inlinable + package init(center: CGPoint, size: CGSize) { + self.init( + x: center.x - size.width * 0.5, + y: center.y - size.height * 0.5, + width: size.width, height: size.height + ) + } + + @inlinable + package var isFinite: Bool { + x.isFinite && y.isFinite && width.isFinite && height.isFinite + } + + @inlinable + package func flushNullToZero() -> CGRect { + isNull ? .zero : self + } + + @inlinable + package func offset(by offset: CGSize) -> CGRect { + offsetBy(dx: offset.width, dy: offset.height) + } + + @inlinable + package func scaledBy(x: CGFloat = 1, y: CGFloat = 1) -> CGRect { + if isNull || isInfinite { + return self + } + return CGRect(x: x * self.x, y: y * self.y, width: width * x, height: height * y) + } + + @inlinable + package func scaled(by scale: CGFloat) -> CGRect { + scaledBy(x: scale, y: scale) + } + + @inlinable + package func hasIntersection(_ rect: CGRect) -> Bool { + !intersection(rect).isEmpty + } + + @inlinable + package var maxXY: CGPoint { + CGPoint(x: maxX, y: maxY) + } + + @inlinable + package var minXY: CGPoint { + CGPoint(x: minX, y: minY) + } +} + +// MARK: CGRect + OpenSwiftUI Extensions + +extension CGRect { + @inlinable + package init(position: CGPoint, size: CGSize, anchor: UnitPoint) { + self.init( + x: position.x - size.width * anchor.x, + y: position.y - size.height * anchor.y, + width: size.width, + height: size.height + ) + } + + @inlinable + package subscript(axis: Axis) -> ClosedRange { + guard !isNull else { return 0 ... 0 } + let x0 = origin[axis], x1 = x0 + size[axis] + var lower = min(x0, x1), upper = max(x0, x1) + if !(lower <= upper) { + (lower, upper) = (0, 0) + } + return ClosedRange(uncheckedBounds: (lower: lower, upper: upper)) + } + + @inlinable + package subscript(edge: Edge) -> CGFloat { + switch edge { + case .leading: return minX + case .trailing: return maxX + case .top: return minY + case .bottom: return maxY + } + } + + @inlinable + package mutating func finalizeLayoutDirection(_ layoutDirection: LayoutDirection, parentSize: CGSize) { + guard layoutDirection == .rightToLeft else { return } + origin.x = parentSize.width - maxX + } + + package func distance(to other: CGRect, in axis: Axis) -> CGFloat { + let selfOrigin = origin[axis] + let selfSize = size[axis] + let otherOrigin = other.origin[axis] + let otherSize = other.size[axis] + + return abs((otherOrigin + otherSize) - (selfSize / 2 + selfOrigin)) - (selfSize / 2 + otherSize) + } +} + +extension CGRect { + package var cornerPoints: [CGPoint] { + [ + origin, + origin.offsetBy(dx: width), + origin.offsetBy(dx: width, dy: height), + origin.offsetBy(dy: height), + ] + } + + package init(cornerPoints p: ArraySlice) { + let p0 = p[0] + let p1 = p[1] + let p2 = p[2] + let p3 = p[3] + + let minX = min(min(p0.x, p1.x), min(p2.x, p3.x)) + let minY = min(min(p0.y, p1.y), min(p2.y, p3.y)) + let maxX = max(max(p0.x, p1.x), max(p2.x, p3.x)) + let maxY = max(max(p0.y, p1.y), max(p2.y, p3.y)) + + self.init(x: minX, y: minY, width: maxX - minX, height: maxY - minY) + } + + package init?(exactCornerPoints p: [CGPoint]) { + let p0 = p[0] + let p1 = p[1] + let p2 = p[2] + let p3 = p[3] + guard p0.x == p3.x && p1.x == p2.x && p0.y == p1.y && p2.y == p3.y else { + return nil + } + self.init(x: p0.x, y: p0.y, width: p1.x - p0.x, height: p2.y - p0.y) + } + + package init(cornerPoints p: [CGPoint]) { + self.init(cornerPoints: p[0...3]) + } + + package func mapCorners(f: (inout [CGPoint]) -> Void) -> CGRect { + if isNull || isInfinite { + return self + } + var cornerPoints = cornerPoints + f(&cornerPoints) + return CGRect(cornerPoints: cornerPoints) + } +} + +extension CGRect { + /// Returns the minimum distance from the rectangle to the given point + package func distance(to point: CGPoint) -> CGFloat { + let dx = abs(point.x - midX) - width / 2 + let dy = abs(point.y - midY) - height / 2 + if min(dx, dy) <= 0 { + return max(dx, dy) + } else { + return sqrt(dx * dx + dy * dy) + } + } + + /// Returns the perpendicular distance from the point to the nearest edge or corner + package func perpendicularDistance(to point: CGPoint) -> CGFloat { + let dx = abs(point.x - midX) - width / 2 + let dy = abs(point.y - midY) - height / 2 + return max(dx, dy) + } + + package func containsAny(of points: [CGPoint]) -> Bool { + points.contains { contains($0) } + } +} + +package struct LoggableRect: CustomStringConvertible { + private var rect: CGRect + + package init(_ rect: CGRect) { + self.rect = rect + } + + package var description: String { + "(\(rect.x), \(rect.y), \(rect.width), \(rect.height))" + } +} + +extension CGRect { + package var loggable: LoggableRect { + LoggableRect(self) + } +} + +// MARK: CGRect + Animatable + +extension CGRect: Animatable { + public var animatableData: AnimatablePair { + @inlinable + get { .init(origin.animatableData, size.animatableData) } + @inlinable + set { (origin.animatableData, size.animatableData) = (newValue.first, newValue.second) } + } +} + +// MARK: CGRect + ProtobufMessage + +extension CGRect: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) { + encoder.cgFloatField(1, x) + encoder.cgFloatField(2, y) + encoder.cgFloatField(3, width) + encoder.cgFloatField(4, height) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var rect = CGRect.zero + while let field = try decoder.nextField() { + switch field.tag { + case 1: rect.x = try decoder.cgFloatField(field) + case 2: rect.y = try decoder.cgFloatField(field) + case 3: rect.size.width = try decoder.cgFloatField(field) + case 4: rect.size.height = try decoder.cgFloatField(field) + default: try decoder.skipField(field) + } + } + self = rect + } +} diff --git a/Sources/OpenSwiftUICore/Extension/CGSize+Extension.swift b/Sources/OpenSwiftUICore/Extension/CGSize+Extension.swift new file mode 100644 index 00000000..5f0d1d66 --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/CGSize+Extension.swift @@ -0,0 +1,168 @@ +// +// CGSize+Extension.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation + +extension CGSize { + @inlinable + package init(_ point: CGPoint) { + self.init(width: point.x, height: point.y) + } + + @inlinable + package func scaledBy(x: CGFloat = 1, y: CGFloat = 1) -> CGSize { + CGSize( + width: width == 0 ? 0 : width * x, + height: height == 0 ? 0 : height * y + ) + } + + @inlinable + package func scaled(by scale: CGFloat) -> CGSize { + self.scaledBy(x: scale, y: scale) + } + + @inlinable + package func scaled(by scale: CGSize) -> CGSize { + self.scaledBy(x: scale.width, y: scale.height) + } + + @inlinable + package func increasedBy(dWidth: CGFloat = 0, dHeight: CGFloat = 0) -> CGSize { + CGSize(width: width + dWidth, height: height + dHeight) + } + + @inlinable + package var isFinite: Bool { + width.isFinite && height.isFinite + } + + @inlinable + package var isNan: Bool { + width.isNaN || height.isNaN + } + + @inlinable + package var hasZero: Bool { + width == 0 || height == 0 + } + + @inlinable + package var isNegative: Bool { + width < 0 || height < 0 + } + + @inlinable + package var isNonEmpty: Bool { + width > 0 && height > 0 + } + + @inlinable + package var flushingNaNs: CGSize { + CGSize( + width: !width.isNaN ? width : 0, + height: !height.isNaN ? height : 0 + ) + } + + @inlinable + package var flushingNegatives: CGSize { + CGSize( + width: max(width, 0.0), + height: max(height, 0.0) + ) + } + + @inlinable + package func approximates(_ other: CGSize, epsilon: CGFloat) -> Bool { + width.approximates(other.width, epsilon: epsilon) + && height.approximates(other.height, epsilon: epsilon) + } +} + +extension CGSize { + @inlinable + package subscript(d: Axis) -> CGFloat { + get { d == .horizontal ? width : height } + set { if d == .horizontal { width = newValue } else { height = newValue } } + } + + @inlinable + package init(_ l1: CGFloat, in first: Axis, by l2: CGFloat) { + self = first == .horizontal ? CGSize(width: l1, height: l2) : CGSize(width: l2, height: l1) + } +} + +extension CGSize { + @inlinable + package func contains(point p: CGPoint) -> Bool { + !(p.x < 0) && !(p.y < 0) && p.x < width && p.y < height + } + + @inlinable + func containsAny(of points: [CGPoint]) -> Bool { + for p in points where contains(point: p) { + return true + } + return false + } +} + +extension CGSize { + package static let invalidValue: CGSize = CGSize(width: Double.nan, height: Double.nan) +} + +package struct HashableSize: Equatable, Hashable { + package var width: CGFloat + package var height: CGFloat + package init(_ value: CGSize) { + self.width = value.width + self.height = value.height + } + package var value: CGSize { + get { CGSize(width: width, height: height) } + set { (width, height) = (newValue.width, newValue.height) } + } +} + +extension CGSize: Animatable { + public typealias AnimatableData = AnimatablePair + + public var animatableData: AnimatableData { + @inlinable + get { .init(width, height) } + @inlinable + set { (width, height) = newValue[] } + } +} + +extension CGSize: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) { + encoder.cgFloatField(1, width) + encoder.cgFloatField(2, height) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var width: CGFloat = .zero + var height: CGFloat = .zero + while let field = try decoder.nextField() { + switch field.tag { + case 1: width = try decoder.cgFloatField(field) + case 2: height = try decoder.cgFloatField(field) + default: try decoder.skipField(field) + } + } + self.init(width: width, height: height) + } +} + +extension CGSize { + @inlinable + package static func + (lhs: CGSize, rhs: CGPoint) -> CGPoint { + return CGPoint(x: rhs.x + lhs.width, y: rhs.y + lhs.height) + } +} diff --git a/Sources/OpenSwiftUICore/Extension/CGSize+Math.swift b/Sources/OpenSwiftUICore/Extension/CGSize+Math.swift new file mode 100644 index 00000000..81eac7aa --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/CGSize+Math.swift @@ -0,0 +1,76 @@ +// +// CGSize+Math.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation + +extension CGSize { + @inlinable + prefix package static func - (operand: CGSize) -> CGSize { + var result = operand + result.width = -result.width + result.height = -result.height + return result + } + + @inlinable + package static func += (lhs: inout CGSize, rhs: CGSize) { + lhs.width += rhs.width + lhs.height += rhs.height + } + + @inlinable + package static func + (lhs: CGSize, rhs: CGSize) -> CGSize { + var result = lhs + result += rhs + return result + } + + @inlinable + package static func -= (lhs: inout CGSize, rhs: CGSize) { + lhs.width -= rhs.width + lhs.height -= rhs.height + } + + @inlinable + package static func - (lhs: CGSize, rhs: CGSize) -> CGSize { + var result = lhs + result -= rhs + return result + } + + @inlinable + package static func *= (lhs: inout CGSize, rhs: Double) { + lhs.width *= CGFloat(rhs) + lhs.height *= CGFloat(rhs) + } + + @inlinable + package static func * (lhs: CGSize, rhs: Double) -> CGSize { + var result = lhs + result *= rhs + return result + } + + @inlinable + package static func /= (lhs: inout CGSize, rhs: Double) { + lhs *= 1 / rhs + } + + @inlinable + package static func / (lhs: CGSize, rhs: Double) -> CGSize { + var result = lhs + result /= rhs + return result + } +} + +package func mix(_ lhs: CGSize, _ rhs: CGSize, by t: Double) -> CGSize { + CGSize( + width: (rhs.width - lhs.width) * t + lhs.width, + height: (rhs.height - lhs.height) * t + lhs.height + ) +} diff --git a/Sources/OpenSwiftUICore/Extension/Collection+Extension.swift b/Sources/OpenSwiftUICore/Extension/Collection+Extension.swift new file mode 100644 index 00000000..0b7a0dca --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/Collection+Extension.swift @@ -0,0 +1,31 @@ +// +// Collection+Extension.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +extension Collection { + package func index(atOffset n: Int) -> Index { + index(startIndex, offsetBy: n) + } + + package func index(atOffset n: Int, limitedBy limit: Index) -> Index? { + index(startIndex, offsetBy: n, limitedBy: limit) + } + + package func offset(of i: Index) -> Int { + distance(from: startIndex, to: i) + } + + package subscript(safe index: Index) -> Element? { + guard index >= startIndex, index < endIndex else { + return nil + } + return self[index] + } + + package func withContiguousStorage(_ body: (UnsafeBufferPointer) throws -> R) rethrows -> R { + try withContiguousStorageIfAvailable(body) ?? ContiguousArray(self).withUnsafeBufferPointer(body) + } +} diff --git a/Sources/OpenSwiftUICore/Extension/Comparable+Extension.swift b/Sources/OpenSwiftUICore/Extension/Comparable+Extension.swift new file mode 100644 index 00000000..0f68dc8e --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/Comparable+Extension.swift @@ -0,0 +1,33 @@ +// +// Comparable+Extension.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +extension Comparable { + @inlinable + package func clamp(min minValue: Self, max maxValue: Self) -> Self { + min(max(minValue, self), maxValue) + } + + package mutating func formMin(_ other: Self) { + self = min(self, other) + } + + package mutating func formMax(_ other: Self) { + self = max(self, other) + } + + @inlinable + package mutating func clamp(to limits: ClosedRange) { + self = clamp(min: limits.lowerBound, max: limits.upperBound) + } + + @inlinable + package func clamped(to limits: ClosedRange) -> Self { + var result = self + result.clamp(to: limits) + return result + } +} diff --git a/Sources/OpenSwiftUICore/Util/OptionSet+Extension.swift b/Sources/OpenSwiftUICore/Extension/OptionSet+Extension.swift similarity index 100% rename from Sources/OpenSwiftUICore/Util/OptionSet+Extension.swift rename to Sources/OpenSwiftUICore/Extension/OptionSet+Extension.swift diff --git a/Sources/OpenSwiftUICore/Extension/Round+Extension.swift b/Sources/OpenSwiftUICore/Extension/Round+Extension.swift new file mode 100644 index 00000000..4383a863 --- /dev/null +++ b/Sources/OpenSwiftUICore/Extension/Round+Extension.swift @@ -0,0 +1,188 @@ +// +// Round+Extension.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation +#if canImport(Darwin) +public import CoreGraphics +#endif + +// MARK: - FloatingPoint + Round + +extension FloatingPoint { + @inlinable + package mutating func round(_ rule: FloatingPointRoundingRule, toMultipleOf m: Self) { + if m == 1 { + round(rule) + } else { + self /= m + round(rule) + self *= m + } + } + + @inlinable + package mutating func round(toMultipleOf m: Self) { + round(.toNearestOrAwayFromZero, toMultipleOf: m) + } + + @inlinable + package func rounded(_ rule: FloatingPointRoundingRule, toMultipleOf m: Self) -> Self { + var r = self + r.round(rule, toMultipleOf: m) + return r + } + + @inlinable + package func rounded(toMultipleOf m: Self) -> Self { + rounded(.toNearestOrAwayFromZero, toMultipleOf: m) + } + + @inlinable + package mutating func roundToNearestOrUp(toMultipleOf m: Self) { + self += m / 2 + round(.down, toMultipleOf: m) + } + + @inlinable + package func roundedToNearestOrUp(toMultipleOf m: Self) -> Self { + var r = self + r.roundToNearestOrUp(toMultipleOf: m) + return r + } + + @inlinable + package func approximates(_ value: Self, epsilon: Self) -> Bool { + abs(self - value) < epsilon + } +} + +// MARK: - CGPoint + Round + +extension CGPoint { + @inlinable + package mutating func round(_ rule: FloatingPointRoundingRule, toMultipleOf m: CGFloat) { + x.round(rule, toMultipleOf: m) + y.round(rule, toMultipleOf: m) + } + + @inlinable + package mutating func round(toMultipleOf m: CGFloat) { + round(.toNearestOrAwayFromZero, toMultipleOf: m) + } + + @inlinable + package func rounded(_ rule: FloatingPointRoundingRule, toMultipleOf m: CGFloat) -> CGPoint { + var r = self + r.round(rule, toMultipleOf: m) + return r + } + + @inlinable + package func rounded(toMultipleOf m: CGFloat) -> CGPoint { + rounded(.toNearestOrAwayFromZero, toMultipleOf: m) + } + + @inlinable + package mutating func roundToNearestOrUp(toMultipleOf m: CGFloat) { + x.roundToNearestOrUp(toMultipleOf: m) + y.roundToNearestOrUp(toMultipleOf: m) + } + + @inlinable + package func roundedToNearestOrUp(toMultipleOf m: CGFloat) -> CGPoint { + var r = self + r.roundToNearestOrUp(toMultipleOf: m) + return r + } +} + +// MARK: - CGSize + Round + +extension CGSize { + @inlinable + package mutating func round(_ rule: FloatingPointRoundingRule, toMultipleOf m: CGFloat) { + width.round(rule, toMultipleOf: m) + height.round(rule, toMultipleOf: m) + } + + @inlinable + package mutating func round(toMultipleOf m: CGFloat) { + round(.toNearestOrAwayFromZero, toMultipleOf: m) + } + + @inlinable + package func rounded(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero, toMultipleOf m: CGFloat) -> CGSize { + var r = self + r.round(rule, toMultipleOf: m) + return r + } + + @inlinable + package func rounded(toMultipleOf m: CGFloat) -> CGSize { + rounded(.toNearestOrAwayFromZero, toMultipleOf: m) + } +} + +// MARK: - CGRect + Round + +extension CGRect { + @inlinable + package mutating func roundCoordinatesToNearestOrUp(toMultipleOf m: CGFloat) { + self = standardized + var max = origin + size + origin.roundToNearestOrUp(toMultipleOf: m) + max.roundToNearestOrUp(toMultipleOf: m) + size.width = max.x - x + size.height = max.y - y + + size.round(toMultipleOf: m) + } + + @inlinable + package func roundedCoordinatesToNearestOrUp(toMultipleOf m: CGFloat) -> CGRect { + var r = self + r.roundCoordinatesToNearestOrUp(toMultipleOf: m) + return r + } +} + +// MARK: - EdgeInsets + Round + +extension EdgeInsets { + @inlinable + package mutating func round(_ rule: FloatingPointRoundingRule, toMultipleOf m: CGFloat) { + top.round(rule, toMultipleOf: m) + leading.round(rule, toMultipleOf: m) + bottom.round(rule, toMultipleOf: m) + trailing.round(rule, toMultipleOf: m) + } + + @inlinable + package mutating func round(toMultipleOf m: CGFloat) { + round(.toNearestOrAwayFromZero, toMultipleOf: m) + } + + @inlinable + package func rounded(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero, toMultipleOf m: CGFloat) -> EdgeInsets { + var r = self + r.round(rule, toMultipleOf: m) + return r + } + + @inlinable + package func rounded(toMultipleOf m: CGFloat) -> EdgeInsets { + rounded(.toNearestOrAwayFromZero, toMultipleOf: m) + } + + @inlinable + package func approximates(_ other: EdgeInsets, epsilon: CGFloat) -> Bool { + top.approximates(other.top, epsilon: epsilon) + && leading.approximates(other.leading, epsilon: epsilon) + && bottom.approximates(other.bottom, epsilon: epsilon) + && trailing.approximates(other.trailing, epsilon: epsilon) + } +} diff --git a/Sources/OpenSwiftUICore/Util/UnsafePointer+Extension.swift b/Sources/OpenSwiftUICore/Extension/UnsafePointer+Extension.swift similarity index 100% rename from Sources/OpenSwiftUICore/Util/UnsafePointer+Extension.swift rename to Sources/OpenSwiftUICore/Extension/UnsafePointer+Extension.swift diff --git a/Sources/OpenSwiftUICore/Graphic/Data/Axis.swift b/Sources/OpenSwiftUICore/Graphic/Data/Axis.swift deleted file mode 100644 index 903e3ac5..00000000 --- a/Sources/OpenSwiftUICore/Graphic/Data/Axis.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Axis.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete - -/// The horizontal or vertical dimension in a 2D coordinate system. -@frozen -public enum Axis: Int8, CaseIterable { - /// The horizontal dimension. - case horizontal - /// The vertical dimension. - case vertical - - /// An efficient set of axes. - @frozen - public struct Set: OptionSet { - public let rawValue: Int8 - - public init(rawValue: Int8) { - self.rawValue = rawValue - } - - public static let horizontal = Set(.horizontal) - public static let vertical = Set(.vertical) - - init(_ axis: Axis) { - self.init(rawValue: 1 << axis.rawValue) - } - } -} - -extension Axis: CustomStringConvertible { - public var description: String { - switch self { - case .horizontal: "horizontal" - case .vertical: "vertical" - } - } -} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/AlignmentID.swift b/Sources/OpenSwiftUICore/Layout/Alignment/AlignmentID.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/AlignmentID.swift rename to Sources/OpenSwiftUICore/Layout/Alignment/AlignmentID.swift diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/AlignmentKey.swift b/Sources/OpenSwiftUICore/Layout/Alignment/AlignmentKey.swift similarity index 88% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/AlignmentKey.swift rename to Sources/OpenSwiftUICore/Layout/Alignment/AlignmentKey.swift index 6a6eb815..c784dc01 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/AlignmentKey.swift +++ b/Sources/OpenSwiftUICore/Layout/Alignment/AlignmentKey.swift @@ -8,11 +8,11 @@ @usableFromInline @frozen -struct AlignmentKey: Hashable, Comparable { +package struct AlignmentKey: Hashable, Comparable { private let bits: UInt @usableFromInline - static func < (lhs: AlignmentKey, rhs: AlignmentKey) -> Bool { + package static func < (lhs: AlignmentKey, rhs: AlignmentKey) -> Bool { lhs.bits < rhs.bits } diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/HorizontalAlignment.swift b/Sources/OpenSwiftUICore/Layout/Alignment/HorizontalAlignment.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/HorizontalAlignment.swift rename to Sources/OpenSwiftUICore/Layout/Alignment/HorizontalAlignment.swift diff --git a/Sources/OpenSwiftUICore/Layout/Alignment/TextAlignment.swift b/Sources/OpenSwiftUICore/Layout/Alignment/TextAlignment.swift new file mode 100644 index 00000000..53ae2ac0 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Alignment/TextAlignment.swift @@ -0,0 +1,43 @@ +// +// TextAlignment.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +/// An alignment position for text along the horizontal axis. +@frozen +public enum TextAlignment: Hashable, CaseIterable { + case leading + case center + case trailing + + package var value: CGFloat { + switch self { + case .leading: 0.0 + case .center: 0.5 + case .trailing: 1.0 + } + } +} + +extension TextAlignment: ProtobufEnum { + package var protobufValue: UInt { + switch self { + case .leading: 1 + case .center: 2 + case .trailing: 3 + } + } + + package init?(protobufValue: UInt) { + switch protobufValue { + case 1: self = .leading + case 2: self = .center + case 3: self = .trailing + default: return nil + } + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Alignment/VAlignment.swift b/Sources/OpenSwiftUICore/Layout/Alignment/VAlignment.swift new file mode 100644 index 00000000..63fbfd68 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Alignment/VAlignment.swift @@ -0,0 +1,24 @@ +// +// VAlignment.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +/// An alignment in the vertical axis. +@frozen +public enum _VAlignment { + case top + case center + case bottom + + package var value: CGFloat { + switch self { + case .top: 0.0 + case .center: 0.5 + case .bottom: 1.0 + } + } +} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/VerticalAlignment.swift b/Sources/OpenSwiftUICore/Layout/Alignment/VerticalAlignment.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/VerticalAlignment.swift rename to Sources/OpenSwiftUICore/Layout/Alignment/VerticalAlignment.swift diff --git a/Sources/OpenSwiftUICore/Layout/Context/PositionAwarePlacementContext.swift b/Sources/OpenSwiftUICore/Layout/Context/PositionAwarePlacementContext.swift new file mode 100644 index 00000000..814690df --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Context/PositionAwarePlacementContext.swift @@ -0,0 +1,10 @@ +// +// PositionAwarePlacementContext.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP + +package struct _PositionAwarePlacementContext { + +} diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift index 741773a0..4a62c96c 100644 --- a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift @@ -30,10 +30,7 @@ extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { /// The named coordinate space that is added by the system for the innermost /// containing scroll view that allows scrolling along the provided axis. public static func scrollView(axis: Axis) -> Self { - switch axis { - case .horizontal: NamedCoordinateSpace(name: .horizontalScrollView) - case .vertical: NamedCoordinateSpace(name: .verticalScrollView) - } + NamedCoordinateSpace(name: axis == .horizontal ? .horizontalScrollView : .verticalScrollView) } /// The named coordinate space that is added by the system for the innermost diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Direction/LayoutDirection.swift b/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirection.swift similarity index 91% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Direction/LayoutDirection.swift rename to Sources/OpenSwiftUICore/Layout/Direction/LayoutDirection.swift index 7ec9c032..b08364d8 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Direction/LayoutDirection.swift +++ b/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirection.swift @@ -72,8 +72,8 @@ extension UITraitEnvironmentLayoutDirection { // MARK: - CodableLayoutDirection -struct CodableLayoutDirection: CodableProxy { - var base: LayoutDirection +package struct CodableLayoutDirection: CodableProxy { + package var base: LayoutDirection private enum CodingValue: Int, Codable { case leftToRight @@ -85,7 +85,7 @@ struct CodableLayoutDirection: CodableProxy { self.base = base } - init(from decoder: any Decoder) throws { + package init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() let value = try container.decode(CodingValue.self) switch value { @@ -94,7 +94,7 @@ struct CodableLayoutDirection: CodableProxy { } } - func encode(to encoder: any Encoder) throws { + package func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() let value: CodingValue = switch base { case .leftToRight: .leftToRight @@ -107,13 +107,9 @@ struct CodableLayoutDirection: CodableProxy { // MARK: - LayoutDirection + CodableByProxy extension LayoutDirection: CodableByProxy { - var codingProxy: CodableLayoutDirection { + package var codingProxy: CodableLayoutDirection { CodableLayoutDirection(base: self) } - - static func unwrap(codingProxy: CodableLayoutDirection) -> LayoutDirection { - codingProxy.base - } } // MARK: - LayoutDirectionKey diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/AbsoluteEdge.swift b/Sources/OpenSwiftUICore/Layout/Edge/AbsoluteEdge.swift similarity index 83% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/AbsoluteEdge.swift rename to Sources/OpenSwiftUICore/Layout/Edge/AbsoluteEdge.swift index 46b849a7..02375499 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/AbsoluteEdge.swift +++ b/Sources/OpenSwiftUICore/Layout/Edge/AbsoluteEdge.swift @@ -35,8 +35,8 @@ extension AbsoluteEdge.Set { } if edges.contains(.leading) { switch layoutDirection { - case .leftToRight: result.insert(.left) - case .rightToLeft: result.insert(.right) + case .leftToRight: result.insert(.left) + case .rightToLeft: result.insert(.right) } } if edges.contains(.bottom) { @@ -44,8 +44,8 @@ extension AbsoluteEdge.Set { } if edges.contains(.trailing) { switch layoutDirection { - case .leftToRight: result.insert(.right) - case .rightToLeft: result.insert(.left) + case .leftToRight: result.insert(.right) + case .rightToLeft: result.insert(.left) } } self = result @@ -59,10 +59,10 @@ extension AbsoluteEdge { package var opposite: AbsoluteEdge { switch self { - case .top: .bottom - case .left: .right - case .bottom: .top - case .right: .left + case .top: .bottom + case .left: .right + case .bottom: .top + case .right: .left } } } diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/Edge.swift b/Sources/OpenSwiftUICore/Layout/Edge/Edge.swift similarity index 97% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/Edge.swift rename to Sources/OpenSwiftUICore/Layout/Edge/Edge.swift index a9a98fc2..481f1572 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/Edge.swift +++ b/Sources/OpenSwiftUICore/Layout/Edge/Edge.swift @@ -46,9 +46,9 @@ public enum Edge: Int8, CaseIterable { // MARK: Edge + CodableByProxy extension Edge: CodableByProxy { - var codingProxy: Int8 { rawValue } + package var codingProxy: Int8 { rawValue } - static func unwrap(codingProxy: Int8) -> Edge { + package static func unwrap(codingProxy: Int8) -> Edge { Edge(rawValue: codingProxy) ?? .top } } diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/EdgeInsets.swift b/Sources/OpenSwiftUICore/Layout/Edge/EdgeInsets.swift similarity index 93% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/EdgeInsets.swift rename to Sources/OpenSwiftUICore/Layout/Edge/EdgeInsets.swift index 385babcd..4f500999 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/EdgeInsets.swift +++ b/Sources/OpenSwiftUICore/Layout/Edge/EdgeInsets.swift @@ -144,13 +144,13 @@ extension EdgeInsets: Animatable, _VectorMath { // MARK: - CodableEdgeInsets -struct CodableEdgeInsets: CodableProxy { - var base: EdgeInsets +package struct CodableEdgeInsets: CodableProxy { + package var base: EdgeInsets @inline(__always) init(base: EdgeInsets) { self.base = base } - init(from decoder: Decoder) throws { + package init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() let top = try container.decode(CGFloat.self) let leading = try container.decode(CGFloat.self) @@ -159,7 +159,7 @@ struct CodableEdgeInsets: CodableProxy { base = EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) } - func encode(to encoder: Encoder) throws { + package func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() try container.encode(base.top) try container.encode(base.leading) @@ -171,7 +171,5 @@ struct CodableEdgeInsets: CodableProxy { // MARK: - EdgeInsets + CodableByProxy extension EdgeInsets: CodableByProxy { - var codingProxy: CodableEdgeInsets { CodableEdgeInsets(base: self) } - - static func unwrap(codingProxy: CodableEdgeInsets) -> EdgeInsets { codingProxy.base } + package var codingProxy: CodableEdgeInsets { CodableEdgeInsets(base: self) } } diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/Anchor.swift b/Sources/OpenSwiftUICore/Layout/Geometry/Anchor.swift new file mode 100644 index 00000000..de74e03f --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Geometry/Anchor.swift @@ -0,0 +1,10 @@ +// +// Anchor.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP + +package struct AnchorGeometry {} + +@frozen public struct Anchor {} diff --git a/Sources/OpenSwiftUICore/Graphic/Data/Angle.swift b/Sources/OpenSwiftUICore/Layout/Geometry/Angle.swift similarity index 67% rename from Sources/OpenSwiftUICore/Graphic/Data/Angle.swift rename to Sources/OpenSwiftUICore/Layout/Geometry/Angle.swift index 0605c3d3..767010cc 100644 --- a/Sources/OpenSwiftUICore/Graphic/Data/Angle.swift +++ b/Sources/OpenSwiftUICore/Layout/Geometry/Angle.swift @@ -1,8 +1,8 @@ // // Angle.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete /// A geometric angle whose value you access in either radians or degrees. @@ -43,13 +43,36 @@ public struct Angle { } extension Angle: Hashable, Comparable { + @inlinable public static func < (lhs: Angle, rhs: Angle) -> Bool { lhs.radians < rhs.radians } } -extension Angle: _VectorMath { - public var animatableData: Double { +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif os(WASI) +import WASILibc +#endif + +package func cos(_ angle: Angle) -> Double { + cos(angle.radians) +} + +package func sin(_ angle: Angle) -> Double { + sin(angle.radians) +} + +package func tan(_ angle: Angle) -> Double { + tan(angle.radians) +} + +extension Angle: Animatable, _VectorMath { + public typealias AnimatableData = Double + + public var animatableData: AnimatableData { get { radians * 128.0 } set { radians = newValue / 128.0 } } diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/Axis.swift b/Sources/OpenSwiftUICore/Layout/Geometry/Axis.swift new file mode 100644 index 00000000..3141c249 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Geometry/Axis.swift @@ -0,0 +1,89 @@ +// +// Axis.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +/// The horizontal or vertical dimension in a 2D coordinate system. +@frozen +public enum Axis: Int8, CaseIterable { + /// The horizontal dimension. + case horizontal + + /// The vertical dimension. + case vertical + + package init(edge: Edge) { + switch edge { + case .leading, .trailing: self = .horizontal + case .top, .bottom: self = .vertical + } + } + + @inlinable + package var otherAxis: Axis { + self == .horizontal ? .vertical : .horizontal + } + + @inlinable + package var perpendicularEdges: (min: Edge, max: Edge) { + self == .vertical ? (.top, .bottom) : (.leading, .trailing) + } + + /// An efficient set of axes. + @frozen + public struct Set: OptionSet { + public let rawValue: Int8 + + public init(rawValue: Int8) { + self.rawValue = rawValue + } + + public static let horizontal: Axis.Set = Set(.horizontal) + public static let vertical: Axis.Set = Set(.vertical) + package static let both: Axis.Set = [.horizontal, .vertical] + + package init(_ a: Axis) { + self.init(rawValue: 1 << a.rawValue) + } + + package func contains(_ a: Axis) -> Bool { + contains(Axis.Set(a)) + } + + package func isOrthogonal(to other: Axis.Set) -> Bool { + symmetricDifference(other) == .both + } + } +} + +extension Axis { + package enum Alignment: CGFloat { + case min = 0.0 + case center = 0.5 + case max = 1.0 + + package init(_ y: _VAlignment) { + switch y { + case .top: self = .min + case .center: self = .center + case .bottom: self = .max + } + } + + package init(_ x: TextAlignment) { + switch x { + case .leading: self = .min + case .center: self = .center + case .trailing: self = .max + } + } + } +} + +extension Axis: CustomStringConvertible { + public var description: String { self == .horizontal ? "horizontal" : "vertical" } +} diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/UnitPoint.swift b/Sources/OpenSwiftUICore/Layout/Geometry/UnitPoint.swift new file mode 100644 index 00000000..0b5b5b1b --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Geometry/UnitPoint.swift @@ -0,0 +1,275 @@ +// +// UnitPoint.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation +#if canImport(Darwin) +import CoreGraphics +#endif + +/// A normalized 2D point in a view's coordinate space. +/// +/// Use a unit point to represent a location in a view without having to know +/// the view's rendered size. The point stores a value in each dimension that +/// indicates the fraction of the view's size in that dimension --- measured +/// from the view's origin --- where the point appears. For example, you can +/// create a unit point that represents the center of any view by using the +/// value `0.5` for each dimension: +/// +/// let unitPoint = UnitPoint(x: 0.5, y: 0.5) +/// +/// To project the unit point into the rendered view's coordinate space, +/// multiply each component of the unit point with the corresponding +/// component of the view's size: +/// +/// let projectedPoint = CGPoint( +/// x: unitPoint.x * size.width, +/// y: unitPoint.y * size.height +/// ) +/// +/// You can perform this calculation yourself if you happen to know a view's +/// size, or if you want to use the unit point for some custom purpose, but +/// OpenSwiftUI typically does this for you to carry out operations that +/// you request, like when you: +/// +/// * Transform a shape using a shape modifier. For example, to rotate a +/// shape with ``Shape/rotation(_:anchor:)``, you indicate an anchor point +/// that you want to rotate the shape around. +/// * Override the alignment of the view in a ``Grid`` cell using the +/// ``View/gridCellAnchor(_:)`` view modifier. The grid aligns the projection +/// of a unit point onto the view with the projection of the same unit point +/// onto the cell. +/// * Create a gradient that has a center, or start and stop points, relative +/// to the shape that you are styling. See the gradient methods in +/// ``ShapeStyle``. +/// +/// You can create custom unit points with explicit values, like the example +/// above, or you can use one of the built-in unit points that OpenSwiftUI provides, +/// like ``zero``, ``center``, or ``topTrailing``. The built-in values +/// correspond to the alignment positions of the similarly named, built-in +/// ``Alignment`` types. +/// +/// > Note: A unit point with one or more components outside the range `[0, 1]` +/// projects to a point outside of the view. +/// +/// ### Layout direction +/// +/// When a person configures their device to use a left-to-right language like +/// English, the system places the view's origin in its top-left corner, +/// with positive x toward the right and positive y toward the bottom of the +/// view. In a right-to-left environment, the origin moves to the upper-right +/// corner, and the positive x direction changes to be toward the left. You +/// don't typically need to do anything to handle this change, because OpenSwiftUI +/// applies the change to all aspects of the system. For example, see the +/// discussion about layout direction in ``HorizontalAlignment``. +/// +/// It’s important to test your app for the different locales that you +/// distribute your app in. For more information about the localization process, +/// see [Localization](https://developer.apple.com/documentation/xcode/localization) +@frozen +public struct UnitPoint: Hashable { + /// The normalized distance from the origin to the point in the horizontal + /// direction. + public var x: CGFloat + + /// The normalized distance from the origin to the point in the vertical + /// dimension. + public var y: CGFloat + + /// Creates a unit point at the origin. + /// + /// A view's origin appears in the top-left corner in a left-to-right + /// language environment, with positive x toward the right. It appears in + /// the top-right corner in a right-to-left language, with positive x toward + /// the left. Positive y is always toward the bottom of the view. + @inlinable + public init() { + self.init(x: 0, y: 0) + } + + /// Creates a unit point with the specified horizontal and vertical offsets. + /// + /// Values outside the range `[0, 1]` project to points outside of a view. + /// + /// - Parameters: + /// - x: The normalized distance from the origin to the point in the + /// horizontal direction. + /// - y: The normalized distance from the origin to the point in the + /// vertical direction. + @inlinable + public init(x: CGFloat, y: CGFloat) { + self.x = x + self.y = y + } + + package init(_ point: CGPoint, in rect: CGRect) { + self.init(x: (point.x - rect.x) / rect.width, y: (point.y - rect.y) / rect.height) + } + + package init(edge: Edge) { + switch edge { + case .top: self.init(x: 0.5, y: 0.0) + case .leading: self.init(x: 0.0, y: 0.5) + case .bottom: self.init(x: 0.5, y: 1.0) + case .trailing: self.init(x: 1.0, y: 0.5) + } + } + + package func `in`(_ size: CGSize) -> CGPoint { + CGPoint(x: size.width * x, y: size.height * y) + } + + package func `in`(_ rect: CGRect) -> CGPoint { + CGPoint(x: rect.width * x + rect.x, y: rect.height * y + rect.y) + } + + /// The origin of a view. + /// + /// A view's origin appears in the top-left corner in a left-to-right + /// language environment, with positive x toward the right. It appears in + /// the top-right corner in a right-to-left language, with positive x toward + /// the left. Positive y is always toward the bottom of the view. + public static let zero: UnitPoint = UnitPoint(x: 0.0, y: 0.0) + + /// A point that's centered in a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/center`` alignment. + public static let center: UnitPoint = UnitPoint(x: 0.5, y: 0.5) + + /// A point that's centered vertically on the leading edge of a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/leading`` alignment. + /// The leading edge appears on the left in a left-to-right language + /// environment and on the right in a right-to-left environment. + public static let leading: UnitPoint = UnitPoint(x: 0.0, y: 0.5) + + /// A point that's centered vertically on the trailing edge of a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/trailing`` alignment. + /// The trailing edge appears on the right in a left-to-right language + /// environment and on the left in a right-to-left environment. + public static let trailing: UnitPoint = UnitPoint(x: 1, y: 0.5) + + /// A point that's centered horizontally on the top edge of a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/top`` alignment. + public static let top: UnitPoint = UnitPoint(x: 0.5, y: 0.0) + + /// A point that's centered horizontally on the bottom edge of a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/bottom`` alignment. + public static let bottom: UnitPoint = UnitPoint(x: 0.5, y: 1.0) + + /// A point that's in the top, leading corner of a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/topLeading`` alignment. + /// The leading edge appears on the left in a left-to-right language + /// environment and on the right in a right-to-left environment. + public static let topLeading: UnitPoint = UnitPoint(x: 0.0, y: 0.0) + + /// A point that's in the top, trailing corner of a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/topTrailing`` alignment. + /// The trailing edge appears on the right in a left-to-right language + /// environment and on the left in a right-to-left environment. + public static let topTrailing: UnitPoint = UnitPoint(x: 1.0, y: 0.0) + + /// A point that's in the bottom, leading corner of a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/bottomLeading`` alignment. + /// The leading edge appears on the left in a left-to-right language + /// environment and on the right in a right-to-left environment. + public static let bottomLeading: UnitPoint = UnitPoint(x: 0.0, y: 1.0) + + /// A point that's in the bottom, trailing corner of a view. + /// + /// This point occupies the position where the horizontal and vertical + /// alignment guides intersect for ``Alignment/bottomTrailing`` alignment. + /// The trailing edge appears on the right in a left-to-right language + /// environment and on the left in a right-to-left environment. + public static let bottomTrailing: UnitPoint = UnitPoint(x: 1.0, y: 1.0) + + package static let infinity: UnitPoint = UnitPoint(x: .infinity, y: .infinity) +} + +// MARK: - UnitPoint + Axis + +extension UnitPoint { + package subscript(d: Axis) -> CGFloat { + get { d == .horizontal ? x : y } + set { if d == .horizontal { x = newValue } else { y = newValue } } + } + + package init(_ l1: CGFloat, in first: Axis, by l2: CGFloat) { + self = first == .horizontal ? UnitPoint(x: l1, y: l2) : UnitPoint(x: l2, y: l1) + } +} + +// MARK: - UnitPoint + Animatable + +extension UnitPoint: Animatable { + public var animatableData: AnimatablePair { + get { AnimatablePair(x * 128.0, y * 128.0) } + set { x = newValue.first / 128.0 ; y = newValue.second / 128.0 } + } +} + +// MARK: - UnitPoint + Codable + +extension UnitPoint: CodableByProxy { + package var codingProxy: CodableUnitPoint { + CodingProxy(self) + } +} + +package struct CodableUnitPoint: CodableProxy { + package var base: UnitPoint + package init(_ base: UnitPoint) { + self.base = base + } + package func encode(to encoder: any Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(base.x) + try container.encode(base.y) + } + + package init(from decoder: any Decoder) throws { + var container = try decoder.unkeyedContainer() + let x = try container.decode(CGFloat.self) + let y = try container.decode(CGFloat.self) + base = UnitPoint(x: x, y: y) + } +} + +// MARK: - UnitPoint + ProtobufMessage + +extension UnitPoint: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.cgFloatField(1, x) + encoder.cgFloatField(2, y) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var x: CGFloat = .zero + var y: CGFloat = .zero + while let field = try decoder.nextField() { + switch field.tag { + case 1: x = try decoder.cgFloatField(field) + case 2: y = try decoder.cgFloatField(field) + default: try decoder.skipField(field) + } + } + self.init(x: x, y: y) + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/UnitRect.swift b/Sources/OpenSwiftUICore/Layout/Geometry/UnitRect.swift new file mode 100644 index 00000000..4bf59096 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Geometry/UnitRect.swift @@ -0,0 +1,43 @@ +// +// UnitRect.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +package struct UnitRect: Hashable { + package var x: CGFloat + package var y: CGFloat + package var width: CGFloat + package var height: CGFloat + + @inlinable + package init(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) { + self.x = x + self.y = y + self.width = width + self.height = height + } + + @inlinable + package init(point: UnitPoint) { + self.x = point.x + self.y = point.y + self.width = .zero + self.height = .zero + } + + @inlinable + package func `in`(_ size: CGSize) -> CGRect { + CGRect(x: x * size.width, y: y * size.height, width: width * size.width, height: height * size.height) + } + + @inlinable + package func `in`(_ rect: CGRect) -> CGRect { + CGRect(x: x * rect.width + rect.x, y: y * rect.height + rect.y, width: width * rect.width, height: height * rect.height) + } + + package static let one = UnitRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) +} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/SafeAreaInsets.swift b/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/SafeAreaInsets.swift deleted file mode 100644 index 17b754ec..00000000 --- a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/SafeAreaInsets.swift +++ /dev/null @@ -1,16 +0,0 @@ -// TODO -package struct SafeAreaInsets { - var space: UniqueID - var elements: [Element] - var next: OptionalValue - - indirect enum OptionalValue { - case insets(SafeAreaInsets) - case empty - } - - struct Element { - var regions: SafeAreaRegions - var insets: EdgeInsets - } -} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/LayoutComputer.swift b/Sources/OpenSwiftUICore/Layout/LayoutComputer/LayoutComputer.swift similarity index 95% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/LayoutComputer.swift rename to Sources/OpenSwiftUICore/Layout/LayoutComputer/LayoutComputer.swift index d496f549..46811c73 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/LayoutComputer.swift +++ b/Sources/OpenSwiftUICore/Layout/LayoutComputer/LayoutComputer.swift @@ -33,10 +33,7 @@ extension LayoutComputer { func spacing() -> Spacing { preconditionFailure("") } func lengthThatFits(_ size: _ProposedSize, in axis: Axis) -> CGFloat { let result = sizeThatFits(size) - return switch axis { - case .horizontal: result.width - case .vertical: result.height - } + return result[axis] } func sizeThatFits(_: _ProposedSize) -> CGSize { preconditionFailure("") } diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ProposedSize.swift b/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ProposedSize.swift deleted file mode 100644 index 2e293147..00000000 --- a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ProposedSize.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// ProposedSize.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete - -import Foundation - -public struct _ProposedSize: Hashable { - var width: CGFloat? - var height: CGFloat? - - static let unspecified = _ProposedSize(width: nil, height: nil) - - @inline(__always) - static var zero: _ProposedSize { - _ProposedSize(width: .zero, height: .zero) - } - - @inline(__always) - static var infinity: _ProposedSize { - _ProposedSize(width: .infinity, height: .infinity) - } - - @inline(__always) - init(width: CGFloat? = nil, height: CGFloat? = nil) { - self.width = width - self.height = height - } - - @inline(__always) - init(size: CGSize) { - width = size.width - height = size.height - } -} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewFrame.swift b/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewFrame.swift deleted file mode 100644 index ac02af2c..00000000 --- a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewFrame.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// ViewFrame.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete - -struct ViewFrame { - var origin: ViewOrigin - var size: ViewSize -} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewGeometry.swift b/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewGeometry.swift deleted file mode 100644 index d5e9e181..00000000 --- a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewGeometry.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ViewGeometry.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete - -import Foundation -import OpenGraphShims - -struct ViewGeometry: Equatable { - var origin: ViewOrigin - var dimensions: ViewDimensions - - @inline(__always) - static var zero: ViewGeometry { ViewGeometry(origin: .zero, dimensions: .zero) } -} - -extension Attribute where Value == ViewGeometry { - func origin() -> Attribute { - self[keyPath: \.origin] - } - - func size() -> Attribute { - self[keyPath: \.dimensions.size] - } -} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewOrigin.swift b/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewOrigin.swift deleted file mode 100644 index d8737a20..00000000 --- a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewOrigin.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ViewOrigin.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete - -package import Foundation - -package struct ViewOrigin: Equatable { - var value: CGPoint - - @inline(__always) - static var zero: ViewOrigin { ViewOrigin(value: .zero) } -} - -extension ViewOrigin: Animatable { - package var animatableData: AnimatablePair { - get { .init(value.x, value.y) } - set { value = .init(x: newValue.first, y: newValue.second) } - } -} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewSize.swift b/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewSize.swift deleted file mode 100644 index bd6d4198..00000000 --- a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewSize.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ViewSize.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete - -import Foundation - -package struct ViewSize: Equatable { - var value: CGSize - var _proposal: CGSize - - @inline(__always) - static var zero: ViewSize { ViewSize(value: .zero, _proposal: .zero) } -} - -extension CGSize { - static let invalidValue: CGSize = CGSize(width: -.infinity, height: -.infinity) -} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Padding/PaddingLayout.swift b/Sources/OpenSwiftUICore/Layout/Padding/PaddingLayout.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Padding/PaddingLayout.swift rename to Sources/OpenSwiftUICore/Layout/Padding/PaddingLayout.swift diff --git a/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsets.swift b/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsets.swift new file mode 100644 index 00000000..6e70a06a --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsets.swift @@ -0,0 +1,44 @@ +// +// SafeAreaRegions.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP +// ID: C4DC82F2A500E9B6DEA3064A36584B42 (SwiftUICore) + +package struct SafeAreaInsets: Equatable { + package enum OptionalValue: Equatable { + case empty + indirect case insets(SafeAreaInsets) + } + + package struct Element: Equatable { + package var regions: SafeAreaRegions + package var insets: EdgeInsets + + package init(regions: SafeAreaRegions, insets: EdgeInsets) { + self.regions = regions + self.insets = insets + } + } + + package var space: CoordinateSpace.ID + package var elements: [Element] + package var next: OptionalValue + + package init(space: CoordinateSpace.ID, elements: [Element]) { + self.space = space + self.elements = elements + self.next = .empty + } + + package init(space: CoordinateSpace.ID, elements: [Element], next: OptionalValue) { + self.space = space + self.elements = elements + self.next = next + } + + package func resolve(regions: SafeAreaRegions, in ctx: _PositionAwarePlacementContext) -> EdgeInsets { + preconditionFailure("TODO") + } +} diff --git a/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsetsModifier.swift b/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsetsModifier.swift new file mode 100644 index 00000000..6cf0f802 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaInsetsModifier.swift @@ -0,0 +1,74 @@ +// +// ViewBuilder.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP + +package import OpenGraphShims + +package struct _SafeAreaInsetsModifier: /* MultiViewModifier, */ PrimitiveViewModifier, Equatable { + var elements: [SafeAreaInsets.Element] + var nextInsets: SafeAreaInsets.OptionalValue? + + package init() { + elements = [] + nextInsets = nil + } + + package init(elements: [SafeAreaInsets.Element], nextInsets: SafeAreaInsets.OptionalValue? = nil) { + self.elements = elements + self.nextInsets = nextInsets + } + + nonisolated package static func _makeView(modifier: _GraphValue<_SafeAreaInsetsModifier>, inputs: _ViewInputs, body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs) -> _ViewOutputs { + preconditionFailure("TODO") + } +} + +extension _SafeAreaInsetsModifier { + @MainActor + @preconcurrency + package init(insets: EdgeInsets, nextInsets: SafeAreaInsets.OptionalValue? = nil) { + preconditionFailure("TODO") + } +} + +extension _PositionAwarePlacementContext { + package func safeAreaInsets(matching regions: SafeAreaRegions = .all) -> EdgeInsets { + preconditionFailure("TODO") + } +} + +package typealias SafeAreaInsetsModifier = ModifiedContent<_PaddingLayout, _SafeAreaInsetsModifier> + +extension View { + @MainActor + @preconcurrency + public func _safeAreaInsets(_ insets: EdgeInsets) -> some View { + preconditionFailure("TODO") + } + + @MainActor + @preconcurrency + package func safeAreaInsets(_ insets: EdgeInsets, next: SafeAreaInsets.OptionalValue? = nil) -> ModifiedContent { + preconditionFailure("TODO") + } +} + +package struct ResolvedSafeAreaInsets: Rule, AsyncAttribute { + package init( + regions: SafeAreaRegions, + environment: Attribute, + size: Attribute, + position: Attribute, + transform: Attribute, + safeAreaInsets: OptionalAttribute + ) { + preconditionFailure("TODO") + } + + package var value: EdgeInsets { + preconditionFailure("TODO") + } +} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/SafeAreaRegions.swift b/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaRegions.swift similarity index 86% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/SafeAreaRegions.swift rename to Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaRegions.swift index 131303ee..a65e3d7f 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Edge/SafeAreaRegions.swift +++ b/Sources/OpenSwiftUICore/Layout/SafeArea/SafeAreaRegions.swift @@ -1,8 +1,8 @@ // // SafeAreaRegions.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete /// A set of symbolic safe area regions. @@ -23,6 +23,6 @@ public struct SafeAreaRegions: OptionSet { /// All safe area regions. public static let all = SafeAreaRegions(rawValue: .max) + + package static let background = SafeAreaRegions(rawValue: 1 << 0) } - -extension SafeAreaRegions: Sendable {} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/Separators/Divider.swift b/Sources/OpenSwiftUICore/Layout/Separators/Divider.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/Separators/Divider.swift rename to Sources/OpenSwiftUICore/Layout/Separators/Divider.swift diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/Separators/Spacer.swift b/Sources/OpenSwiftUICore/Layout/Separators/Spacer.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/Separators/Spacer.swift rename to Sources/OpenSwiftUICore/Layout/Separators/Spacer.swift diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/Stack/HVStack.swift b/Sources/OpenSwiftUICore/Layout/Stack/HVStack.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/Stack/HVStack.swift rename to Sources/OpenSwiftUICore/Layout/Stack/HVStack.swift diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/Stack/HVStackLayout.swift b/Sources/OpenSwiftUICore/Layout/Stack/HVStackLayout.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/Stack/HVStackLayout.swift rename to Sources/OpenSwiftUICore/Layout/Stack/HVStackLayout.swift diff --git a/Sources/OpenSwiftUICore/Layout/Transform/Placement.swift b/Sources/OpenSwiftUICore/Layout/Transform/Placement.swift new file mode 100644 index 00000000..cf197c06 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Transform/Placement.swift @@ -0,0 +1,68 @@ +// +// Placement.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation + +/// The position and proposed size of a child view, as determined by a layout. +public struct _Placement: Equatable { + + public var proposedSize: CGSize { + get { proposedSize_.fixingUnspecifiedDimensions() } + set { proposedSize_ = _ProposedSize(newValue) } + } + + package var proposedSize_: _ProposedSize + + /// The relative position in the child's actual size that will be placed at + /// `anchorPosition`. + public var anchor: UnitPoint + + /// The location that the anchor will be given in the layout’s coordinate + /// space. + public var anchorPosition: CGPoint + + /// Creates an instance with the given `proposedSize`, anchoring `anchor` at + /// `anchorPosition`. + public init(proposedSize: CGSize, anchoring anchor: UnitPoint = .topLeading, at anchorPosition: CGPoint) { + self.proposedSize_ = _ProposedSize(proposedSize) + self.anchor = anchor + self.anchorPosition = anchorPosition + } + + package init(proposedSize: _ProposedSize, anchoring anchor: UnitPoint, at anchorPosition: CGPoint) { + self.proposedSize_ = proposedSize + self.anchor = anchor + self.anchorPosition = anchorPosition + } + + package init(proposedSize: _ProposedSize, at anchorPosition: CGPoint) { + self.proposedSize_ = proposedSize + self.anchor = .topLeading + self.anchorPosition = anchorPosition + } + + package init(proposedSize: CGSize, aligning anchor: UnitPoint, in area: CGSize) { + self.proposedSize_ = _ProposedSize(proposedSize) + self.anchor = anchor + self.anchorPosition = anchor.in(area) + } + + package init(proposedSize: _ProposedSize, aligning anchor: UnitPoint, in area: CGSize) { + self.proposedSize_ = proposedSize + self.anchor = anchor + self.anchorPosition = anchor.in(area) + } +} + +@available(*, unavailable) +extension _Placement: Sendable {} + +extension _Placement { + package func frameOrigin(childSize: CGSize) -> CGPoint { + anchorPosition - CGSize(anchor.in(childSize)) + } +} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ProjectionTransform.swift b/Sources/OpenSwiftUICore/Layout/Transform/ProjectionTransform.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ProjectionTransform.swift rename to Sources/OpenSwiftUICore/Layout/Transform/ProjectionTransform.swift diff --git a/Sources/OpenSwiftUICore/Layout/Transform/ProposedSize.swift b/Sources/OpenSwiftUICore/Layout/Transform/ProposedSize.swift new file mode 100644 index 00000000..f1fe5408 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Transform/ProposedSize.swift @@ -0,0 +1,76 @@ +// +// ProposedSize.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +public struct _ProposedSize { + package var width: CGFloat? + package var height: CGFloat? + + package init(width: CGFloat? = nil, height: CGFloat? = nil) { + self.width = width + self.height = height + } + + package init() { + self.width = nil + self.height = nil + } + + package func fixingUnspecifiedDimensions(at defaults: CGSize) -> CGSize { + CGSize(width: width ?? defaults.width, height: height ?? defaults.height) + } + + package func fixingUnspecifiedDimensions() -> CGSize { + CGSize(width: width ?? 10.0, height: height ?? 10.0) + } + + package func scaled(by s: CGFloat) -> _ProposedSize { + _ProposedSize(width: width.map { $0 * s }, height: height.map { $0 * s }) + } + + package static let zero = _ProposedSize(width: 0, height: 0) + package static let infinity = _ProposedSize(width: .infinity, height: .infinity) + package static let unspecified = _ProposedSize(width: nil, height: nil) +} + +@available(*, unavailable) +extension _ProposedSize: Sendable {} + +extension _ProposedSize: Hashable {} + +extension _ProposedSize { + package init(_ s: CGSize) { + width = s.width + height = s.height + } +} + +extension CGSize { + package init?(_ p: _ProposedSize) { + guard let width = p.width, let height = p.height else { return nil } + self.init(width: width, height: height) + } +} + +extension _ProposedSize { + package func inset(by insets: EdgeInsets) -> _ProposedSize { + _ProposedSize( + width: width.map { max($0 - insets.leading - insets.trailing, .zero) }, + height: height.map { max($0 - insets.top - insets.bottom, .zero) } + ) + } + + package subscript(axis: Axis) -> CGFloat? { + get { axis == .horizontal ? width : height } + set { if axis == .horizontal { width = newValue } else { height = newValue } } + } + + package init(_ l1: CGFloat?, in first: Axis, by l2: CGFloat?) { + self = first == .horizontal ? _ProposedSize(width: l1, height: l2) : _ProposedSize(width: l2, height: l1) + } +} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/SizeThatFitsObserver.swift b/Sources/OpenSwiftUICore/Layout/Transform/SizeThatFitsObserver.swift similarity index 83% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/SizeThatFitsObserver.swift rename to Sources/OpenSwiftUICore/Layout/Transform/SizeThatFitsObserver.swift index 12b97461..5eaf09e3 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/SizeThatFitsObserver.swift +++ b/Sources/OpenSwiftUICore/Layout/Transform/SizeThatFitsObserver.swift @@ -7,7 +7,7 @@ import Foundation -struct SizeThatFitsObserver { +package struct SizeThatFitsObserver { var proposal: _ProposedSize var callback: (CGSize, CGSize) -> Void } diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/Spacing.swift b/Sources/OpenSwiftUICore/Layout/Transform/Spacing.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/Spacing.swift rename to Sources/OpenSwiftUICore/Layout/Transform/Spacing.swift diff --git a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/ViewDimensions.swift b/Sources/OpenSwiftUICore/Layout/Transform/ViewDimensions.swift similarity index 80% rename from Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/ViewDimensions.swift rename to Sources/OpenSwiftUICore/Layout/Transform/ViewDimensions.swift index 3dc590ea..92ab9580 100644 --- a/Sources/OpenSwiftUICore/Layout/LayoutAdjustments/Alignment/ViewDimensions.swift +++ b/Sources/OpenSwiftUICore/Layout/Transform/ViewDimensions.swift @@ -1,8 +1,8 @@ // // ViewDimensions.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete public import Foundation @@ -58,8 +58,8 @@ public import Foundation /// /// The example above indents the second text view because the subtraction /// moves the second text view's leading guide in the negative x direction, -/// which is to the left in the view's coordinate space. As a result, -/// SwiftUI moves the second text view to the right, relative to the first +/// Openwhich is to the left in the view's coordinate space. As a result, +/// OpenSwiftUI moves the second text view to the right, relative to the first /// text view, to keep their leading guides aligned: /// /// ![A screenshot of two strings. The first says Default and the second, @@ -71,7 +71,7 @@ public import Foundation /// /// The discussion above describes a left-to-right language environment, /// but you don't change your guide calculation to operate in a right-to-left -/// environment. SwiftUI moves the view's origin from the left to the right side +/// environment. OpenSwiftUI moves the view's origin from the left to the right side /// of the view and inverts the positive x direction. As a result, /// the existing calculation produces the same effect, but in the opposite /// direction. @@ -93,14 +93,25 @@ public import Foundation /// string appears horizontally offset to the left from the right side of the /// first string by about the width of one character.](ViewDimensions-2-iOS) public struct ViewDimensions { - let guideComputer: LayoutComputer - var size: ViewSize + package let guideComputer: LayoutComputer /// The view's width. - public var width: CGFloat { size.value.width } + public var width: CGFloat { size.width } /// The view's height. - public var height: CGFloat { size.value.height } + public var height: CGFloat { size.height } + + package var size: ViewSize + + package init(guideComputer: LayoutComputer, size: ViewSize) { + self.guideComputer = guideComputer + self.size = size + } + + package init(guideComputer: LayoutComputer, size: CGSize, proposal: _ProposedSize) { + self.guideComputer = guideComputer + self.size = ViewSize(size, proposal: proposal) + } /// Gets the value of the given horizontal guide. /// @@ -136,10 +147,6 @@ public struct ViewDimensions { self[guide.key] } - subscript(key: AlignmentKey) -> CGFloat { - self[explicit: key] ?? key.id.defaultValue(in: self) - } - /// Gets the explicit value of the given horizontal alignment guide. /// /// Find the horizontal offset of a particular guide in the corresponding @@ -177,13 +184,32 @@ public struct ViewDimensions { public subscript(explicit guide: VerticalAlignment) -> CGFloat? { self[explicit: guide.key] } +} - subscript(explicit key: AlignmentKey) -> CGFloat? { - guideComputer.delegate.explicitAlignment(key, at: size) +@available(*, unavailable) +extension ViewDimensions: Sendable {} + +extension ViewDimensions: Equatable {} + +extension ViewDimensions { + package static let invalidValue = ViewDimensions(guideComputer: .defaultValue, size: .invalidValue) + package static let zero = ViewDimensions(guideComputer: .defaultValue, size: .zero) + + package func at(_ topLeadingCorner: CGPoint) -> ViewGeometry { + preconditionFailure("TODO") } - @inline(__always) - static var zero: ViewDimensions { ViewDimensions(guideComputer: .defaultValue, size: .zero) } + package func centered(in setting: CGSize) -> ViewGeometry { + preconditionFailure("TODO") + } + + package subscript(key: AlignmentKey) -> CGFloat { + Update.assertIsLocked() + return self[explicit: key] ?? key.id.defaultValue(in: self) + } + + package subscript(explicit key: AlignmentKey) -> CGFloat? { + Update.assertIsLocked() + return guideComputer.delegate.explicitAlignment(key, at: size) + } } - -extension ViewDimensions: Equatable {} diff --git a/Sources/OpenSwiftUICore/Layout/Transform/ViewFrame.swift b/Sources/OpenSwiftUICore/Layout/Transform/ViewFrame.swift new file mode 100644 index 00000000..0b46202b --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Transform/ViewFrame.swift @@ -0,0 +1,47 @@ +// +// ViewFrame.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +package struct ViewFrame: Equatable { + package var origin: ViewOrigin + package var size: ViewSize + + @inlinable + package init(origin: ViewOrigin, size: ViewSize) { + self.origin = origin + self.size = size + } + + @inlinable + package init(size: ViewSize) { + self.origin = ViewOrigin() + self.size = size + } +} + +extension ViewFrame { + package mutating func round(toMultipleOf m: CGFloat) { + var rect = CGRect(self) + rect.roundCoordinatesToNearestOrUp(toMultipleOf: m) + origin.value = rect.origin + size.value = rect.size + } +} + +extension CGRect { + package init(_ frame: ViewFrame) { + self.init(origin: frame.origin.value, size: frame.size.value) + } +} + +extension ViewFrame: Animatable { + package var animatableData: CGRect.AnimatableData { + get { .init(origin.animatableData, size.animatableData) } + set { (origin.animatableData, size.animatableData) = (newValue.first, newValue.second) } + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Transform/ViewGeometry.swift b/Sources/OpenSwiftUICore/Layout/Transform/ViewGeometry.swift new file mode 100644 index 00000000..68967916 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Transform/ViewGeometry.swift @@ -0,0 +1,63 @@ +// +// ViewGeometry.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation +package import OpenGraphShims + +package struct ViewGeometry: Equatable { + package var origin: ViewOrigin + package var dimensions: ViewDimensions + + package init(origin: ViewOrigin, dimensions: ViewDimensions) { + self.origin = origin + self.dimensions = dimensions + } + + package init(dimensions: ViewDimensions) { + self.init(origin: ViewOrigin(), dimensions: dimensions) + } + + package init(origin: CGPoint, dimensions: ViewDimensions) { + self.init(origin: ViewOrigin(origin), dimensions: dimensions) + } + + package init(placement p: _Placement, dimensions d: ViewDimensions) { + self.origin = ViewOrigin(p.frameOrigin(childSize: d.size.value)) + self.dimensions = d + } + + package subscript(guide: HorizontalAlignment) -> CGFloat { dimensions[guide] } + package subscript(guide: VerticalAlignment) -> CGFloat { dimensions[guide] } + package subscript(explicit guide: HorizontalAlignment) -> CGFloat? { dimensions[explicit: guide] } + package subscript(explicit guide: VerticalAlignment) -> CGFloat? { dimensions[explicit: guide] } +} + +extension Attribute where Value == ViewGeometry { + package func origin() -> Attribute { self[keyPath: \.origin] } + package func size() -> Attribute { self[keyPath: \.dimensions.size] } +} + +extension ViewGeometry { + package var frame: CGRect { + CGRect(origin: origin.value, size: dimensions.size.value) + } + + package static let invalidValue = ViewGeometry(origin: ViewOrigin(invalid: ()), dimensions: .invalidValue) + + package var isInvalid: Bool { origin.x.isNaN } + + package static let zero = ViewGeometry(origin: CGPoint.zero, dimensions: .zero) + + package subscript(key: AlignmentKey) -> CGFloat { dimensions[key] } + + package subscript(explicit key: AlignmentKey) -> CGFloat? { dimensions[explicit: key] } + + package mutating func finalizeLayoutDirection(_ layoutDirection: LayoutDirection, parentSize: CGSize) { + guard layoutDirection == .rightToLeft else { return } + origin.x = parentSize.width - frame.maxX + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Transform/ViewOrigin.swift b/Sources/OpenSwiftUICore/Layout/Transform/ViewOrigin.swift new file mode 100644 index 00000000..5aa97591 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Transform/ViewOrigin.swift @@ -0,0 +1,53 @@ +// +// ViewOrigin.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +package struct ViewOrigin: Equatable { + package var value: CGPoint + + @inlinable + package init(invalid: Void) { + self.init(CGPoint(x: Double.nan, y: Double.nan)) + } + + @inlinable + package init(_ value: CGPoint) { + self.value = value + } + + @inlinable + package init() { + self.init(.zero) + } + + @inlinable + package var x: CGFloat { + get { value.x } + set { value.x = newValue } + } + + @inlinable + package var y: CGFloat { + get { value.y } + set { value.y = newValue } + } +} + +extension ViewOrigin { + package subscript(d: Axis) -> CGFloat { + get { d == .horizontal ? x : y } + set { if d == .horizontal { x = newValue } else { y = newValue } } + } +} + +extension ViewOrigin: Animatable { + package var animatableData: CGPoint.AnimatableData { + get { value.animatableData } + set { value.animatableData = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Transform/ViewSize.swift b/Sources/OpenSwiftUICore/Layout/Transform/ViewSize.swift new file mode 100644 index 00000000..f648f256 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Transform/ViewSize.swift @@ -0,0 +1,96 @@ +// +// ViewSize.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +package struct ViewSize: Equatable { + package var value: CGSize + var _proposal: CGSize + + @inline(__always) + init(value: CGSize, proposal: CGSize) { + self.value = value + self._proposal = proposal + } + + @inlinable + package init(_ size: CGSize, proposal: _ProposedSize) { + self.value = size + self._proposal = CGSize(width: proposal.width ?? .nan, height: proposal.height ?? .nan) + } + + @inlinable + package static func fixed(_ size: CGSize) -> ViewSize { + self.init(value: size, proposal: size) + } + + @inlinable + package var width: CGFloat { + get { value.width } + set { value.width = newValue } + } + + @inlinable + package var height: CGFloat { + get { value.height } + set { value.height = newValue } + } + + @inlinable + package var proposal: _ProposedSize { + get { + _ProposedSize( + width: _proposal.width.isNaN ? nil : _proposal.width, + height: _proposal.height.isNaN ? nil : _proposal.height + ) + } + set { + _proposal = CGSize(width: newValue.width ?? .nan, height: newValue.height ?? .nan) + } + } + + package mutating func didSetAnimatableData(_ value: CGSize) { + _proposal = value + } + + package static var zero: ViewSize { .fixed(.zero) } + package static var invalidValue: ViewSize { .fixed(.invalidValue) } +} + +extension ViewSize { + package subscript(d: Axis) -> CGFloat { + get { d == .horizontal ? width : height } + set { if d == .horizontal { width = newValue } else { height = newValue } } + } + + package func inset(by insets: EdgeInsets) -> ViewSize { + let newWidth = max(value.width - (insets.leading + insets.trailing), 0) + let newHeight = max(value.height - (insets.top + insets.bottom), 0) + return ViewSize( + value: CGSize(width: newWidth, height: newHeight), + proposal: CGSize( + width: newWidth.isNaN ? 0 : newWidth, + height: newHeight.isNaN ? 0 : newHeight + ) + ) + } +} + +extension ViewSize: Animatable { + package var animatableData: CGSize.AnimatableData { + get { value.animatableData } + set { value.animatableData = newValue } + } +} + +package import OpenGraphShims + +extension Attribute where Value == ViewSize { + package var cgSize: Attribute { + self[keyPath: \.value] + } +} diff --git a/Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewTransform.swift b/Sources/OpenSwiftUICore/Layout/Transform/ViewTransform.swift similarity index 100% rename from Sources/OpenSwiftUICore/Layout/LayoutFundamentals/internal/ViewTransform.swift rename to Sources/OpenSwiftUICore/Layout/Transform/ViewTransform.swift diff --git a/Sources/OpenSwiftUICore/Modifier/ViewModifier/SafeAreaInsetsModifier.swift b/Sources/OpenSwiftUICore/Modifier/ViewModifier/SafeAreaInsetsModifier.swift deleted file mode 100644 index 1ccbc42e..00000000 --- a/Sources/OpenSwiftUICore/Modifier/ViewModifier/SafeAreaInsetsModifier.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// ViewBuilder.swift -// OpenSwiftUI -// -// Audited for RELEASE_2023 -// Status: WIP - -struct _SafeAreaInsetsModifier: PrimitiveViewModifier/*, MultiViewModifier*/ { - var elements: [SafeAreaInsets.Element] - var nextInsets: SafeAreaInsets.OptionalValue? - - var insets: EdgeInsets = .init() // FIXME -} diff --git a/Sources/OpenSwiftUICore/Shape/Path/Path.swift b/Sources/OpenSwiftUICore/Shape/Path/Path.swift index 8fc258fa..79cdeee1 100644 --- a/Sources/OpenSwiftUICore/Shape/Path/Path.swift +++ b/Sources/OpenSwiftUICore/Shape/Path/Path.swift @@ -401,8 +401,8 @@ extension Path { // MARK: - CodablePath[WIP] -struct CodablePath: CodableProxy { - var base: Path +package struct CodablePath: CodableProxy { + package var base: Path private enum Error: Swift.Error { case invalidPath @@ -424,10 +424,10 @@ struct CodablePath: CodableProxy { } // TODO: - func encode(to _: Encoder) throws {} + package func encode(to _: Encoder) throws {} // TODO: - init(from _: Decoder) throws { + package init(from _: Decoder) throws { base = Path() } @@ -440,9 +440,9 @@ struct CodablePath: CodableProxy { // MARK: - Path + CodableByProxy extension Path: CodableByProxy { - var codingProxy: CodablePath { CodablePath(base: self) } - - static func unwrap(codingProxy: CodablePath) -> Path { codingProxy.base } + package var codingProxy: CodablePath { + CodablePath(base: self) + } } // MARK: - PathDrawingStyle diff --git a/Sources/OpenSwiftUICore/Util/Comparable+Extension.swift b/Sources/OpenSwiftUICore/Util/Comparable+Extension.swift deleted file mode 100644 index d49352bf..00000000 --- a/Sources/OpenSwiftUICore/Util/Comparable+Extension.swift +++ /dev/null @@ -1,21 +0,0 @@ -extension Comparable { - package func clamped(to range: ClosedRange) -> Self { - var value = self - value.clamp(to: range) - return value - } - - package mutating func clamp(to range: ClosedRange) { - self = OpenSwiftUICore.clamp(self, min: range.lowerBound, max: range.upperBound) - } -} - -package func clamp(_ value: Value, min minValue: Value, max maxValue: Value) -> Value { - if value < minValue { - minValue - } else if value > maxValue { - maxValue - } else { - value - } -} diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift b/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift index ef741d96..7a43f76d 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift @@ -65,7 +65,7 @@ package final class ViewGraph: GraphHost { OGSubgraph.current = data.globalSubgraph rootView = Attribute(type: Body.self).identifier _rootTransform = Attribute(RootTransform()) - _zeroPoint = Attribute(value: .zero) + _zeroPoint = Attribute(value: ViewOrigin()) // TODO _proposedSize = Attribute(value: .zero) // TODO @@ -382,31 +382,32 @@ struct RootGeometry: Rule, AsyncAttribute { // | | y: i.t+(p.height-f.height)*0.5=14 // └──────────────────────────────────────────────────────────────────────────────┘ var value: ViewGeometry { - let layoutComputer = childLayoutComputer ?? .defaultValue - let insets = safeAreaInsets?.insets ?? EdgeInsets() - let proposal = proposedSize.value.inset(by: insets) - let fittingSize = layoutComputer.delegate.sizeThatFits(_ProposedSize(size: proposal)) - - var x = insets.leading - var y = insets.top - if ViewGraph.current.centersRootView { - x += (proposal.width - fittingSize.width) * 0.5 - y += (proposal.height - fittingSize.height) * 0.5 - } - - let layoutDirection = layoutDirection ?? .leftToRight - switch layoutDirection { - case .leftToRight: - break - case .rightToLeft: - x = proposedSize.value.width - CGRect(origin: CGPoint(x: x, y: y), size: fittingSize).maxX - } - return ViewGeometry( - origin: ViewOrigin(value: CGPoint(x: x, y: y)), - dimensions: ViewDimensions( - guideComputer: layoutComputer, - size: ViewSize(value: fittingSize, _proposal: proposal) - ) - ) + preconditionFailure("TODO") +// let layoutComputer = childLayoutComputer ?? .defaultValue +// let insets = safeAreaInsets?.insets ?? EdgeInsets() +// let proposal = proposedSize.value.inset(by: insets) +// let fittingSize = layoutComputer.delegate.sizeThatFits(_ProposedSize(size: proposal)) +// +// var x = insets.leading +// var y = insets.top +// if ViewGraph.current.centersRootView { +// x += (proposal.width - fittingSize.width) * 0.5 +// y += (proposal.height - fittingSize.height) * 0.5 +// } +// +// let layoutDirection = layoutDirection ?? .leftToRight +// switch layoutDirection { +// case .leftToRight: +// break +// case .rightToLeft: +// x = proposedSize.value.width - CGRect(origin: CGPoint(x: x, y: y), size: fittingSize).maxX +// } +// return ViewGeometry( +// origin: ViewOrigin(value: CGPoint(x: x, y: y)), +// dimensions: ViewDimensions( +// guideComputer: layoutComputer, +// size: ViewSize(value: fittingSize, _proposal: proposal) +// ) +// ) } } diff --git a/Sources/OpenSwiftUICore/View/Input/ViewInputs.swift b/Sources/OpenSwiftUICore/View/Input/ViewInputs.swift index cadd1346..e1a4afd4 100644 --- a/Sources/OpenSwiftUICore/View/Input/ViewInputs.swift +++ b/Sources/OpenSwiftUICore/View/Input/ViewInputs.swift @@ -120,10 +120,9 @@ public struct _ViewInputs { set { if let newValue { base.options.formUnion(.viewStackOrientationIsDefined) - switch newValue { - case .horizontal: + if newValue == .horizontal { base.options.formUnion(.viewStackOrientationIsHorizontal) - case .vertical: + } else { base.options.subtract(.viewStackOrientationIsHorizontal) } } else { diff --git a/Tests/OpenSwiftUICoreTests/Extension/CGAffineTransform+ExtensionTests.swift b/Tests/OpenSwiftUICoreTests/Extension/CGAffineTransform+ExtensionTests.swift new file mode 100644 index 00000000..b9816ea7 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Extension/CGAffineTransform+ExtensionTests.swift @@ -0,0 +1,23 @@ +// +// CGAffineTransform+ExtensionTests.swift +// OpenSwiftUICoreTests + +#if canImport(Darwin) +import Testing +import CoreGraphics +import OpenSwiftUICore + +struct CGAffineTransform_ExtensionTests { + @Test( + arguments: [ + (CGAffineTransform.identity, ""), + (CGAffineTransform(scaleX: 2, y: 4), "0d000000402500008040"), + (CGAffineTransform(translationX: 2, y: 4), "2d000000403500008040"), + ] + ) + func pbMessage(transfrom: CGAffineTransform, hexString: String) throws { + try transfrom.testPBEncoding(hexString: hexString) + try transfrom.testPBDecoding(hexString: hexString) + } +} +#endif diff --git a/Tests/OpenSwiftUICoreTests/Extension/CGRect+ExtensionTests.swift b/Tests/OpenSwiftUICoreTests/Extension/CGRect+ExtensionTests.swift new file mode 100644 index 00000000..3d220ec5 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Extension/CGRect+ExtensionTests.swift @@ -0,0 +1,36 @@ +// +// CGRect+ExtensionTests.swift +// OpenSwiftUICoreTests + +import Foundation +import Numerics +import OpenSwiftUICore +import Testing + +struct CGRect_ExtensionTests { + @Test(arguments: [ + (CGRect(x: 0, y: 0, width: 2, height: 1), CGPoint(x: 1, y: 1), 0.0, 0.0), + (CGRect(x: 0, y: 0, width: 2, height: 1), CGPoint(x: 1, y: 2), 1.0, 1.0), + (CGRect(x: 0, y: 0, width: 2, height: 1), CGPoint(x: 3, y: 2), 1.4142135623730951, 1.0), + (CGRect(x: 0, y: 0, width: 2, height: 1), CGPoint(x: 3, y: 3), 2.23606797749979, 2.0), + (CGRect(x: 0, y: 0, width: 2, height: 1), CGPoint(x: -3, y: -4), 5.0, 4.0), + (CGRect(x: 0, y: 0, width: 2, height: 1), CGPoint(x: 1, y: 0.5), -0.5, -0.5), + (CGRect(x: 0, y: 0, width: 2, height: 1), CGPoint(x: 3, y: 0.5), 1.0, 1.0), + + ]) + func distance(rect: CGRect, point: CGPoint, expectedDistance: CGFloat, expectedPerpendicularDistance: CGFloat) { + #expect(rect.distance(to: point).isApproximatelyEqual(to: expectedDistance)) + #expect(rect.perpendicularDistance(to: point).isApproximatelyEqual(to: expectedPerpendicularDistance)) + } + + @Test( + arguments: [ + (CGRect.zero, ""), + (CGRect(x: 1, y: 2, width: 4, height: 8), "0d0000803f15000000401d000080402500000041") + ] + ) + func pbMessage(rect: CGRect, hexString: String) throws { + try rect.testPBEncoding(hexString: hexString) + try rect.testPBDecoding(hexString: hexString) + } +} diff --git a/Tests/OpenSwiftUICoreTests/Layout/LayoutAdjustments/Alignment/AlignmentIDTests.swift b/Tests/OpenSwiftUICoreTests/Layout/Alignment/AlignmentIDTests.swift similarity index 100% rename from Tests/OpenSwiftUICoreTests/Layout/LayoutAdjustments/Alignment/AlignmentIDTests.swift rename to Tests/OpenSwiftUICoreTests/Layout/Alignment/AlignmentIDTests.swift diff --git a/Tests/OpenSwiftUICoreTests/Layout/LayoutAdjustments/Edge/AbsoluteEdgeTests.swift b/Tests/OpenSwiftUICoreTests/Layout/Edge/AbsoluteEdgeTests.swift similarity index 100% rename from Tests/OpenSwiftUICoreTests/Layout/LayoutAdjustments/Edge/AbsoluteEdgeTests.swift rename to Tests/OpenSwiftUICoreTests/Layout/Edge/AbsoluteEdgeTests.swift diff --git a/Tests/OpenSwiftUICoreTests/Layout/LayoutFundamentals/ProposedSizeTests.swift b/Tests/OpenSwiftUICoreTests/Layout/Transform/ProposedSizeTests.swift similarity index 100% rename from Tests/OpenSwiftUICoreTests/Layout/LayoutFundamentals/ProposedSizeTests.swift rename to Tests/OpenSwiftUICoreTests/Layout/Transform/ProposedSizeTests.swift