diff --git a/Package.resolved b/Package.resolved index 0c0b78252..c200ce266 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "4e71b47a8b8bd086a75a13f4c246fcad640776da2cea47fbd57e6a8f43ee3b03", + "originHash" : "4b26e194c344de78e727de247c6c767bb827826c2340b7b32611453992a62d3d", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "2cec508df7d16801a1bb5b659b906cec465b213e" + "revision" : "6ef0aae7ac472a4c47cd0569000138f5c852eb67" } }, { @@ -16,7 +16,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenBox", "state" : { "branch" : "main", - "revision" : "e1626f862c9c6da0813a9b8bb4172978054bc6b6" + "revision" : "de81c145fd3ef582d4cb41b2190febf487ce6125" } }, { @@ -25,7 +25,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenGraph", "state" : { "branch" : "main", - "revision" : "90da6a61a746df56dcdde3ef05cdd04d64377c2b" + "revision" : "5204ae0d031a520df99b5b2f32c5c9ddbfdb5036" } }, { diff --git a/Package.swift b/Package.swift index 108c6c9c2..8ec4fa3f0 100644 --- a/Package.swift +++ b/Package.swift @@ -31,6 +31,14 @@ let includePath = SDKPath.appending("/usr/lib/swift") var sharedCSettings: [CSetting] = [ .unsafeFlags(["-I", includePath], .when(platforms: .nonDarwinPlatforms)), + .unsafeFlags(["-fmodules"]), + .define("__COREFOUNDATION_FORSWIFTFOUNDATIONONLY__", to: "1", .when(platforms: .nonDarwinPlatforms)), + .define("_WASI_EMULATED_SIGNAL", .when(platforms: [.wasi])), +] + +var sharedCxxSettings: [CXXSetting] = [ + .unsafeFlags(["-I", includePath], .when(platforms: .nonDarwinPlatforms)), + .unsafeFlags(["-fcxx-modules"]), .define("__COREFOUNDATION_FORSWIFTFOUNDATIONONLY__", to: "1", .when(platforms: .nonDarwinPlatforms)), .define("_WASI_EMULATED_SIGNAL", .when(platforms: [.wasi])), ] @@ -79,6 +87,27 @@ let bridgeFramework = Context.environment["OPENSWIFTUI_BRIDGE_FRAMEWORK"] ?? "Sw // MARK: - Targets +let cOpenSwiftUITarget = Target.target( + name: "COpenSwiftUI", + publicHeadersPath: ".", + cSettings: sharedCSettings + [ + .headerSearchPath("../OpenSwiftUI_SPI"), + ], + cxxSettings: sharedCxxSettings +) +let openSwiftUISPITarget = Target.target( + name: "OpenSwiftUI_SPI", + dependencies: [ + .product(name: "OpenBox", package: "OpenBox"), + ], + publicHeadersPath: ".", + cSettings: sharedCSettings + [.define("_GNU_SOURCE", .when(platforms: .nonDarwinPlatforms))], + cxxSettings: sharedCxxSettings +) +let coreGraphicsShims = Target.target( + name: "CoreGraphicsShims", + swiftSettings: sharedSwiftSettings +) // NOTE: // In macOS: Mac Catalyst App will use macOS-varient build of SwiftUI.framework in /System/Library/Framework and iOS varient of SwiftUI.framework in /System/iOSSupport/System/Library/Framework // Add `|| Mac Catalyst` check everywhere in `OpenSwiftUICore` and `OpenSwiftUI_SPI`. @@ -86,6 +115,7 @@ let openSwiftUICoreTarget = Target.target( name: "OpenSwiftUICore", dependencies: [ "OpenSwiftUI_SPI", + "CoreGraphicsShims", .product(name: "OpenGraphShims", package: "OpenGraph"), .product(name: "OpenBoxShims", package: "OpenBox"), ], @@ -96,6 +126,7 @@ let openSwiftUITarget = Target.target( dependencies: [ "OpenSwiftUICore", "COpenSwiftUI", + "CoreGraphicsShims", .target(name: "CoreServices", condition: .when(platforms: [.iOS])), .product(name: "OpenGraphShims", package: "OpenGraph"), .product(name: "OpenBoxShims", package: "OpenBox"), @@ -118,7 +149,7 @@ let openSwiftUIBridgeTarget = Target.target( sources: ["Bridgeable.swift", bridgeFramework], swiftSettings: sharedSwiftSettings ) -let OpenSwiftUI_SPITestTarget = Target.testTarget( +let openSwiftUISPITestTarget = Target.testTarget( name: "OpenSwiftUI_SPITests", dependencies: [ "OpenSwiftUI_SPI", @@ -167,11 +198,22 @@ let openSwiftUIBridgeTestTarget = Target.testTarget( // Workaround iOS CI build issue (We need to disable this on iOS CI) let supportMultiProducts: Bool = envEnable("OPENSWIFTUI_SUPPORT_MULTI_PRODUCTS", default: true) +let libraryType: Product.Library.LibraryType? +switch Context.environment["OPENSWIFTUI_LIBRARY_TYPE"] { +case "dynamic": + libraryType = .dynamic +case "static": + libraryType = .static +default: + libraryType = nil +} + var products: [Product] = [ - .library(name: "OpenSwiftUI", targets: ["OpenSwiftUI"]) + .library(name: "OpenSwiftUI", type: libraryType, targets: ["OpenSwiftUI"]) ] if supportMultiProducts { products += [ + .library(name: "OpenSwiftUICore", type: libraryType, targets: ["OpenSwiftUICore"]), .library(name: "OpenSwiftUI_SPI", targets: ["OpenSwiftUI_SPI"]), .library(name: "OpenSwiftUIExtension", targets: ["OpenSwiftUIExtension"]), .library(name: "OpenSwiftUIBridge", targets: ["OpenSwiftUIBridge"]) @@ -194,26 +236,17 @@ let package = Package( .apt(["libgtk-4-dev clang"]), ] ), - .target( - name: "OpenSwiftUI_SPI", - publicHeadersPath: ".", - cSettings: sharedCSettings - ), - .target( - name: "COpenSwiftUI", - publicHeadersPath: ".", - cSettings: sharedCSettings + [ - .headerSearchPath("../OpenSwiftUI_SPI"), - ] - ), .binaryTarget(name: "CoreServices", path: "PrivateFrameworks/CoreServices.xcframework"), + coreGraphicsShims, + cOpenSwiftUITarget, + openSwiftUISPITarget, openSwiftUICoreTarget, openSwiftUITarget, openSwiftUIExtensionTarget, openSwiftUIBridgeTarget, - OpenSwiftUI_SPITestTarget, + openSwiftUISPITestTarget, openSwiftUICoreTestTarget, openSwiftUITestTarget, openSwiftUICompatibilityTestTarget, @@ -274,7 +307,7 @@ if attributeGraphCondition { openSwiftUICoreTarget.addAGSettings() openSwiftUITarget.addAGSettings() - OpenSwiftUI_SPITestTarget.addAGSettings() + openSwiftUISPITestTarget.addAGSettings() openSwiftUICoreTestTarget.addAGSettings() openSwiftUITestTarget.addAGSettings() openSwiftUICompatibilityTestTarget.addAGSettings() @@ -291,7 +324,7 @@ if renderBoxCondition { openSwiftUICoreTarget.addRBSettings() openSwiftUITarget.addRBSettings() - OpenSwiftUI_SPITestTarget.addRBSettings() + openSwiftUISPITestTarget.addRBSettings() openSwiftUICoreTestTarget.addRBSettings() openSwiftUITestTarget.addRBSettings() openSwiftUICompatibilityTestTarget.addRBSettings() diff --git a/Sources/CoreGraphicsShims/CGAffineTransform.swift b/Sources/CoreGraphicsShims/CGAffineTransform.swift new file mode 100644 index 000000000..0e0c27752 --- /dev/null +++ b/Sources/CoreGraphicsShims/CGAffineTransform.swift @@ -0,0 +1,64 @@ +// +// CGAffineTransform.swift +// CoreGraphicsShims + +#if !canImport(CoreGraphics) +public import Foundation + +// FIXME: Use Silica or other implementation +public struct CGAffineTransform: Equatable { + public init() { + a = .zero + b = .zero + c = .zero + d = .zero + tx = .zero + ty = .zero + } + + public init(a: Double, b: Double, c: Double, d: Double, tx: Double, ty: Double) { + self.a = a + self.b = b + self.c = c + self.d = d + self.tx = tx + self.ty = ty + } + + public var a: Double + public var b: Double + public var c: Double + public var d: Double + public var tx: Double + public var ty: Double + + public static let identity = CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0) + + public func concatenating(_ transform: CGAffineTransform) -> CGAffineTransform { + preconditionFailure("Unimplemented") + } + + public func inverted() -> CGAffineTransform { + preconditionFailure("Unimplemented") + } +} + +extension CGPoint { + public func applying(_ t: CGAffineTransform) -> CGPoint { + preconditionFailure("Unimplemented") + } +} + +extension CGSize { + public func applying(_ t: CGAffineTransform) -> CGSize { + preconditionFailure("Unimplemented") + } +} + +extension CGRect { + public func applying(_ t: CGAffineTransform) -> CGRect { + preconditionFailure("Unimplemented") + } +} + +#endif diff --git a/Sources/CoreGraphicsShims/CGLine.swift b/Sources/CoreGraphicsShims/CGLine.swift new file mode 100644 index 000000000..d792f90d5 --- /dev/null +++ b/Sources/CoreGraphicsShims/CGLine.swift @@ -0,0 +1,21 @@ +// +// CGLine.swift +// CoreGraphicsShims + +#if !canImport(CoreGraphics) + +/// Line join styles +public enum CGLineJoin: Int32, @unchecked Sendable { + case miter = 0 + case round = 1 + case bevel = 2 +} + +/// Line cap styles +public enum CGLineCap : Int32, @unchecked Sendable { + case butt = 0 + case round = 1 + case square = 2 +} + +#endif diff --git a/Sources/CoreGraphicsShims/Export.swift b/Sources/CoreGraphicsShims/Export.swift new file mode 100644 index 000000000..92457f173 --- /dev/null +++ b/Sources/CoreGraphicsShims/Export.swift @@ -0,0 +1,9 @@ +// +// Export.swift +// CoreGraphicsShims + +#if canImport(CoreGraphics) +@_exported import CoreGraphics +#else +@_exported import CoreFoundation +#endif diff --git a/Sources/OpenSwiftUI/Integration/Graphic/UIKit/UIKitConversions.swift b/Sources/OpenSwiftUI/Integration/Graphic/UIKit/UIKitConversions.swift index 823777298..5520728fe 100644 --- a/Sources/OpenSwiftUI/Integration/Graphic/UIKit/UIKitConversions.swift +++ b/Sources/OpenSwiftUI/Integration/Graphic/UIKit/UIKitConversions.swift @@ -203,4 +203,30 @@ extension DynamicTypeSize { } } +extension LayoutDirection { + /// Create a direction from its UITraitEnvironmentLayoutDirection equivalent. + public init?(_ uiLayoutDirection: UITraitEnvironmentLayoutDirection) { + switch uiLayoutDirection { + case .unspecified: + return nil + case .leftToRight: + self = .leftToRight + case .rightToLeft: + self = .rightToLeft + @unknown default: + return nil + } + } +} + +extension UITraitEnvironmentLayoutDirection { + /// Creates a trait environment layout direction from the specified OpenSwiftUI layout direction. + public init(_ layoutDirection: LayoutDirection) { + switch layoutDirection { + case .leftToRight: self = .leftToRight + case .rightToLeft: self = .rightToLeft + } + } +} + #endif diff --git a/Sources/OpenSwiftUICore/Extension/CGAffineTransform+Extension.swift b/Sources/OpenSwiftUICore/Extension/CGAffineTransform+Extension.swift index e0d174f4c..6bb4c21a1 100644 --- a/Sources/OpenSwiftUICore/Extension/CGAffineTransform+Extension.swift +++ b/Sources/OpenSwiftUICore/Extension/CGAffineTransform+Extension.swift @@ -5,48 +5,8 @@ // Audited for iOS 18.0 // Status: Complete -#if canImport(CoreGraphics) -package import CoreGraphics -#else +package import CoreGraphicsShims package import Foundation -// FIXME: Use Silica or other implementation -public struct CGAffineTransform: Equatable { - public init() { - a = .zero - b = .zero - c = .zero - d = .zero - tx = .zero - ty = .zero - } - - public init(a: Double, b: Double, c: Double, d: Double, tx: Double, ty: Double) { - self.a = a - self.b = b - self.c = c - self.d = d - self.tx = tx - self.ty = ty - } - - public var a: Double - public var b: Double - public var c: Double - public var d: Double - public var tx: Double - public var ty: Double - - public static let identity = CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0) - - public func concatenating(_ transform: CGAffineTransform) -> CGAffineTransform { - preconditionFailure("Unimplemented") - } - - public func inverted() -> CGAffineTransform { - preconditionFailure("Unimplemented") - } -} -#endif extension CGAffineTransform { package init(rotation: Angle) { diff --git a/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirection.swift b/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirection.swift index b08364d83..f1872f761 100644 --- a/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirection.swift +++ b/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirection.swift @@ -1,12 +1,13 @@ // // LayoutDirection.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete -// ID: 9DE75C3EAC30FFAB943BCC50F6D5E8C1 +// ID: 9DE75C3EAC30FFAB943BCC50F6D5E8C1 (SwiftUI) +// ID: 54C853EF26D00A0E6B1785C3902A74F4 (SwiftUICore) -import Foundation +package import Foundation // MARK: - LayoutDirection @@ -27,73 +28,64 @@ import Foundation /// value. OpenSwiftUI horizontally flips the x position of each view within its /// parent, so layout calculations automatically produce the desired effect /// for both modes without any changes. -public enum LayoutDirection: Hashable, CaseIterable { +public enum LayoutDirection: Hashable, CaseIterable, Sendable { /// A left-to-right layout direction. case leftToRight /// A right-to-left layout direction. case rightToLeft -} -extension LayoutDirection: Sendable {} - -#if canImport(UIKit) -// MARK: - UIKit integration - -public import UIKit - -extension LayoutDirection { - /// Create a direction from its UITraitEnvironmentLayoutDirection equivalent. - public init?(_ uiLayoutDirection: UITraitEnvironmentLayoutDirection) { - switch uiLayoutDirection { - case .unspecified: - return nil - case .leftToRight: - self = .leftToRight - case .rightToLeft: - self = .rightToLeft - @unknown default: - return nil + package func convert(_ rect: CGRect, to layoutDirection: LayoutDirection, in size: CGSize) -> CGRect { + guard self != layoutDirection else { + return rect } + return CGRect(origin: CGPoint(x: size.width - rect.x - rect.width, y: rect.y), size: rect.size) } -} -extension UITraitEnvironmentLayoutDirection { - /// Creates a trait environment layout direction from the specified OpenSwiftUI layout direction. - public init(_ layoutDirection: LayoutDirection) { - switch layoutDirection { - case .leftToRight: self = .leftToRight - case .rightToLeft: self = .rightToLeft + package var opposite: LayoutDirection { + switch self { + case .leftToRight: .rightToLeft + case .rightToLeft: .leftToRight } } } -#endif +// MARK: - LayoutDirectionKey + +private struct LayoutDirectionKey: EnvironmentKey { + static let defaultValue: LayoutDirection = .leftToRight +} + +extension EnvironmentValues { + /// The layout direction associated with the current environment. + public var layoutDirection: LayoutDirection { + get { self[LayoutDirectionKey.self] } + set { self[LayoutDirectionKey.self] = newValue } + } +} + +// MARK: - LayoutDirection + CodableByProxy + +extension LayoutDirection: CodableByProxy { + package var codingProxy: CodableLayoutDirection { + CodableLayoutDirection(self) + } +} // MARK: - CodableLayoutDirection package struct CodableLayoutDirection: CodableProxy { package var base: LayoutDirection - + + package init(_ base: LayoutDirection) { + self.base = base + } + private enum CodingValue: Int, Codable { case leftToRight case rightToLeft } - - @inline(__always) - init(base: LayoutDirection) { - self.base = base - } - - package init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let value = try container.decode(CodingValue.self) - switch value { - case .leftToRight: base = .leftToRight - case .rightToLeft: base = .rightToLeft - } - } - + package func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() let value: CodingValue = switch base { @@ -102,27 +94,13 @@ package struct CodableLayoutDirection: CodableProxy { } try container.encode(value) } -} - -// MARK: - LayoutDirection + CodableByProxy - -extension LayoutDirection: CodableByProxy { - package var codingProxy: CodableLayoutDirection { - CodableLayoutDirection(base: self) - } -} - -// MARK: - LayoutDirectionKey -private struct LayoutDirectionKey: EnvironmentKey { - static let defaultValue: LayoutDirection = .leftToRight -} - -extension EnvironmentValues { - /// The layout direction associated with the current environment. - public var layoutDirection: LayoutDirection { - get { self[LayoutDirectionKey.self] } - set { self[LayoutDirectionKey.self] = newValue } - _modify { yield &self[LayoutDirectionKey.self] } + package init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let value = try container.decode(CodingValue.self) + switch value { + case .leftToRight: base = .leftToRight + case .rightToLeft: base = .rightToLeft + } } } diff --git a/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirectionBehavior.swift b/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirectionBehavior.swift new file mode 100644 index 000000000..b7dd17672 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Direction/LayoutDirectionBehavior.swift @@ -0,0 +1,40 @@ +// +// LayoutDirectionBehavior.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +/// A description of what should happen when the layout direction changes. +/// +/// A `LayoutDirectionBehavior` can be used with the `layoutDirectionBehavior` +/// view modifier or the `layoutDirectionBehavior` property of `Shape`. +public enum LayoutDirectionBehavior: Hashable, Sendable { + /// A behavior that doesn't mirror when the layout direction changes. + case fixed + + /// A behavior that mirrors when the layout direction has the specified + /// value. + /// + /// If you develop your view or shape in an LTR context, you can use + /// `.mirrors(in: .rightToLeft)` (which is equivalent to `.mirrors`) to + /// mirror your content when the layout direction is RTL (and keep the + /// original version in LTR). If you developer in an RTL context, you can + /// use `.mirrors(in: .leftToRight)` to mirror your content when the layout + /// direction is LTR (and keep the original version in RTL). + case mirrors(in: LayoutDirection) + + /// A behavior that mirrors when the layout direction is right-to-left. + public static var mirrors: LayoutDirectionBehavior { + .mirrors(in: .rightToLeft) + } + + package func shouldFlip(in direction: @autoclosure () -> LayoutDirection?) -> Bool { + switch self { + case .fixed: + return false + case let .mirrors(`in`): + return direction() == `in` + } + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/ProjectionTransform.swift b/Sources/OpenSwiftUICore/Layout/Geometry/ProjectionTransform.swift index 4f844d297..41a7be1ca 100644 --- a/Sources/OpenSwiftUICore/Layout/Geometry/ProjectionTransform.swift +++ b/Sources/OpenSwiftUICore/Layout/Geometry/ProjectionTransform.swift @@ -6,6 +6,7 @@ // Status: Complete public import Foundation +public import CoreGraphicsShims #if canImport(QuartzCore) public import QuartzCore #endif diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/Spacing.swift b/Sources/OpenSwiftUICore/Layout/Geometry/Spacing.swift index 16fec2302..a6979877e 100644 --- a/Sources/OpenSwiftUICore/Layout/Geometry/Spacing.swift +++ b/Sources/OpenSwiftUICore/Layout/Geometry/Spacing.swift @@ -8,7 +8,7 @@ import Foundation -struct Spacing { +package struct Spacing { // TODO static let zero = Spacing(minima: [:]) static let zeroHorizontal = Spacing(minima: [:]) diff --git a/Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift b/Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift index cd43f1c10..93d584d6c 100644 --- a/Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift +++ b/Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift @@ -8,6 +8,7 @@ // ID: 1CC2FE016A82CF91549A64E942CE8ED4 (SwiftUICore) package import Foundation +package import CoreGraphicsShims @_spi(ForOpenSwiftUIOnly) public struct ViewTransform: Equatable, CustomStringConvertible { diff --git a/Sources/OpenSwiftUICore/Modifier/ViewModifier.swift b/Sources/OpenSwiftUICore/Modifier/ViewModifier.swift index d72f877c2..6c3d16421 100644 --- a/Sources/OpenSwiftUICore/Modifier/ViewModifier.swift +++ b/Sources/OpenSwiftUICore/Modifier/ViewModifier.swift @@ -138,7 +138,7 @@ extension UnaryViewModifier { package protocol MultiViewModifier: ViewModifier {} extension MultiViewModifier { - nonisolated static func _makeViewList( + nonisolated public static func _makeViewList( modifier: _GraphValue, inputs: _ViewListInputs, body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs diff --git a/Sources/OpenSwiftUICore/Render/RendererLeafView.swift b/Sources/OpenSwiftUICore/Render/RendererLeafView.swift index 15f40430d..204f86e1a 100644 --- a/Sources/OpenSwiftUICore/Render/RendererLeafView.swift +++ b/Sources/OpenSwiftUICore/Render/RendererLeafView.swift @@ -5,7 +5,7 @@ // Audited for iOS 18.0 // Status: WIP -import Foundation +package import Foundation package protocol RendererLeafView: /*ContentResponder,*/ PrimitiveView, UnaryView { static var requiresMainThread: Bool { get } @@ -26,3 +26,18 @@ extension RendererLeafView { _ViewOutputs() } } + +package protocol LeafViewLayout { + func spacing() -> Spacing + func sizeThatFits(in proposedSize: _ProposedSize) -> CGSize +} + +extension LeafViewLayout { + package func spacing() -> Spacing { + preconditionFailure("") + } + + package static func makeLeafLayout(_ outputs: inout _ViewOutputs, view: _GraphValue, inputs: _ViewInputs) { + preconditionFailure("TODO") + } +} diff --git a/Sources/OpenSwiftUICore/Semantic/Semantics.swift b/Sources/OpenSwiftUICore/Semantic/Semantics.swift index 41cf5e425..9e795cc69 100644 --- a/Sources/OpenSwiftUICore/Semantic/Semantics.swift +++ b/Sources/OpenSwiftUICore/Semantic/Semantics.swift @@ -138,6 +138,7 @@ extension Semantics { } } +// 2019.9.1 OS release (iOS 13) let openSwiftUI_v1_os_versions = dyld_build_version_t(version: 0x07E3_0901) let openSwiftUI_autumn_2019_os_versions = dyld_build_version_t(version: 0x07E3_0902) let openSwiftUI_late_fall_2019_os_versions = dyld_build_version_t(version: 0x07E3_1015) @@ -145,18 +146,25 @@ let openSwiftUI_v1_3_1_os_versions = dyld_build_version_t(version: 0x07E3_1201) let openSwiftUI_v1_4_os_versions = dyld_build_version_t(version: 0x07E4_0301) let openSwiftUI_late_spring_2020_os_versions = dyld_build_version_t(version: 0x07E4_0415) let openSwiftUI_summer_2020_os_versions = dyld_build_version_t(version: 0x07E4_0601) +// 2020.9.1 OS release (iOS 14) let openSwiftUI_v2_os_versions = dyld_build_version_t(version: 0x07E4_0901) let openSwiftUI_v2_1_os_versions = dyld_build_version_t(version: 0x07E4_1015) let openSwiftUI_v2_3_os_versions = dyld_build_version_t(version: 0x07E5_0301) +// 2021.9.1 OS release (iOS 15) let openSwiftUI_v3_0_os_versions = dyld_build_version_t(version: 0x07E5_0901) +// 2021.12.1 OS release let openSwiftUI_v3_2_os_versions = dyld_build_version_t(version: 0x07E5_1201) let openSwiftUI_v3_4_os_versions = dyld_build_version_t(version: 0x07E6_0301) +// 2022.9.1 OS release (iOS 16) let openSwiftUI_v4_0_os_versions = dyld_build_version_t(version: 0x07E6_0901) let openSwiftUI_v4_4_os_versions = dyld_build_version_t(version: 0x07E6_2300) +// 2023.9.1 OS release (iOS 17) let openSwiftUI_v5_0_os_versions = dyld_build_version_t(version: 0x07E7_0901) let openSwiftUI_v5_2_os_versions = dyld_build_version_t(version: 0x07E7_0d01) +// 2024 OS release (iOS 18) let openSwiftUI_v6_0_os_versions = dyld_build_version_t(version: 0x07E8_0000) let openSwiftUI_v6_1_os_versions = dyld_build_version_t(version: 0x07E8_0100) let openSwiftUI_v6_2_os_versions = dyld_build_version_t(version: 0x07E8_0200) let openSwiftUI_v6_4_os_versions = dyld_build_version_t(version: 0x07E8_0400) +// 2025 OS release (iOS 19) let openSwiftUI_v7_0_os_versions = dyld_build_version_t(version: 0x07E9_0000) diff --git a/Sources/OpenSwiftUICore/Shape/FillStyle.swift b/Sources/OpenSwiftUICore/Shape/FillStyle.swift index 4da66cd66..b230c84d2 100644 --- a/Sources/OpenSwiftUICore/Shape/FillStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/FillStyle.swift @@ -1,8 +1,8 @@ // // FillStyle.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete /// A style for rasterizing vector shapes. @@ -34,3 +34,22 @@ public struct FillStyle: Equatable { self.isAntialiased = antialiased } } + +extension FillStyle: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) { + encoder.boolField(1, isEOFilled, defaultValue: false) + encoder.boolField(2, isAntialiased, defaultValue: true) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var style = FillStyle() + while let field = try decoder.nextField() { + switch field.tag { + case 1: style.isEOFilled = try decoder.boolField(field) + case 2: style.isAntialiased = try decoder.boolField(field) + default: try decoder.skipField(field) + } + } + self = style + } +} diff --git a/Sources/OpenSwiftUICore/Shape/InsettableShape.swift b/Sources/OpenSwiftUICore/Shape/InsettableShape.swift new file mode 100644 index 000000000..8ce862a31 --- /dev/null +++ b/Sources/OpenSwiftUICore/Shape/InsettableShape.swift @@ -0,0 +1,371 @@ +// +// InsettableShape.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP + +public import Foundation + +// MARK: - InsettableShape + +/// A shape type that is able to inset itself to produce another shape. +public protocol InsettableShape: Shape { + + /// The type of the inset shape. + associatedtype InsetShape: InsettableShape + + /// Returns `self` inset by `amount`. + func inset(by amount: CGFloat) -> InsetShape +} + +// MARK: - InsettableShape + Extension (disfavoredOverload) + +extension InsettableShape { + /// Returns a view that is the result of insetting `self` by + /// `style.lineWidth / 2`, stroking the resulting shape with + /// `style`, and then filling with `content`. + @inlinable + @_disfavoredOverload + public func strokeBorder(_ content: S, style: StrokeStyle, antialiased: Bool = true) -> some View where S: ShapeStyle { + inset(by: style.lineWidth * 0.5) + .stroke(style: style) + .fill(content, style: FillStyle(antialiased: antialiased)) + } + + /// Returns a view that is the result of insetting `self` by + /// `style.lineWidth / 2`, stroking the resulting shape with + /// `style`, and then filling with the foreground color. + @inlinable + @_disfavoredOverload + public func strokeBorder(style: StrokeStyle, antialiased: Bool = true) -> some View { + inset(by: style.lineWidth * 0.5) + .stroke(style: style) + .fill(style: FillStyle(antialiased: antialiased)) + } + + /// Returns a view that is the result of filling the `lineWidth`-sized + /// border (aka inner stroke) of `self` with `content`. This is + /// equivalent to insetting `self` by `lineWidth / 2` and stroking the + /// resulting shape with `lineWidth` as the line-width. + @inlinable + @_disfavoredOverload + public func strokeBorder(_ content: S, lineWidth: CGFloat = 1, antialiased: Bool = true) -> some View where S: ShapeStyle { + strokeBorder( + content, + style: StrokeStyle(lineWidth: lineWidth), + antialiased: antialiased + ) + } + + /// Returns a view that is the result of filling the `lineWidth`-sized + /// border (aka inner stroke) of `self` with the foreground color. + /// This is equivalent to insetting `self` by `lineWidth / 2` and + /// stroking the resulting shape with `lineWidth` as the line-width. + @inlinable + @_disfavoredOverload + public func strokeBorder(lineWidth: CGFloat = 1, antialiased: Bool = true) -> some View { + strokeBorder( + style: StrokeStyle(lineWidth: lineWidth), + antialiased: antialiased + ) + } +} + +// MARK: - Retangle + InsettableShape + +extension Rectangle: InsettableShape { + @inlinable + public func inset(by amount: CGFloat) -> some InsettableShape { + _Inset(amount: amount) + } + + @usableFromInline + @frozen + struct _Inset: InsettableShape { + @usableFromInline + var amount: CGFloat + + @inlinable + init(amount: CGFloat) { + self.amount = amount + } + + @usableFromInline + nonisolated func path(in rect: CGRect) -> Path { + Path(rect.insetBy(dx: amount, dy: amount)) + } + + @usableFromInline + nonisolated var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } + + @usableFromInline + var animatableData: CGFloat { + get { amount } + set { amount = newValue } + } + + @inlinable + func inset(by amount: CGFloat) -> Self { + var copy = self + copy.amount += amount + return copy + } + } +} + +// MARK: - RoundedRectangle + InsettableShape + +extension RoundedRectangle: InsettableShape { + @inlinable + public func inset(by amount: CGFloat) -> some InsettableShape { + _Inset(base: self, amount: amount) + } + + @usableFromInline + @frozen + struct _Inset: InsettableShape { + @usableFromInline + var base: RoundedRectangle + + @usableFromInline + var amount: CGFloat + + @inlinable + init(base: RoundedRectangle, amount: CGFloat) { + (self.base, self.amount) = (base, amount) + } + + @usableFromInline + nonisolated func path(in rect: CGRect) -> Path { + preconditionFailure("TODO") + } + + @usableFromInline + nonisolated var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } + + @usableFromInline + var animatableData: AnimatablePair { + get { AnimatablePair(base.animatableData, amount) } + set { (base.animatableData, amount) = (newValue.first, newValue.second) } + } + + @inlinable + func inset(by amount: CGFloat) -> Self { + var copy = self + copy.amount += amount + return copy + } + } +} + +// MARK: - UnevenRoundedRectangle + InsettableShape + +extension UnevenRoundedRectangle: InsettableShape { + @inlinable + public func inset(by amount: CGFloat) -> some InsettableShape { + _Inset(base: self, amount: amount) + } + + @usableFromInline + @frozen + struct _Inset: InsettableShape { + @usableFromInline + var base: UnevenRoundedRectangle + + @usableFromInline + var amount: CGFloat + + @inlinable + init(base: UnevenRoundedRectangle, amount: CGFloat) { + (self.base, self.amount) = (base, amount) + } + + @usableFromInline + nonisolated func path(in rect: CGRect) -> Path { + preconditionFailure("TODO") + } + + @usableFromInline + var animatableData: AnimatablePair { + get { AnimatablePair(base.animatableData, amount) } + set { (base.animatableData, amount) = (newValue.first, newValue.second) } + } + + @inlinable + func inset(by amount: CGFloat) -> Self { + var copy = self + copy.amount += amount + return copy + } + } +} + +// MARK: - Capsule + InsettableShape + +extension Capsule: InsettableShape { + @inlinable + public func inset(by amount: CGFloat) -> some InsettableShape { + _Inset(amount: _Inset._makeInset(amount, style: style)) + } + + @usableFromInline + @frozen + struct _Inset: InsettableShape { + @usableFromInline + var amount: CGFloat + + @inlinable + init(amount: CGFloat) { + self.amount = amount + } + + @usableFromInline + nonisolated func path(in rect: CGRect) -> Path { + preconditionFailure("TODO") + } + + @usableFromInline + nonisolated var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } + + @usableFromInline + var animatableData: CGFloat { + get { amount } + set { amount = newValue } + } + + @inlinable + func inset(by amount: CGFloat) -> Self { + let (inset, style) = Self._extractInset(self.amount) + return Self(amount: Self._makeInset(inset + amount, style: style)) + } + + @_alwaysEmitIntoClient + static func _makeInset(_ inset: CGFloat, style: RoundedCornerStyle) -> CGFloat { + var u = unsafeBitCast(inset, to: UInt.self) + u = (u & ~1) | (style == .circular ? 0 : 1) + return unsafeBitCast(u, to: CGFloat.self) + } + + @_alwaysEmitIntoClient + static func _extractInset(_ inset: CGFloat) -> (CGFloat, RoundedCornerStyle) { + let u = unsafeBitCast(inset, to: UInt.self) + return ( + unsafeBitCast(u & ~1, to: CGFloat.self), + (u & 1) == 0 ? .circular : .continuous + ) + } + } +} + +// MARK: - Ellipse + InsettableShape + +extension Ellipse: InsettableShape { + @inlinable + public func inset(by amount: CGFloat) -> some InsettableShape { + _Inset(amount: amount) + } + + @usableFromInline + @frozen + struct _Inset: InsettableShape { + @usableFromInline + var amount: CGFloat + + @inlinable + init(amount: CGFloat) { + self.amount = amount + } + + @usableFromInline + nonisolated func path(in rect: CGRect) -> Path { + preconditionFailure("TODO") + } + + @usableFromInline + nonisolated var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } + + @usableFromInline + var animatableData: CGFloat { + get { amount } + set { amount = newValue } + } + + @inlinable + func inset(by amount: CGFloat) -> Self { + var copy = self + copy.amount += amount + return copy + } + } +} + +// MARK: - Circle + InsettableShape + +extension Circle: InsettableShape { + @inlinable + public func inset(by amount: CGFloat) -> some InsettableShape { + _Inset(amount: amount) + } + + @usableFromInline + @frozen + struct _Inset: InsettableShape { + @usableFromInline + var amount: CGFloat + + @inlinable + init(amount: CGFloat) { + self.amount = amount + } + + @usableFromInline + nonisolated func path(in rect: CGRect) -> Path { + preconditionFailure("TODO") + } + + @usableFromInline + nonisolated var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } + + @usableFromInline + var animatableData: CGFloat { + get { amount } + set { amount = newValue } + } + + @inlinable + func inset(by amount: CGFloat) -> InsetShape { + var copy = self + copy.amount += amount + return copy + } + + @usableFromInline + typealias InsetShape = Self + } +} + +extension Rectangle { + struct AsymmetricalInset: Shape { + let rectangle: Rectangle + let insets: EdgeInsets + + func path(in rect: CGRect) -> Path { + rectangle.path(in: rect.inset(by: insets)) + } + } + + package func outset(by insets: EdgeInsets) -> some Shape { + AsymmetricalInset(rectangle: self, insets: -insets) + } +} diff --git a/Sources/OpenSwiftUICore/Shape/Path/Path.swift b/Sources/OpenSwiftUICore/Shape/Path.swift similarity index 74% rename from Sources/OpenSwiftUICore/Shape/Path/Path.swift rename to Sources/OpenSwiftUICore/Shape/Path.swift index 79cdeee1c..c835ce426 100644 --- a/Sources/OpenSwiftUICore/Shape/Path/Path.swift +++ b/Sources/OpenSwiftUICore/Shape/Path.swift @@ -1,18 +1,18 @@ // // Path.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: WIP -// ID: 31FD92B70C320DDD253E93C7417D779A RELEASE_2021 -// ID: 3591905F51357E95FA93E39751507471 RELEASE_2024 +// ID: 31FD92B70C320DDD253E93C7417D779A (SwiftUI) +// ID: 3591905F51357E95FA93E39751507471 (SwiftUICore) public import Foundation - -#if canImport(CoreGraphics) +package import OpenBoxShims import OpenSwiftUI_SPI -public import CoreGraphics +public import CoreGraphicsShims +#if canImport(CoreGraphics) @_silgen_name("__CGPathParseString") private func __CGPathParseString(_ path: CGMutablePath, _ utf8CString: UnsafePointer) -> Bool #endif @@ -21,9 +21,84 @@ private func __CGPathParseString(_ path: CGMutablePath, _ utf8CString: UnsafePoi /// The outline of a 2D shape. @frozen -public struct Path/*: Equatable, LosslessStringConvertible*/ { - var storage: Path.Storage - +public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { + @usableFromInline + final package class PathBox: Equatable { + + private enum Kind: UInt8 { + case cgPath + case obPath + case buffer + } + + private var kind: Kind + + #if canImport(CoreGraphics) + private var data: PathData + + @inline(__always) + init(_ path: CGPath) { + kind = .cgPath + //data = PathData(path) + preconditionFailure("TODO") + } + #endif + + package init(takingPath path: OBPath) { + kind = .obPath + //data = PathData(path) + preconditionFailure("TODO") + } + + #if canImport(CoreGraphics) + private func prepareBuffer() { + let obPath: OBPath + switch kind { + case .cgPath: + // data.cgPath + // let rbPath = OBPathMakeWithCGPath + preconditionFailure("TODO") + case .obPath: + obPath = data.obPath.assumingMemoryBound(to: OBPath.self).pointee + case .buffer: + return + } + // OBPath.Storage.init + // storage.appendPath + obPath.release() + } + #endif + + @usableFromInline + package static func == (lhs: PathBox, rhs: PathBox) -> Bool { + preconditionFailure("TODO") + } + } + + @usableFromInline + @frozen + package enum Storage: Equatable { + case empty + case rect(CGRect) + case ellipse(CGRect) + indirect case roundedRect(FixedRoundedRect) + @available(*, deprecated, message: "obsolete") + indirect case stroked(StrokedPath) + @available(*, deprecated, message: "obsolete") + indirect case trimmed(TrimmedPath) + case path(PathBox) + } + + package var storage: Path.Storage + + package init(storage: Path.Storage) { + self.storage = storage + } + + package init(box: Path.PathBox) { + self.storage = .path(box) + } + /// Creates an empty path. public init() { storage = .empty @@ -55,7 +130,7 @@ public struct Path/*: Equatable, LosslessStringConvertible*/ { return } storage = .path(PathBox(path.mutableCopy()!)) - + preconditionFailure("TODO") } #endif @@ -102,7 +177,7 @@ public struct Path/*: Equatable, LosslessStringConvertible*/ { storage = .rect(rect) return } - storage = .roundedRect(FixedRoundedRect(rect: rect, cornerSize: cornerSize, style: style)) + storage = .roundedRect(FixedRoundedRect(rect, cornerSize: cornerSize, style: style)) } /// Creates a path containing a rounded rectangle. @@ -127,10 +202,9 @@ public struct Path/*: Equatable, LosslessStringConvertible*/ { storage = .rect(rect) return } - storage = .roundedRect(FixedRoundedRect(rect: rect, cornerSize: CGSize(width: cornerRadius, height: cornerRadius), style: style)) + storage = .roundedRect(FixedRoundedRect(rect, cornerRadius: cornerRadius, style: style)) } - #if OPENSWIFTUI_SUPPORT_2022_API /// Creates a path as the given rounded rectangle, which may have /// uneven corner radii. /// @@ -145,12 +219,14 @@ public struct Path/*: Equatable, LosslessStringConvertible*/ { /// - style: The corner style. Defaults to the `continous` style /// if not specified. /// - @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) public init(roundedRect rect: CGRect, cornerRadii: RectangleCornerRadii, style: RoundedCornerStyle = .continuous) { + guard !rect.isNull else { + storage = .empty + return + } preconditionFailure("TODO") } - #endif - + /// Creates a path as an ellipse within the given rectangle. /// /// This is a convenience function that creates a path of an @@ -208,6 +284,7 @@ public struct Path/*: Equatable, LosslessStringConvertible*/ { return nil } storage = .path(PathBox(mutablePath)) + preconditionFailure("TODO") #else return nil #endif @@ -225,7 +302,15 @@ public struct Path/*: Equatable, LosslessStringConvertible*/ { preconditionFailure("TODO") } #endif - + + package func retainOBPath() -> OBPath { + preconditionFailure("TODO") + } + + package mutating func withMutableBuffer(do body: (UnsafeMutableRawPointer) -> Void) { + preconditionFailure("TODO") + } + /// A Boolean value indicating whether the path contains zero elements. public var isEmpty: Bool { preconditionFailure("TODO") @@ -247,127 +332,11 @@ public struct Path/*: Equatable, LosslessStringConvertible*/ { public func contains(_ p: CGPoint, eoFill: Bool = false) -> Bool { preconditionFailure("TODO") } - - /// Calls `body` with each element in the path. - public func forEach(_ body: (Path.Element) -> Void) { - preconditionFailure("TODO") - } - /// Returns a stroked copy of the path using `style` to define how the - /// stroked outline is created. - public func strokedPath(_ style: StrokeStyle) -> Path { + package func contains(points: [CGPoint], eoFill: Bool = false, origin: CGPoint = .zero) -> BitVector64 { preconditionFailure("TODO") } - /// Returns a partial copy of the path. - /// - /// The returned path contains the region between `from` and `to`, both of - /// which must be fractions between zero and one defining points - /// linearly-interpolated along the path. - public func trimmedPath(from: CGFloat, to: CGFloat) -> Path { - preconditionFailure("TODO") - } -} - -#if canImport(CoreGraphics) - -// MARK: - Path.PathBox - -extension Path { - @usableFromInline - final package class PathBox: Equatable { - #if OPENSWIFTUI_RELEASE_2024 // Also on RELEASE_2023 - private var kind: Kind -//// var data: PathData - private init() { - kind = .buffer -// // TODO - } - private enum Kind: UInt8 { - case cgPath - case rbPath - case buffer - } - - init(_ path: CGPath) { - preconditionFailure("TODO") - } - - init(_ mutablePath: CGMutablePath) { - preconditionFailure("TODO") - } - - // FIXME - @usableFromInline - package static func == (lhs: Path.PathBox, rhs: Path.PathBox) -> Bool { - lhs.kind == rhs.kind - } - #elseif OPENSWIFTUI_RELEASE_2021 - let cgPath: CGMutablePath - var bounds: UnsafeAtomicLazy - - init(_ path: CGPath) { - cgPath = path as! CGMutablePath - bounds = UnsafeAtomicLazy(cache: nil) - } - - init(_ mutablePath: CGMutablePath) { - cgPath = mutablePath - bounds = UnsafeAtomicLazy(cache: nil) - } - - deinit { - bounds.destroy() - } - - @usableFromInline - package static func == (lhs: PathBox, rhs: PathBox) -> Bool { - lhs.cgPath === rhs.cgPath - } - - var boundingRect: CGRect { - if let cache = bounds.cache { - return cache - } else { - let boundingBox = cgPath.boundingBoxOfPath - bounds.$cache.withMutableData { rect in - if rect == nil { - rect = boundingBox - } - } - return boundingBox - } - } - - private func clearCache() { - bounds.cache = nil - } - #endif - } -} - -#endif - -// MARK: - Path.Storage - -extension Path { - @usableFromInline - @frozen enum Storage: Equatable { - case rect(CGRect) - case ellipse(CGRect) - indirect case roundedRect(FixedRoundedRect) -// indirect case stroked(StrokedPath) -// indirect case trimmed(TrimmedPath) - #if canImport(CoreGraphics) - case path(PathBox) - #endif - case empty - } -} - -// MARK: - Path.Element - -extension Path { /// An element of a path. @frozen public enum Element: Equatable { @@ -397,57 +366,90 @@ extension Path { /// After closing the subpath, the current point becomes undefined. case closeSubpath } -} -// MARK: - CodablePath[WIP] + /// Calls `body` with each element in the path. + public func forEach(_ body: (Path.Element) -> Void) { + preconditionFailure("TODO") + } -package struct CodablePath: CodableProxy { - package var base: Path + /// Returns a stroked copy of the path using `style` to define how the + /// stroked outline is created. + public func strokedPath(_ style: StrokeStyle) -> Path { + preconditionFailure("TODO") + } - private enum Error: Swift.Error { - case invalidPath + /// Returns a partial copy of the path. + /// + /// The returned path contains the region between `from` and `to`, both of + /// which must be fractions between zero and one defining points + /// linearly-interpolated along the path. + public func trimmedPath(from: CGFloat, to: CGFloat) -> Path { + preconditionFailure("TODO") } - private enum CodingKind: UInt8, Codable { - case empty - case rect - case ellipse - case roundedRect - case stroked - case trimmed - case data + package func rect() -> CGRect? { + preconditionFailure("TODO") } - private enum CodingKeys: Hashable, CodingKey { - case kind - case value + package func roundedRect() -> FixedRoundedRect? { + preconditionFailure("TODO") } +} + +@available(*, unavailable) +extension Path.Storage: Sendable {} + +@available(*, unavailable) +extension Path.PathBox: Sendable {} + +// MARK: - Path + Shape + +extension Path: Shape { + nonisolated public func path(in _: CGRect) -> Path { self } + + public typealias AnimatableData = EmptyAnimatableData - // TODO: - package func encode(to _: Encoder) throws {} + public typealias Body = _ShapeView +} + +// MARK: - Path + ProtobufMessage - // TODO: - package init(from _: Decoder) throws { - base = Path() +extension Path: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + preconditionFailure("TODO") } - @inline(__always) - init(base: Path) { - self.base = base + package init(from decoder: inout ProtobufDecoder) throws { + preconditionFailure("TODO") } } -// MARK: - Path + CodableByProxy +// MARK: - StrokedPath + +@available(*, deprecated, message: "obsolete") +@usableFromInline +package struct StrokedPath: Equatable { + public init(path: Path, style: StrokeStyle) {} -extension Path: CodableByProxy { - package var codingProxy: CodablePath { - CodablePath(base: self) + @usableFromInline + package static func == (a: StrokedPath, b: StrokedPath) -> Bool { + true } } -// MARK: - PathDrawingStyle +@available(*, unavailable) +extension StrokedPath: Sendable {} + +// MARK: - TrimmedPath -package enum PathDrawingStyle { - case fill(FillStyle) - case stroke(StrokeStyle) +@available(*, deprecated, message: "obsolete") +@usableFromInline +package struct TrimmedPath: Equatable { + @usableFromInline + package static func == (a: TrimmedPath, b: TrimmedPath) -> Swift.Bool { + true + } } + +@available(*, unavailable) +extension TrimmedPath: Sendable {} diff --git a/Sources/OpenSwiftUICore/Shape/Path/FixedRoundedRect.swift b/Sources/OpenSwiftUICore/Shape/Path/FixedRoundedRect.swift deleted file mode 100644 index a8bf00778..000000000 --- a/Sources/OpenSwiftUICore/Shape/Path/FixedRoundedRect.swift +++ /dev/null @@ -1,31 +0,0 @@ -// FixedRoundedRect.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: WIP - -import Foundation - -@usableFromInline -struct FixedRoundedRect: Equatable { - var rect: CGRect - var cornerSize: CGSize - var style: RoundedCornerStyle - - func contains(_ roundedRect: FixedRoundedRect) -> Bool { - guard rect.insetBy(dx: -0.001, dy: -0.001).contains(roundedRect.rect) else { - return false - } - guard !(cornerSize.width <= roundedRect.cornerSize.width && cornerSize.height <= roundedRect.cornerSize.height) else { - return true - } - let minCornerWidth = min(abs(rect.size.width) / 2, cornerSize.width) - let minCornerHeight = min(abs(rect.size.height) / 2, cornerSize.height) - let factor = 0.292893 // 1 - cos(45 * Double.pi / 180) - return rect.insetBy(dx: minCornerWidth * factor, dy: minCornerHeight * factor).contains(roundedRect.rect) - } - - func distance(to point: CGPoint) -> CGFloat { - preconditionFailure("TODO") - } -} diff --git a/Sources/OpenSwiftUICore/Shape/PathDrawingStyle.swift b/Sources/OpenSwiftUICore/Shape/PathDrawingStyle.swift new file mode 100644 index 000000000..aeac6dddd --- /dev/null +++ b/Sources/OpenSwiftUICore/Shape/PathDrawingStyle.swift @@ -0,0 +1,11 @@ +// +// PathDrawingStyle.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package enum PathDrawingStyle { + case fill(FillStyle) + case stroke(StrokeStyle) +} diff --git a/Sources/OpenSwiftUICore/Shape/Rectangle.swift b/Sources/OpenSwiftUICore/Shape/Rectangle.swift deleted file mode 100644 index df410d94e..000000000 --- a/Sources/OpenSwiftUICore/Shape/Rectangle.swift +++ /dev/null @@ -1,27 +0,0 @@ -public import Foundation - -/// A rectangular shape aligned inside the frame of the view containing it. -@frozen -public struct Rectangle: Shape { - public func path(in rect: CGRect) -> Path { - preconditionFailure("TODO") - } - - /// Creates a new rectangle shape. - @inlinable - public init() {} - - public typealias AnimatableData = EmptyAnimatableData - - public typealias Body = _ShapeView -} - - -extension Shape where Self == Rectangle { - /// A rectangular shape aligned inside the frame of the view containing it. - public static var rect: Rectangle { - Rectangle() - } -} - -public struct RectangleCornerRadii {} diff --git a/Sources/OpenSwiftUICore/Shape/RoundedCorner.swift b/Sources/OpenSwiftUICore/Shape/RoundedCorner.swift new file mode 100644 index 000000000..ea1ce24bc --- /dev/null +++ b/Sources/OpenSwiftUICore/Shape/RoundedCorner.swift @@ -0,0 +1,277 @@ +// +// RoundedCorner.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP + +public import Foundation +package import CoreGraphicsShims + +// MARK: - RoundedCornerStyle + +/// Defines the shape of a rounded rectangle's corners. +public enum RoundedCornerStyle: Sendable { + /// Quarter-circle rounded rect corners. + case circular + + /// Continuous curvature rounded rect corners. + case continuous +} + +// MARK: - FixedRoundedRect [WIP] + +@usableFromInline +package struct FixedRoundedRect: Equatable { + package var rect: CGRect + + package var cornerSize: CGSize + + package var style: RoundedCornerStyle + + package init(_ rect: CGRect, cornerSize: CGSize, style: RoundedCornerStyle) { + self.rect = rect + self.cornerSize = cornerSize + self.style = style + } + + package init(_ rect: CGRect) { + self.rect = rect + self.cornerSize = .zero + self.style = .circular + } + + package init(_ rect: CGRect, cornerRadius: CGFloat, style: RoundedCornerStyle) { + self.rect = rect + self.cornerSize = CGSize(width: cornerRadius, height: cornerRadius) + self.style = style + } + + package var isRounded: Bool { + cornerSize != .zero + } + + package var isUniform: Bool { + cornerSize.width == cornerSize.height + } + + package var needsContinuousCorners: Bool { + style == .continuous && isRounded + } + + 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: (OBPath) -> R) -> R + + package func contains(_ point: CGPoint) -> Bool { + // TODO: OBPath + preconditionFailure("TODO") + } + + package func applying(_ m: CGAffineTransform) -> FixedRoundedRect { + FixedRoundedRect( + rect.applying(m), + cornerSize: cornerSize.isFinite ? cornerSize.applying(m) : cornerSize, // FIXME + style: style + ) + } + + package func contains(_ rhs: FixedRoundedRect) -> Bool { + guard rect.insetBy(dx: -0.001, dy: -0.001).contains(rhs.rect) else { + return false + } + guard !(cornerSize.width <= rhs.cornerSize.width && cornerSize.height <= rhs.cornerSize.height) else { + return true + } + let minCornerWidth = min(abs(rect.size.width) / 2, cornerSize.width) + let minCornerHeight = min(abs(rect.size.height) / 2, cornerSize.height) + let factor = 1 - cos(45 * Double.pi / 180) + return rect.insetBy(dx: minCornerWidth * factor, dy: minCornerHeight * factor).contains(rhs.rect) + } + + package func contains(rect: CGRect) -> Bool { + contains(FixedRoundedRect(rect)) + } + + package func contains(path: Path, offsetBy delta: CGSize) -> Bool { + var rhs: FixedRoundedRect + switch path.storage { + case .rect(let cGRect): + rhs = FixedRoundedRect(cGRect) + case .ellipse(let cGRect): + let size = cGRect.size + if size.width == size.height { + rhs = FixedRoundedRect(cGRect, cornerRadius: size.width / 2, style: .circular) + } else { + rhs = FixedRoundedRect(path.boundingRect) + } + case let .roundedRect(fixedRoundedRect): + rhs = fixedRoundedRect + default: + rhs = FixedRoundedRect(path.boundingRect) + } + rhs.rect.origin += delta + return contains(rhs) + } + + package func hasIntersection(_ rect: CGRect) -> Bool { + !self.rect.intersection(rect).isEmpty + } + + package func insetBy(dx: CGFloat, dy: CGFloat) -> FixedRoundedRect? { + guard dx != 0 || dy != 0 else { + return self + } + let insetedRect = rect.insetBy(dx: dx, dy: dy) + guard !insetedRect.isEmpty else { + return nil + } + return FixedRoundedRect( + insetedRect, + cornerSize: CGSize(width: max(cornerSize.width - dx, 0), height: max(cornerSize.height - dy, 0)), + style: style + ) + } + + #if canImport(CoreGraphics) + package var cgPath: CGPath { + preconditionFailure("TODO") + } + #endif + + @usableFromInline + package static func == (a: FixedRoundedRect, b: FixedRoundedRect) -> Bool { + a.rect == b.rect && a.cornerSize == b.cornerSize && a.style == b.style + } +} + +@available(*, unavailable) +extension FixedRoundedRect: Sendable {} + +extension FixedRoundedRect: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + try encoder.messageField(1, rect, defaultValue: .zero) + try encoder.messageField(2, cornerSize, defaultValue: .zero) + encoder.enumField(3, style, defaultValue: .circular) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var fixedRoundedRect = FixedRoundedRect(.zero) + while let field = try decoder.nextField() { + switch field.tag { + case 1: fixedRoundedRect.rect = try decoder.messageField(field) + case 2: fixedRoundedRect.cornerSize = try decoder.messageField(field) + case 3: fixedRoundedRect.style = try decoder.enumField(field) ?? .circular + default: try decoder.skipField(field) + } + } + self = fixedRoundedRect + } +} + +extension RoundedCornerStyle: ProtobufEnum { + package var protobufValue: UInt { + switch self { + case .circular: 0 + case .continuous: 1 + } + } + + package init?(protobufValue value: UInt) { + switch value { + case 0: self = .circular + case 1: self = .continuous + default: return nil + } + } +} + +// MARK: - RectangleCornerRadii + +/// Describes the corner radius values of a rounded rectangle with +/// uneven corners. +@frozen +public struct RectangleCornerRadii: Equatable, Animatable { + @usableFromInline + package var topLeft: CGFloat + + @usableFromInline + package var topRight: CGFloat + + @usableFromInline + package var bottomRight: CGFloat + + @usableFromInline + package var bottomLeft: CGFloat + + /// The radius of the top-leading corner. + @_alwaysEmitIntoClient + public var topLeading: CGFloat { + get { topLeft } + set { topLeft = newValue } + } + + /// The radius of the bottom-leading corner. + @_alwaysEmitIntoClient + public var bottomLeading: CGFloat { + get { bottomLeft } + set { bottomLeft = newValue } + } + + /// The radius of the bottom-trailing corner. + @_alwaysEmitIntoClient + public var bottomTrailing: CGFloat { + get { bottomRight } + set { bottomRight = newValue } + } + + /// The radius of the top-trailing corner. + @_alwaysEmitIntoClient + public var topTrailing: CGFloat { + get { topRight } + set { topRight = newValue } + } + + @usableFromInline + package init(topLeft: CGFloat, topRight: CGFloat, bottomRight: CGFloat, bottomLeft: CGFloat) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomRight = bottomRight + self.bottomLeft = bottomLeft + } + + /// Creates a new set of corner radii for a rounded rectangle with + /// uneven corners. + /// + /// - Parameters: + /// - topLeading: the radius of the top-leading corner. + /// - bottomLeading: the radius of the bottom-leading corner. + /// - bottomTrailing: the radius of the bottom-trailing corner. + /// - topTrailing: the radius of the top-trailing corner. + @_alwaysEmitIntoClient + public init(topLeading: CGFloat = 0, bottomLeading: CGFloat = 0, bottomTrailing: CGFloat = 0, topTrailing: CGFloat = 0) { + self.init( + topLeft: topLeading, topRight: topTrailing, + bottomRight: bottomTrailing, bottomLeft: bottomLeading + ) + } + + public var animatableData: AnimatablePair, AnimatablePair> { + get { AnimatablePair(AnimatablePair(topLeft, topRight), AnimatablePair(bottomLeft, bottomRight)) } + set { + topLeft = newValue.first.first + topRight = newValue.first.second + bottomLeft = newValue.second.first + bottomRight = newValue.second.second + } + } +} diff --git a/Sources/OpenSwiftUICore/Shape/RoundedCornerStyle.swift b/Sources/OpenSwiftUICore/Shape/RoundedCornerStyle.swift deleted file mode 100644 index b22d661e7..000000000 --- a/Sources/OpenSwiftUICore/Shape/RoundedCornerStyle.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// RoundedCornerStyle.swift -// OpenSwiftUICore -// -// Audited for iOS 18.0 -// Status: Complete - -/// Defines the shape of a rounded rectangle's corners. -public enum RoundedCornerStyle: Sendable { - /// Quarter-circle rounded rect corners. - case circular - - /// Continuous curvature rounded rect corners. - case continuous -} diff --git a/Sources/OpenSwiftUICore/Shape/RoundedRectangle.swift b/Sources/OpenSwiftUICore/Shape/RoundedRectangle.swift deleted file mode 100644 index 9b4b1556f..000000000 --- a/Sources/OpenSwiftUICore/Shape/RoundedRectangle.swift +++ /dev/null @@ -1,29 +0,0 @@ -public import Foundation - -@frozen -public struct RoundedRectangle: Shape { - public var cornerSize: CGSize - public var style: RoundedCornerStyle - - @inlinable - public init(cornerSize: CGSize, style: RoundedCornerStyle = .circular) { - self.cornerSize = cornerSize - self.style = style - } - - @inlinable - public init(cornerRadius: CGFloat, style: RoundedCornerStyle = .circular) { - let cornerSize = CGSize(width: cornerRadius, height: cornerRadius) - self.init(cornerSize: cornerSize, style: style) - } - - public func path(in rect: CGRect) -> Path { - preconditionFailure("TODO") - } - public var animatableData: CGSize.AnimatableData { - get { cornerSize.animatableData } - set { cornerSize.animatableData = newValue } - } - - public typealias Body = _ShapeView -} diff --git a/Sources/OpenSwiftUICore/Shape/Shape.swift b/Sources/OpenSwiftUICore/Shape/Shape.swift index a7f8f29db..f51f976aa 100644 --- a/Sources/OpenSwiftUICore/Shape/Shape.swift +++ b/Sources/OpenSwiftUICore/Shape/Shape.swift @@ -1,12 +1,14 @@ // // Shape.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 -// Status: WIP +// Audited for iOS 18.0 +// Status: Blocked by GeometryProxy public import Foundation +// MARK: - Shape + /// A 2D shape that you can use when drawing a view. /// /// Shapes without an explicit fill or stroke get a default fill based on the @@ -15,15 +17,14 @@ public import Foundation /// You can define shapes in relation to an implicit frame of reference, such as /// the natural size of the view that contains it. Alternatively, you can define /// shapes in terms of absolute coordinates. -public protocol Shape: Animatable, View { +public protocol Shape: Sendable, Animatable, View, _RemoveGlobalActorIsolation { /// Describes this shape as a path within a rectangular frame of reference. /// /// - Parameter rect: The frame of reference for describing this shape. /// /// - Returns: A path that describes this shape. - func path(in rect: CGRect) -> Path - - #if OPENSWIFTUI_SUPPORT_2021_API + nonisolated func path(in rect: CGRect) -> Path + /// An indication of how to style a shape. /// /// OpenSwiftUI looks at a shape's role when deciding how to apply a @@ -31,21 +32,138 @@ public protocol Shape: Animatable, View { /// default implementation with a value of ``ShapeRole/fill``. If you /// create a composite shape, you can provide an override of this property /// to return another value, if appropriate. - static var role: ShapeRole { get } - #endif + nonisolated static var role: ShapeRole { get } + + /// Returns the behavior this shape should use for different layout + /// directions. + /// + /// If the layoutDirectionBehavior for a Shape is one that mirrors, the + /// shape's path will be mirrored horizontally when in the specified layout + /// direction. When mirrored, the individual points of the path will be + /// transformed. + /// + /// Defaults to `.mirrors` when deploying on iOS 17.0, macOS 14.0, + /// tvOS 17.0, watchOS 10.0 and later, and to `.fixed` if not. + /// To mirror a path when deploying to earlier releases, either use + /// `View.flipsForRightToLeftLayoutDirection` for a filled or stroked + /// shape or conditionally mirror the points in the path of the shape. + nonisolated var layoutDirectionBehavior: LayoutDirectionBehavior { get } + + /// Returns the size of the view that will render the shape, given + /// a proposed size. + /// + /// Implement this method to tell the container of the shape how + /// much space the shape needs to render itself, given a size + /// proposal. + /// + /// See ``Layout/sizeThatFits(proposal:subviews:cache:)`` + /// for more details about how the layout system chooses the size of + /// views. + /// + /// - Parameters: + /// - proposal: A size proposal for the container. + /// + /// - Returns: A size that indicates how much space the shape needs. + nonisolated func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize } extension Shape { - public var body: _ShapeView { - _ShapeView(shape: self, style: ForegroundStyle()) + /// Returns the original proposal, with nil components replaced by + /// a small positive value. + nonisolated public func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize { + proposal.replacingUnspecifiedDimensions() } } -#if OPENSWIFTUI_SUPPORT_2021_API + +// MARK: - ShapeRole + +/// Ways of styling a shape. +public enum ShapeRole: Sendable { + /// Indicates to the shape's style that OpenSwiftUI fills the shape. + case fill + + /// Indicates to the shape's style that OpenSwiftUI applies a stroke to + /// the shape's path. + case stroke + + /// Indicates to the shape's style that OpenSwiftUI uses the shape as a + /// separator. + case separator +} extension Shape { public static var role: ShapeRole { .fill } } -#endif + +extension Shape { + public var layoutDirectionBehavior: LayoutDirectionBehavior { + isDeployedOnOrAfter(.v5) ? .mirrors(in: .rightToLeft) : .fixed + } + + package func effectivePath(in rect: CGRect) -> Path { + // _threadGeometryProxyData + preconditionFailure("TODO") + } +} + +/// An absolute shape that has been stroked. +@frozen +public struct _StrokedShape: Shape where S: Shape { + /// The source shape. + public var shape: S + + /// The stroke style. + public var style: StrokeStyle + + @inlinable + public init(shape: S, style: StrokeStyle) { + self.shape = shape + self.style = style + } + + nonisolated public func path(in rect: CGRect) -> Path { + shape.path(in: rect).strokedPath(style) + } + + nonisolated public static var role: ShapeRole { + .stroke + } + + nonisolated public var layoutDirectionBehavior: LayoutDirectionBehavior { + shape.layoutDirectionBehavior + } + + public var animatableData: AnimatablePair { + get { + AnimatablePair(shape.animatableData, style.animatableData) + } + set { + shape.animatableData = newValue.first + style.animatableData = newValue.second + } + } + + nonisolated public func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize { + shape.sizeThatFits(proposal) + } +} + +extension Shape { + /// Returns a new shape that is a stroked copy of `self`, using the + /// contents of `style` to define the stroke characteristics. + @inlinable + nonisolated public func stroke(style: StrokeStyle) -> some Shape { + return _StrokedShape(shape: self, style: style) + } + + /// Returns a new shape that is a stroked copy of `self` with + /// line-width defined by `lineWidth` and all other properties of + /// `StrokeStyle` having their default values. + @inlinable + nonisolated public func stroke(lineWidth: CGFloat = 1) -> some Shape { + return stroke(style: StrokeStyle(lineWidth: lineWidth)) + } +} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeRole.swift b/Sources/OpenSwiftUICore/Shape/ShapeRole.swift index 7c20b2c90..be96821f1 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeRole.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeRole.swift @@ -5,16 +5,3 @@ // Audited for iOS 18.0 // Status: Complete -/// Ways of styling a shape. -public enum ShapeRole: Sendable { - /// Indicates to the shape's style that OpenSwiftUI fills the shape. - case fill - - /// Indicates to the shape's style that OpenSwiftUI applies a stroke to - /// the shape's path. - case stroke - - /// Indicates to the shape's style that OpenSwiftUI uses the shape as a - /// separator. - case separator -} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/BackgroundModifier.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/BackgroundModifier.swift new file mode 100644 index 000000000..9e492ac19 --- /dev/null +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/BackgroundModifier.swift @@ -0,0 +1,32 @@ +// +// BackgroundModifier.swift +// OpenSwiftUICore + +// FIXME +public enum Alignment { + case center +} + +@frozen +public struct _BackgroundModifier: ViewModifier, MultiViewModifier, PrimitiveViewModifier where Background: View { + public var background: Background + + public var alignment: Alignment + + @inlinable + public init(background: Background, alignment: Alignment = .center) { + self.background = background + self.alignment = alignment + } + + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + preconditionFailure("TODO") + } +} + +@available(*, unavailable) +extension _BackgroundModifier : Sendable {} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyle.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyle.swift index 63869408d..b4860e1e9 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyle.swift @@ -143,10 +143,6 @@ extension ShapeStyle { legacyMakeShapeView(view: view, inputs: inputs) } - static func legacyMakeShapeView(view: _GraphValue<_ShapeView>, inputs: _ViewInputs) -> _ViewOutputs where S: Shape { - _ShapeView._makeView(view: view, inputs: inputs) - } - public func _apply(to shape: inout _ShapeStyle_Shape) { guard Resolved.self != Never.self else { return diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRenderedShape.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRenderedShape.swift index 9eda78c81..d1b118d6d 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRenderedShape.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRenderedShape.swift @@ -8,7 +8,9 @@ extension ShapeStyle { package typealias RenderedShape = _ShapeStyle_RenderedShape + package typealias InterpolatorGroup = _ShapeStyle_InterpolatorGroup } + package struct _ShapeStyle_RenderedShape { package enum Shape { case empty @@ -18,3 +20,6 @@ package struct _ShapeStyle_RenderedShape { case alphaMask(DisplayList.Item) } } + +final package class _ShapeStyle_InterpolatorGroup/*: DisplayList.InterpolatorGroup*/ { +} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeadView.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeadView.swift index c33b2b87d..13f40fa13 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeadView.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeadView.swift @@ -10,11 +10,77 @@ package import OpenGraphShims package protocol ShapeStyledLeafView: ContentResponder { static var animatesSize: Bool { get } + associatedtype ShapeUpdateData = Void + mutating func mustUpdate(data: ShapeUpdateData, position: Attribute) -> Bool - typealias FramedShape = (shape: _ShapeStyle_RenderedShape.Shape, frame: CGRect) + + typealias FramedShape = (shape: ShapeStyle.RenderedShape.Shape, frame: CGRect) + func shape(in size: CGSize) -> FramedShape + static var hasBackground: Bool { get } + func backgroundShape(in size: CGSize) -> FramedShape + func isClear(styles: _ShapeStyle_Pack) -> Bool } + +extension ShapeStyledLeafView { + package static var animatesSize: Bool { true } + + package static var hasBackground: Bool { false } + + package func backgroundShape(in size: CGSize) -> FramedShape { + (shape: .path(Path(), FillStyle()), frame: .zero) + } + + package func isClear(styles: ShapeStyle.Pack) -> Bool { + styles.isClear(name: .foreground) && styles.isClear(name: .background) + } + + package func contains(points: [PlatformPoint], size: CGSize) -> BitVector64 { + preconditionFailure("TODO") + } + + package func contentPath(size: CGSize) -> Path { + preconditionFailure("TODO") + } + + package static func makeLeafView( + view: _GraphValue, + inputs: _ViewInputs, + styles: Attribute, + interpolatorGroup: ShapeStyle.InterpolatorGroup? = nil, + data: ShapeUpdateData + ) -> _ViewOutputs { + preconditionFailure("TODO") + } +} + +extension ShapeStyledLeafView where ShapeUpdateData == () { + package mutating func mustUpdate(data: ShapeUpdateData, position: Attribute) -> Bool { + preconditionFailure("TODO") + } + + @inlinable + package static func makeLeafView( + view: _GraphValue, + inputs: _ViewInputs, + styles: Attribute, + interpolatorGroup: ShapeStyle.InterpolatorGroup? = nil, + data: ShapeUpdateData + ) -> _ViewOutputs { + preconditionFailure("TODO") + } +} + +package struct ShapeStyledResponderData: ContentResponder where V: ShapeStyledLeafView { + package func contains(points: [PlatformPoint], size: CGSize) -> BitVector64 { + preconditionFailure("TODO") + } + + package func contentPath(size: CGSize) -> Path { + preconditionFailure("TODO") + } +} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeView.swift b/Sources/OpenSwiftUICore/Shape/ShapeView.swift index de1870f83..a9b75af52 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeView.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeView.swift @@ -1,16 +1,134 @@ // // ShapeView.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 -// Status: WIP +// Audited for iOS 18.0 +// Status: Blocked by _ShapeView.makeView +public import Foundation + +// MARK: - Shape + Extension (disfavoredOverload) + +extension Shape { + /// Fills this shape with a color or gradient. + /// + /// - Parameters: + /// - content: The color or gradient to use when filling this shape. + /// - style: The style options that determine how the fill renders. + /// - Returns: A shape filled with the color or gradient you supply. + @inlinable + @_disfavoredOverload + nonisolated public func fill(_ content: S, style: FillStyle = FillStyle()) -> some View where S: ShapeStyle { + _ShapeView(shape: self, style: content, fillStyle: style) + } + + /// Fills this shape with the foreground color. + /// + /// - Parameter style: The style options that determine how the fill + /// renders. + /// - Returns: A shape filled with the foreground color. + @inlinable + @_disfavoredOverload + nonisolated public func fill(style: FillStyle = FillStyle()) -> some View { + _ShapeView(shape: self, style: .foreground, fillStyle: style) + } + + /// Traces the outline of this shape with a color or gradient. + /// + /// The following example adds a dashed purple stroke to a `Capsule`: + /// + /// Capsule() + /// .stroke( + /// Color.purple, + /// style: StrokeStyle( + /// lineWidth: 5, + /// lineCap: .round, + /// lineJoin: .miter, + /// miterLimit: 0, + /// dash: [5, 10], + /// dashPhase: 0 + /// ) + /// ) + /// + /// - Parameters: + /// - content: The color or gradient with which to stroke this shape. + /// - style: The stroke characteristics --- such as the line's width and + /// whether the stroke is dashed --- that determine how to render this + /// shape. + /// - Returns: A stroked shape. + @inlinable + @_disfavoredOverload + nonisolated public func stroke(_ content: S, style: StrokeStyle) -> some View where S: ShapeStyle { + _ShapeView( + shape: stroke(style: style), + style: content + ) + } + + /// Traces the outline of this shape with a color or gradient. + /// + /// The following example draws a circle with a purple stroke: + /// + /// Circle().stroke(Color.purple, lineWidth: 5) + /// + /// - Parameters: + /// - content: The color or gradient with which to stroke this shape. + /// - lineWidth: The width of the stroke that outlines this shape. + /// - Returns: A stroked shape. + @inlinable + @_disfavoredOverload + nonisolated public func stroke(_ content: S, lineWidth: CGFloat = 1) -> some View where S: ShapeStyle { + _ShapeView( + shape: stroke(style: StrokeStyle(lineWidth: lineWidth)), + style: content + ) + } +} + +// MARK: - Shape + View + +extension Shape { + public var body: _ShapeView { + _ShapeView(shape: self, style: ForegroundStyle()) + } +} + +extension Shape { + nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + makeView(view: view, inputs: inputs) + } + + nonisolated public static func _makeViewList(view: _GraphValue, inputs: _ViewListInputs) -> _ViewListOutputs { + makeViewList(view: view, inputs: inputs) + } +} + +// MARK: - ShapeStyle + View + +extension ShapeStyle where Self: View, Body == _ShapeView { + public var body: _ShapeView { + _ShapeView(shape: Rectangle(), style: self) + } +} + +// MARK: - _ShapeView + +/// A view that renders a shape in a provided shape style. +/// +/// You don't use this view directly. Instead you create a Shape and use the +/// ``Shape/fill(_:style:)`` modifier to provide a shape and fill style +/// +/// Rectangle() +/// .fill(.red) +/// @frozen -public struct _ShapeView: /*ShapeStyledLeafView, */UnaryView, PrimitiveView where Content: Shape, Style: ShapeStyle { +public struct _ShapeView: View, UnaryView, ShapeStyledLeafView, PrimitiveView/*, LeafViewLayout*/ where Content: Shape, Style: ShapeStyle { public var shape: Content + public var style: Style + public var fillStyle: FillStyle - + @inlinable public init(shape: Content, style: Style, fillStyle: FillStyle = FillStyle()) { self.shape = shape @@ -18,7 +136,452 @@ public struct _ShapeView: /*ShapeStyledLeafView, */UnaryView, Pr self.fillStyle = fillStyle } - public static func _makeView(view: _GraphValue<_ShapeView>, inputs: _ViewInputs) -> _ViewOutputs { + nonisolated public static func _makeView(view: _GraphValue<_ShapeView>, inputs: _ViewInputs) -> _ViewOutputs { preconditionFailure("TODO") } + + package func shape(in size: CGSize) -> FramedShape { + let path = shape.effectivePath(in: CGRect(origin: .zero, size: size)) + // FIXME + return FramedShape(shape: .path(path, fillStyle), frame: CGRect(origin: .zero, size: size)) + } + + package func sizeThatFits(in proposedSize: _ProposedSize) -> CGSize { + shape.sizeThatFits(.init(proposedSize)) + } +} + +@available(*, unavailable) +extension _ShapeView: Sendable {} + +extension ShapeStyle { + package static func legacyMakeShapeView(view: _GraphValue<_ShapeView>, inputs: _ViewInputs) -> _ViewOutputs where S: Shape { + _ShapeView._makeView(view: view, inputs: inputs) + } +} + +// MARK: - ShapeView + +/// A view that provides a shape that you can use for drawing operations. +/// +/// Use this type with the drawing methods on ``Shape`` to apply multiple fills +/// and/or strokes to a shape. For example, the following code applies a fill +/// and stroke to a capsule shape: +/// +/// Capsule() +/// .fill(.yellow) +/// .stroke(.blue, lineWidth: 8) +/// +public protocol ShapeView: View { + associatedtype Content: Shape + var shape: Self.Content { get } +} + +// MARK: - Shape + Extension + +extension Shape { + /// Fills this shape with a color or gradient. + /// + /// - Parameters: + /// - content: The color or gradient to use when filling this shape. + /// - style: The style options that determine how the fill renders. + /// - Returns: A shape filled with the color or gradient you supply. + @_alwaysEmitIntoClient + nonisolated public func fill(_ content: S = .foreground, style: FillStyle = FillStyle()) -> _ShapeView where S: ShapeStyle { + _ShapeView(shape: self, style: content, fillStyle: style) + } + + /// Traces the outline of this shape with a color or gradient. + /// + /// The following example adds a dashed purple stroke to a `Capsule`: + /// + /// Capsule() + /// .stroke( + /// Color.purple, + /// style: StrokeStyle( + /// lineWidth: 5, + /// lineCap: .round, + /// lineJoin: .miter, + /// miterLimit: 0, + /// dash: [5, 10], + /// dashPhase: 0 + /// ) + /// ) + /// + /// - Parameters: + /// - content: The color or gradient with which to stroke this shape. + /// - style: The stroke characteristics --- such as the line's width and + /// whether the stroke is dashed --- that determine how to render this + /// shape. + /// - Returns: A stroked shape. + @_alwaysEmitIntoClient + nonisolated public func stroke(_ content: S, style: StrokeStyle, antialiased: Bool = true) -> StrokeShapeView where S: ShapeStyle { + StrokeShapeView( + shape: self, + style: content, + strokeStyle: style, + isAntialiased: antialiased, + background: EmptyView() + ) + } + + /// Traces the outline of this shape with a color or gradient. + /// + /// The following example draws a circle with a purple stroke: + /// + /// Circle().stroke(Color.purple, lineWidth: 5) + /// + /// - Parameters: + /// - content: The color or gradient with which to stroke this shape. + /// - lineWidth: The width of the stroke that outlines this shape. + /// - Returns: A stroked shape. + @_alwaysEmitIntoClient + nonisolated public func stroke(_ content: S, lineWidth: CGFloat = 1, antialiased: Bool = true) -> StrokeShapeView where S: ShapeStyle { + stroke( + content, + style: StrokeStyle(lineWidth: lineWidth), + antialiased: antialiased + ) + } +} + +// MARK: - InsettableShape + Extension + +extension InsettableShape { + /// Returns a view that is the result of insetting `self` by + /// `style.lineWidth / 2`, stroking the resulting shape with + /// `style`, and then filling with `content`. + @_alwaysEmitIntoClient + nonisolated public func strokeBorder(_ content: S = .foreground, style: StrokeStyle, antialiased: Bool = true) -> StrokeBorderShapeView where S: ShapeStyle { + StrokeBorderShapeView( + shape: self, + style: content, + strokeStyle: style, + isAntialiased: antialiased, + background: EmptyView() + ) + } + + /// Returns a view that is the result of filling the `lineWidth`-sized + /// border (aka inner stroke) of `self` with `content`. This is + /// equivalent to insetting `self` by `lineWidth / 2` and stroking the + /// resulting shape with `lineWidth` as the line-width. + @_alwaysEmitIntoClient + nonisolated public func strokeBorder(_ content: S = .foreground, lineWidth: CGFloat = 1, antialiased: Bool = true) -> StrokeBorderShapeView where S: ShapeStyle { + strokeBorder( + content, + style: StrokeStyle(lineWidth: lineWidth), + antialiased: antialiased + ) + } +} + +extension _ShapeView: ShapeView {} + +// MARK: - FillShapeView + +/// A shape provider that fills its shape. +/// +/// You do not create this type directly, it is the return type of `Shape.fill`. +@frozen +public struct FillShapeView: ShapeView, PrimitiveView, UnaryView where Content: Shape, Style: ShapeStyle, Background: View { + @usableFromInline + typealias ViewType = ModifiedContent<_ShapeView, _BackgroundModifier> + + @usableFromInline + var view: ViewType + + /// The shape that this type draws and provides for other drawing + /// operations. + @_alwaysEmitIntoClient + public var shape: Content { + get { view.content.shape } + set { view.content.shape = newValue } + } + + /// The style that fills this view's shape. + @_alwaysEmitIntoClient + public var style: Style { + get { view.content.style } + set { view.content.style = newValue } + } + + /// The fill style used when filling this view's shape. + @_alwaysEmitIntoClient + public var fillStyle: FillStyle { + get { view.content.fillStyle } + set { view.content.fillStyle = newValue } + } + + /// The background shown beneath this view. + @_alwaysEmitIntoClient + public var background: Background { + get { view.modifier.background } + set { view.modifier.background = newValue } + } + + /// Create a FillShapeView. + @_alwaysEmitIntoClient + public init(shape: Content, style: Style, fillStyle: FillStyle, background: Background) { + view = .init( + content: _ShapeView( + shape: shape, + style: style, + fillStyle: fillStyle + ), + modifier: .init(background: background) + ) + } + + nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + ViewType._makeView(view: view[offset: { .of(&$0.view) }], inputs: inputs) + } +} + +@available(*, unavailable) +extension FillShapeView: Sendable {} + +// MARK: - StrokeShapeView + +/// A shape provider that strokes its shape. +/// +/// You don't create this type directly; it's the return type of +/// `Shape.stroke`. +@frozen +public struct StrokeShapeView : ShapeView, PrimitiveView, UnaryView where Content: Shape, Style: ShapeStyle, Background: View { + @usableFromInline + typealias ViewType = ModifiedContent<_ShapeView<_StrokedShape, Style>, _BackgroundModifier> + + @usableFromInline + var view: ViewType + + /// The shape that this type draws and provides for other drawing + /// operations. + @_alwaysEmitIntoClient + public var shape: Content { + get { view.content.shape.shape } + set { view.content.shape.shape = newValue } + } + + /// The style that strokes this view's shape. + @_alwaysEmitIntoClient + public var style: Style { + get { view.content.style } + set { view.content.style = newValue } + } + + /// The stroke style used when stroking this view's shape. + @_alwaysEmitIntoClient + public var strokeStyle: StrokeStyle { + get { view.content.shape.style } + set { view.content.shape.style = newValue } + } + + /// Whether this shape should be drawn antialiased. + @_alwaysEmitIntoClient + public var isAntialiased: Swift.Bool { + get { view.content.fillStyle.isAntialiased } + set { view.content.fillStyle.isAntialiased = newValue } + } + + /// The background shown beneath this view. + @_alwaysEmitIntoClient + public var background: Background { + get { view.modifier.background } + set { view.modifier.background = newValue } + } + + /// Create a StrokeShapeView. + @_alwaysEmitIntoClient + public init(shape: Content, style: Style, strokeStyle: StrokeStyle, isAntialiased: Swift.Bool, background: Background) { + view = .init( + content: _ShapeView( + shape: _StrokedShape(shape: shape, style: strokeStyle), + style: style, + fillStyle: .init(antialiased: isAntialiased) + ), + modifier: .init(background: background) + ) + } + + nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + ViewType._makeView(view: view[offset: { .of(&$0.view) }], inputs: inputs) + } +} + +@available(*, unavailable) +extension StrokeShapeView: Sendable {} + +// MARK: - StrokeBorderShapeView + +/// A shape provider that strokes the border of its shape. +/// +/// You don't create this type directly; it's the return type of +/// `Shape.strokeBorder`. +@frozen +public struct StrokeBorderShapeView: ShapeView, PrimitiveView, UnaryView where Content: InsettableShape, Style: ShapeStyle, Background: View { + @usableFromInline + typealias ViewType = ModifiedContent<_ShapeView<_StrokedShape, Style>, _BackgroundModifier> + + /// The shape that this type draws and provides for other drawing + /// operations. + public var shape: Content + + @usableFromInline + var view: ViewType + + /// The style that strokes the border of this view's shape. + @_alwaysEmitIntoClient + public var style: Style { + get { view.content.style } + set { view.content.style = newValue } + } + + /// The stroke style used when stroking this view's shape. + @_alwaysEmitIntoClient + public var strokeStyle: StrokeStyle { + get { view.content.shape.style } + set { view.content.shape.style = newValue } + } + + /// Whether this shape should be drawn antialiased. + @_alwaysEmitIntoClient + public var isAntialiased: Swift.Bool { + get { view.content.fillStyle.isAntialiased } + set { view.content.fillStyle.isAntialiased = newValue } + } + + /// The background shown beneath this view. + @_alwaysEmitIntoClient + public var background: Background { + get { view.modifier.background } + set { view.modifier.background = newValue } + } + + /// Create a stroke border shape. + @_alwaysEmitIntoClient + public init(shape: Content, style: Style, strokeStyle: StrokeStyle, isAntialiased: Swift.Bool, background: Background) { + self.shape = shape + view = .init( + content: _ShapeView( + shape: _StrokedShape( + shape: shape.inset(by: strokeStyle.lineWidth * 0.5), + style: strokeStyle + ), + style: style, + fillStyle: .init(antialiased: isAntialiased) + ), + modifier: .init(background: background) + ) + } + + nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + ViewType._makeView(view: view[offset: { .of(&$0.view) }], inputs: inputs) + } +} + +@available(*, unavailable) +extension StrokeBorderShapeView: Sendable {} + +// MARK: - ShapeView + Extension + +extension ShapeView { + /// Fills this shape with a color or gradient. + /// + /// - Parameters: + /// - content: The color or gradient to use when filling this shape. + /// - style: The style options that determine how the fill renders. + /// - Returns: A shape filled with the color or gradient you supply. + @_alwaysEmitIntoClient + nonisolated public func fill(_ content: S = .foreground, style: FillStyle = FillStyle()) -> FillShapeView where S: ShapeStyle { + FillShapeView( + shape: shape, + style: content, + fillStyle: style, + background: self + ) + } + + /// Traces the outline of this shape with a color or gradient. + /// + /// The following example adds a dashed purple stroke to a `Capsule`: + /// + /// Capsule() + /// .stroke( + /// Color.purple, + /// style: StrokeStyle( + /// lineWidth: 5, + /// lineCap: .round, + /// lineJoin: .miter, + /// miterLimit: 0, + /// dash: [5, 10], + /// dashPhase: 0 + /// ) + /// ) + /// + /// - Parameters: + /// - content: The color or gradient with which to stroke this shape. + /// - style: The stroke characteristics --- such as the line's width and + /// whether the stroke is dashed --- that determine how to render this + /// shape. + /// - Returns: A stroked shape. + @_alwaysEmitIntoClient + nonisolated public func stroke(_ content: S, style: StrokeStyle, antialiased: Swift.Bool = true) -> StrokeShapeView where S: ShapeStyle { + StrokeShapeView( + shape: shape, + style: content, + strokeStyle: style, + isAntialiased: antialiased, + background: self + ) + } + + /// Traces the outline of this shape with a color or gradient. + /// + /// The following example draws a circle with a purple stroke: + /// + /// Circle().stroke(Color.purple, lineWidth: 5) + /// + /// - Parameters: + /// - content: The color or gradient with which to stroke this shape. + /// - lineWidth: The width of the stroke that outlines this shape. + /// - Returns: A stroked shape. + @_alwaysEmitIntoClient + nonisolated public func stroke(_ content: S, lineWidth: CGFloat = 1, antialiased: Swift.Bool = true) -> StrokeShapeView where S: ShapeStyle { + stroke( + content, + style: StrokeStyle(lineWidth: lineWidth), + antialiased: antialiased + ) + } +} + +extension ShapeView where Content: InsettableShape { + /// Returns a view that's the result of insetting this view by half of its style's line width. + /// + /// This method strokes the resulting shape with + /// `style` and fills it with `content`. + @_alwaysEmitIntoClient + nonisolated public func strokeBorder(_ content: S = .foreground, style: StrokeStyle, antialiased: Bool = true) -> StrokeBorderShapeView where S: ShapeStyle { + StrokeBorderShapeView( + shape: shape, + style: content, + strokeStyle: style, + isAntialiased: antialiased, + background: self + ) + } + + /// Returns a view that's the result of filling an inner stroke of this view with the content you supply. + /// + /// This is equivalent to insetting `self` by `lineWidth / 2` and stroking the + /// resulting shape with `lineWidth` as the line-width. + @_alwaysEmitIntoClient + nonisolated public func strokeBorder(_ content: S = .foreground, lineWidth: CGFloat = 1, antialiased: Bool = true) -> StrokeBorderShapeView where S: ShapeStyle { + strokeBorder( + content, + style: StrokeStyle(lineWidth: lineWidth), + antialiased: antialiased + ) + } } diff --git a/Sources/OpenSwiftUICore/Shape/Shapes.swift b/Sources/OpenSwiftUICore/Shape/Shapes.swift new file mode 100644 index 000000000..db3e9ba7a --- /dev/null +++ b/Sources/OpenSwiftUICore/Shape/Shapes.swift @@ -0,0 +1,287 @@ +// +// Shapes.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP + +public import Foundation + +// MARK: - Rectangle + Extension + +extension Shape where Self == Rectangle { + /// A rectangular shape aligned inside the frame of the view containing it. + @_alwaysEmitIntoClient + public static var rect: Rectangle { .init() } +} + +// MARK: - Rectangle + +/// A rectangular shape aligned inside the frame of the view containing it. +@frozen +public struct Rectangle: Shape { + nonisolated public func path(in rect: CGRect) -> Path { + Path(rect) + } + + nonisolated public var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } + + /// Creates a new rectangle shape. + @inlinable + public init() {} +} + +// MARK: - RoundedRectangle + Extension + + extension Shape where Self == RoundedRectangle { + /// A rectangular shape with rounded corners, aligned inside the frame of + /// the view containing it. + @_alwaysEmitIntoClient + public static func rect(cornerSize: CGSize, style: RoundedCornerStyle = .continuous) -> Self { + .init(cornerSize: cornerSize, style: style) + } + + /// A rectangular shape with rounded corners, aligned inside the frame of + /// the view containing it. + @_alwaysEmitIntoClient + public static func rect(cornerRadius: CGFloat, style: RoundedCornerStyle = .continuous) -> Self { + .init(cornerRadius: cornerRadius, style: style) + } +} + +// MARK: - RoundedRectangle + +/// A rectangular shape with rounded corners, aligned inside the frame of the +/// view containing it. +@frozen +public struct RoundedRectangle: Shape { + /// The width and height of the rounded rectangle's corners. + public var cornerSize: CGSize + + /// The style of corners drawn by the rounded rectangle. + public var style: RoundedCornerStyle + + /// Creates a new rounded rectangle shape. + /// + /// - Parameters: + /// - cornerSize: the width and height of the rounded corners. + /// - style: the style of corners drawn by the shape. + @inlinable + public init(cornerSize: CGSize, style: RoundedCornerStyle = .continuous) { + self.cornerSize = cornerSize + self.style = style + } + + /// Creates a new rounded rectangle shape. + /// + /// - Parameters: + /// - cornerRadius: the radius of the rounded corners. + /// - style: the style of corners drawn by the shape. + @inlinable + nonisolated public init(cornerRadius: CGFloat, style: RoundedCornerStyle = .continuous) { + let cornerSize = CGSize(width: cornerRadius, height: cornerRadius) + self.init(cornerSize: cornerSize, style: style) + } + + nonisolated public func path(in rect: CGRect) -> Path { + Path(roundedRect: rect, cornerSize: cornerSize, style: style) + } + + nonisolated public var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } + + public var animatableData: CGSize.AnimatableData { + get { cornerSize.animatableData } + set { cornerSize.animatableData = newValue } + } +} + +// MARK: - UnevenRoundedRectangle + Extension + +extension Shape where Self == UnevenRoundedRectangle { + /// A rectangular shape with rounded corners with different values, aligned + /// inside the frame of the view containing it. + @_alwaysEmitIntoClient + public static func rect(cornerRadii: RectangleCornerRadii, style: RoundedCornerStyle = .continuous) -> Self { + .init(cornerRadii: cornerRadii, style: style) + } + + /// A rectangular shape with rounded corners with different values, aligned + /// inside the frame of the view containing it. + + @_alwaysEmitIntoClient + public static func rect(topLeadingRadius: CGFloat = 0, bottomLeadingRadius: CGFloat = 0, bottomTrailingRadius: CGFloat = 0, topTrailingRadius: CGFloat = 0, style: RoundedCornerStyle = .continuous) -> Self { + .init( + topLeadingRadius: topLeadingRadius, + bottomLeadingRadius: bottomLeadingRadius, + bottomTrailingRadius: bottomTrailingRadius, + topTrailingRadius: topTrailingRadius, style: style + ) + } +} + +// MARK: - UnevenRoundedRectangle + +/// A rectangular shape with rounded corners with different values, aligned +/// inside the frame of the view containing it. +@frozen +public struct UnevenRoundedRectangle: Shape { + /// The radii of each corner of the rounded rectangle. + public var cornerRadii: RectangleCornerRadii + + /// The style of corners drawn by the rounded rectangle. + public var style: RoundedCornerStyle + + /// Creates a new rounded rectangle shape with uneven corners. + /// + /// - Parameters: + /// - cornerRadii: the radii of each corner. + /// - style: the style of corners drawn by the shape. + @inlinable + public init(cornerRadii: RectangleCornerRadii, style: RoundedCornerStyle = .continuous) { + self.cornerRadii = cornerRadii + self.style = style + } + + /// Creates a new rounded rectangle shape with uneven corners. + @_alwaysEmitIntoClient + public init(topLeadingRadius: CGFloat = 0, bottomLeadingRadius: CGFloat = 0, bottomTrailingRadius: CGFloat = 0, topTrailingRadius: CGFloat = 0, style: RoundedCornerStyle = .continuous) { + self.init( + cornerRadii: .init( + topLeading: topLeadingRadius, + bottomLeading: bottomLeadingRadius, + bottomTrailing: bottomTrailingRadius, + topTrailing: topTrailingRadius + ), + style: style + ) + } + + nonisolated public func path(in rect: CGRect) -> Path { + Path(roundedRect: rect, cornerRadii: cornerRadii, style: style) + } + + public var animatableData: RectangleCornerRadii.AnimatableData { + get { cornerRadii.animatableData } + set { cornerRadii.animatableData = newValue } + } +} + +// MARK: - Capsule + Extension + +extension Shape where Self == Capsule { + /// A capsule shape aligned inside the frame of the view containing it. + /// + /// A capsule shape is equivalent to a rounded rectangle where the corner + /// radius is chosen as half the length of the rectangle's smallest edge. + @_alwaysEmitIntoClient + public static var capsule: Capsule { + .init() + } + + /// A capsule shape aligned inside the frame of the view containing it. + /// + /// A capsule shape is equivalent to a rounded rectangle where the corner + /// radius is chosen as half the length of the rectangle's smallest edge. + @_alwaysEmitIntoClient + public static func capsule(style: RoundedCornerStyle) -> Self { + .init(style: style) + } +} + +// MARK: - Capsule + +/// A capsule shape aligned inside the frame of the view containing it. +/// +/// A capsule shape is equivalent to a rounded rectangle where the corner radius +/// is chosen as half the length of the rectangle's smallest edge. +@frozen +public struct Capsule: Shape { + public var style: RoundedCornerStyle + + /// Creates a new capsule shape. + /// + /// - Parameters: + /// - style: the style of corners drawn by the shape. + @inlinable + public init(style: RoundedCornerStyle = .continuous) { + self.style = style + } + + nonisolated public func path(in r: CGRect) -> Path { + let radius = min(r.width, r.height) / 2 + return Path(roundedRect: r, cornerRadius: radius, style: style) + } + + nonisolated public var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } +} + +// MARK: - Ellipse + Extension + +extension Shape where Self == Ellipse { + /// An ellipse aligned inside the frame of the view containing it. + @_alwaysEmitIntoClient + public static var ellipse: Ellipse { .init() } +} + +// MARK: - Ellipse + +/// An ellipse aligned inside the frame of the view containing it. +@frozen +public struct Ellipse: Shape { + nonisolated public func path(in rect: CGRect) -> Path { + Path(ellipseIn: rect) + } + + /// Creates a new ellipse shape. + @inlinable + public init() {} + + nonisolated public var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } +} + +// MARK: - Circle + Extension + +extension Shape where Self == Circle { + /// A circle centered on the frame of the view containing it. + /// + /// The circle's radius equals half the length of the frame rectangle's + /// smallest edge. + @_alwaysEmitIntoClient + public static var circle: Circle { .init() } +} + +// MARK: - Circle + +/// A circle centered on the frame of the view containing it. +/// +/// The circle's radius equals half the length of the frame rectangle's smallest +/// edge. +@frozen public struct Circle: Shape { + nonisolated public func path(in rect: CGRect) -> Path { + preconditionFailure("TODO") + } + + /// Creates a new circle shape. + @inlinable + public init() {} + + nonisolated public var layoutDirectionBehavior: LayoutDirectionBehavior { + .fixed + } +} + +extension Circle { + nonisolated public func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize { + let size = proposal.replacingUnspecifiedDimensions() + let minValue = min(size.width, size.height) + return CGSize(width: minValue, height: minValue) + } +} diff --git a/Sources/OpenSwiftUICore/Shape/StrokeStyle.swift b/Sources/OpenSwiftUICore/Shape/StrokeStyle.swift index 5082afe9d..2c11f71cd 100644 --- a/Sources/OpenSwiftUICore/Shape/StrokeStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/StrokeStyle.swift @@ -1,28 +1,12 @@ // // StrokeStyle.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete public import Foundation -#if canImport(CoreGraphics) -public import CoreGraphics -#else -/// Line join styles -public enum CGLineJoin: Int32, @unchecked Sendable { - case miter = 0 - case round = 1 - case bevel = 2 -} - -/// Line cap styles -public enum CGLineCap : Int32, @unchecked Sendable { - case butt = 0 - case round = 1 - case square = 2 -} -#endif +public import CoreGraphicsShims /// The characteristics of a stroke that traces a path. @frozen diff --git a/Sources/OpenSwiftUICore/Util/ImpossibleActor.swift b/Sources/OpenSwiftUICore/Util/ImpossibleActor.swift new file mode 100644 index 000000000..c4fe416b9 --- /dev/null +++ b/Sources/OpenSwiftUICore/Util/ImpossibleActor.swift @@ -0,0 +1,15 @@ +// +// ImpossibleActor.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +@globalActor +public actor _ImpossibleActor: Sendable { + public static var shared = _ImpossibleActor() +} + +@_marker +@_ImpossibleActor +public protocol _RemoveGlobalActorIsolation {} diff --git a/Sources/OpenSwiftUICore/View/Responder/ContentResponder.swift b/Sources/OpenSwiftUICore/View/Responder/ContentResponder.swift index 879ddbf90..a7f5a34df 100644 --- a/Sources/OpenSwiftUICore/View/Responder/ContentResponder.swift +++ b/Sources/OpenSwiftUICore/View/Responder/ContentResponder.swift @@ -15,17 +15,17 @@ package protocol ContentResponder { } extension ContentResponder { - func contains(points: [CGPoint], size: CGSize) -> BitVector64 { + package func contains(points: [CGPoint], size: CGSize) -> BitVector64 { guard !points.isEmpty else { return BitVector64() } let rect = CGRect(origin: .zero, size: size) return points.mapBool { rect.contains($0) } } - func contentPath(size: CGSize) -> Path { + package func contentPath(size: CGSize) -> Path { Path(CGRect(origin: .zero, size: size)) } - func contentPath(size: CGSize, kind: ContentShapeKinds) -> Path { + package func contentPath(size: CGSize, kind: ContentShapeKinds) -> Path { if kind == .interaction { return contentPath(size: size) } else { diff --git a/Sources/OpenSwiftUI_SPI/Shims/CSymbols/OpenSwiftUI_CSymbols.c b/Sources/OpenSwiftUI_SPI/Shims/CSymbols/OpenSwiftUI_CSymbols.c index f4f2e7e5d..3b97f1f0d 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/CSymbols/OpenSwiftUI_CSymbols.c +++ b/Sources/OpenSwiftUI_SPI/Shims/CSymbols/OpenSwiftUI_CSymbols.c @@ -2,10 +2,6 @@ // OpenSwiftUI_CSymbols.c // OpenSwiftUI_SPI -#ifdef __linux__ -#define _GNU_SOURCE -#endif - #include "OpenSwiftUI_CSymbols.h" #include diff --git a/Sources/OpenSwiftUI_SPI/Util/PathData.c b/Sources/OpenSwiftUI_SPI/Util/PathData.c new file mode 100644 index 000000000..f5e5bb8b1 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Util/PathData.c @@ -0,0 +1,5 @@ +// +// PathData.c +// OpenSwiftUI_SPI + +#include "PathData.h" diff --git a/Sources/OpenSwiftUI_SPI/Util/PathData.h b/Sources/OpenSwiftUI_SPI/Util/PathData.h new file mode 100644 index 000000000..19b5655f5 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Util/PathData.h @@ -0,0 +1,25 @@ +// +// PathData.h +// OpenSwiftUI_SPI + +#ifndef PathData_h +#define PathData_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN +#include + +typedef union PathData { + CGPathRef cgPath; + void * obPath; // FIXME +} PathData; + +#else +typedef union PathData { + void *cgPath; + void *obPath; // FIXME +} PathData; +#endif + +#endif /* PathData_h */ diff --git a/Tests/OpenSwiftUICoreTests/Util/TimerUtilsTests.swift b/Tests/OpenSwiftUICoreTests/Util/TimerUtilsTests.swift index 5d7abb8e6..b503108d1 100644 --- a/Tests/OpenSwiftUICoreTests/Util/TimerUtilsTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/TimerUtilsTests.swift @@ -113,6 +113,9 @@ final class TimerUtilsXCTests: XCTestCase { } func testWithDelayRunsOnMainRunLoop() async throws { + #if canImport(Darwin) + throw XCTSkip("Skip flaky test case temporary") + #endif let expectation = expectation(description: "withDelay body call") let _ = withDelay(0.2) { XCTAssertTrue(Thread.isMainThread)