Skip to content

Commit

Permalink
CaseIterable and Codable support
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed May 12, 2024
1 parent 11b195d commit 2c9c556
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 18 deletions.
127 changes: 127 additions & 0 deletions Sources/ThemePark/CodableTheme.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Foundation
import CoreGraphics

enum CodableColor: Codable {
case components(String, [CGFloat])
case catalog(String)

init(_ color: PlatformColor) {
switch color.type {
case .componentBased:
let cgColor = color.cgColor
let colorSpaceName = cgColor.colorSpace?.name as? String ?? ""
let components = cgColor.components ?? []

self = .components(colorSpaceName, components)
case .catalog:
self = .catalog(color.colorNameComponent)
case .pattern:
preconditionFailure()
@unknown default:
preconditionFailure()
}
}

var color: PlatformColor? {
switch self {
case let .components(spaceName, components):
guard
let cgColorSpace = CGColorSpace(name: spaceName as CFString),
let cgColor = CGColor(colorSpace: cgColorSpace, components: components)
else {
return nil
}

return PlatformColor(cgColor: cgColor)
case let .catalog(name):
return PlatformColor(named: name)
}
}
}

struct CodableFont: Codable {
let name: String
let size: CGFloat

init(_ font: PlatformFont) {
self.name = font.fontName
self.size = font.pointSize
}

var font: PlatformFont? {
PlatformFont(name: name, size: size)
}
}

struct CodableStyle: Codable {
let codableColor: CodableColor
let codableFont: CodableFont?

init(style: Style) {
self.codableColor = CodableColor(style.color)
self.codableFont = style.font.map { CodableFont($0) }
}

var style: Style? {
guard let color = codableColor.color else {
return nil
}

return Style(color: color, font: codableFont?.font)
}
}

/// Capable of encoding and decoding all possible queries within a Styler.
///
/// > Warning: Pretty much everything about this process is inefficient. It's here for convenience only.
public struct CodableStyler {
private let styles: [Query: CodableStyle]
public let supportedVariants: Set<Variant>

public init(_ styler: any Styling) {
self.supportedVariants = styler.supportedVariants

let allContexts = Variant.allCases.flatMap { variant in
ControlState.allCases.map { Query.Context(controlState: $0, variant: variant) }
}

var styles = [Query: CodableStyle]()
for key in Query.Key.allCases {
for context in allContexts {
let query = Query(key: key, context: context)
let style = styler.style(for: query)

styles[query] = CodableStyle(style: style)
}
}

self.styles = styles
}
}

extension CodableStyler: Styling {
public func style(for query: Query) -> Style {
styles[query]?.style ?? Style.fallback(for: query)
}
}

extension CodableStyler: Codable {
enum CodingKeys: String, CodingKey {
case styles
case supportedVariants
}

public init(from decoder: any Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)

self.supportedVariants = try values.decode(Set<Variant>.self, forKey: .supportedVariants)
self.styles = try values.decode([Query: CodableStyle].self, forKey: .styles)
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(supportedVariants, forKey: .supportedVariants)
try container.encode(styles, forKey: .styles)
}
}
30 changes: 23 additions & 7 deletions Sources/ThemePark/Query.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI

public enum ControlState: Hashable, Sendable {
public enum ControlState: Hashable, Sendable, Codable, CaseIterable {
case active
case inactive
case hover
Expand All @@ -19,16 +19,16 @@ public enum ControlState: Hashable, Sendable {
#endif
}

public struct Query: Hashable, Sendable {
public enum Key: Hashable, Sendable {
public enum Editor: Hashable, Sendable {
public struct Query: Hashable, Sendable, Codable {
public enum Key: Hashable, Sendable, Codable {
public enum Editor: Hashable, Sendable, Codable, CaseIterable {
case background
case accessoryForeground
case accessoryBackground
case cursor
}

public enum Gutter: Hashable, Sendable {
public enum Gutter: Hashable, Sendable, Codable, CaseIterable {
case background
case label
}
Expand All @@ -38,13 +38,21 @@ public struct Query: Hashable, Sendable {
case syntax(SyntaxSpecifier)
}

public struct Context: Hashable, Sendable {
public struct Context: Hashable, Sendable, Codable {
public var controlState: ControlState
public var variant: Variant

public init(controlState: ControlState = .active, colorScheme: ColorScheme, colorSchemeContrast: ColorSchemeContrast = .standard) {
self.init(
controlState: controlState,
variant: Variant(colorScheme: colorScheme, colorSchemeContrast: colorSchemeContrast)
)
}

public init(controlState: ControlState = .active, variant: Variant) {
self.controlState = controlState
self.variant = Variant(colorScheme: colorScheme, colorSchemeContrast: colorSchemeContrast)
self.variant = variant

}
}

Expand All @@ -56,3 +64,11 @@ public struct Query: Hashable, Sendable {
self.context = context
}
}

extension Query.Key: CaseIterable {
public static var allCases: [Query.Key] {
Editor.allCases.map { .editor($0) } +
Gutter.allCases.map { .gutter($0) } +
SyntaxSpecifier.allCases.map { .syntax($0) }
}
}
79 changes: 79 additions & 0 deletions Sources/ThemePark/Style.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ public struct Style: Hashable {
}
}

extension Style {
static func fallback(for query: Query) -> Style {
let lightScheme = query.context.variant.colorScheme == .light

switch query.key {
case .editor(.background), .gutter(.background), .editor(.accessoryBackground):
#if os(macOS)
return Style(color: .windowBackgroundColor)
#else
return Style(color: lightScheme ? .white : .black)
#endif
default:
#if os(macOS)
return Style(color: .labelColor)
#else
return Style(color: .label)
#endif
}
}
}

public struct Variant: Hashable, Sendable {
public var colorScheme: ColorScheme
public var colorSchemeContrast: ColorSchemeContrast
Expand Down Expand Up @@ -96,6 +117,64 @@ public struct Variant: Hashable, Sendable {
#endif
}

extension Variant: CaseIterable {
public static var allCases: [Variant] {
zip(ColorScheme.allCases, ColorSchemeContrast.allCases)
.map { Variant(colorScheme: $0, colorSchemeContrast: $1) }
}
}

extension Variant: Codable {
enum CodingKeys: String, CodingKey {
case colorScheme
case colorSchemeContrast
}

public init(from decoder: any Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)

switch try values.decode(String.self, forKey: .colorScheme) {
case "dark":
self.colorScheme = .dark
case "light":
self.colorScheme = .light
default:
throw DecodingError.dataCorrupted(.init(codingPath: values.codingPath, debugDescription: "unrecogized value for colorScheme"))
}

switch try values.decode(String.self, forKey: .colorSchemeContrast) {
case "increased":
self.colorSchemeContrast = .increased
case "standard":
self.colorSchemeContrast = .standard
default:
throw DecodingError.dataCorrupted(.init(codingPath: values.codingPath, debugDescription: "unrecogized value for colorSchemeContrast"))
}
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

switch colorScheme {
case .dark:
try container.encode("dark", forKey: .colorScheme)
case .light:
try container.encode("light", forKey: .colorScheme)
@unknown default:
try container.encode("light", forKey: .colorScheme)
}

switch colorSchemeContrast {
case .increased:
try container.encode("increased", forKey: .colorSchemeContrast)
case .standard:
try container.encode("standard", forKey: .colorSchemeContrast)
@unknown default:
try container.encode("standard", forKey: .colorSchemeContrast)
}
}
}

public protocol Styling {
func style(for query: Query) -> Style
var supportedVariants: Set<Variant> { get }
Expand Down
Loading

0 comments on commit 2c9c556

Please sign in to comment.