From 496134962c0a6fe3e6559eed57f2c2b3cac1f6dc Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 24 Nov 2024 22:57:56 +0800 Subject: [PATCH] Add ResolvedPaint (#163) * Add ResolvedPaint implementation * Add PaintTests --- .../OpenSwiftUICore/Graphic/Color/Color.swift | 2 +- .../Graphic/Color/ColorResolved.swift | 15 +- .../OpenSwiftUICore/Graphic/Color/Paint.swift | 176 ++++++++++++++++++ .../Graphic/Gradient/ResolvedGradient.swift | 9 + .../Graphic/ResolvedPaint.swift | 15 -- Sources/OpenSwiftUICore/Shape/FillStyle.swift | 2 - Sources/OpenSwiftUICore/Shape/Path/Path.swift | 7 + .../OpenSwiftUICore/Shape/StrokeStyle.swift | 2 - .../Graphics/Color/ColorMatrixTests.swift | 2 +- .../Graphics/Color/ColorResolvedTests.swift | 2 +- .../Graphics/Color/PaintTests.swift | 94 ++++++++++ 11 files changed, 297 insertions(+), 29 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Graphic/Color/Paint.swift create mode 100644 Sources/OpenSwiftUICore/Graphic/Gradient/ResolvedGradient.swift delete mode 100644 Sources/OpenSwiftUICore/Graphic/ResolvedPaint.swift create mode 100644 Tests/OpenSwiftUICoreTests/Graphics/Color/PaintTests.swift diff --git a/Sources/OpenSwiftUICore/Graphic/Color/Color.swift b/Sources/OpenSwiftUICore/Graphic/Color/Color.swift index b2c00a9f..cba73679 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/Color.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/Color.swift @@ -49,7 +49,7 @@ public import CoreGraphics /// /// ![A screenshot of a green leaf.](Color-1) /// -/// Because SwiftUI treats colors as ``View`` instances, you can also +/// Because OpenSwiftUI treats colors as ``View`` instances, you can also /// directly add them to a view hierarchy. For example, you can layer /// a rectangle beneath a sun image using colors defined above: /// diff --git a/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift b/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift index 8347f66c..cf797b8e 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift @@ -5,7 +5,7 @@ // Audited for iOS 18.0 // Status: WIP -import Foundation +package import Foundation import OpenSwiftUI_SPI // MARK: - Color.Resolved @@ -51,13 +51,14 @@ extension Color { // MARK: - Color.Resolved + ResolvedPaint -extension Color.Resolved/*: ResolvedPaint*/ { - // func draw(path: Path, style: paathDrawingStyle, in context: GraphicsContext, bounds: CGRect?) - - var isClear: Bool { opacity == 0 } - var isOpaque: Bool { opacity == 1 } +extension Color.Resolved: ResolvedPaint { + package func draw(path: Path, style: PathDrawingStyle, in context: GraphicsContext, bounds: CGRect?) { + // TODO + } -// static leafProtobufTag: CodableResolvedPaint.Tag? + package var isClear: Bool { opacity == 0 } + package var isOpaque: Bool { opacity == 1 } + package static var leafProtobufTag: CodableResolvedPaint.Tag? { .color } } // MARK: - Color.Resolved + ShapeStyle diff --git a/Sources/OpenSwiftUICore/Graphic/Color/Paint.swift b/Sources/OpenSwiftUICore/Graphic/Color/Paint.swift new file mode 100644 index 00000000..102a1a79 --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Color/Paint.swift @@ -0,0 +1,176 @@ +// +// Paint.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Blocked by Gradient, Image and Shader + +package import Foundation + +// MARK: - ResolvedPaint + +package protocol ResolvedPaint: Equatable, Animatable, ProtobufEncodableMessage { + func draw(path: Path, style: PathDrawingStyle, in context: GraphicsContext, bounds: CGRect?) + var isClear: Bool { get } + var isOpaque: Bool { get } + var resolvedGradient: ResolvedGradient? { get } + var isCALayerCompatible: Bool { get } + static var leafProtobufTag: CodableResolvedPaint.Tag? { get } + func encodePaint(to encoder: inout ProtobufEncoder) throws +} + +// MARK: - ResolvedPaint + Default Implementations + +extension ResolvedPaint { + package var isClear: Bool { false } + package var isOpaque: Bool { false } + package var resolvedGradient: ResolvedGradient? { nil } + package var isCALayerCompatible: Bool { true } + package func encodePaint(to encoder: inout ProtobufEncoder) throws { + if let tag = Self.leafProtobufTag { + try encoder.messageField(tag.rawValue, self) + } else { + try encode(to: &encoder) + } + } +} + +// MARK: - AnyResolvedPaint + +package class AnyResolvedPaint: Equatable { + package func draw(path: Path, style: PathDrawingStyle, in ctx: GraphicsContext, bounds: CGRect?) {} + package var protobufPaint: Any? { nil } + package var isClear: Bool { false } + package var isOpaque: Bool { false } + package var resolvedGradient: ResolvedGradient? { nil } + package var isCALayerCompatible: Bool { false } + package func isEqual(to other: AnyResolvedPaint) -> Bool { false } + package func visit(_ visitor: inout V) where V : ResolvedPaintVisitor {} + package func encode(to encoder: any Encoder) throws { preconditionFailure("") } + package func encode(to encoder: inout ProtobufEncoder) throws { preconditionFailure("") } + package static func == (lhs: AnyResolvedPaint, rhs: AnyResolvedPaint) -> Bool { lhs.isEqual(to: rhs) } +} + +// MARK: - _AnyResolvedPaint + +final package class _AnyResolvedPaint

: AnyResolvedPaint where P: ResolvedPaint { + package let paint: P + package init(_ paint: P) { + self.paint = paint + } + + override package func draw(path: Path, style: PathDrawingStyle, in ctx: GraphicsContext, bounds: CGRect?) { + paint.draw(path: path, style: style, in: ctx, bounds: bounds) + } + + override package var protobufPaint: Any? { + paint + } + + override package var isClear: Bool { + paint.isClear + } + + override package var isOpaque: Bool { + paint.isOpaque + } + + override package var resolvedGradient: ResolvedGradient? { + paint.resolvedGradient + } + + override package var isCALayerCompatible: Bool { + paint.isCALayerCompatible + } + + override package func isEqual(to other: AnyResolvedPaint) -> Bool { + guard let other = other as? _AnyResolvedPaint

else { + return false + } + return paint == other.paint + } + + override package func visit(_ visitor: inout V) where V : ResolvedPaintVisitor { + visitor.visitPaint(paint) + } + + override package func encode(to encoder: inout ProtobufEncoder) throws { + try paint.encodePaint(to: &encoder) + } +} + +// FIXME +extension AnyResolvedPaint: @unchecked Sendable {} +extension _AnyResolvedPaint: @unchecked Sendable {} + +// MARK: - ResolvedPaintVisitor + +package protocol ResolvedPaintVisitor { + mutating func visitPaint

(_ paint: P) where P: ResolvedPaint +} + +// MARK: - CodableResolvedPaint [TODO] + +package struct CodableResolvedPaint: ProtobufMessage { + package struct Tag: Equatable, ProtobufTag { + package let rawValue: UInt + + package init(rawValue: UInt) { + self.rawValue = rawValue + } + + package static let color: CodableResolvedPaint.Tag = .init(rawValue: 1) + package static let linearGradient: CodableResolvedPaint.Tag = .init(rawValue: 2) + package static let radialGradient: CodableResolvedPaint.Tag = .init(rawValue: 3) + package static let angularGradient: CodableResolvedPaint.Tag = .init(rawValue: 4) + package static let ellipticalGradient: CodableResolvedPaint.Tag = .init(rawValue: 5) + package static let image: CodableResolvedPaint.Tag = .init(rawValue: 6) + package static let anchorRect: CodableResolvedPaint.Tag = .init(rawValue: 7) + package static let shader: CodableResolvedPaint.Tag = .init(rawValue: 8) + package static let meshGradient: CodableResolvedPaint.Tag = .init(rawValue: 9) + } + + package var base: AnyResolvedPaint + + package init(_ paint: AnyResolvedPaint) { + base = paint + } + + package func encode(to encoder: inout ProtobufEncoder) throws { + try base.encode(to: &encoder) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var base: AnyResolvedPaint? + while let field = try decoder.nextField() { + switch field.tag { + case Tag.color.rawValue: + let color: Color.Resolved = try decoder.messageField(field) + base = _AnyResolvedPaint(color) + case Tag.linearGradient.rawValue: + break // TODO + case Tag.radialGradient.rawValue: + break // TODO + case Tag.angularGradient.rawValue: + break // TODO + case Tag.ellipticalGradient.rawValue: + break // TODO + case Tag.image.rawValue: + break // TODO + case Tag.anchorRect.rawValue: + break // TODO + case Tag.shader.rawValue: + break // TODO + case Tag.meshGradient.rawValue: + break // TODO + default: + try decoder.skipField(field) + } + } + if let base { + self.init(base) + } else { + throw ProtobufDecoder.DecodingError.failed + } + } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Gradient/ResolvedGradient.swift b/Sources/OpenSwiftUICore/Graphic/Gradient/ResolvedGradient.swift new file mode 100644 index 00000000..c2c946ea --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Gradient/ResolvedGradient.swift @@ -0,0 +1,9 @@ +// +// ResolvedGradient.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Empty + +package struct ResolvedGradient: Equatable { +} diff --git a/Sources/OpenSwiftUICore/Graphic/ResolvedPaint.swift b/Sources/OpenSwiftUICore/Graphic/ResolvedPaint.swift deleted file mode 100644 index 0983a8d8..00000000 --- a/Sources/OpenSwiftUICore/Graphic/ResolvedPaint.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ResolvedPaint.swift -// OpenSwiftUICore -// -// Audited for iOS 18.0 -// Status: TODO - -package class AnyResolvedPaint: Equatable { - package static func == (lhs: AnyResolvedPaint, rhs: AnyResolvedPaint) -> Bool { - preconditionFailure("TODO") - } -} - -// FIXME -extension AnyResolvedPaint: @unchecked Sendable {} diff --git a/Sources/OpenSwiftUICore/Shape/FillStyle.swift b/Sources/OpenSwiftUICore/Shape/FillStyle.swift index eaa2c7fe..4da66cd6 100644 --- a/Sources/OpenSwiftUICore/Shape/FillStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/FillStyle.swift @@ -34,5 +34,3 @@ public struct FillStyle: Equatable { self.isAntialiased = antialiased } } - -extension FillStyle: Sendable {} diff --git a/Sources/OpenSwiftUICore/Shape/Path/Path.swift b/Sources/OpenSwiftUICore/Shape/Path/Path.swift index 44ae9511..8fc258fa 100644 --- a/Sources/OpenSwiftUICore/Shape/Path/Path.swift +++ b/Sources/OpenSwiftUICore/Shape/Path/Path.swift @@ -444,3 +444,10 @@ extension Path: CodableByProxy { static func unwrap(codingProxy: CodablePath) -> Path { codingProxy.base } } + +// MARK: - PathDrawingStyle + +package enum PathDrawingStyle { + case fill(FillStyle) + case stroke(StrokeStyle) +} diff --git a/Sources/OpenSwiftUICore/Shape/StrokeStyle.swift b/Sources/OpenSwiftUICore/Shape/StrokeStyle.swift index fffee96c..5082afe9 100644 --- a/Sources/OpenSwiftUICore/Shape/StrokeStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/StrokeStyle.swift @@ -79,5 +79,3 @@ extension StrokeStyle: Animatable { } } } - -extension StrokeStyle: Sendable {} diff --git a/Tests/OpenSwiftUICoreTests/Graphics/Color/ColorMatrixTests.swift b/Tests/OpenSwiftUICoreTests/Graphics/Color/ColorMatrixTests.swift index 3eeb992e..5fc7a317 100644 --- a/Tests/OpenSwiftUICoreTests/Graphics/Color/ColorMatrixTests.swift +++ b/Tests/OpenSwiftUICoreTests/Graphics/Color/ColorMatrixTests.swift @@ -1,6 +1,6 @@ // // ColorMatrixTests.swift -// OpenSwiftUITests +// OpenSwiftUICoreTests @testable import OpenSwiftUICore import Testing diff --git a/Tests/OpenSwiftUICoreTests/Graphics/Color/ColorResolvedTests.swift b/Tests/OpenSwiftUICoreTests/Graphics/Color/ColorResolvedTests.swift index 3fe910f8..a793e74e 100644 --- a/Tests/OpenSwiftUICoreTests/Graphics/Color/ColorResolvedTests.swift +++ b/Tests/OpenSwiftUICoreTests/Graphics/Color/ColorResolvedTests.swift @@ -1,6 +1,6 @@ // // ColorResolvedTests.swift -// OpenSwiftUITests +// OpenSwiftUICoreTests #if canImport(Darwin) diff --git a/Tests/OpenSwiftUICoreTests/Graphics/Color/PaintTests.swift b/Tests/OpenSwiftUICoreTests/Graphics/Color/PaintTests.swift new file mode 100644 index 00000000..4de32a7f --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Graphics/Color/PaintTests.swift @@ -0,0 +1,94 @@ +// +// PaintTests.swift +// OpenSwiftUICoreTests + +@testable import OpenSwiftUICore +import Testing +import Foundation + +struct PaintTests { + @Test + func anyResolvedPaintEquality() { + let color1 = Color.Resolved(red: 1, green: 0, blue: 0, opacity: 1) + let color2 = Color.Resolved(red: 1, green: 0, blue: 0, opacity: 1) + let color3 = Color.Resolved(red: 0, green: 1, blue: 0, opacity: 1) + + let paint1 = _AnyResolvedPaint(color1) + let paint2 = _AnyResolvedPaint(color2) + let paint3 = _AnyResolvedPaint(color3) + + #expect(paint1 == paint2) + #expect(paint1 != paint3) + } + + @Test + func resolvedPaintProperties() { + // Test with a clear color + let clearColor = Color.Resolved(red: 0, green: 0, blue: 0, opacity: 0) + let clearPaint = _AnyResolvedPaint(clearColor) + + #expect(clearPaint.isClear == true) + #expect(clearPaint.isOpaque == false) + #expect(clearPaint.resolvedGradient == nil) + #expect(clearPaint.isCALayerCompatible == true) + + // Test with an opaque color + let opaqueColor = Color.Resolved(red: 1, green: 1, blue: 1, opacity: 1) + let opaquePaint = _AnyResolvedPaint(opaqueColor) + + #expect(opaquePaint.isClear == false) + #expect(opaquePaint.isOpaque == true) + #expect(opaquePaint.resolvedGradient == nil) + #expect(opaquePaint.isCALayerCompatible == true) + } + + @Test + func codableResolvedPaintEncoding() throws { + let color = Color.Resolved(red: 1, green: 0, blue: 0, opacity: 1) + let paint = _AnyResolvedPaint(color) + let codablePaint = CodableResolvedPaint(paint) + + var encoder = ProtobufEncoder() + try codablePaint.encode(to: &encoder) + + let data = try ProtobufEncoder.encoding { encoder in + try codablePaint.encode(to: &encoder) + } + #expect(data.hexString == "0a0a0d0000803f250000803f") + } + + @Test + func codableResolvedPaintDecoding() throws { + // Create encoded data for a red color + let color = Color.Resolved(red: 1, green: 0, blue: 0, opacity: 1) + let paint = _AnyResolvedPaint(color) + let originalCodablePaint = CodableResolvedPaint(paint) + + let data = try #require(Data(hexString: "0a0a0d0000803f250000803f")) + var decoder = ProtobufDecoder(data) + let decodedPaint = try CodableResolvedPaint(from: &decoder) + + #expect(originalCodablePaint.base == decodedPaint.base) + } + + @Test + func resolvedPaintVisitor() { + struct TestVisitor: ResolvedPaintVisitor { + var visitedColor: Color.Resolved? + + mutating func visitPaint

(_ paint: P) where P: ResolvedPaint { + if let colorPaint = paint as? Color.Resolved { + visitedColor = colorPaint + } + } + } + + let color = Color.Resolved(red: 1, green: 0, blue: 0, opacity: 1) + let paint = _AnyResolvedPaint(color) + + var visitor = TestVisitor() + paint.visit(&visitor) + + #expect(visitor.visitedColor == color) + } +}