diff --git a/Package.resolved b/Package.resolved index 0364f348b..c783dbfe5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "373d4af9c37ac358f7c48ef2ced6e6c83689073755afce25ca6121fd7de0cce7", + "originHash" : "76c8cae8132f5d5205a1166d013709a22af196cc16b4ce364daefb62bb6b6f32", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "5eb0f26ea5a5bbd5068f6b3daf3a97dd3682b234" + "revision" : "381059629386a9d6004ebf39c51a0ffb8e67bea8" } }, { @@ -25,7 +25,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenCoreGraphics", "state" : { "branch" : "main", - "revision" : "cd89c292c4ed4c25d9468a12d9490cc18304ff37" + "revision" : "03ddedb7e6b364573c37445f3aee267353cd4710" } }, { @@ -43,7 +43,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenRenderBox", "state" : { "branch" : "main", - "revision" : "ebed504f2785edfe500ebd0552a1bcf6d37071ba" + "revision" : "ca047c126042599d90fa103e703de8322eb573eb" } }, { diff --git a/Package.swift b/Package.swift index c791383c9..4421d427c 100644 --- a/Package.swift +++ b/Package.swift @@ -161,7 +161,7 @@ let compatibilityTestCondition = envBoolValue("COMPATIBILITY_TEST") let useLocalDeps = envBoolValue("USE_LOCAL_DEPS") let attributeGraphCondition = envBoolValue("ATTRIBUTEGRAPH", default: buildForDarwinPlatform && !isSPIBuild) -let renderBoxCondition = envBoolValue("OPENRENDERBOX_RENDERBOX", default: buildForDarwinPlatform && !isSPIBuild) +let renderBoxCondition = envBoolValue("RENDERBOX", default: buildForDarwinPlatform && !isSPIBuild) // For #39 let anyAttributeFix = envBoolValue("ANY_ATTRIBUTE_FIX", default: !buildForDarwinPlatform) @@ -192,6 +192,13 @@ let bridgeFramework = envStringValue("OPENSWIFTUI_BRIDGE_FRAMEWORK", default: "S // Workaround iOS CI build issue (We need to disable this on iOS CI) let supportMultiProducts: Bool = envBoolValue("SUPPORT_MULTI_PRODUCTS", default: true) +/// CGFloat and CGRect def in CFCGTypes.h will conflict with Foundation's CGSize/CGRect def on Linux. +/// macOS: true -> no issue +/// macOS: false -> use Swift implementation with OpenCoreGraphics Swift CGPath +/// Linux: true + No CGPathRef support in ORBPath -> confilict with Foundation def +/// Linux: false -> use Swift implementation with OpenCoreGraphics Swift CGPath +let cfCGTypes = envBoolValue("CF_CGTYPES", default: buildForDarwinPlatform) + // MARK: - Shared Settings var sharedCSettings: [CSetting] = [ @@ -373,6 +380,15 @@ if enableRuntimeConcurrencyCheck { sharedSwiftSettings.append(.define("OPENSWIFTUI_ENABLE_RUNTIME_CONCURRENCY_CHECK")) } +if cfCGTypes { + sharedCSettings.append(.define("OPENSWIFTUI_CF_CGTYPES")) + sharedCxxSettings.append(.define("OPENSWIFTUI_CF_CGTYPES")) + sharedSwiftSettings.append(.define("OPENSWIFTUI_CF_CGTYPES")) + sharedCSettings.append(.define("OPENRENDERBOX_CF_CGTYPES")) + sharedCxxSettings.append(.define("OPENRENDERBOX_CF_CGTYPES")) + sharedSwiftSettings.append(.define("OPENRENDERBOX_CF_CGTYPES")) +} + // MARK: - Extension extension Target { @@ -390,15 +406,15 @@ extension Target { // "could not determine executable path for bundle 'RenderBox.framework'" dependencies.append(.product(name: "RenderBox", package: "DarwinPrivateFrameworks")) var swiftSettings = swiftSettings ?? [] - swiftSettings.append(.define("OPENRENDERBOX_RENDERBOX")) + swiftSettings.append(.define("OPENSWIFTUI_RENDERBOX")) self.swiftSettings = swiftSettings var cSettings = cSettings ?? [] - cSettings.append(.define("OPENRENDERBOX_RENDERBOX")) + cSettings.append(.define("OPENSWIFTUI_RENDERBOX")) self.cSettings = cSettings var cxxSettings = cxxSettings ?? [] - cxxSettings.append(.define("OPENRENDERBOX_RENDERBOX")) + cxxSettings.append(.define("OPENSWIFTUI_RENDERBOX")) self.cxxSettings = cxxSettings } diff --git a/Scripts/CI/darwin_setup_build.sh b/Scripts/CI/darwin_setup_build.sh index 6662ab251..f5e00a0ac 100755 --- a/Scripts/CI/darwin_setup_build.sh +++ b/Scripts/CI/darwin_setup_build.sh @@ -8,8 +8,10 @@ filepath() { REPO_ROOT="$(dirname $(dirname $(dirname $(filepath $0))))" cd $REPO_ROOT -Scripts/CI/opencoregraphics_setup.sh +# The order of these scripts matters. +# The more foundational frameworks should be set up last. Scripts/CI/openattributegraph_setup.sh Scripts/CI/openrenderbox_setup.sh +Scripts/CI/opencoregraphics_setup.sh Scripts/CI/openobservation_setup.sh Scripts/CI/framework_setup.sh diff --git a/Sources/OpenSwiftUICore/Shape/Path.swift b/Sources/OpenSwiftUICore/Shape/Path.swift index 552b1ecef..790e37720 100644 --- a/Sources/OpenSwiftUICore/Shape/Path.swift +++ b/Sources/OpenSwiftUICore/Shape/Path.swift @@ -2,7 +2,7 @@ // Path.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: WIP // ID: 31FD92B70C320DDD253E93C7417D779A (SwiftUI) // ID: 3591905F51357E95FA93E39751507471 (SwiftUICore) @@ -12,61 +12,166 @@ package import OpenRenderBoxShims import OpenSwiftUI_SPI public import OpenCoreGraphicsShims -// MARK: - Path +// MARK: - Path [WIP] /// The outline of a 2D shape. +@available(OpenSwiftUI_v1_0, *) @frozen public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { + + // MARK: - Path.PathBox + @usableFromInline final package class PathBox: Equatable { - private enum Kind: UInt8 { + #if canImport(CoreGraphics) || !OPENSWIFTUI_CF_CGTYPES case cgPath - case obPath + #endif + case rbPath case buffer } private var kind: Kind - - #if canImport(CoreGraphics) private var data: PathData + #if canImport(CoreGraphics) || !OPENSWIFTUI_CF_CGTYPES @inline(__always) init(_ path: CGPath) { kind = .cgPath - //data = PathData(path) - _openSwiftUIUnimplementedFailure() + data = PathData(cgPath: .passUnretained(path)) } #endif package init(takingPath path: ORBPath) { - kind = .obPath - //data = PathData(path) - _openSwiftUIUnimplementedFailure() + kind = .rbPath + data = PathData(rbPath: path) } - #if canImport(CoreGraphics) private func prepareBuffer() { - let obPath: ORBPath + let path: ORBPath switch kind { + #if canImport(CoreGraphics) || !OPENSWIFTUI_CF_CGTYPES case .cgPath: - // data.cgPath - // let rbPath = ORBPathMakeWithCGPath - _openSwiftUIUnimplementedFailure() - case .obPath: - obPath = data.obPath.assumingMemoryBound(to: ORBPath.self).pointee + path = ORBPath(cgPath: data.cgPath.takeRetainedValue()) + #endif + case .rbPath: + path = data.rbPath case .buffer: return } - // ORBPath.Storage.init - // storage.appendPath - obPath.release() + withUnsafeMutablePointer(to: &data) { pointer in + let storage = unsafeBitCast(pointer, to: ORBPath.Storage.self) + storage.initialize(capacity: 96, source: nil) + storage.append(path: path) + kind = .buffer + path.release() + } + } + + private static let bufferCallbacks: UnsafePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + var callbacks = ORBPath.empty.callbacks.pointee + callbacks.retain = { object in + UnsafeRawPointer( + Unmanaged.fromOpaque(object) + .retain() + .toOpaque() + ) + } + callbacks.release = { object in + Unmanaged.fromOpaque(object) + .release() + } + callbacks.apply = { object, info, callback in + let box = object.assumingMemoryBound(to: PathBox.self).pointee + let storage = withUnsafeMutablePointer(to: &box.data) { + unsafeBitCast($0, to: ORBPath.Storage.self) + } + return storage.apply(info: info, callback: callback) + } + callbacks.isEqual = { lhs, rhs in + let lhsBox = lhs.assumingMemoryBound(to: PathBox.self).pointee + let rhsBox = rhs.assumingMemoryBound(to: PathBox.self).pointee + let lhsStorage = withUnsafeMutablePointer(to: &lhsBox.data) { + unsafeBitCast($0, to: ORBPath.Storage.self) + } + let rhsStorage = withUnsafeMutablePointer(to: &rhsBox.data) { + unsafeBitCast($0, to: ORBPath.Storage.self) + } + return lhsStorage.isEqual(to: rhsStorage) + } + callbacks.isEmpty = { object in + let box = object.assumingMemoryBound(to: PathBox.self).pointee + let storage = withUnsafeMutablePointer(to: &box.data) { + unsafeBitCast($0, to: ORBPath.Storage.self) + } + return storage.isEmpty + } + callbacks.isSingleElement = { object in + let box = object.assumingMemoryBound(to: PathBox.self).pointee + let storage = withUnsafeMutablePointer(to: &box.data) { + unsafeBitCast($0, to: ORBPath.Storage.self) + } + return storage.isEmpty + } + callbacks.bezierOrder = { object in + let box = object.assumingMemoryBound(to: PathBox.self).pointee + let storage = withUnsafeMutablePointer(to: &box.data) { + unsafeBitCast($0, to: ORBPath.Storage.self) + } + return storage.bezierOrder + } + callbacks.boundingRect = { object in + let box = object.assumingMemoryBound(to: PathBox.self).pointee + let storage = withUnsafeMutablePointer(to: &box.data) { + unsafeBitCast($0, to: ORBPath.Storage.self) + } + return storage.boundingRect + } + pointer.initialize(to: callbacks) + return UnsafePointer(pointer) + }() + + #if canImport(CoreGraphics) || !OPENSWIFTUI_CF_CGTYPES + @inline(__always) + fileprivate var cgPath: CGPath { + let rbPath: ORBPath + switch kind { + case .cgPath: + return data.cgPath.takeUnretainedValue() + case .rbPath: + rbPath = data.rbPath + case .buffer: + let storage = unsafeBitCast(self, to: ORBPath.Storage.self) + rbPath = ORBPath(storage: storage, callbacks: Self.bufferCallbacks) + } + return rbPath.cgPath } #endif + @inline(__always) + fileprivate var rbPath: ORBPath { + switch kind { + case .cgPath: + return ORBPath(cgPath: data.cgPath.takeUnretainedValue()) + case .rbPath: + return data.rbPath + case .buffer: + let storage = unsafeBitCast(self, to: ORBPath.Storage.self) + return ORBPath(storage: storage, callbacks: Self.bufferCallbacks) + } + } + + @inline(__always) + fileprivate func retainRBPath() -> ORBPath { + let rbPath = rbPath + rbPath.retain() + return rbPath + } + @usableFromInline package static func == (lhs: PathBox, rhs: PathBox) -> Bool { - _openSwiftUIUnimplementedFailure() + return lhs.rbPath.isEqual(to: rhs.rbPath) } } @@ -98,9 +203,8 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { public init() { storage = .empty } - - #if canImport(CoreGraphics) + #if canImport(CoreGraphics) || !OPENSWIFTUI_CF_CGTYPES /// Creates a path from an immutable shape path. /// /// - Parameter path: The immutable CoreGraphics path to initialize @@ -113,7 +217,7 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { } storage = .path(PathBox(path)) } - + /// Creates a path from a copy of a mutable shape path. /// /// - Parameter path: The CoreGraphics path to initialize the new @@ -125,10 +229,9 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { return } storage = .path(PathBox(path.mutableCopy()!)) - _openSwiftUIUnimplementedFailure() } #endif - + /// Creates a path containing a rectangle. /// /// This is a convenience function that creates a path of a @@ -149,7 +252,7 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { } storage = .rect(rect) } - + /// Creates a path containing a rounded rectangle. /// /// This is a convenience function that creates a path of a rounded @@ -219,6 +322,7 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { storage = .empty return } + // TODO: addRoundedRect _openSwiftUIUnimplementedFailure() } @@ -252,7 +356,7 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { } storage = rect.isInfinite ? .rect(rect) : .ellipse(rect) } - + /// Creates an empty path, then executes a closure to add its /// initial elements. /// @@ -263,7 +367,6 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { storage = .empty callback(&self) } - /// Initializes from the result of a previous call to /// `Path.stringRepresentation`. Fails if the `string` does not @@ -279,8 +382,8 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { return nil } storage = .path(PathBox(mutablePath)) - _openSwiftUIUnimplementedFailure() #else + _openSwiftUIPlatformUnimplementedWarning() return nil #endif } @@ -288,18 +391,58 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { /// A description of the path that may be used to recreate the path /// via `init?(_:)`. public var description: String { - _openSwiftUIUnimplementedFailure() + #if canImport(Darwin) + _CGPathCopyDescription(cgPath, 0.0) + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif } - #if canImport(CoreGraphics) + #if canImport(CoreGraphics) || !OPENSWIFTUI_CF_CGTYPES /// An immutable path representing the elements in the path. public var cgPath: CGPath { - _openSwiftUIUnimplementedFailure() + #if canImport(Darwin) + switch storage { + case .empty: + CGPath(rect: .null, transform: nil) + case let .rect(rect): + CGPath(rect: rect, transform: nil) + case let .ellipse(rect): + CGPath(ellipseIn: rect, transform: nil) + case let .roundedRect(fixedRoundedRect): + fixedRoundedRect.cgPath + case .stroked, .trimmed: + _openSwiftUIUnreachableCode() + case let .path(pathBox): + pathBox.cgPath + } + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif } #endif - package func retainORBPath() -> ORBPath { - _openSwiftUIUnimplementedFailure() + package func retainRBPath() -> ORBPath { + switch storage { + case .empty: + ORBPath.empty + case let .rect(rect): + ORBPath(rect: rect, transform: nil) + case let .ellipse(rect): + ORBPath(ellipseIn: rect, transform: nil) + case let .roundedRect(fixedRoundedRect): + ORBPath( + roundedRect: fixedRoundedRect.rect, + cornerWidth: fixedRoundedRect.cornerSize.width, + cornerHeight: fixedRoundedRect.cornerSize.height, + style: fixedRoundedRect.style == .circular ? .circular : .continuous, + transform: nil + ) + case .stroked, .trimmed: + _openSwiftUIUnreachableCode() + case let .path(pathBox): + pathBox.retainRBPath() + } } package mutating func withMutableBuffer(do body: (UnsafeMutableRawPointer) -> Void) { @@ -319,7 +462,7 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { public var boundingRect: CGRect { _openSwiftUIUnimplementedFailure() } - + /// Returns true if the path contains a specified point. /// /// If `eoFill` is true, this method uses the even-odd rule to define which @@ -383,11 +526,33 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { } package func rect() -> CGRect? { - _openSwiftUIUnimplementedFailure() + switch storage { + case let .rect(rect): + rect + default: + nil + } } package func roundedRect() -> FixedRoundedRect? { - _openSwiftUIUnimplementedFailure() + switch storage { + case let .rect(rect): + FixedRoundedRect(rect) + case let .ellipse(rect): + if rect.width == rect.height { + FixedRoundedRect( + rect, + cornerRadius: rect.width / 2, + style: .circular + ) + } else { + nil + } + case let .roundedRect(fixedRoundedRect): + fixedRoundedRect + default: + nil + } } } @@ -448,3 +613,255 @@ package struct TrimmedPath: Equatable { @available(*, unavailable) extension TrimmedPath: Sendable {} + +// MARK: - Path + Extension [WIP] + +@available(OpenSwiftUI_v1_0, *) +extension Path { + public mutating func move(to end: CGPoint) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addLine(to end: CGPoint) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addQuadCurve( + to end: CGPoint, + control: CGPoint + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addCurve( + to end: CGPoint, + control1: CGPoint, + control2: CGPoint + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func closeSubpath() { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addRect( + _ rect: CGRect, + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addRoundedRect( + in rect: CGRect, + cornerSize: CGSize, + style: RoundedCornerStyle = .continuous, + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + @available(OpenSwiftUI_v4_0, *) + public mutating func addRoundedRect( + in rect: CGRect, + cornerRadii: RectangleCornerRadii, + style: RoundedCornerStyle = .continuous, + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addEllipse( + in rect: CGRect, + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addRects( + _ rects: [CGRect], + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addLines(_ lines: [CGPoint]) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addRelativeArc( + center: CGPoint, + radius: CGFloat, + startAngle: Angle, + delta: Angle, + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addArc( + center: CGPoint, + radius: CGFloat, + startAngle: Angle, + endAngle: Angle, + clockwise: Bool, + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addArc( + tangent1End: CGPoint, + tangent2End: CGPoint, + radius: CGFloat, + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + public mutating func addPath( + _ path: Path, + transform: CGAffineTransform = .identity + ) { + _openSwiftUIUnimplementedFailure() + } + + public var currentPoint: CGPoint? { + get { _openSwiftUIUnimplementedFailure() } + } + + @available(OpenSwiftUI_v5_0, *) + public func normalized(eoFill: Bool = true) -> Path { + _openSwiftUIUnimplementedFailure() + } + + @available(OpenSwiftUI_v5_0, *) + public func intersection( + _ other: Path, + eoFill: Bool = false + ) -> Path { + _openSwiftUIUnimplementedFailure() + } + + @available(OpenSwiftUI_v5_0, *) + public func union( + _ other: Path, + eoFill: Bool = false + ) -> Path { + _openSwiftUIUnimplementedFailure() + } + + @available(OpenSwiftUI_v5_0, *) + public func subtracting( + _ other: Path, + eoFill: Bool = false + ) -> Path { + _openSwiftUIUnimplementedFailure() + } + + @available(OpenSwiftUI_v5_0, *) + public func symmetricDifference( + _ other: Path, + eoFill: Bool = false + ) -> Path { + _openSwiftUIUnimplementedFailure() + } + + @available(OpenSwiftUI_v5_0, *) + public func lineIntersection( + _ other: Path, + eoFill: Bool = false + ) -> Path { + _openSwiftUIUnimplementedFailure() + } + + @available(OpenSwiftUI_v5_0, *) + public func lineSubtraction( + _ other: Path, + eoFill: Bool = false + ) -> Path { + _openSwiftUIUnimplementedFailure() + } + + package mutating func formTrivialUnion(_ path: Path) { + _openSwiftUIUnimplementedFailure() + } + + public func applying(_ transform: CGAffineTransform) -> Path { + _openSwiftUIUnimplementedFailure() + } + + public func offsetBy( + dx: CGFloat, + dy: CGFloat + ) -> Path { + _openSwiftUIUnimplementedFailure() + } +} + +// MARK: - RenderBox + +private let temporaryPathCallbacks: UnsafePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + var callbacks = ORBPath.empty.callbacks.pointee + callbacks.retain = { _ in + _openSwiftUIUnreachableCode() + } + callbacks.release = { _ in + _openSwiftUIUnreachableCode() + } + callbacks.apply = { object, info, callback in + let storage = unsafeBitCast(object, to: ORBPath.Storage.self) + return storage.apply(info: info, callback: callback) + } + callbacks.isEqual = { lhs, rhs in + let lhs = unsafeBitCast(lhs, to: ORBPath.Storage.self) + let rhs = unsafeBitCast(rhs, to: ORBPath.Storage.self) + return lhs.isEqual(to: rhs) + } + callbacks.isEmpty = { object in + let storage = unsafeBitCast(object, to: ORBPath.Storage.self) + return storage.isEmpty + } + callbacks.isSingleElement = { object in + let storage = unsafeBitCast(object, to: ORBPath.Storage.self) + return storage.isSingleElement + } + callbacks.boundingRect = { object in + let storage = unsafeBitCast(object, to: ORBPath.Storage.self) + return storage.boundingRect + } + callbacks.cgPath = { object in + let storage = unsafeBitCast(object, to: ORBPath.Storage.self) + let cgPath = storage.cgPath + return cgPath.map { .passUnretained($0) } + } + pointer.initialize(to: callbacks) + return UnsafePointer(pointer) +}() + +extension ORBPath { + static func withTemporaryPath( + do body: (ORBPath) -> R, + builder: (UnsafeMutableRawPointer) -> () + ) -> R { + return withUnsafeTemporaryAllocation( + of: UInt8.self, + capacity: 128 + ) { bufferPointer in + let pointer = UnsafeMutableRawBufferPointer( + mutating: UnsafeRawBufferPointer(bufferPointer) + ).baseAddress! + let storage: ORBPath.Storage = unsafeBitCast(pointer, to: ORBPath.Storage.self) + storage.initialize(capacity: 128, source: nil) + builder(pointer) + let path = ORBPath( + storage: storage, + callbacks: temporaryPathCallbacks + ) + let result = body(path) + storage.destroy() + return result + } + } +} diff --git a/Sources/OpenSwiftUICore/Shape/PathData.swift b/Sources/OpenSwiftUICore/Shape/PathData.swift new file mode 100644 index 000000000..6e88586d9 --- /dev/null +++ b/Sources/OpenSwiftUICore/Shape/PathData.swift @@ -0,0 +1,110 @@ +// +// PathData.swift +// OpenSwiftUICore + +#if !OPENSWIFTUI_CF_CGTYPES + +package import OpenCoreGraphicsShims +package import OpenRenderBoxShims + +// MARK: - PathData + +/// A union-like structure matching the C PathData union layout. +/// Size: 0x70 (112) bytes to match the buffer size. +/// +/// C definition: +/// +/// typedef union PathData { +/// CGPathRef cgPath; // 8 bytes (pointer) +/// ORBPath rbPath; // 16 bytes (2 pointers) +/// uint8_t buffer[0x70]; // 112 bytes +/// } PathData; +package struct PathData { + // 112 bytes of raw storage (0x70) + package typealias Buffer = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 + ) + + private var storage: Buffer = ( + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ) + + package init() {} + + // MARK: - CGPath access + + package init(cgPath: Unmanaged) { + self.cgPath = cgPath + } + + package var cgPath: Unmanaged { + get { + withUnsafeBytes(of: storage) { buffer in + buffer.load(as: Unmanaged.self) + } + } + set { + withUnsafeMutableBytes(of: &storage) { buffer in + buffer.storeBytes(of: newValue, as: Unmanaged.self) + } + } + } + + // MARK: - ORBPath access + + package init(rbPath: ORBPath) { + self.rbPath = rbPath + } + + package var rbPath: ORBPath { + get { + withUnsafeBytes(of: storage) { buffer in + buffer.load(as: ORBPath.self) + } + } + set { + withUnsafeMutableBytes(of: &storage) { buffer in + buffer.storeBytes(of: newValue, as: ORBPath.self) + } + } + } + + // MARK: - Buffer access + + package init(buffer: Buffer) { + self.storage = buffer + } + + package var buffer: Buffer { + get { storage } + set { storage = newValue } + } +} + +#endif diff --git a/Sources/OpenSwiftUICore/Shape/RoundedCorner.swift b/Sources/OpenSwiftUICore/Shape/RoundedCornerStyle.swift similarity index 79% rename from Sources/OpenSwiftUICore/Shape/RoundedCorner.swift rename to Sources/OpenSwiftUICore/Shape/RoundedCornerStyle.swift index 367de674d..91d49768e 100644 --- a/Sources/OpenSwiftUICore/Shape/RoundedCorner.swift +++ b/Sources/OpenSwiftUICore/Shape/RoundedCornerStyle.swift @@ -1,17 +1,21 @@ // -// RoundedCorner.swift +// RoundedCornerStyle.swift // OpenSwiftUICore // -// Audited for 6.0.87 -// Status: WIP +// Audited for 6.5.4 +// Status: Complete public import Foundation package import OpenCoreGraphicsShims +import OpenSwiftUI_SPI +package import OpenRenderBoxShims // MARK: - RoundedCornerStyle /// Defines the shape of a rounded rectangle's corners. +@available(OpenSwiftUI_v1_0, *) public enum RoundedCornerStyle: Sendable { + /// Quarter-circle rounded rect corners. case circular @@ -19,8 +23,9 @@ public enum RoundedCornerStyle: Sendable { case continuous } -// MARK: - FixedRoundedRect [WIP] +// MARK: - FixedRoundedRect +@available(OpenSwiftUI_v1_0, *) @usableFromInline package struct FixedRoundedRect: Equatable { package var rect: CGRect @@ -62,25 +67,54 @@ package struct FixedRoundedRect: Equatable { package var clampedCornerSize: CGSize { let minRadius = min(abs(rect.width) / 2, abs(rect.height) / 2) return CGSize(width: min(minRadius, cornerSize.width), height: min(minRadius, cornerSize.height)) - } package var clampedCornerRadius: CGFloat { min(min(rect.width, rect.height) / 2, cornerSize.width) } - // TODO: RenderBox - // package func withTemporaryPath(_ body: (ORBPath) -> R) -> R + #if canImport(Darwin) + package func withTemporaryPath(_ body: (ORBPath) -> R) -> R { + ORBPath.withTemporaryPath(do: body) { pointer in + let storage = unsafeBitCast(pointer, to: ORBPath.Storage.self) + var points: [CGFloat] = [ + rect.x, + rect.y, + rect.width, + rect.height, + cornerSize.width, + cornerSize.height + ] + storage.append( + element: style == .continuous ? .fixedRoundedRectContinuous : .fixedRoundedRectCircular, + points: &points, + userInfo: nil + ) + } + } package func contains(_ point: CGPoint) -> Bool { - // TODO: ORBPath - _openSwiftUIUnimplementedFailure() + withTemporaryPath { path in + path.contains(point: point, eoFill: false) + } } + #endif package func applying(_ m: CGAffineTransform) -> FixedRoundedRect { - FixedRoundedRect( - rect.applying(m), - cornerSize: cornerSize.isFinite ? cornerSize.applying(m) : cornerSize, // FIXME + let newRect = rect.applying(m) + let newCornerSize: CGSize + if cornerSize.isFinite { + let appliedSize = cornerSize.applying(m) + newCornerSize = CGSize( + width: copysign(appliedSize.width, cornerSize.width), + height: copysign(appliedSize.height, cornerSize.height) + ) + } else { + newCornerSize = cornerSize + } + return FixedRoundedRect( + newRect, + cornerSize: newCornerSize, style: style ) } @@ -142,9 +176,19 @@ package struct FixedRoundedRect: Equatable { ) } - #if canImport(CoreGraphics) + #if canImport(CoreGraphics) || !OPENSWIFTUI_CF_CGTYPES package var cgPath: CGPath { - _openSwiftUIUnimplementedFailure() + #if canImport(Darwin) + let clamped = clampedCornerSize + return _CGPathCreateRoundedRect( + rect, + clamped.width, + clamped.height, + needsContinuousCorners ? .continuous : .circular + ) + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif } #endif @@ -157,6 +201,8 @@ package struct FixedRoundedRect: Equatable { @available(*, unavailable) extension FixedRoundedRect: Sendable {} +// MARK: - FixedRoundedRect + Protobuf + extension FixedRoundedRect: ProtobufMessage { package func encode(to encoder: inout ProtobufEncoder) throws { try encoder.messageField(1, rect, defaultValue: .zero) @@ -178,6 +224,8 @@ extension FixedRoundedRect: ProtobufMessage { } } +// MARK: - RoundedCornerStyle + Protobuf + extension RoundedCornerStyle: ProtobufEnum { package var protobufValue: UInt { switch self { @@ -199,6 +247,7 @@ extension RoundedCornerStyle: ProtobufEnum { /// Describes the corner radius values of a rounded rectangle with /// uneven corners. +@available(OpenSwiftUI_v4_0, *) @frozen public struct RectangleCornerRadii: Equatable, Animatable { @usableFromInline diff --git a/Sources/OpenSwiftUI_SPI/OpenSwiftUIBase.h b/Sources/OpenSwiftUI_SPI/OpenSwiftUIBase.h index 3d560a7b8..696e4a892 100644 --- a/Sources/OpenSwiftUI_SPI/OpenSwiftUIBase.h +++ b/Sources/OpenSwiftUI_SPI/OpenSwiftUIBase.h @@ -94,4 +94,24 @@ #endif #endif +// Marks functions which return a CF type that needs to be released by the caller but whose names are not consistent with CoreFoundation naming rules. The recommended fix to this is to rename the functions, but this macro can be used to let the clang static analyzer know of any exceptions that cannot be fixed. +// This macro is ONLY to be used in exceptional circumstances, not to annotate functions which conform to the CoreFoundation naming rules. +#ifndef OPENSWIFTUI_RETURNS_RETAINED +#if __has_feature(attribute_cf_returns_retained) +#define OPENSWIFTUI_RETURNS_RETAINED __attribute__((cf_returns_retained)) +#else +#define OPENSWIFTUI_RETURNS_RETAINED +#endif +#endif + +// Marks functions which return a CF type that may need to be retained by the caller but whose names are not consistent with CoreFoundation naming rules. The recommended fix to this is to rename the functions, but this macro can be used to let the clang static analyzer know of any exceptions that cannot be fixed. +// This macro is ONLY to be used in exceptional circumstances, not to annotate functions which conform to the CoreFoundation naming rules. +#ifndef OPENSWIFTUI_RETURNS_NOT_RETAINED +#if __has_feature(attribute_cf_returns_not_retained) +#define OPENSWIFTUI_RETURNS_NOT_RETAINED __attribute__((cf_returns_not_retained)) +#else +#define OPENSWIFTUI_RETURNS_NOT_RETAINED +#endif +#endif + #endif /* OpenSwiftUIBase_h */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h index ab602bda2..f3c2e9e59 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h @@ -14,6 +14,14 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN +/// Defines the shape of a rounded rectangle's corners. +typedef OPENSWIFTUI_ENUM(uint8_t, PathRoundedCornerStyle) { + /// Quarter-circle rounded rect corners. + PathRoundedCornerStyleCircular = 0, + /// Continuous curvature rounded rect corners. + PathRoundedCornerStyleContinuous = 1, +}; + /// Parses a path string and appends the path elements to a mutable path. /// /// The string format uses space-separated numbers followed by command characters: @@ -59,12 +67,13 @@ NSString * _CGPathCopyDescription(CGPathRef path, CGFloat step); /// - rect: The rectangle to create the path from. /// - cornerWidth: The horizontal radius of the rounded corners. /// - cornerHeight: The vertical radius of the rounded corners. -/// - useRB: If `YES`, uses RenderBox for path creation (when available). -/// If `NO`, uses CoreGraphics directly. +/// - style: The corner style to use. ``CGPathRoundedCornerStyleCircular`` +/// uses quarter-circle corners. ``CGPathRoundedCornerStyleContinuous`` +/// uses continuous curvature corners. /// - Returns: A new path representing the rounded rectangle. Returns a plain /// rectangle path if either corner dimension is 0 or if the rect is empty. -CF_RETURNS_RETAINED -CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, BOOL useRB); +OPENSWIFTUI_RETURNS_RETAINED +CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, PathRoundedCornerStyle style); OPENSWIFTUI_ASSUME_NONNULL_END diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m index 797051594..62cdfa2d2 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m @@ -10,10 +10,10 @@ #import #import -#if OPENRENDERBOX_RENDERBOX -@import RenderBox; +#if OPENSWIFTUI_RENDERBOX +#include #else -@import OpenRenderBox; +#include #endif BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) { @@ -213,7 +213,7 @@ void copy_path_iter(void * __nullable info, const CGPathElement * element) { return (__bridge_transfer NSString *)(info.description); } -CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, BOOL useRB) { +CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, PathRoundedCornerStyle style) { // Clamp corner dimensions to be non-negative if (cornerWidth < 0.0) { cornerWidth = 0.0; @@ -227,21 +227,21 @@ CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cor return CGPathCreateWithRect(rect, NULL); } - if (useRB) { - #if OPENRENDERBOX_RENDERBOX - // RBPath rbPath = RBPathMakeRoundedRect(NULL, rect, cornerWidth, cornerHeight, YES); - // CGPathRef cgPath = RBPathCopyCGPath(rbPath); - // RBPathRelease(rbPath); - // return cgPath; + if (style == PathRoundedCornerStyleContinuous) { + #if OPENSWIFTUI_RENDERBOX + RBPath rbPath = RBPathMakeRoundedRect(rect, cornerWidth, cornerHeight, RBPathRoundedCornerStyleContinuous, NULL); + CGPathRef cgPath = RBPathCopyCGPath(rbPath); + RBPathRelease(rbPath); + return cgPath; #else - // ORBPath rbPath = ORBPathMakeRoundedRect(NULL, rect, cornerWidth, cornerHeight, YES); - // CGPathRef cgPath = ORBPathCopyCGPath(rbPath); - // ORBPathRelease(rbPath); - // return cgPath; + ORBPath rbPath = ORBPathMakeRoundedRect(rect, cornerWidth, cornerHeight, ORBPathRoundedCornerStyleContinuous, NULL); + CGPathRef cgPath = ORBPathCopyCGPath(rbPath); + ORBPathRelease(rbPath); + return cgPath; #endif } - // Use CoreGraphics path creation + // Use CoreGraphics path creation (circular style) CGFloat width = CGRectGetWidth(rect); CGFloat height = CGRectGetHeight(rect); diff --git a/Sources/OpenSwiftUI_SPI/Util/PathData.h b/Sources/OpenSwiftUI_SPI/Util/PathData.h index 19b5655f5..4ef39e66d 100644 --- a/Sources/OpenSwiftUI_SPI/Util/PathData.h +++ b/Sources/OpenSwiftUI_SPI/Util/PathData.h @@ -2,6 +2,8 @@ // PathData.h // OpenSwiftUI_SPI +#if OPENSWIFTUI_CF_CGTYPES + #ifndef PathData_h #define PathData_h @@ -9,17 +11,34 @@ #if OPENSWIFTUI_TARGET_OS_DARWIN #include +#endif + +#if OPENSWIFTUI_RENDERBOX +#include +#else +#include +#endif + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +OPENSWIFTUI_IMPLICIT_BRIDGING_ENABLED typedef union PathData { + #if OPENSWIFTUI_TARGET_OS_DARWIN CGPathRef cgPath; - void * obPath; // FIXME + #endif + #if OPENSWIFTUI_RENDERBOX + RBPath rbPath; + #else + ORBPath rbPath; + #endif + uint8_t buffer[0x70]; } PathData; -#else -typedef union PathData { - void *cgPath; - void *obPath; // FIXME -} PathData; -#endif +OPENSWIFTUI_ASSUME_NONNULL_END + +OPENSWIFTUI_IMPLICIT_BRIDGING_DISABLED #endif /* PathData_h */ + +#endif /* OPENSWIFTUI_CF_CGTYPES */ diff --git a/Tests/OpenSwiftUICoreTests/Shape/Path/FixedRoundedRectTests.swift b/Tests/OpenSwiftUICoreTests/Shape/Path/FixedRoundedRectTests.swift index 52edd8e89..bd8f5895d 100644 --- a/Tests/OpenSwiftUICoreTests/Shape/Path/FixedRoundedRectTests.swift +++ b/Tests/OpenSwiftUICoreTests/Shape/Path/FixedRoundedRectTests.swift @@ -3,11 +3,220 @@ // OpenSwiftUITests @testable import OpenSwiftUICore +import Foundation import Testing struct FixedRoundedRectTests { + // MARK: - Initialization + + @Test + func initWithRect() { + let rect = CGRect(x: 10, y: 20, width: 100, height: 50) + let roundedRect = FixedRoundedRect(rect) + #expect(roundedRect.rect == rect) + #expect(roundedRect.cornerSize == .zero) + #expect(roundedRect.style == .circular) + } + + @Test + func initWithCornerSize() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let cornerSize = CGSize(width: 10, height: 15) + let roundedRect = FixedRoundedRect(rect, cornerSize: cornerSize, style: .continuous) + #expect(roundedRect.rect == rect) + #expect(roundedRect.cornerSize == cornerSize) + #expect(roundedRect.style == .continuous) + } + + @Test + func initWithCornerRadius() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let roundedRect = FixedRoundedRect(rect, cornerRadius: 12, style: .circular) + #expect(roundedRect.rect == rect) + #expect(roundedRect.cornerSize == CGSize(width: 12, height: 12)) + #expect(roundedRect.style == .circular) + } + + // MARK: - Properties + + @Test + func isRounded() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let notRounded = FixedRoundedRect(rect) + #expect(notRounded.isRounded == false) + + let rounded = FixedRoundedRect(rect, cornerRadius: 10, style: .circular) + #expect(rounded.isRounded == true) + + let partiallyRounded = FixedRoundedRect(rect, cornerSize: CGSize(width: 5, height: 0), style: .circular) + #expect(partiallyRounded.isRounded == true) + } + + @Test + func isUniform() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let uniform = FixedRoundedRect(rect, cornerRadius: 10, style: .circular) + #expect(uniform.isUniform == true) + + let nonUniform = FixedRoundedRect(rect, cornerSize: CGSize(width: 10, height: 15), style: .circular) + #expect(nonUniform.isUniform == false) + } + + @Test + func needsContinuousCorners() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let circularRounded = FixedRoundedRect(rect, cornerRadius: 10, style: .circular) + #expect(circularRounded.needsContinuousCorners == false) + + let continuousRounded = FixedRoundedRect(rect, cornerRadius: 10, style: .continuous) + #expect(continuousRounded.needsContinuousCorners == true) + + let continuousNotRounded = FixedRoundedRect(rect, cornerRadius: 0, style: .continuous) + #expect(continuousNotRounded.needsContinuousCorners == false) + } + + @Test + func clampedCornerSize() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + // Corner size fits within rect + let normal = FixedRoundedRect(rect, cornerSize: CGSize(width: 10, height: 10), style: .circular) + #expect(normal.clampedCornerSize == CGSize(width: 10, height: 10)) + + // Corner size exceeds half of smaller dimension (height=50, so max=25) + let oversized = FixedRoundedRect(rect, cornerSize: CGSize(width: 30, height: 30), style: .circular) + #expect(oversized.clampedCornerSize == CGSize(width: 25, height: 25)) + + // Non-uniform corner size + let nonUniform = FixedRoundedRect(rect, cornerSize: CGSize(width: 40, height: 10), style: .circular) + #expect(nonUniform.clampedCornerSize == CGSize(width: 25, height: 10)) + } + @Test - func contains() { - // TODO + func clampedCornerRadius() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let normal = FixedRoundedRect(rect, cornerRadius: 10, style: .circular) + #expect(normal.clampedCornerRadius == 10) + + let oversized = FixedRoundedRect(rect, cornerRadius: 30, style: .circular) + #expect(oversized.clampedCornerRadius == 25) + } + + // MARK: - Equality + + @Test + func equality() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let a = FixedRoundedRect(rect, cornerRadius: 10, style: .circular) + let b = FixedRoundedRect(rect, cornerRadius: 10, style: .circular) + let c = FixedRoundedRect(rect, cornerRadius: 10, style: .continuous) + let d = FixedRoundedRect(rect, cornerRadius: 15, style: .circular) + #expect(a == b) + #expect(a != c) + #expect(a != d) + } + + // MARK: - Transformations + + @Test + func insetBy() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let roundedRect = FixedRoundedRect(rect, cornerRadius: 10, style: .circular) + + let inset = roundedRect.insetBy(dx: 5, dy: 5) + #expect(inset != nil) + #expect(inset?.rect == CGRect(x: 5, y: 5, width: 90, height: 40)) + #expect(inset?.cornerSize == CGSize(width: 5, height: 5)) + + // Zero inset returns self + let noInset = roundedRect.insetBy(dx: 0, dy: 0) + #expect(noInset == roundedRect) + + // Inset that makes rect empty returns nil + let tooMuchInset = roundedRect.insetBy(dx: 60, dy: 30) + #expect(tooMuchInset == nil) + } + + @Test + func hasIntersection() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let roundedRect = FixedRoundedRect(rect, cornerRadius: 10, style: .circular) + + #expect(roundedRect.hasIntersection(CGRect(x: 50, y: 25, width: 20, height: 10)) == true) + #expect(roundedRect.hasIntersection(CGRect(x: 200, y: 200, width: 20, height: 10)) == false) + #expect(roundedRect.hasIntersection(CGRect(x: 90, y: 40, width: 20, height: 20)) == true) + } + + // MARK: - Contains + + @Test + func containsFixedRoundedRect() { + let outer = FixedRoundedRect( + CGRect(x: 0, y: 0, width: 100, height: 100), + cornerRadius: 10, + style: .circular + ) + let inner = FixedRoundedRect( + CGRect(x: 10, y: 10, width: 80, height: 80), + cornerRadius: 5, + style: .circular + ) + let outside = FixedRoundedRect( + CGRect(x: 200, y: 200, width: 50, height: 50), + cornerRadius: 5, + style: .circular + ) + + #expect(outer.contains(inner) == true) + #expect(outer.contains(outside) == false) + #expect(inner.contains(outer) == false) + } + + @Test + func containsRect() { + let roundedRect = FixedRoundedRect( + CGRect(x: 0, y: 0, width: 100, height: 100), + cornerRadius: 10, + style: .circular + ) + + #expect(roundedRect.contains(rect: CGRect(x: 20, y: 20, width: 60, height: 60)) == true) + #expect(roundedRect.contains(rect: CGRect(x: 200, y: 200, width: 10, height: 10)) == false) + } + + #if canImport(Darwin) + @Test(arguments: [ + (CGPoint(x: 50, y: 50), true), // Center point - inside + (CGPoint(x: 200, y: 200), false), // Point clearly outside + (CGPoint(x: 50, y: 1), true), // Near top edge - inside + (CGPoint(x: 1, y: 50), true), // Near left edge - inside + (CGPoint(x: 5, y: 5), true), // Corner region - inside rounded corner + (CGPoint(x: 1, y: 1), false), // Corner - outside due to rounding + (CGPoint(x: 99, y: 99), false), // Bottom-right corner - outside due to rounding + (CGPoint(x: 95, y: 95), true), // Bottom-right corner region - inside + ] as [(CGPoint, Bool)]) + func containsPoint(point: CGPoint, expected: Bool) { + let roundedRect = FixedRoundedRect( + CGRect(x: 0, y: 0, width: 100, height: 100), + cornerRadius: 10, + style: .circular + ) + #expect(roundedRect.contains(point) == expected) + } + + @Test(arguments: [ + (CGRect(x: 0, y: 0, width: 100, height: 100), 10.0, RoundedCornerStyle.circular), + (CGRect(x: 10, y: 20, width: 80, height: 60), 15.0, RoundedCornerStyle.continuous), + (CGRect(x: 0, y: 0, width: 50, height: 50), 5.0, RoundedCornerStyle.circular), + ] as [(CGRect, CGFloat, RoundedCornerStyle)]) + func withTemporaryPath(rect: CGRect, cornerRadius: CGFloat, style: RoundedCornerStyle) { + let roundedRect = FixedRoundedRect(rect, cornerRadius: cornerRadius, style: style) + let boundingRect = roundedRect.withTemporaryPath { path in + path.storage.boundingRect + } + #expect(boundingRect.origin.x == rect.origin.x) + #expect(boundingRect.origin.y == rect.origin.y) + #expect(boundingRect.size.width == rect.size.width) + #expect(boundingRect.size.height == rect.size.height) } + #endif } diff --git a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift index 79b58b22c..8de7b8595 100644 --- a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift +++ b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift @@ -97,33 +97,28 @@ struct CGPath_OpenSwiftUITests { @Test func zeroCornerWidth() { let rect = CGRect(x: 0, y: 0, width: 100, height: 50) - let path = _CGPathCreateRoundedRect(rect, 0, 10, false) - // Should return a plain rectangle + let path = _CGPathCreateRoundedRect(rect, 0, 10, .circular) #expect(_CGPathCopyDescription(path, 0) == " 0 0 m 100 0 l 100 50 l 0 50 l h") } @Test func zeroCornerHeight() { let rect = CGRect(x: 0, y: 0, width: 100, height: 50) - let path = _CGPathCreateRoundedRect(rect, 10, 0, false) - // Should return a plain rectangle + let path = _CGPathCreateRoundedRect(rect, 10, 0, .circular) #expect(_CGPathCopyDescription(path, 0) == " 0 0 m 100 0 l 100 50 l 0 50 l h") } @Test func negativeCornerValues() { let rect = CGRect(x: 0, y: 0, width: 100, height: 50) - let path = _CGPathCreateRoundedRect(rect, -5, -5, false) - // Negative values are clamped to 0, so should return a plain rectangle + let path = _CGPathCreateRoundedRect(rect, -5, -5, .circular) #expect(_CGPathCopyDescription(path, 0) == " 0 0 m 100 0 l 100 50 l 0 50 l h") } @Test func emptyRect() { - // A rect with zero width or height is considered empty by CGRectIsEmpty let rect = CGRect(x: 0, y: 0, width: 0, height: 50) - let path = _CGPathCreateRoundedRect(rect, 10, 10, false) - // Empty rect returns a plain rectangle path + let path = _CGPathCreateRoundedRect(rect, 10, 10, .circular) #expect(_CGPathCopyDescription(path, 0) == " 0 0 m 0 0 l 0 50 l 0 50 l h") } }