| | |
| | | import SwiftUI |
| | | import AppKit |
| | | |
| | | private struct AppGridThemeColor { |
| | | let red: Double |
| | | let green: Double |
| | | let blue: Double |
| | | |
| | | func color(opacity: Double = 1) -> Color { |
| | | Color(red: red, green: green, blue: blue).opacity(opacity) |
| | | } |
| | | |
| | | static func mix(_ lhs: AppGridThemeColor, _ rhs: AppGridThemeColor, amount: Double) -> AppGridThemeColor { |
| | | AppGridThemeColor( |
| | | red: lhs.red + (rhs.red - lhs.red) * amount, |
| | | green: lhs.green + (rhs.green - lhs.green) * amount, |
| | | blue: lhs.blue + (rhs.blue - lhs.blue) * amount |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private struct AppGridThemeStop { |
| | | let color: AppGridThemeColor |
| | | let opacity: Double |
| | | |
| | | func swiftUIColor() -> Color { |
| | | color.color(opacity: opacity) |
| | | } |
| | | |
| | | static func mix(_ lhs: AppGridThemeStop, _ rhs: AppGridThemeStop, amount: Double) -> AppGridThemeStop { |
| | | AppGridThemeStop( |
| | | color: .mix(lhs.color, rhs.color, amount: amount), |
| | | opacity: lhs.opacity + (rhs.opacity - lhs.opacity) * amount |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private struct AppGridThemePalette { |
| | | let base: [AppGridThemeStop] |
| | | let accent: [AppGridThemeStop] |
| | | let preview: [AppGridThemeStop] |
| | | } |
| | | |
| | | enum AppGridTheme: String, CaseIterable, Identifiable { |
| | | case defaultLight |
| | | case deepBlue |
| | |
| | | case colorful |
| | | |
| | | static let storageKey = "appGridThemeID" |
| | | static let deepBlueTuningStorageKey = "appGridThemeTuning.deepBlue" |
| | | static let colorfulTuningStorageKey = "appGridThemeTuning.colorful" |
| | | static let fallback = AppGridTheme.defaultLight |
| | | static let settingsDisplayOrder: [AppGridTheme] = [ |
| | | .defaultLight, |
| | | .black, |
| | | .pink, |
| | | .green, |
| | | .purple, |
| | | .blue, |
| | | .deepBlue, |
| | | .colorful |
| | | ] |
| | | static var freeThemes: [AppGridTheme] { |
| | | allCases.filter(\.isFreeTheme) |
| | | } |
| | |
| | | Color.white |
| | | } |
| | | |
| | | var supportsColorTuning: Bool { |
| | | self == .deepBlue || self == .colorful |
| | | } |
| | | |
| | | var defaultColorTuning: Double { |
| | | switch self { |
| | | case .deepBlue: |
| | | return 0 |
| | | case .colorful: |
| | | return 0 |
| | | case .defaultLight, .black, .pink, .purple, .green, .blue: |
| | | return 0.5 |
| | | } |
| | | } |
| | | |
| | | var tuningStorageKey: String? { |
| | | switch self { |
| | | case .deepBlue: |
| | | return Self.deepBlueTuningStorageKey |
| | | case .colorful: |
| | | return Self.colorfulTuningStorageKey |
| | | case .defaultLight, .black, .pink, .purple, .green, .blue: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | func normalizedTuning(_ value: Double?) -> Double { |
| | | min(1, max(0, value ?? defaultColorTuning)) |
| | | } |
| | | |
| | | var backgroundBaseColors: [Color] { |
| | | backgroundBaseColors(tuning: nil) |
| | | } |
| | | |
| | | func backgroundBaseColors(tuning: Double?) -> [Color] { |
| | | if supportsColorTuning { |
| | | return tunedPalette(tuning: tuning).base.map { $0.swiftUIColor() } |
| | | } |
| | | switch self { |
| | | case .defaultLight: |
| | | return [ |
| | |
| | | Color(red: 0.45, green: 0.96, blue: 1.00) |
| | | ] |
| | | case .colorful: |
| | | return [ |
| | | Color(red: 0.10, green: 0.78, blue: 1.00), |
| | | Color(red: 0.34, green: 0.96, blue: 0.94), |
| | | Color(red: 0.73, green: 0.78, blue: 1.00), |
| | | Color(red: 1.00, green: 0.58, blue: 0.94) |
| | | ] |
| | | return tunedPalette(tuning: tuning).base.map { $0.swiftUIColor() } |
| | | } |
| | | } |
| | | |
| | | var backgroundAccentColors: [Color] { |
| | | backgroundAccentColors(tuning: nil) |
| | | } |
| | | |
| | | func backgroundAccentColors(tuning: Double?) -> [Color] { |
| | | if supportsColorTuning { |
| | | return tunedPalette(tuning: tuning).accent.map { $0.swiftUIColor() } |
| | | } |
| | | switch self { |
| | | case .defaultLight: |
| | | return [Color.clear, Color.clear] |
| | |
| | | Color(red: 0.00, green: 0.90, blue: 1.00).opacity(0.18) |
| | | ] |
| | | case .colorful: |
| | | return [ |
| | | Color.white.opacity(0.16), |
| | | Color(red: 0.00, green: 0.78, blue: 1.00).opacity(0.22), |
| | | Color(red: 1.00, green: 0.45, blue: 0.95).opacity(0.22), |
| | | Color(red: 0.35, green: 1.00, blue: 0.92).opacity(0.18) |
| | | ] |
| | | return tunedPalette(tuning: tuning).accent.map { $0.swiftUIColor() } |
| | | } |
| | | } |
| | | |
| | | var previewColors: [Color] { |
| | | previewColors(tuning: nil) |
| | | } |
| | | |
| | | func previewColors(tuning: Double?) -> [Color] { |
| | | if supportsColorTuning { |
| | | return tunedPalette(tuning: tuning).preview.map { $0.swiftUIColor() } |
| | | } |
| | | switch self { |
| | | case .defaultLight: |
| | | return [Color.white, Color(red: 0.88, green: 0.90, blue: 0.92)] |
| | |
| | | case .blue: |
| | | return [Color(red: 0.70, green: 0.91, blue: 1.00), Color(red: 0.22, green: 0.58, blue: 1.00)] |
| | | case .colorful: |
| | | return [ |
| | | Color(red: 0.10, green: 0.78, blue: 1.00), |
| | | Color(red: 0.34, green: 0.96, blue: 0.94), |
| | | Color(red: 0.75, green: 0.82, blue: 1.00), |
| | | Color(red: 1.00, green: 0.56, blue: 0.94) |
| | | ] |
| | | return tunedPalette(tuning: tuning).preview.map { $0.swiftUIColor() } |
| | | } |
| | | } |
| | | |
| | | private func tunedPalette(tuning: Double?) -> AppGridThemePalette { |
| | | let palettes = self == .deepBlue ? Self.darkColorfulPalettes : Self.lightColorfulPalettes |
| | | let normalized = normalizedTuning(tuning) |
| | | guard palettes.count > 1 else { |
| | | return palettes[0] |
| | | } |
| | | let scaled = normalized * Double(palettes.count - 1) |
| | | let lowerIndex = min(palettes.count - 1, max(0, Int(floor(scaled)))) |
| | | let upperIndex = min(palettes.count - 1, lowerIndex + 1) |
| | | let amount = scaled - Double(lowerIndex) |
| | | guard lowerIndex != upperIndex else { |
| | | return palettes[lowerIndex] |
| | | } |
| | | return Self.mixPalettes(palettes[lowerIndex], palettes[upperIndex], amount: amount) |
| | | } |
| | | |
| | | private static func mixPalettes(_ lhs: AppGridThemePalette, _ rhs: AppGridThemePalette, amount: Double) -> AppGridThemePalette { |
| | | AppGridThemePalette( |
| | | base: zip(lhs.base, rhs.base).map { .mix($0.0, $0.1, amount: amount) }, |
| | | accent: zip(lhs.accent, rhs.accent).map { .mix($0.0, $0.1, amount: amount) }, |
| | | preview: zip(lhs.preview, rhs.preview).map { .mix($0.0, $0.1, amount: amount) } |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private extension AppGridTheme { |
| | | static let darkColorfulPalettes: [AppGridThemePalette] = [ |
| | | AppGridThemePalette( |
| | | base: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.01, green: 0.03, blue: 0.08), opacity: 0.98), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.02, green: 0.13, blue: 0.20), opacity: 0.96), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.02, green: 0.28, blue: 0.28), opacity: 0.88), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.06, green: 0.08, blue: 0.18), opacity: 0.94) |
| | | ], |
| | | accent: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.88, blue: 0.78), opacity: 0.22), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.10, green: 0.54, blue: 1.00), opacity: 0.18), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.08, green: 0.78, blue: 0.44), opacity: 0.16), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.45, green: 0.22, blue: 0.92), opacity: 0.12) |
| | | ], |
| | | preview: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.01, green: 0.06, blue: 0.12), opacity: 1), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.02, green: 0.42, blue: 0.38), opacity: 1) |
| | | ] |
| | | ), |
| | | AppGridThemePalette( |
| | | base: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.03, green: 0.02, blue: 0.08), opacity: 0.98), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.17, green: 0.04, blue: 0.19), opacity: 0.95), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.28, green: 0.04, blue: 0.20), opacity: 0.88), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.05, green: 0.06, blue: 0.16), opacity: 0.96) |
| | | ], |
| | | accent: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.22, blue: 0.62), opacity: 0.18), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.70, green: 0.20, blue: 1.00), opacity: 0.17), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.10, green: 0.72, blue: 1.00), opacity: 0.13), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.58, blue: 0.82), opacity: 0.12) |
| | | ], |
| | | preview: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.05, green: 0.03, blue: 0.10), opacity: 1), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.44, green: 0.06, blue: 0.32), opacity: 1) |
| | | ] |
| | | ), |
| | | AppGridThemePalette( |
| | | base: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.02, blue: 0.08), opacity: 0.99), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.02, green: 0.08, blue: 0.22), opacity: 0.97), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.22, blue: 0.36), opacity: 0.88), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.03, green: 0.04, blue: 0.13), opacity: 0.96) |
| | | ], |
| | | accent: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.62, blue: 1.00), opacity: 0.20), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.24, green: 0.34, blue: 1.00), opacity: 0.18), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 1.00, blue: 0.92), opacity: 0.12), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.52, green: 0.20, blue: 1.00), opacity: 0.12) |
| | | ], |
| | | preview: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.03, blue: 0.10), opacity: 1), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.30, blue: 0.52), opacity: 1) |
| | | ] |
| | | ), |
| | | AppGridThemePalette( |
| | | base: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.01, green: 0.04, blue: 0.04), opacity: 0.99), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.02, green: 0.14, blue: 0.10), opacity: 0.95), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.02, green: 0.30, blue: 0.17), opacity: 0.86), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.04, green: 0.07, blue: 0.14), opacity: 0.96) |
| | | ], |
| | | accent: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.42, green: 1.00, blue: 0.52), opacity: 0.16), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.88, blue: 0.78), opacity: 0.15), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.24, green: 0.44, blue: 1.00), opacity: 0.13), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.80, green: 0.80, blue: 0.28), opacity: 0.10) |
| | | ], |
| | | preview: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.01, green: 0.06, blue: 0.05), opacity: 1), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.03, green: 0.34, blue: 0.18), opacity: 1) |
| | | ] |
| | | ), |
| | | AppGridThemePalette( |
| | | base: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.02, green: 0.02, blue: 0.07), opacity: 0.98), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.10, green: 0.05, blue: 0.22), opacity: 0.95), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.18, green: 0.08, blue: 0.35), opacity: 0.86), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.02, green: 0.08, blue: 0.16), opacity: 0.96) |
| | | ], |
| | | accent: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.72, green: 0.48, blue: 1.00), opacity: 0.17), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.26, green: 0.86, blue: 1.00), opacity: 0.15), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.52, blue: 0.86), opacity: 0.13), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.18, green: 0.92, blue: 0.76), opacity: 0.10) |
| | | ], |
| | | preview: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.03, green: 0.02, blue: 0.08), opacity: 1), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.24, green: 0.10, blue: 0.48), opacity: 1) |
| | | ] |
| | | ) |
| | | ] |
| | | |
| | | static let lightColorfulPalettes: [AppGridThemePalette] = [ |
| | | AppGridThemePalette( |
| | | base: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.10, green: 0.78, blue: 1.00), opacity: 0.95), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.34, green: 0.96, blue: 0.94), opacity: 0.92), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.73, green: 0.78, blue: 1.00), opacity: 0.90), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.58, blue: 0.94), opacity: 0.86) |
| | | ], |
| | | accent: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 1.00, blue: 1.00), opacity: 0.16), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.78, blue: 1.00), opacity: 0.22), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.45, blue: 0.95), opacity: 0.22), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.35, green: 1.00, blue: 0.92), opacity: 0.18) |
| | | ], |
| | | preview: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.10, green: 0.78, blue: 1.00), opacity: 1), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.56, blue: 0.94), opacity: 1) |
| | | ] |
| | | ), |
| | | AppGridThemePalette( |
| | | base: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.72, green: 0.98, blue: 0.72), opacity: 0.94), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.48, green: 0.88, blue: 1.00), opacity: 0.92), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.88, blue: 0.42), opacity: 0.84), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.62, blue: 0.82), opacity: 0.84) |
| | | ], |
| | | accent: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 1.00, blue: 1.00), opacity: 0.20), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.20, green: 0.92, blue: 0.64), opacity: 0.20), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.66, blue: 0.20), opacity: 0.16), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.50, green: 0.52, blue: 1.00), opacity: 0.16) |
| | | ], |
| | | preview: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.50, green: 0.92, blue: 1.00), opacity: 1), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.78, blue: 0.38), opacity: 1) |
| | | ] |
| | | ), |
| | | AppGridThemePalette( |
| | | base: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.78, green: 0.90, blue: 1.00), opacity: 0.95), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.56, green: 0.72, blue: 1.00), opacity: 0.90), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.78, green: 0.65, blue: 1.00), opacity: 0.86), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.72, blue: 0.96), opacity: 0.84) |
| | | ], |
| | | accent: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 1.00, blue: 1.00), opacity: 0.18), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.00, green: 0.70, blue: 1.00), opacity: 0.18), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.82, green: 0.36, blue: 1.00), opacity: 0.18), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.48, blue: 0.74), opacity: 0.16) |
| | | ], |
| | | preview: [ |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 0.62, green: 0.80, blue: 1.00), opacity: 1), |
| | | AppGridThemeStop(color: AppGridThemeColor(red: 1.00, green: 0.70, blue: 0.96), opacity: 1) |
| | | ] |
| | | ) |
| | | ] |
| | | } |