diff --git a/Sources/ThemePark/CodableTheme.swift b/Sources/ThemePark/CodableTheme.swift new file mode 100644 index 0000000..d1777d8 --- /dev/null +++ b/Sources/ThemePark/CodableTheme.swift @@ -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 + + 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.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) + } +} diff --git a/Sources/ThemePark/Query.swift b/Sources/ThemePark/Query.swift index 0ff33ec..5a02714 100644 --- a/Sources/ThemePark/Query.swift +++ b/Sources/ThemePark/Query.swift @@ -1,6 +1,6 @@ import SwiftUI -public enum ControlState: Hashable, Sendable { +public enum ControlState: Hashable, Sendable, Codable, CaseIterable { case active case inactive case hover @@ -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 } @@ -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 + } } @@ -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) } + } +} diff --git a/Sources/ThemePark/Style.swift b/Sources/ThemePark/Style.swift index 13cd166..0dd1129 100644 --- a/Sources/ThemePark/Style.swift +++ b/Sources/ThemePark/Style.swift @@ -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 @@ -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 { get } diff --git a/Sources/ThemePark/SyntaxSpecifier.swift b/Sources/ThemePark/SyntaxSpecifier.swift index 72125f1..17b0199 100644 --- a/Sources/ThemePark/SyntaxSpecifier.swift +++ b/Sources/ThemePark/SyntaxSpecifier.swift @@ -1,8 +1,8 @@ import Foundation -public enum SyntaxSpecifier: Hashable, Sendable { - public enum Operator: Hashable, Sendable { - public enum Call: Hashable, Sendable { +public enum SyntaxSpecifier: Hashable, Sendable, Codable { + public enum Operator: Hashable, Sendable, Codable { + public enum Call: Hashable, Sendable, Codable, CaseIterable { case function case method case macro @@ -11,7 +11,7 @@ public enum SyntaxSpecifier: Hashable, Sendable { case call(Call?) } - public enum Definition: Hashable, Sendable { + public enum Definition: Hashable, Sendable, Codable, CaseIterable { case function case method case macro @@ -19,7 +19,7 @@ public enum SyntaxSpecifier: Hashable, Sendable { case property } - public enum Keyword: Hashable, Sendable { + public enum Keyword: Hashable, Sendable, Codable { case definition(Definition?) case `import` case conditional @@ -28,15 +28,15 @@ public enum SyntaxSpecifier: Hashable, Sendable { case `operator`(Operator?) } - public enum Literal: Hashable, Sendable { - public enum Number: Hashable, Sendable { + public enum Literal: Hashable, Sendable, Codable { + public enum Number: Hashable, Sendable, Codable, CaseIterable { case float case integer case scientific case octal } - public enum String: Hashable, Sendable { + public enum String: Hashable, Sendable, Codable, CaseIterable { case uri case escape } @@ -47,13 +47,13 @@ public enum SyntaxSpecifier: Hashable, Sendable { case regularExpression } - public enum Comment: Hashable, Sendable { + public enum Comment: Hashable, Sendable, Codable, CaseIterable { case line case block case semanticallySignificant } - public enum Identifier: Hashable, Sendable { + public enum Identifier: Hashable, Sendable, Codable, CaseIterable { case variable case constant case function @@ -62,7 +62,7 @@ public enum SyntaxSpecifier: Hashable, Sendable { case type } - public enum Punctuation: Hashable, Sendable { + public enum Punctuation: Hashable, Sendable, Codable, CaseIterable { case delimiter } @@ -125,3 +125,48 @@ extension SyntaxSpecifier { "variable.builtin": .identifier(.variable), ] } + +extension SyntaxSpecifier: CaseIterable { + public static var allCases: [SyntaxSpecifier] { + let allKeywords = SyntaxSpecifier.Keyword.allCases.map { SyntaxSpecifier.keyword($0) } + [.keyword(nil)] + let allLiterals = SyntaxSpecifier.Literal.allCases.map { SyntaxSpecifier.literal($0) } + [.literal(nil)] + let allComments = SyntaxSpecifier.Comment.allCases.map { SyntaxSpecifier.comment($0) } + [.comment(nil)] + let allIdentifiers = SyntaxSpecifier.Identifier.allCases.map { SyntaxSpecifier.identifier($0) } + [.identifier(nil)] + let allOperators = SyntaxSpecifier.Operator.allCases.map { SyntaxSpecifier.operator($0) } + [.operator(nil)] + let allPunctuation = SyntaxSpecifier.Punctuation.allCases.map { SyntaxSpecifier.punctuation($0) } + [.punctuation(nil)] + let allDefinitions = SyntaxSpecifier.Definition.allCases.map { SyntaxSpecifier.definition($0) } + [.definition(nil)] + + let base: [SyntaxSpecifier] = [ + .text, + .invisible, + .context + ] + + return base + allKeywords + allLiterals + allComments + allIdentifiers + allOperators + allPunctuation + allDefinitions + } +} + +extension SyntaxSpecifier.Operator: CaseIterable { + public static var allCases: [SyntaxSpecifier.Operator] { + let allCalls = Self.Call.allCases.map { Self.call($0) } + [.call(nil)] + + return [Self.call(nil)] + allCalls + } +} + +extension SyntaxSpecifier.Keyword: CaseIterable { + public static var allCases: [SyntaxSpecifier.Keyword] { + let allOperators = SyntaxSpecifier.Operator.allCases.map { Self.operator($0) } + [.operator(nil)] + + return allOperators + } +} + +extension SyntaxSpecifier.Literal: CaseIterable { + public static var allCases: [SyntaxSpecifier.Literal] { + let allStrings = Self.String.allCases.map { Self.string($0) } + [Self.string(nil)] + let allNumbers = Self.Number.allCases.map { Self.number($0) } + [Self.number(nil)] + + return [.boolean, .regularExpression] + allNumbers + allStrings + } +}