Skip to content

Commit

Permalink
Add ResolvedPaint (#163)
Browse files Browse the repository at this point in the history
* Add ResolvedPaint implementation

* Add PaintTests
  • Loading branch information
Kyle-Ye authored Nov 24, 2024
1 parent c7af959 commit 4961349
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 29 deletions.
2 changes: 1 addition & 1 deletion Sources/OpenSwiftUICore/Graphic/Color/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
///
Expand Down
15 changes: 8 additions & 7 deletions Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Audited for iOS 18.0
// Status: WIP

import Foundation
package import Foundation
import OpenSwiftUI_SPI

// MARK: - Color.Resolved
Expand Down Expand Up @@ -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
Expand Down
176 changes: 176 additions & 0 deletions Sources/OpenSwiftUICore/Graphic/Color/Paint.swift
Original file line number Diff line number Diff line change
@@ -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<V>(_ 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<P>: 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<P> else {
return false
}
return paint == other.paint
}

override package func visit<V>(_ 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<P>(_ 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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//
// ResolvedGradient.swift
// OpenSwiftUICore
//
// Audited for iOS 18.0
// Status: Empty

package struct ResolvedGradient: Equatable {
}
15 changes: 0 additions & 15 deletions Sources/OpenSwiftUICore/Graphic/ResolvedPaint.swift

This file was deleted.

2 changes: 0 additions & 2 deletions Sources/OpenSwiftUICore/Shape/FillStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,3 @@ public struct FillStyle: Equatable {
self.isAntialiased = antialiased
}
}

extension FillStyle: Sendable {}
7 changes: 7 additions & 0 deletions Sources/OpenSwiftUICore/Shape/Path/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 0 additions & 2 deletions Sources/OpenSwiftUICore/Shape/StrokeStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,3 @@ extension StrokeStyle: Animatable {
}
}
}

extension StrokeStyle: Sendable {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// ColorMatrixTests.swift
// OpenSwiftUITests
// OpenSwiftUICoreTests

@testable import OpenSwiftUICore
import Testing
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// ColorResolvedTests.swift
// OpenSwiftUITests
// OpenSwiftUICoreTests

#if canImport(Darwin)

Expand Down
94 changes: 94 additions & 0 deletions Tests/OpenSwiftUICoreTests/Graphics/Color/PaintTests.swift
Original file line number Diff line number Diff line change
@@ -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<P>(_ 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)
}
}

0 comments on commit 4961349

Please sign in to comment.