diff --git a/Package.swift b/Package.swift index 610dd78f4..377a030a2 100644 --- a/Package.swift +++ b/Package.swift @@ -110,7 +110,7 @@ let OpenSwiftUI_SPITestTarget = Target.testTarget( "OpenSwiftUI_SPI", // For ProtocolDescriptor symbol linking "OpenSwiftUI", - .product(name: "Numerics", package: "swift-numerics") + .product(name: "Numerics", package: "swift-numerics"), ], exclude: ["README.md"], swiftSettings: sharedSwiftSettings @@ -119,6 +119,7 @@ let openSwiftUICoreTestTarget = Target.testTarget( name: "OpenSwiftUICoreTests", dependencies: [ "OpenSwiftUICore", + .product(name: "Numerics", package: "swift-numerics"), ], exclude: ["README.md"], swiftSettings: sharedSwiftSettings @@ -133,6 +134,9 @@ let openSwiftUITestTarget = Target.testTarget( ) let openSwiftUICompatibilityTestTarget = Target.testTarget( name: "OpenSwiftUICompatibilityTests", + dependencies: [ + .product(name: "Numerics", package: "swift-numerics"), + ], exclude: ["README.md"], swiftSettings: sharedSwiftSettings ) diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentalView.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentalView.swift new file mode 100644 index 000000000..9d4cfeb52 --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentalView.swift @@ -0,0 +1,17 @@ +// +// EnvironmentalView.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP + +package protocol EnvironmentalView: PrimitiveView, UnaryView { + associatedtype EnvironmentBody: View + func body(environment: EnvironmentValues) -> EnvironmentBody +} + +extension EnvironmentalView { + nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + preconditionFailure("") + } +} diff --git a/Sources/OpenSwiftUICore/Data/Other/AtomicBox.swift b/Sources/OpenSwiftUICore/Data/Other/AtomicBox.swift deleted file mode 100644 index 7500b1002..000000000 --- a/Sources/OpenSwiftUICore/Data/Other/AtomicBox.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// AtomicBox.swift -// OpenSwiftUICore -// -// Audited for iOS 18.0 -// Status: Complete -// ID: 82B2D47816BC992595021D60C278AFF0 - -import Foundation - -// FIXME: -// Replace with Swift's Mutex and Atomic to simplify cross-platform maintain cost -// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0433-mutex.md -// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0410-atomics.md -#if canImport(Darwin) -private final class AtomicBuffer: ManagedBuffer { - static func allocate(value: Value) -> AtomicBuffer { - let buffer = AtomicBuffer.create(minimumCapacity: 1) { buffer in - os_unfair_lock_s() - } - buffer.withUnsafeMutablePointerToElements { pointer in - pointer.initialize(to: value) - } - return unsafeDowncast(buffer, to: AtomicBuffer.self) - } -} -#else -private final class AtomicBuffer: ManagedBuffer { - static func allocate(value: Value) -> AtomicBuffer { - let buffer = AtomicBuffer.create(minimumCapacity: 1) { buffer in - NSLock() - } - buffer.withUnsafeMutablePointerToElements { pointer in - pointer.initialize(to: value) - } - return unsafeDowncast(buffer, to: AtomicBuffer.self) - } -} -#endif - -@propertyWrapper -package struct AtomicBox { - private let buffer: AtomicBuffer - - package init(wrappedValue: Value) { - buffer = AtomicBuffer.allocate(value: wrappedValue) - } - - @inline(__always) - package var wrappedValue: Value { - get { - #if canImport(Darwin) - os_unfair_lock_lock(&buffer.header) - defer { os_unfair_lock_unlock(&buffer.header) } - #else - buffer.header.lock() - defer { buffer.header.unlock() } - #endif - return buffer.withUnsafeMutablePointerToElements { $0.pointee } - } - nonmutating _modify { - #if canImport(Darwin) - os_unfair_lock_lock(&buffer.header) - defer { os_unfair_lock_unlock(&buffer.header) } - #else - buffer.header.lock() - defer { buffer.header.unlock() } - #endif - yield &buffer.withUnsafeMutablePointerToElements { $0 }.pointee - } - } - - @inline(__always) - package func access(_ body: (inout Value) throws -> T) rethrows -> T { - try body(&wrappedValue) - } - - package var projectedValue: AtomicBox { self } -} - -extension AtomicBox: @unchecked Sendable where Value: Sendable {} - -extension AtomicBox where Value: ExpressibleByNilLiteral { - package init() { - self.init(wrappedValue: nil) - } -} diff --git a/Sources/OpenSwiftUICore/Graphic/Color/Color+CGColor.swift b/Sources/OpenSwiftUICore/Graphic/Color/Color+CGColor.swift new file mode 100644 index 000000000..a496bca7d --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Color/Color+CGColor.swift @@ -0,0 +1,107 @@ +// +// Color+CoreGraphics.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Blocked by ResolvedGradient +// ID: A45C110C8134A7DB30776EFC6CE1E8A6 (SwiftUICore?) + +#if canImport(Darwin) + +public import CoreGraphics + +// MARK: - CGColor + Color + +extension Color { + /// Creates a color from a Core Graphics color. + /// + /// - Parameter color: A + /// [CGColor](https://developer.apple.com/documentation/CoreGraphics/CGColor) instance + /// from which to create a color. + @available(*, deprecated, message: "Use Color(cgColor:) when converting a CGColor, or create a standard Color directly") + public init(_ cgColor: CGColor) { + self.init(cgColor: cgColor) + } +} + +extension Color { + /// Creates a color from a Core Graphics color. + /// + /// - Parameter color: A + /// [CGColor](https://developer.apple.com/documentation/CoreGraphics/CGColor) instance + /// from which to create a color. + public init(cgColor: CGColor) { + self.init(provider: cgColor) + } +} + +extension CGColor: ColorProvider { + package func resolve(in environment: EnvironmentValues) -> Color.Resolved { + Color.Resolved(self) + } + + package var staticColor: CGColor? { + self + } +} + +extension Color.Resolved { + package static let srgb: CGColorSpace = CGColorSpace(name: CGColorSpace.sRGB)! + package static let srgbExtended: CGColorSpace = CGColorSpace(name: CGColorSpace.extendedSRGB)! + package static let srgbLinear: CGColorSpace = CGColorSpace(name: CGColorSpace.linearSRGB)! + package static let srgbExtendedLinear: CGColorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB)! + package static let displayP3: CGColorSpace = CGColorSpace(name: CGColorSpace.displayP3)! + + package init(_ cgColor: CGColor) { + self = Color.Resolved(failableCGColor: cgColor) ?? .init(linearWhite: 0) + } + + package init?(failableCGColor cgColor: CGColor) { + let colorSpace: Color.RGBColorSpace + let convertedColor: CGColor + switch cgColor.colorSpace { + case Color.Resolved.srgb, Color.Resolved.srgbExtended: + colorSpace = .sRGB + convertedColor = cgColor + case Color.Resolved.srgbLinear, Color.Resolved.srgbExtendedLinear: + colorSpace = .sRGBLinear + convertedColor = cgColor + case Color.Resolved.displayP3: + colorSpace = .displayP3 + convertedColor = cgColor + default: + guard let color = cgColor.converted(to: Color.Resolved.srgbExtended, intent: .defaultIntent, options: nil) else { + return nil + } + colorSpace = .sRGB + convertedColor = color + } + guard let components = convertedColor.components else { + return nil + } + let red = Float(components[0]) + let green = Float(components[1]) + let blue = Float(components[2]) + let alpha = Float(cgColor.alpha) + self.init(colorSpace: colorSpace, red: red, green: green, blue: blue, opacity: alpha) + } +} + +extension Color.Resolved { + private static let cache: ObjectCache = ObjectCache { resolved in + var components: [CGFloat] = [CGFloat(resolved.red), CGFloat(resolved.green), CGFloat(resolved.blue), CGFloat(resolved.opacity)] + return CGColor(colorSpace: Self.srgbExtended, components: &components)! + } + + public var cgColor: CGColor { + Self.cache[self] + } +} + +//extension ResolvedGradient { +// package var cgGradient: CGGradient? { +// get +// } +//} + +#endif diff --git a/Sources/OpenSwiftUICore/Graphic/Color/Color+ShapeStyle.swift b/Sources/OpenSwiftUICore/Graphic/Color/Color+ShapeStyle.swift new file mode 100644 index 000000000..99aa2e943 --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Color/Color+ShapeStyle.swift @@ -0,0 +1,72 @@ +// +// Color+ShapeStyle.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package import Foundation + +extension Color: ShapeStyle { + package func fallbackColor(in env: EnvironmentValues) -> Color? { self } + package func resolvePaint(in environment: EnvironmentValues) -> Color.Resolved { resolve(in: environment) } + + @available(*, deprecated, message: "obsolete") + @_alwaysEmitIntoClient + nonisolated public static func _makeView(view: _GraphValue<_ShapeView>, inputs: _ViewInputs) -> _ViewOutputs where S: Shape { + _ShapeView._makeView(view: view, inputs: inputs) + } +} + +extension ColorProvider { + package func apply(color: Color, to shape: inout _ShapeStyle_Shape) { + _apply(color: color, to: &shape) + } + + package func _apply(color: Color, to shape: inout _ShapeStyle_Shape) { + switch shape.operation { + case let .prepareText(level): + if level >= 1 { + let opacity = color.provider.opacity(at: level, environment: shape.environment) + shape.result = .preparedText(.foregroundColor(color.opacity(Double(opacity)))) + } else { + shape.result = .preparedText(.foregroundColor(color)) + } + case let .resolveStyle(name, levels): + guard levels.lowerBound != levels.upperBound else { + return + } + let resolved = resolve(in: shape.environment) + let opacity = color.provider.opacity(at: levels.lowerBound, environment: shape.environment) + + var newPack: _ShapeStyle_Pack + switch shape.result { + case let .pack(pack): newPack = pack + default: newPack = .init() + } + newPack[name, levels.lowerBound] = .init(.color(resolved.multiplyingOpacity(by: opacity))) + shape.result = .pack(newPack) + case let .fallbackColor(level): + if level >= 1 { + let opacity = color.provider.opacity(at: level, environment: shape.environment) + shape.result = .color(color.opacity(Double(opacity))) + } else { + shape.result = .color(color) + } + default: + break + } + } +} + +// MARK: - Color.Resolved + ResolvedPaint + +extension Color.Resolved: ResolvedPaint { + package func draw(path: Path, style: PathDrawingStyle, in context: GraphicsContext, bounds: CGRect?) { + context.draw(path, with: .color(self), style: style) + } + + package var isClear: Bool { opacity == 0 } + package var isOpaque: Bool { opacity == 1 } + package static var leafProtobufTag: CodableResolvedPaint.Tag? { .color } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Color/Color.swift b/Sources/OpenSwiftUICore/Graphic/Color/Color.swift index cba736794..205efe2a4 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/Color.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/Color.swift @@ -1,16 +1,18 @@ // // Color.swift -// OpenSwiftUI +// OpenSwiftUICore // // Audited for iOS 18.0 -// Status: WIP -// ID: 3A792CB70CFCF892676D7ADF8BCA260F +// Status: Complete +// ID: 3A792CB70CFCF892676D7ADF8BCA260F (SwiftUICore) import Foundation #if canImport(Darwin) public import CoreGraphics #endif +// MARK: - Color + /// A representation of a color that adapts to a given context. /// /// You can create a color in one of several ways: @@ -72,81 +74,78 @@ public import CoreGraphics /// For example, a color can have distinct light and dark variants /// that the system chooses from at render time. @frozen -public struct Color { -// package var provider: AnyColorBox -// -// package init(provider: AnyColorBox) { -// self.provider = provider -// } -// -// package init

(provider: P) where P: ColorProvider { -// -// } -} - -extension Color { - public func _apply(to shape: _ShapeStyle_Shape) { - // TODO +public struct Color: Hashable, CustomStringConvertible, Sendable { + package var provider: AnyColorBox + + package init(box: AnyColorBox) { + self.provider = box } -} - -// MARK: - Color + ColorSpace - -extension Color { - public enum RGBColorSpace: Sendable { - case sRGB - case sRGBLinear - case displayP3 + + package init

(provider: P) where P: ColorProvider { + self.init(box: ColorBox(base: provider)) } - - public init(_ colorSpace: RGBColorSpace = .sRGB, red: Double, green: Double, blue: Double, opacity: Double = 1) { - // self.init(red: red, green: green, blue: blue, opacity: opacity) + + /// Creates a color that represents the specified custom color. + public init(_ color: T) where T: Hashable, T: ShapeStyle, T.Resolved == Color.Resolved { + self.init(provider: CustomColorProvider(base: color)) } - - public init(_ colorSpace: RGBColorSpace = .sRGB, white: Double, opacity: Double = 1) { + + /// Evaluates this color to a resolved color given the current + /// `context`. + public func resolve(in environment: EnvironmentValues) -> Color.Resolved { + provider.resolve(in: environment) } - - public init(hue: Double, saturation: Double, brightness: Double, opacity: Double = 1) { - + + #if canImport(Darwin) + /// A Core Graphics representation of the color, if available. + /// + /// You can get a + /// [CGColor](https://developer.apple.com/documentation/CoreGraphics/CGColor) + /// instance from a constant SwiftUI color. This includes colors you create + /// from a Core Graphics color, from RGB or HSB components, or from constant + /// UIKit and AppKit colors. + /// + /// For a dynamic color, like one you load from an Asset Catalog using + /// ``init(_:bundle:)``, or one you create from a dynamic UIKit or AppKit + /// color, this property is `nil`. To evaluate all types of colors, use the + /// `resolve(in:)` method. + @available(*, deprecated, renamed: "resolve(in:)") + public var cgColor: CoreGraphics.CGColor? { + provider.staticColor } -} - -package func HSBToRGB(hue: Double, saturation: Double, brightness: Double) -> (red: Double, green: Double, blue: Double) { - preconditionFailure("TODO") -} - - -extension Color.RGBColorSpace: Hashable {} -extension Color.RGBColorSpace: Equatable {} - -#if canImport(Darwin) - -// MARK: - CGColor + Color - -extension Color { - @available(*, deprecated, message: "Use Color(cgColor:) when converting a CGColor, or create a standard Color directly") - public init(_ cgColor: CGColor) { - self.init(cgColor: cgColor) + #endif + + /// Hashes the essential components of the color by feeding them into the + /// given hash function. + /// + /// - Parameters: + /// - hasher: The hash function to use when combining the components of + /// the color. + public func hash(into hasher: inout Hasher) { + provider.hash(into: &hasher) + } + + /// Indicates whether two colors are equal. + /// + /// - Parameters: + /// - lhs: The first color to compare. + /// - rhs: The second color to compare. + /// - Returns: A Boolean that's set to `true` if the two colors are equal. + public static func == (lhs: Color, rhs: Color) -> Bool { + lhs.provider.isEqual(to: rhs.provider) + } + + public var description: String { + provider.description } } extension Color { - public init(cgColor: CGColor) { -// ColorBox + public func _apply(to shape: inout _ShapeStyle_Shape) { + provider.apply(to: &shape) } } -//extension CGColor: ColorProvider { -// package func resolve(in environment: EnvironmentValues) -> <> { -// <#code#> -// } -// -// staticColor -//} - -#endif - - // MARK: - ColorProvider package protocol ColorProvider: Hashable { @@ -167,20 +166,139 @@ extension ColorProvider { package var kitColor: AnyObject? { nil } package var colorDescription: String { String(describing: self) } package func opacity(at level: Int, environment: EnvironmentValues) -> Float { - preconditionFailure("TODO") + environment.systemColorDefinition.base.opacity(at: level, environment: environment) } } -// MARK: - ColorBox +// MARK: - Color + View -//private ColorBox

where P: ColorProvider { -// let base: P -//} +extension Color: EnvironmentalView, View { + package func body(environment: EnvironmentValues) -> ColorView { + ColorView(resolve(in: environment)) + } + + public typealias Body = Never + + package typealias EnvironmentBody = ColorView +} + +// MARK: - AnyColorBox @usableFromInline -package class AnyColorBox: AnyShapeStyleBox { +package class AnyColorBox: AnyShapeStyleBox, @unchecked Sendable { + override package final func apply(to shape: inout _ShapeStyle_Shape) { + guard case let .fallbackColor(level) = shape.operation else { + apply(color: Color(box: self), to: &shape) + return + } + let color: Color + if level >= 1 { + let opacity = opacity(at: level, environment: shape.environment) + color = Color(box: self).opacity(Double(opacity)) + } else { + color = Color(box: self) + } + shape.result = .color(color) + } + + package func resolve(in environment: EnvironmentValues) -> Color.Resolved { preconditionFailure("") } + package func apply(color: Color, to shape: inout _ShapeStyle_Shape) { preconditionFailure("") } + #if canImport(Darwin) + package var staticColor: CGColor? { preconditionFailure("") } + #endif + package var kitColor: AnyObject? { preconditionFailure("") } + package func hash(into hasher: inout Hasher) { preconditionFailure("") } + package var description: String { preconditionFailure("") } + package func opacity(at level: Int, environment: EnvironmentValues) -> Float { preconditionFailure("") } +} + +@available(*, unavailable) +extension AnyColorBox : Sendable {} + +// MARK: - ColorBox + +private final class ColorBox

: AnyColorBox, @unchecked Sendable where P: ColorProvider { + let base: P + + init(base: P) { + self.base = base + } + + override func resolve(in environment: EnvironmentValues) -> Color.Resolved { + base.resolve(in: environment) + } + override func apply(color: Color, to shape: inout _ShapeStyle_Shape) { + base.apply(color: color, to: &shape) + } + + #if canImport(Darwin) + override var staticColor: CGColor? { + base.staticColor + } + #endif + + override var kitColor: AnyObject? { + base.kitColor + } + + override func isEqual(to other: AnyShapeStyleBox) -> Bool { + guard let other = other as? ColorBox

else { return false } + return base == other.base + } + + override func hash(into hasher: inout Hasher) { + base.hash(into: &hasher) + } + + override var description: String { + base.colorDescription + } + + override func opacity(at level: Int, environment: EnvironmentValues) -> Float { + base.opacity(at: level, environment: environment) + } +} + +#if canImport(Darwin) + +// MARK: - ObjcColor + +@objc +final package class ObjcColor: NSObject { + package let color: Color + + package init(_ color: Color) { + self.color = color + super.init() + } + + @objc + override package func isEqual(_ object: Any?) -> Bool { + guard let other = object as? ObjcColor else { return false } + return color == other.color + } + + @objc + override package var hash: Int { + var hasher = Hasher() + color.hash(into: &hasher) + return hasher.finalize() + } } +#endif + +// MARK: - CustomColorProvider -//private CustomColorProvider +private struct CustomColorProvider

: ColorProvider where P: Hashable, P: ShapeStyle, P.Resolved == Color.Resolved { + let base: P + + func resolve(in environment: EnvironmentValues) -> Color.Resolved { + base.resolve(in: environment) + } + + var colorDescription: String { + String(describing: base) + } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Color/ColorOperations.swift b/Sources/OpenSwiftUICore/Graphic/Color/ColorOperations.swift new file mode 100644 index 000000000..1b344ba61 --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Color/ColorOperations.swift @@ -0,0 +1,27 @@ +// +// ColorOperations.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP +// ID: B495DF025D9B78431A787E266E7D8FB1 (SwiftUI?) +// ID: F28C5F7FF836E967BAC87540A3CB4F65 (SwiftUICore?) + +extension Color { + public func opacity(_ opacity: Double) -> Color { + Color(provider: OpacityColor(base: self, opacity: opacity)) + } + + private struct OpacityColor: ColorProvider { + func resolve(in environment: EnvironmentValues) -> Color.Resolved { + preconditionFailure("TODO") + } + + var base: Color + var opacity: Double + } + + // TODO +} + +// TODO diff --git a/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift b/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift index cf797b8e4..38507f569 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift @@ -3,19 +3,36 @@ // OpenSwiftUICore // // Audited for iOS 18.0 -// Status: WIP +// Status: Blocked by ResolvedGradient +// ID: 7C95FE02C0C9104041ABD4890B043CBE (SwiftUICore?) -package import Foundation +import Foundation import OpenSwiftUI_SPI // MARK: - Color.Resolved -extension Color { +extension Color { + /// A concrete color value. + /// + /// `Color.Resolved` is a set of RGBA values that represent a color that can + /// be shown. The values are in Linear sRGB color space, extended range. This is + /// a low-level type, most colors are represented by the `Color` type. + /// + /// - SeeAlso: `Color` @frozen public struct Resolved: Hashable { + /// The amount of red in the color in the sRGB linear color space. public var linearRed: Float + + /// The amount of green in the color in the sRGB linear color space. public var linearGreen: Float + + /// The amount of blue in the color in the sRGB linear color space. public var linearBlue: Float + /// The degree of opacity in the color, given in the range `0` to `1`. + /// + /// A value of `0` means 100% transparency, while a value of `1` means + /// 100% opacity. public var opacity: Float package init(linearRed: Float, linearGreen: Float, linearBlue: Float, opacity: Float = 1) { @@ -29,41 +46,77 @@ extension Color { Color.Resolved(linearRed: linearRed, linearGreen: linearGreen, linearBlue: linearBlue, opacity: opacity * self.opacity) } -// package func over(_ s: Resolved) -> Color.Resolved { -// preconditionFailure("TODO") -// } + package func over(_ s: Color.Resolved) -> Color.Resolved { + let red = s.red * s.opacity * (1 - opacity) + red * opacity + let green = s.green * s.opacity * (1 - opacity) + green * opacity + let blue = s.blue * s.opacity * (1 - opacity) + blue * opacity + + let factor = 1 - (1 - opacity) * (1 - s.opacity) + guard factor != .zero else { return .init(linearWhite: 0) } + return .init(red: red / factor, green: green / factor, blue: blue / factor, opacity: factor) + } } package struct ResolvedVibrant: Equatable { package var scale: Float package var bias: (Float, Float, Float) - // package var colorMatrix: _ColorMatrix { preconditionFailure("TODO") } + + package var colorMatrix: _ColorMatrix { + return _ColorMatrix( + row1: (scale, 0, 0, 0, bias.0), + row2: (0, scale, 0, 0, bias.1), + row3: (0, 0, scale, 0, bias.2), + row4: (0, 0, 0, 1, 0) + ) + } package static func == (lhs: ResolvedVibrant, rhs: ResolvedVibrant) -> Bool { lhs.scale == rhs.scale && lhs.bias == rhs.bias } } + /// Creates a constant color with the values specified by the resolved + /// color. public init(_ resolved: Resolved) { - // TODO + self.init(provider: ResolvedColorProvider(color: resolved)) } } -// MARK: - Color.Resolved + ResolvedPaint - -extension Color.Resolved: ResolvedPaint { - package func draw(path: Path, style: PathDrawingStyle, in context: GraphicsContext, bounds: CGRect?) { - // TODO +private struct ResolvedColorProvider: ColorProvider { + var color: Color.Resolved + + func resolve(in environment: EnvironmentValues) -> Color.Resolved { + color + } + + #if canImport(Darwin) + var staticColor: CGColor? { + color.cgColor } + #endif - package var isClear: Bool { opacity == 0 } - package var isOpaque: Bool { opacity == 1 } - package static var leafProtobufTag: CodableResolvedPaint.Tag? { .color } + var colorDescription: String { + let linearRed = color.linearRed + let linearGreen = color.linearGreen + let linearBlue = color.linearBlue + let opacity = color.opacity + + if linearRed == 0.0 && linearGreen == 0.0 && linearBlue == 0.0 { + if opacity == 0.0 { + return "clear" + } else if opacity == 1.0 { + return "black" + } + } else if linearRed == 1.0 && linearGreen == 1.0 && linearBlue == 1.0 && opacity == 1.0 { + return "white" + } + return color.description + } } // MARK: - Color.Resolved + ShapeStyle -extension Color.Resolved: ShapeStyle/*, PrimitiveShapeStyle*/ { +extension Color.Resolved: ShapeStyle, PrimitiveShapeStyle { public func _apply(to shape: inout _ShapeStyle_Shape) { preconditionFailure("TODO") } @@ -134,12 +187,12 @@ extension Color.ResolvedVibrant: Animatable { // MARK: - Color.Resolved + extension extension Color.Resolved { - package static let clear: Color.Resolved = Color.Resolved(linearRed: 0, linearGreen: 0, linearBlue: 0, opacity: 0) - package static let black: Color.Resolved = Color.Resolved(linearRed: 0, linearGreen: 0, linearBlue: 0, opacity: 1) - package static let gray_75: Color.Resolved = Color.Resolved(linearRed: 0.522522, linearGreen: 0.522522, linearBlue: 0.522522, opacity: 1) - package static let gray_50: Color.Resolved = Color.Resolved(linearRed: 0.214041, linearGreen: 0.214041, linearBlue: 0.214041, opacity: 1) - package static let gray_25: Color.Resolved = Color.Resolved(linearRed: 0.0508761, linearGreen: 0.0508761, linearBlue: 0.0508761, opacity: 1) - package static let white: Color.Resolved = Color.Resolved(linearRed: 1, linearGreen: 1, linearBlue: 1, opacity: 1) + package static let clear: Color.Resolved = Color.Resolved(linearWhite: 0, opacity: 0) + package static let black: Color.Resolved = Color.Resolved(linearWhite: 0) + package static let gray_75: Color.Resolved = Color.Resolved(linearWhite: 0.522522, opacity: 1) + package static let gray_50: Color.Resolved = Color.Resolved(linearWhite: 0.214041, opacity: 1) + package static let gray_25: Color.Resolved = Color.Resolved(linearWhite: 0.0508761, opacity: 1) + package static let white: Color.Resolved = Color.Resolved(linearWhite: 1) package static let red: Color.Resolved = Color.Resolved(linearRed: 1, linearGreen: 0, linearBlue: 0, opacity: 1) package static let blue: Color.Resolved = Color.Resolved(linearRed: 0, linearGreen: 0, linearBlue: 1, opacity: 1) package static let green: Color.Resolved = Color.Resolved(linearRed: 0, linearGreen: 1, linearBlue: 0, opacity: 1) @@ -194,6 +247,30 @@ extension Color.Resolved { // MARK: - Color.Resolved + Display P3 +extension Color { + struct DisplayP3: ColorProvider { + #if canImport(Darwin) + private static let p3ColorSpace = CGColorSpace(name: CGColorSpace.displayP3)! + #endif + + let red: CGFloat + let green: CGFloat + let blue: CGFloat + let opacity: Float + + func resolve(in environment: EnvironmentValues) -> Color.Resolved { + Color.Resolved(displayP3Red: Float(red), green: Float(green), blue: Float(blue), opacity: opacity) + } + + #if canImport(Darwin) + var staticColor: CGColor? { + var components: [CGFloat] = [red, green, blue, CGFloat(opacity)] + return CGColor(colorSpace: Self.p3ColorSpace, components: &components) + } + #endif + } +} + extension Color.Resolved { // SwiftUI iOS 18: // lienarRed: 0.07322389539 @@ -236,6 +313,7 @@ extension Color.Resolved { } // MARK: - Color.Resolved + Codable + extension Color.Resolved: Codable { public func encode(to encoder: any Encoder) throws { var container = encoder.unkeyedContainer() @@ -285,6 +363,7 @@ extension Color.Resolved: ProtobufMessage { // MARK: - Util method +@inline(__always) func sRGBFromLinear(_ linear: Float) -> Float { let nonNegativeLinear = linear > 0 ? linear : -linear let result = if nonNegativeLinear <= 0.0031308 { @@ -297,6 +376,7 @@ func sRGBFromLinear(_ linear: Float) -> Float { return linear > 0 ? result : -result } +@inline(__always) func sRGBToLinear(_ sRGB: Float) -> Float { let nonNegativeSRGB = sRGB > 0 ? sRGB : -sRGB let result = if nonNegativeSRGB <= 0.04045 { diff --git a/Sources/OpenSwiftUICore/Graphic/Color/ColorView.swift b/Sources/OpenSwiftUICore/Graphic/Color/ColorView.swift new file mode 100644 index 000000000..d3679f960 --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Color/ColorView.swift @@ -0,0 +1,19 @@ +// +// ColorView.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Empty + +@MainActor +@preconcurrency +package struct ColorView: PrimitiveView { //FIXME + package var color: Color.Resolved + + package init(_ color: Color.Resolved) { + self.color = color + } + + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + package typealias Body = Swift.Never +} diff --git a/Sources/OpenSwiftUICore/Graphic/Color/CoreColor.swift b/Sources/OpenSwiftUICore/Graphic/Color/CoreColor.swift index 8241207ca..c262c117a 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/CoreColor.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/CoreColor.swift @@ -4,6 +4,7 @@ // // Audited for iOS 18.0 // Status: Complete +// ID: 4330A474F53D66045762501ED6F8A749 (SwiftUICore) #if canImport(Darwin) package import Foundation @@ -17,7 +18,7 @@ extension Color.Resolved { var green: CGFloat = 0 var blue: CGFloat = 0 var alpha: CGFloat = 0 - let result = CoreColorPlatformColorGetComponents(system: isAppKitBased() ? .appKit : .uiKit, color: platformColor, red: &red, green: &green, blue: &blue, alpha: &alpha) + let result = CoreColorPlatformColorGetComponents(system: .defaults, color: platformColor, red: &red, green: &green, blue: &blue, alpha: &alpha) if result { self.init(red: Float(red), green: Float(green), blue: Float(blue), opacity: Float(alpha)) } else { @@ -40,7 +41,7 @@ extension CoreColor { } package static func platformColor(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) -> NSObject? { - CorePlatformColorForRGBA(system: isAppKitBased() ? .appKit : .uiKit, red: red, green: green, blue: blue, alpha: alpha) + CorePlatformColorForRGBA(system: .defaults, red: red, green: green, blue: blue, alpha: alpha) } } diff --git a/Sources/OpenSwiftUICore/Graphic/Color/RGBColorSpace.swift b/Sources/OpenSwiftUICore/Graphic/Color/RGBColorSpace.swift new file mode 100644 index 000000000..68cfc854c --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Color/RGBColorSpace.swift @@ -0,0 +1,158 @@ +// +// RGBColorSpace.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +import Foundation +#if canImport(Darwin) +import CoreGraphics +#endif + +extension Color { + /// A profile that specifies how to interpret a color value for display. + public enum RGBColorSpace: Sendable { + /// The extended red, green, blue (sRGB) color space. + /// + /// For information about the sRGB colorimetry and nonlinear + /// transform function, see the IEC 61966-2-1 specification. + /// + /// Standard sRGB color spaces clamp the red, green, and blue + /// components of a color to a range of `0` to `1`, but OpenSwiftUI colors + /// use an extended sRGB color space, so you can use component values + /// outside that range. + case sRGB + + /// The extended sRGB color space with a linear transfer function. + /// + /// This color space has the same colorimetry as ``sRGB``, but uses + /// a linear transfer function. + /// + /// Standard sRGB color spaces clamp the red, green, and blue + /// components of a color to a range of `0` to `1`, but OpenSwiftUI colors + /// use an extended sRGB color space, so you can use component values + /// outside that range. + case sRGBLinear + + /// The Display P3 color space. + /// + /// This color space uses the Digital Cinema Initiatives - Protocol 3 + /// (DCI-P3) primary colors, a D65 white point, and the ``sRGB`` + /// transfer function. + case displayP3 + } + + /// Creates a constant color from red, green, and blue component values. + /// + /// This initializer creates a constant color that doesn't change based + /// on context. For example, it doesn't have distinct light and dark + /// appearances, unlike various system-defined colors, or a color that + /// you load from an Asset Catalog with ``init(_:bundle:)``. + /// + /// A standard sRGB color space clamps each color component — `red`, + /// `green`, and `blue` — to a range of `0` to `1`, but OpenSwiftUI colors + /// use an extended sRGB color space, so + /// you can use component values outside that range. This makes it + /// possible to create colors using the ``RGBColorSpace/sRGB`` or + /// ``RGBColorSpace/sRGBLinear`` color space that make full use of the wider + /// gamut of a diplay that supports ``RGBColorSpace/displayP3``. + /// + /// - Parameters: + /// - colorSpace: The profile that specifies how to interpret the color + /// for display. The default is ``RGBColorSpace/sRGB``. + /// - red: The amount of red in the color. + /// - green: The amount of green in the color. + /// - blue: The amount of blue in the color. + /// - opacity: An optional degree of opacity, given in the range `0` to + /// `1`. A value of `0` means 100% transparency, while a value of `1` + /// means 100% opacity. The default is `1`. + public init(_ colorSpace: Color.RGBColorSpace = .sRGB, red: Double, green: Double, blue: Double, opacity: Double = 1) { + switch colorSpace { + case .sRGB: + self.init(Color.Resolved(red: Float(red), green: Float(green), blue: Float(blue), opacity: Float(opacity))) + case .sRGBLinear: + self.init(Color.Resolved(linearRed: Float(red), linearGreen: Float(green), linearBlue: Float(blue), opacity: Float(opacity))) + case .displayP3: + self.init(provider: DisplayP3(red: red, green: green, blue: blue, opacity: Float(opacity))) + } + } + + /// Creates a constant grayscale color. + /// + /// This initializer creates a constant color that doesn't change based + /// on context. For example, it doesn't have distinct light and dark + /// appearances, unlike various system-defined colors, or a color that + /// you load from an Asset Catalog with ``init(_:bundle:)``. + /// + /// A standard sRGB color space clamps the `white` component + /// to a range of `0` to `1`, but OpenSwiftUI colors + /// use an extended sRGB color space, so + /// you can use component values outside that range. This makes it + /// possible to create colors using the ``RGBColorSpace/sRGB`` or + /// ``RGBColorSpace/sRGBLinear`` color space that make full use of the wider + /// gamut of a diplay that supports ``RGBColorSpace/displayP3``. + /// + /// - Parameters: + /// - colorSpace: The profile that specifies how to interpret the color + /// for display. The default is ``RGBColorSpace/sRGB``. + /// - white: A value that indicates how white + /// the color is, with higher values closer to 100% white, and lower + /// values closer to 100% black. + /// - opacity: An optional degree of opacity, given in the range `0` to + /// `1`. A value of `0` means 100% transparency, while a value of `1` + /// means 100% opacity. The default is `1`. + public init(_ colorSpace: RGBColorSpace = .sRGB, white: Double, opacity: Double = 1) { + self.init(colorSpace, red: white, green: white, blue: white, opacity: opacity) + } + + /// Creates a constant color from hue, saturation, and brightness values. + /// + /// This initializer creates a constant color that doesn't change based + /// on context. For example, it doesn't have distinct light and dark + /// appearances, unlike various system-defined colors, or a color that + /// you load from an Asset Catalog with ``init(_:bundle:)``. + /// + /// - Parameters: + /// - hue: A value in the range `0` to `1` that maps to an angle + /// from 0° to 360° to represent a shade on the color wheel. + /// - saturation: A value in the range `0` to `1` that indicates + /// how strongly the hue affects the color. A value of `0` removes the + /// effect of the hue, resulting in gray. As the value increases, + /// the hue becomes more prominent. + /// - brightness: A value in the range `0` to `1` that indicates + /// how bright a color is. A value of `0` results in black, regardless + /// of the other components. The color lightens as you increase this + /// component. + /// - opacity: An optional degree of opacity, given in the range `0` to + /// `1`. A value of `0` means 100% transparency, while a value of `1` + /// means 100% opacity. The default is `1`. + public init(hue: Double, saturation: Double, brightness: Double, opacity: Double = 1) { + let (red, green, blue) = HSBToRGB(hue: hue, saturation: saturation, brightness: brightness) + self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity) + } +} + +/// Converts HSB (Hue, Saturation, Brightness) values to RGB (Red, Green, Blue). +/// +/// - Parameters: +/// - hue: Hue value in the range [0.0, 1.0]. +/// - saturation: Saturation value in the range [0.0, 1.0]. +/// - brightness: Brightness value in the range [0.0, 1.0]. +/// - Returns: A tuple containing RGB values (red, green, blue) in the range [0.0, 1.0]. +package func HSBToRGB(hue: Double, saturation: Double, brightness: Double) -> (red: Double, green: Double, blue: Double) { + let h = hue == 1.0 ? 0 : hue * 6.0 + let x = Int(h) + let f = h - Double(x) + let p = brightness * (1 - saturation) + let q = brightness * (1 - f * saturation) + let t = brightness * (1 - (1 - f) * saturation) + switch x % 6 { + case 0: return (red: brightness, green: t, blue: p) + case 1: return (red: q, green: brightness, blue: p) + case 2: return (red: p, green: brightness, blue: t) + case 3: return (red: p, green: q, blue: brightness) + case 4: return (red: t, green: p, blue: brightness) + default: return (red: brightness, green: p, blue: q) + } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Color/SystemColor.swift b/Sources/OpenSwiftUICore/Graphic/Color/SystemColor.swift new file mode 100644 index 000000000..4e9270648 --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Color/SystemColor.swift @@ -0,0 +1,275 @@ +// +// SystemColor.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP +// ID: 9E3352CE4697DF56A738786E16992848 (SwiftUICore?) + +import OpenSwiftUI_SPI + +extension Color { + /// A context-dependent red color suitable for use in UI elements. + public static let red: Color = Color(provider: SystemColorType.red) + + /// A context-dependent orange color suitable for use in UI elements. + public static let orange: Color = Color(provider: SystemColorType.orange) + + /// A context-dependent yellow color suitable for use in UI elements. + public static let yellow: Color = Color(provider: SystemColorType.yellow) + + /// A context-dependent green color suitable for use in UI elements. + public static let green: Color = Color(provider: SystemColorType.green) + + /// A context-dependent mint color suitable for use in UI elements + public static let mint: Color = Color(provider: SystemColorType.mint) + + /// A context-dependent teal color suitable for use in UI elements. + public static let teal: Color = Color(provider: SystemColorType.teal) + + /// A context-dependent cyan color suitable for use in UI elements. + public static let cyan: Color = Color(provider: SystemColorType.cyan) + + /// A context-dependent blue color suitable for use in UI elements. + public static let blue: Color = Color(provider: SystemColorType.blue) + + /// A context-dependent indigo color suitable for use in UI elements. + public static let indigo: Color = Color(provider: SystemColorType.indigo) + + /// A context-dependent purple color suitable for use in UI elements. + public static let purple: Color = Color(provider: SystemColorType.purple) + + /// A context-dependent pink color suitable for use in UI elements. + public static let pink: Color = Color(provider: SystemColorType.pink) + + /// A context-dependent brown color suitable for use in UI elements. + public static let brown: Color = Color(provider: SystemColorType.brown) + + /// A white color suitable for use in UI elements. + public static let white: Color = Color(Color.Resolved(linearWhite: 1.0)) + + /// A context-dependent gray color suitable for use in + public static let gray: Color = Color(provider: SystemColorType.gray) + + /// A black color suitable for use in UI elements. + public static let black: Color = Color(Color.Resolved(linearWhite: 1.0)) + + /// A clear color suitable for use in UI elements. + public static let clear: Color = Color(Color.Resolved(linearWhite: 0.0, opacity: 0.0)) + + /// The color to use for primary content. + public static let primary: Color = Color(provider: SystemColorType.primary) + + /// The color to use for secondary content. + public static let secondary: Color = Color(provider: SystemColorType.secondary) +} + +extension ShapeStyle where Self == Color { + /// A context-dependent red color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var red: Color { .red } + + /// A context-dependent orange color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var orange: Color { .orange } + + /// A context-dependent yellow color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var yellow: Color { .yellow } + + /// A context-dependent green color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var green: Color { .green } + + /// A context-dependent mint color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var mint: Color { .mint } + + /// A context-dependent teal color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var teal: Color { .teal } + + /// A context-dependent cyan color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var cyan: Color { .cyan } + + /// A context-dependent blue color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var blue: Color { .blue } + + /// A context-dependent indigo color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var indigo: Color { .indigo } + + /// A context-dependent purple color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var purple: Color { .purple } + + /// A context-dependent pink color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var pink: Color { .pink } + + /// A context-dependent brown color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var brown: Color { .brown } + + /// A white color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var white: Color { .white } + + /// A context-dependent gray color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var gray: Color { .gray } + + /// A black color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var black: Color { .black } + + /// A clear color suitable for use in UI elements. + @_alwaysEmitIntoClient + public static var clear: Color { .clear } +} + +extension Color { + package static let primarySystemFill: Color = Color(provider: SystemColorType.primaryFill) + package static let secondarySystemFill: Color = Color(provider: SystemColorType.secondaryFill) + package static let tertiarySystemFill: Color = Color(provider: SystemColorType.tertiaryFill) + package static let quaternarySystemFill: Color = Color(provider: SystemColorType.quaternaryFill) +} + +extension Color { + package static let tertiary: Color = Color(provider: SystemColorType.tertiary) + package static let quaternary: Color = Color(provider: SystemColorType.quaternary) + package static let quinary: Color = Color(provider: SystemColorType.quinary) +} + +// MARK: - SystemColorType [WIP] + +package enum SystemColorType: ColorProvider { + case red + case orange + case yellow + case green + case teal + case mint + case cyan + case blue + case indigo + case purple + case pink + case brown + case gray + case primary + case secondary + case tertiary + case quaternary + case quinary + case primaryFill, secondaryFill, tertiaryFill, quaternaryFill + + package func resolve(in environment: EnvironmentValues) -> Color.Resolved { + environment.systemColorDefinition.base.value(for: self, environment: environment) + } + + package func apply(color: Color, to shape: inout _ShapeStyle_Shape) { + guard _SemanticFeature_v3.isEnabled else { + _apply(color: color, to: &shape) + return + } + switch self { + case .primary: + LegacyContentStyle(id: .primary, color: color)._apply(to: &shape) + case .secondary: + LegacyContentStyle(id: .secondary, color: color)._apply(to: &shape) + case .tertiary: + LegacyContentStyle(id: .tertiary, color: color)._apply(to: &shape) + case .quaternary: + LegacyContentStyle(id: .quaternary, color: color)._apply(to: &shape) + case .quinary: + LegacyContentStyle(id: .quinary, color: color)._apply(to: &shape) + default: + // TODO: BackgroundMaterialKey + Material + break + } + } + + package var kitColor: AnyObject? { + #if canImport(Darwin) + switch self { + case .red: CoreColor.systemRedColor(with: .defaults) + case .orange: CoreColor.systemOrangeColor(with: .defaults) + case .yellow: CoreColor.systemYellowColor(with: .defaults) + case .green: CoreColor.systemGreenColor(with: .defaults) + case .teal: CoreColor.systemTealColor(with: .defaults) + case .mint: CoreColor.systemMintColor(with: .defaults) + case .cyan: CoreColor.systemCyanColor(with: .defaults) + case .blue: CoreColor.systemBlueColor(with: .defaults) + case .indigo: CoreColor.systemIndigoColor(with: .defaults) + case .purple: CoreColor.systemPurpleColor(with: .defaults) + case .pink: CoreColor.systemPinkColor(with: .defaults) + case .brown: CoreColor.systemBrownColor(with: .defaults) + case .gray: CoreColor.systemGrayColor(with: .defaults) + default: nil + } + #else + nil + #endif + } +} + +// MARK: - SystemColorDefinition [WIP] + +package protocol SystemColorDefinition { + static func value(for type: SystemColorType, environment: EnvironmentValues) -> Color.Resolved + static func opacity(at level: Int, environment: EnvironmentValues) -> Float +} + +extension SystemColorDefinition { + package static func systemRGB(_ r: Float, _ g: Float, _ b: Float, _ a: Float = 100) -> Color.Resolved { + Color.Resolved(red: r / 255.0, green: g / 255.0, blue: b / 255.0, opacity: a * 0.01) + } + + package static func opacity(at level: Int, environment: EnvironmentValues) -> Float { + switch level { + case 0: return 1.0 + case 1: return 0.5 + case 2: return 0.25 + default: return 0.18 + } + } +} + +struct SystemColorDefinitionType { + var base: SystemColorDefinition.Type +} + +private struct SystemColorDefinitionKey: EnvironmentKey { + static var defaultValue: SystemColorDefinitionType { SystemColorDefinitionType(base: CoreUIDefaultSystemColorDefinition.self) } +} + +extension EnvironmentValues { + var systemColorDefinition: SystemColorDefinitionType { + self[SystemColorDefinitionKey.self] + } +} + +struct CoreUIDefaultSystemColorDefinition: SystemColorDefinition { + static func value(for type: SystemColorType, environment: EnvironmentValues) -> Color.Resolved { + // CUIDesignLibraryCacheKey + fatalError("TODO") + } +} + +struct TestingSystemColorDefinition: SystemColorDefinition { + static func value(for type: SystemColorType, environment: EnvironmentValues) -> Color.Resolved { + fatalError("TODO") + } +} + +// MARK: - SystemColorsStyle [TODO] + +//package struct SystemColorsStyle : ShapeStyle, PrimitiveShapeStyle { +// package init() +// package func _apply(to shape: inout _ShapeStyle_Shape) +// @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, *) +// package typealias Resolved = Never +//} diff --git a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift index 20e0f2ad8..e9fd07a01 100644 --- a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift +++ b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift @@ -479,4 +479,18 @@ public struct GraphicsContext { self.init(rawValue: CGBlendMode.plusLighter.rawValue) } } + + // FIXME + package enum ResolvedShading: Sendable { + case backdrop(Color.Resolved) + case color(Color.Resolved) + case style(_ShapeStyle_Pack.Style) + case levels([GraphicsContext.ResolvedShading]) + } +} + +extension GraphicsContext { + package func draw(_ path: Path, with shading: GraphicsContext.ResolvedShading, style: PathDrawingStyle) { + preconditionFailure("TODO") + } } diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStylePack.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStylePack.swift index 2d2249df9..f73f1fa43 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStylePack.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStylePack.swift @@ -21,7 +21,7 @@ package struct _ShapeStyle_Pack: Equatable { package init(_ fill: _ShapeStyle_Pack.Fill) { self.fill = fill self.opacity = 1.0 - self._blend = .normal + self._blend = nil self.effects = [] } @@ -36,13 +36,17 @@ package struct _ShapeStyle_Pack: Equatable { case paint(AnyResolvedPaint) case foregroundMaterial(Color.Resolved/*, ContentStyle.MaterialStyle*/) // case backgroundMaterial(Material.ResolvedMaterial) - // case vibrantColor(Color.ResolvedVibrant) - // case vibrantMatrix(_ColorMatrix) + case vibrantColor(Color.ResolvedVibrant) + case vibrantMatrix(_ColorMatrix) // case multicolor(ResolvedMulticolorStyle) package static func == (a: _ShapeStyle_Pack.Fill, b: _ShapeStyle_Pack.Fill) -> Bool { switch (a, b) { case (.color(let a), .color(let b)): return a == b case (.paint(let a), .paint(let b)): return a == b + // case (.foregroundMaterial(let a), .foregroundMaterial(let b)): return a == b + case (.vibrantColor(let a), .vibrantColor(let b)): return a == b + case (.vibrantMatrix(let a), .vibrantMatrix(let b)): return a == b + // case (.multicolor(let a), .multicolor(let b)): return a == b default: return false } } diff --git a/Sources/OpenSwiftUICore/Util/ThreadUtils.swift b/Sources/OpenSwiftUICore/Util/ThreadUtils.swift index 4470d0cfd..dd5e5506a 100644 --- a/Sources/OpenSwiftUICore/Util/ThreadUtils.swift +++ b/Sources/OpenSwiftUICore/Util/ThreadUtils.swift @@ -4,9 +4,12 @@ // // Audited for iOS 18.0 // Status: Complete +// ID: 82B2D47816BC992595021D60C278AFF0 (SwiftUICore) import Foundation +// MARK: - ThreadSpecific + final package class ThreadSpecific { var key: pthread_key_t let defaultValue: T @@ -51,6 +54,8 @@ final package class ThreadSpecific { } } +// MARK: - Thread + Global helper function + package func onMainThread(do body: @escaping () -> Void) { #if os(WASI) // See #76: Thread and RunLoopMode.common is not available on WASI currently @@ -72,3 +77,85 @@ package func mainThreadPrecondition() { precondition(Thread.isMainThread, "calling into OpenSwiftUI on a non-main thread is not supported") #endif } + +// MARK: - AtomicBuffer + +// FIXME: +// Replace with Swift's Mutex and Atomic to simplify cross-platform maintain cost +// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0433-mutex.md +// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0410-atomics.md +#if canImport(Darwin) +private final class AtomicBuffer: ManagedBuffer { + static func allocate(value: Value) -> AtomicBuffer { + let buffer = AtomicBuffer.create(minimumCapacity: 1) { buffer in + os_unfair_lock_s() + } + buffer.withUnsafeMutablePointerToElements { pointer in + pointer.initialize(to: value) + } + return unsafeDowncast(buffer, to: AtomicBuffer.self) + } +} +#else +private final class AtomicBuffer: ManagedBuffer { + static func allocate(value: Value) -> AtomicBuffer { + let buffer = AtomicBuffer.create(minimumCapacity: 1) { buffer in + NSLock() + } + buffer.withUnsafeMutablePointerToElements { pointer in + pointer.initialize(to: value) + } + return unsafeDowncast(buffer, to: AtomicBuffer.self) + } +} +#endif + +// MARK: - AtomicBox + +@propertyWrapper +package struct AtomicBox { + private let buffer: AtomicBuffer + + package init(wrappedValue: Value) { + buffer = AtomicBuffer.allocate(value: wrappedValue) + } + + @inline(__always) + package var wrappedValue: Value { + get { + #if canImport(Darwin) + os_unfair_lock_lock(&buffer.header) + defer { os_unfair_lock_unlock(&buffer.header) } + #else + buffer.header.lock() + defer { buffer.header.unlock() } + #endif + return buffer.withUnsafeMutablePointerToElements { $0.pointee } + } + nonmutating _modify { + #if canImport(Darwin) + os_unfair_lock_lock(&buffer.header) + defer { os_unfair_lock_unlock(&buffer.header) } + #else + buffer.header.lock() + defer { buffer.header.unlock() } + #endif + yield &buffer.withUnsafeMutablePointerToElements { $0 }.pointee + } + } + + @inline(__always) + package func access(_ body: (inout Value) throws -> T) rethrows -> T { + try body(&wrappedValue) + } + + package var projectedValue: AtomicBox { self } +} + +extension AtomicBox: @unchecked Sendable where Value: Sendable {} + +extension AtomicBox where Value: ExpressibleByNilLiteral { + package init() { + self.init(wrappedValue: nil) + } +} diff --git a/Sources/OpenSwiftUICore/Util/Utils.swift b/Sources/OpenSwiftUICore/Util/Utils.swift index 2ca5ca975..e14edcf75 100644 --- a/Sources/OpenSwiftUICore/Util/Utils.swift +++ b/Sources/OpenSwiftUICore/Util/Utils.swift @@ -46,4 +46,9 @@ package func isAppKitBased() -> Bool { #endif } +extension CoreSystem { + @inline(__always) + static var defaults: CoreSystem = isAppKitBased() ? .appKit : .uiKit +} + #endif diff --git a/Tests/OpenSwiftUICompatibilityTests/View/Graphic/Color/ColorResolvedTests.swift b/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/Graphics/Color/ColorResolvedTests.swift similarity index 100% rename from Tests/OpenSwiftUICompatibilityTests/View/Graphic/Color/ColorResolvedTests.swift rename to Tests/OpenSwiftUICompatibilityTests/SwiftUICore/Graphics/Color/ColorResolvedTests.swift diff --git a/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/Graphics/Color/RGBColorSpaceTests.swift b/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/Graphics/Color/RGBColorSpaceTests.swift new file mode 100644 index 000000000..0b3407fc6 --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/Graphics/Color/RGBColorSpaceTests.swift @@ -0,0 +1,41 @@ +// +// RGBColorSpaceTests.swift +// OpenSwiftUICompatibilityTests + +import Testing +import Numerics + +struct RGBColorSpaceTests { + struct IO { + var input: (hue: Double, saturation: Double, brightness: Double) + var output: (red: Double, green: Double, blue: Double) + } + + #if OPENSWIFTUI_COMPATIBILITY_TEST + @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) + #endif + @Test(arguments: + [ + IO(input: (0.0, 0.0, 0.0), output: (0.0, 0.0, 0.0)), // Black + IO(input: (0.0, 0.0, 1.0), output: (1.0, 1.0, 1.0)), // White + IO(input: (0.0, 1.0, 1.0), output: (1.0, 0.0, 0.0)), // Red + IO(input: (1.0/3.0, 1.0, 1.0), output: (0.0, 1.0, 0.0)), // Green + IO(input: (2.0/3.0, 1.0, 1.0), output: (0.0, 0.0, 1.0)), // Blue + IO(input: (1.0, 1.0, 1.0), output: (1.0, 0.0, 0.0)), // Red + IO(input: (1.0/6.0, 1.0, 1.0), output: (1.0, 1.0, 0.0)), // Yellow + IO(input: (0.5, 1.0, 1.0), output: (0.0, 1.0, 1.0)), // Cyan + IO(input: (5.0/6.0, 1.0, 1.0), output: (1.0, 0.0, 1.0)), // Magenta + IO(input: (0.0, 0.0, 0.5), output: (0.5, 0.5, 0.5)), // Gray + IO(input: (0.0, 1.0, 0.5), output: (0.5, 0.0, 0.0)), // Dark Red + IO(input: (1.0/3.0, 1.0, 0.5), output: (0.0, 0.5, 0.0)), // Dark Green + IO(input: (2.0/3.0, 1.0, 0.5), output: (0.0, 0.0, 0.5)), // Dark Blue + ] + ) + func testHSBToRGB(_ io: IO) { + let color = Color(hue: io.input.hue, saturation: io.input.saturation, brightness: io.input.brightness) + let resoved = color.resolve(in: .init()) + #expect(Double(resoved.red).isApproximatelyEqual(to: io.output.red)) + #expect(Double(resoved.green).isApproximatelyEqual(to: io.output.green)) + #expect(Double(resoved.blue).isApproximatelyEqual(to: io.output.blue)) + } +} diff --git a/Tests/OpenSwiftUICoreTests/Data/Other/AtomicBoxTests.swift b/Tests/OpenSwiftUICoreTests/Data/Other/AtomicBoxTests.swift deleted file mode 100644 index fee54b49b..000000000 --- a/Tests/OpenSwiftUICoreTests/Data/Other/AtomicBoxTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AtomicBoxTests.swift -// OpenSwiftUICoreTests - -import OpenSwiftUICore -import Testing - -struct AtomicBoxTests { - @Test - func expressibleByNilLiteral() { - let box: AtomicBox = AtomicBox() - #expect(box.wrappedValue == nil) - box.wrappedValue = 3 - #expect(box.wrappedValue == 3) - } - - @Test - func access() { - @AtomicBox var box: Int = 3 - #expect($box.access { $0.description } == "3") - } -} diff --git a/Tests/OpenSwiftUICoreTests/Graphics/Color/RGBColorSpaceTests.swift b/Tests/OpenSwiftUICoreTests/Graphics/Color/RGBColorSpaceTests.swift new file mode 100644 index 000000000..a0bafd9ed --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Graphics/Color/RGBColorSpaceTests.swift @@ -0,0 +1,38 @@ +// +// RGBColorSpaceTests.swift +// OpenSwiftUICoreTests + +import OpenSwiftUICore +import Testing +import Numerics + +struct RGBColorSpaceTests { + struct IO { + var input: (hue: Double, saturation: Double, brightness: Double) + var output: (red: Double, green: Double, blue: Double) + } + + @Test(arguments: + [ + IO(input: (0.0, 0.0, 0.0), output: (0.0, 0.0, 0.0)), // Black + IO(input: (0.0, 0.0, 1.0), output: (1.0, 1.0, 1.0)), // White + IO(input: (0.0, 1.0, 1.0), output: (1.0, 0.0, 0.0)), // Red + IO(input: (1.0/3.0, 1.0, 1.0), output: (0.0, 1.0, 0.0)), // Green + IO(input: (2.0/3.0, 1.0, 1.0), output: (0.0, 0.0, 1.0)), // Blue + IO(input: (1.0, 1.0, 1.0), output: (1.0, 0.0, 0.0)), // Red + IO(input: (1.0/6.0, 1.0, 1.0), output: (1.0, 1.0, 0.0)), // Yellow + IO(input: (0.5, 1.0, 1.0), output: (0.0, 1.0, 1.0)), // Cyan + IO(input: (5.0/6.0, 1.0, 1.0), output: (1.0, 0.0, 1.0)), // Magenta + IO(input: (0.0, 0.0, 0.5), output: (0.5, 0.5, 0.5)), // Gray + IO(input: (0.0, 1.0, 0.5), output: (0.5, 0.0, 0.0)), // Dark Red + IO(input: (1.0/3.0, 1.0, 0.5), output: (0.0, 0.5, 0.0)), // Dark Green + IO(input: (2.0/3.0, 1.0, 0.5), output: (0.0, 0.0, 0.5)), // Dark Blue + ] + ) + func testHSBToRGB(_ io: IO) { + let result = HSBToRGB(hue: io.input.hue, saturation: io.input.saturation, brightness: io.input.brightness) + #expect(result.red.isApproximatelyEqual(to: io.output.red)) + #expect(result.green.isApproximatelyEqual(to: io.output.green)) + #expect(result.blue.isApproximatelyEqual(to: io.output.blue)) + } +} diff --git a/Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift b/Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift index cb5788db0..eee933236 100644 --- a/Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift @@ -5,14 +5,16 @@ import OpenSwiftUICore import Testing -struct ThreadUtilsTests { +// MARK: - ThreadSpecificTests + +struct ThreadSpecificTests { static let defaultValue: Int = 1 static let box = ThreadSpecific(defaultValue) @Test func value() async throws { - let box = ThreadUtilsTests.box - #expect(box.value == ThreadUtilsTests.defaultValue) + let box = ThreadSpecificTests.box + #expect(box.value == ThreadSpecificTests.defaultValue) try await withThrowingTaskGroup(of: Int.self) { group in group.addTask { await Task.detached { @@ -31,8 +33,26 @@ struct ThreadUtilsTests { let result = try await group.reduce(0, +) #expect(result == 7) await MainActor.run { - #expect(box.value == ThreadUtilsTests.defaultValue) + #expect(box.value == ThreadSpecificTests.defaultValue) } } } } + +// MARK: - AtomicBoxTests + +struct AtomicBoxTests { + @Test + func expressibleByNilLiteral() { + let box: AtomicBox = AtomicBox() + #expect(box.wrappedValue == nil) + box.wrappedValue = 3 + #expect(box.wrappedValue == 3) + } + + @Test + func access() { + @AtomicBox var box: Int = 3 + #expect($box.access { $0.description } == "3") + } +} diff --git a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CoreColorTests.swift b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CoreColorTests.swift index 414ae4142..2dc672773 100644 --- a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CoreColorTests.swift +++ b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CoreColorTests.swift @@ -4,7 +4,7 @@ import Numerics import OpenSwiftUI_SPI -import OpenSwiftUICore +@testable import OpenSwiftUICore import Testing #if canImport(Darwin) @@ -33,17 +33,17 @@ struct CoreColorTests { var g: CGFloat = 0 var b: CGFloat = 0 var a: CGFloat = 0 - #expect(CoreColorPlatformColorGetComponents(system: isAppKitBased() ? .appKit : .uiKit, color: blackColor, red: &r, green: &g, blue: &b, alpha: &a) == true) + #expect(CoreColorPlatformColorGetComponents(system: .defaults, color: blackColor, red: &r, green: &g, blue: &b, alpha: &a) == true) #expect(r.isApproximatelyEqual(to: 0)) #expect(g.isApproximatelyEqual(to: 0)) #expect(b.isApproximatelyEqual(to: 0)) #expect(a.isApproximatelyEqual(to: 1)) - #expect(CoreColorPlatformColorGetComponents(system: isAppKitBased() ? .appKit : .uiKit, color: grayColor, red: &r, green: &g, blue: &b, alpha: &a) == true) + #expect(CoreColorPlatformColorGetComponents(system: .defaults, color: grayColor, red: &r, green: &g, blue: &b, alpha: &a) == true) #expect(r.isApproximatelyEqual(to: 0.5)) #expect(g.isApproximatelyEqual(to: 0.5)) #expect(b.isApproximatelyEqual(to: 0.5)) #expect(a.isApproximatelyEqual(to: 1)) - #expect(CoreColorPlatformColorGetComponents(system: isAppKitBased() ? .appKit : .uiKit, color: whiteColor, red: &r, green: &g, blue: &b, alpha: &a) == true) + #expect(CoreColorPlatformColorGetComponents(system: .defaults, color: whiteColor, red: &r, green: &g, blue: &b, alpha: &a) == true) #expect(r.isApproximatelyEqual(to: 1)) #expect(g.isApproximatelyEqual(to: 1)) #expect(b.isApproximatelyEqual(to: 1)) @@ -52,9 +52,9 @@ struct CoreColorTests { @Test func platformColorForRGBA() throws { - let blackColorObject = try #require(CorePlatformColorForRGBA(system: isAppKitBased() ? .appKit : .uiKit, red: 0, green: 0, blue: 0, alpha: 1)) - let greyColorObject = try #require(CorePlatformColorForRGBA(system: isAppKitBased() ? .appKit : .uiKit, red: 0.5, green: 0.5, blue: 0.5, alpha: 1)) - let whiteColorObject = try #require(CorePlatformColorForRGBA(system: isAppKitBased() ? .appKit : .uiKit, red: 1, green: 1, blue: 1, alpha: 1)) + let blackColorObject = try #require(CorePlatformColorForRGBA(system: .defaults, red: 0, green: 0, blue: 0, alpha: 1)) + let greyColorObject = try #require(CorePlatformColorForRGBA(system: .defaults, red: 0.5, green: 0.5, blue: 0.5, alpha: 1)) + let whiteColorObject = try #require(CorePlatformColorForRGBA(system: .defaults, red: 1, green: 1, blue: 1, alpha: 1)) #if os(macOS) let blackColor = try #require((blackColorObject as? NSColor)?.usingColorSpace(.deviceRGB)) let greyColor = try #require(greyColorObject as? NSColor) @@ -98,7 +98,7 @@ struct CoreColorTests { @Test func getKitColorClass() { - let colorClass: AnyClass? = CoreColorGetKitColorClass(system: isAppKitBased() ? .appKit : .uiKit) + let colorClass: AnyClass? = CoreColorGetKitColorClass(system: .defaults) #if os(macOS) #expect(colorClass == NSColor.self) #elseif os(iOS)