Neon-Vision-Editor/Neon Vision Editor/UI/ThemeSettings.swift

835 lines
36 KiB
Swift

import SwiftUI
#if os(macOS)
import AppKit
private typealias PlatformColor = NSColor
#else
import UIKit
private typealias PlatformColor = UIColor
#endif
struct EditorTheme {
let text: Color
let background: Color
let cursor: Color
let selection: Color
let syntax: SyntaxColors
let boldKeywords: Bool
let italicComments: Bool
let underlineLinks: Bool
let boldMarkdownHeadings: Bool
}
private struct ThemePalette {
let text: Color
let background: Color
let cursor: Color
let selection: Color
let keyword: Color
let string: Color
let number: Color
let comment: Color
let type: Color
let property: Color
let builtin: Color
}
struct ThemePaletteColors {
let text: Color
let background: Color
let cursor: Color
let selection: Color
let keyword: Color
let string: Color
let number: Color
let comment: Color
let type: Color
let property: Color
let builtin: Color
}
private struct RGBColorComponents {
let red: Double
let green: Double
let blue: Double
let alpha: Double
}
private func colorComponents(_ color: Color) -> RGBColorComponents? {
#if os(macOS)
let platform = PlatformColor(color)
guard let srgb = platform.usingColorSpace(.sRGB) else { return nil }
return RGBColorComponents(
red: Double(srgb.redComponent),
green: Double(srgb.greenComponent),
blue: Double(srgb.blueComponent),
alpha: Double(srgb.alphaComponent)
)
#else
let platform = PlatformColor(color)
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
guard platform.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { return nil }
return RGBColorComponents(red: Double(red), green: Double(green), blue: Double(blue), alpha: Double(alpha))
#endif
}
private func relativeLuminance(_ components: RGBColorComponents) -> Double {
(0.2126 * components.red) + (0.7152 * components.green) + (0.0722 * components.blue)
}
private func blend(_ source: Color, with target: Color, amount: Double) -> Color {
guard let sourceComponents = colorComponents(source), let targetComponents = colorComponents(target) else {
return source
}
let clamped = min(1.0, max(0.0, amount))
return Color(
red: sourceComponents.red + ((targetComponents.red - sourceComponents.red) * clamped),
green: sourceComponents.green + ((targetComponents.green - sourceComponents.green) * clamped),
blue: sourceComponents.blue + ((targetComponents.blue - sourceComponents.blue) * clamped),
opacity: sourceComponents.alpha + ((targetComponents.alpha - sourceComponents.alpha) * clamped)
)
}
private func modeAdjustedEditorBackground(_ background: Color, colorScheme: ColorScheme) -> Color {
guard let components = colorComponents(background) else { return background }
let luminance = relativeLuminance(components)
if colorScheme == .light {
// Keep all themes readable in light/system-light by lifting dark palettes.
if luminance >= 0.78 { return background }
let targetLuminance = 0.96
let normalized = (targetLuminance - luminance) / max(0.0001, 1.0 - luminance)
let mixAmount = min(0.95, max(0.70, normalized))
return blend(background, with: .white, amount: mixAmount)
}
// Keep all themes readable in dark/system-dark by lowering bright palettes.
if luminance <= 0.28 { return background }
let targetLuminance = 0.14
let normalized = (luminance - targetLuminance) / max(0.0001, luminance)
let mixAmount = min(0.90, max(0.55, normalized))
return blend(background, with: .black, amount: mixAmount)
}
private func modeAdjustedSyntaxColor(
_ color: Color,
colorScheme: ColorScheme,
darkenInLight amountLight: Double,
brightenInDark amountDark: Double
) -> Color {
if colorScheme == .light {
return blend(color, with: .black, amount: amountLight)
}
return blend(color, with: .white, amount: amountDark)
}
private func emphasizedSelectionColor(for canonicalName: String, fallback: Color) -> Color {
switch canonicalName {
case "Neon Glow":
return Color(red: 0.95, green: 0.17, blue: 0.56)
case "Neon Flow":
return Color(red: 0.95, green: 0.17, blue: 0.56)
case "Neon Voltage":
return Color(red: 0.00, green: 0.72, blue: 1.00)
case "Laserwave":
return Color(red: 0.98, green: 0.24, blue: 0.88)
case "Cyber Lime":
return Color(red: 0.45, green: 0.78, blue: 0.16)
case "Plasma Storm":
return Color(red: 0.58, green: 0.46, blue: 1.00)
case "Inferno Neon":
return Color(red: 1.00, green: 0.38, blue: 0.16)
case "Ultraviolet Flux":
return Color(red: 0.84, green: 0.36, blue: 1.00)
case "Prism Daylight":
return Color(red: 0.24, green: 0.50, blue: 0.96)
case "Dracula":
return Color(red: 1.00, green: 0.48, blue: 0.78)
case "One Dark Pro":
return Color(red: 0.35, green: 0.67, blue: 0.98)
case "Nord":
return Color(red: 0.56, green: 0.74, blue: 0.73)
case "Tokyo Night":
return Color(red: 0.48, green: 0.64, blue: 0.97)
case "Gruvbox":
return Color(red: 1.00, green: 0.50, blue: 0.10)
case "Arc":
return Color(red: 0.35, green: 0.67, blue: 0.98)
case "Dusk":
return Color(red: 0.93, green: 0.54, blue: 0.94)
case "Aurora":
return Color(red: 0.35, green: 0.96, blue: 0.76)
case "Horizon":
return Color(red: 0.99, green: 0.46, blue: 0.36)
case "Midnight":
return Color(red: 0.25, green: 0.78, blue: 0.98)
case "Mono":
return Color(red: 0.82, green: 0.82, blue: 0.82)
case "Paper":
return Color(red: 0.24, green: 0.50, blue: 0.96)
case "Solar":
return Color(red: 0.99, green: 0.64, blue: 0.24)
case "Pulse":
return Color(red: 0.98, green: 0.54, blue: 0.62)
case "Mocha":
return Color(red: 0.82, green: 0.60, blue: 0.98)
case "Custom":
return fallback
default:
return fallback
}
}
///MARK: Syntax Adjustment Profiles
// Internal strategy for token color adjustment per theme family.
private enum SyntaxAdjustmentProfile {
case standard
case neonRaw
}
private func adjustedSyntaxColor(
_ color: Color,
colorScheme: ColorScheme,
profile: SyntaxAdjustmentProfile,
darkenInLight amountLight: Double,
brightenInDark amountDark: Double
) -> Color {
switch profile {
case .standard:
return modeAdjustedSyntaxColor(
color,
colorScheme: colorScheme,
darkenInLight: amountLight,
brightenInDark: amountDark
)
case .neonRaw:
// Keep Neon Glow vivid in light mode and only apply a tiny dark-mode lift.
if colorScheme == .light { return color }
return blend(color, with: .white, amount: 0.03)
}
}
///MARK: Theme Name Canonicalization
// Canonical theme names shown in settings and used for palette lookup.
let editorThemeNames: [String] = [
"Neon Glow",
"Neon Flow",
"Neon Voltage",
"Laserwave",
"Cyber Lime",
"Plasma Storm",
"Inferno Neon",
"Ultraviolet Flux",
"Prism Daylight",
"Custom",
"Dracula",
"One Dark Pro",
"Nord",
"Tokyo Night",
"Gruvbox",
"Arc",
"Dusk",
"Aurora",
"Horizon",
"Midnight",
"Mono",
"Paper",
"Solar",
"Pulse",
"Mocha"
]
// Normalize persisted theme values so legacy/case variants still resolve correctly.
func canonicalThemeName(_ rawName: String) -> String {
let trimmed = rawName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "Neon Glow" }
if let exact = editorThemeNames.first(where: { $0 == trimmed }) {
return exact
}
if let caseInsensitive = editorThemeNames.first(where: { $0.caseInsensitiveCompare(trimmed) == .orderedSame }) {
return caseInsensitive
}
return "Neon Glow"
}
private func paletteForThemeName(_ name: String, defaults: UserDefaults) -> ThemePalette {
let canonicalName = canonicalThemeName(name)
let palette: ThemePalette = {
switch canonicalName {
case "Neon Glow":
return ThemePalette(
text: Color(red: 0.94, green: 0.94, blue: 0.94),
background: Color(red: 0.06, green: 0.07, blue: 0.09),
cursor: Color(red: 0.30, green: 0.60, blue: 0.90),
selection: Color(red: 0.18, green: 0.24, blue: 0.33),
keyword: Color(red: 0.93, green: 0.00, blue: 0.88),
string: Color(red: 0.00, green: 0.243137, blue: 1.00),
number: Color(red: 1.00, green: 0.02, blue: 0.12),
comment: Color(red: 0.38, green: 0.38, blue: 0.40),
type: Color(red: 0.20, green: 0.82, blue: 0.41),
property: Color(red: 0.86, green: 0.44, blue: 0.84),
builtin: Color(red: 0.92, green: 0.47, blue: 0.53)
)
case "Neon Flow":
return ThemePalette(
text: Color(red: 0.00, green: 0.00, blue: 0.00),
background: Color(red: 1.00, green: 1.00, blue: 1.00),
cursor: Color(red: 0.29, green: 0.60, blue: 0.91),
selection: Color(red: 0.95, green: 0.17, blue: 0.56),
keyword: Color(red: 0.92, green: 0.05, blue: 0.91),
string: Color(red: 0.09, green: 0.56, blue: 0.91),
number: Color(red: 0.95, green: 0.17, blue: 0.56),
comment: Color(red: 0.62, green: 0.62, blue: 0.62),
type: Color(red: 0.08, green: 0.89, blue: 0.37),
property: Color(red: 0.92, green: 0.05, blue: 0.91),
builtin: Color(red: 0.96, green: 0.20, blue: 0.04)
)
case "Neon Voltage":
return ThemePalette(
text: Color(red: 0.95, green: 0.98, blue: 1.00),
background: Color(red: 0.04, green: 0.06, blue: 0.09),
cursor: Color(red: 0.00, green: 0.92, blue: 1.00),
selection: Color(red: 0.12, green: 0.20, blue: 0.30),
keyword: Color(red: 0.00, green: 0.92, blue: 1.00),
string: Color(red: 0.35, green: 1.00, blue: 0.72),
number: Color(red: 1.00, green: 0.72, blue: 0.10),
comment: Color(red: 0.48, green: 0.56, blue: 0.68),
type: Color(red: 0.74, green: 0.52, blue: 1.00),
property: Color(red: 0.32, green: 0.88, blue: 1.00),
builtin: Color(red: 1.00, green: 0.34, blue: 0.74)
)
case "Laserwave":
return ThemePalette(
text: Color(red: 0.96, green: 0.93, blue: 1.00),
background: Color(red: 0.10, green: 0.05, blue: 0.14),
cursor: Color(red: 0.98, green: 0.24, blue: 0.88),
selection: Color(red: 0.25, green: 0.15, blue: 0.34),
keyword: Color(red: 1.00, green: 0.28, blue: 0.86),
string: Color(red: 0.22, green: 0.94, blue: 1.00),
number: Color(red: 1.00, green: 0.60, blue: 0.30),
comment: Color(red: 0.60, green: 0.52, blue: 0.72),
type: Color(red: 0.65, green: 0.72, blue: 1.00),
property: Color(red: 0.36, green: 0.98, blue: 0.84),
builtin: Color(red: 1.00, green: 0.42, blue: 0.56)
)
case "Cyber Lime":
return ThemePalette(
text: Color(red: 0.94, green: 0.98, blue: 0.92),
background: Color(red: 0.07, green: 0.09, blue: 0.06),
cursor: Color(red: 0.18, green: 0.48, blue: 0.96),
selection: Color(red: 0.18, green: 0.25, blue: 0.14),
keyword: Color(red: 0.45, green: 0.78, blue: 0.16),
string: Color(red: 0.13, green: 0.70, blue: 0.50),
number: Color(red: 1.00, green: 0.86, blue: 0.22),
comment: Color(red: 0.54, green: 0.62, blue: 0.50),
type: Color(red: 0.48, green: 0.86, blue: 1.00),
property: Color(red: 0.56, green: 0.78, blue: 0.28),
builtin: Color(red: 1.00, green: 0.48, blue: 0.40)
)
case "Plasma Storm":
return ThemePalette(
text: Color(red: 0.95, green: 0.95, blue: 1.00),
background: Color(red: 0.06, green: 0.05, blue: 0.10),
cursor: Color(red: 0.54, green: 0.44, blue: 1.00),
selection: Color(red: 0.16, green: 0.12, blue: 0.30),
keyword: Color(red: 0.58, green: 0.46, blue: 1.00),
string: Color(red: 0.24, green: 0.90, blue: 1.00),
number: Color(red: 1.00, green: 0.52, blue: 0.20),
comment: Color(red: 0.54, green: 0.50, blue: 0.68),
type: Color(red: 0.80, green: 0.54, blue: 1.00),
property: Color(red: 0.66, green: 0.78, blue: 1.00),
builtin: Color(red: 1.00, green: 0.30, blue: 0.66)
)
case "Inferno Neon":
return ThemePalette(
text: Color(red: 1.00, green: 0.94, blue: 0.90),
background: Color(red: 0.11, green: 0.05, blue: 0.03),
cursor: Color(red: 1.00, green: 0.46, blue: 0.08),
selection: Color(red: 0.28, green: 0.12, blue: 0.08),
keyword: Color(red: 1.00, green: 0.38, blue: 0.16),
string: Color(red: 1.00, green: 0.74, blue: 0.24),
number: Color(red: 1.00, green: 0.26, blue: 0.48),
comment: Color(red: 0.70, green: 0.54, blue: 0.46),
type: Color(red: 1.00, green: 0.56, blue: 0.20),
property: Color(red: 1.00, green: 0.70, blue: 0.32),
builtin: Color(red: 1.00, green: 0.18, blue: 0.30)
)
case "Ultraviolet Flux":
return ThemePalette(
text: Color(red: 0.96, green: 0.93, blue: 1.00),
background: Color(red: 0.08, green: 0.04, blue: 0.12),
cursor: Color(red: 0.84, green: 0.36, blue: 1.00),
selection: Color(red: 0.21, green: 0.12, blue: 0.30),
keyword: Color(red: 0.84, green: 0.36, blue: 1.00),
string: Color(red: 0.32, green: 0.98, blue: 0.96),
number: Color(red: 1.00, green: 0.62, blue: 0.24),
comment: Color(red: 0.60, green: 0.52, blue: 0.72),
type: Color(red: 1.00, green: 0.38, blue: 0.86),
property: Color(red: 0.68, green: 0.72, blue: 1.00),
builtin: Color(red: 1.00, green: 0.28, blue: 0.56)
)
case "Prism Daylight":
return ThemePalette(
text: Color(red: 0.08, green: 0.10, blue: 0.14),
background: Color(red: 0.96, green: 0.97, blue: 0.99),
cursor: Color(red: 0.14, green: 0.34, blue: 0.84),
selection: Color(red: 0.84, green: 0.89, blue: 0.99),
keyword: Color(red: 0.48, green: 0.13, blue: 0.71),
string: Color(red: 0.05, green: 0.48, blue: 0.33),
number: Color(red: 0.71, green: 0.28, blue: 0.03),
comment: Color(red: 0.37, green: 0.42, blue: 0.49),
type: Color(red: 0.00, green: 0.34, blue: 0.76),
property: Color(red: 0.64, green: 0.11, blue: 0.67),
builtin: Color(red: 0.76, green: 0.25, blue: 0.05)
)
case "Dracula":
return ThemePalette(
text: Color(red: 0.97, green: 0.97, blue: 0.95),
background: Color(red: 0.16, green: 0.17, blue: 0.21),
cursor: Color(red: 0.97, green: 0.97, blue: 0.95),
selection: Color(red: 0.27, green: 0.28, blue: 0.35),
keyword: Color(red: 1.00, green: 0.48, blue: 0.78),
string: Color(red: 0.95, green: 0.98, blue: 0.55),
number: Color(red: 0.74, green: 0.58, blue: 0.98),
comment: Color(red: 0.38, green: 0.45, blue: 0.64),
type: Color(red: 0.55, green: 0.91, blue: 0.99),
property: Color(red: 0.31, green: 0.98, blue: 0.48),
builtin: Color(red: 1.00, green: 0.72, blue: 0.42)
)
case "One Dark Pro":
return ThemePalette(
text: Color(red: 0.67, green: 0.70, blue: 0.75),
background: Color(red: 0.16, green: 0.17, blue: 0.20),
cursor: Color(red: 0.32, green: 0.55, blue: 1.00),
selection: Color(red: 0.24, green: 0.27, blue: 0.32),
keyword: Color(red: 0.78, green: 0.47, blue: 0.87),
string: Color(red: 0.60, green: 0.76, blue: 0.47),
number: Color(red: 0.82, green: 0.60, blue: 0.40),
comment: Color(red: 0.36, green: 0.39, blue: 0.44),
type: Color(red: 0.90, green: 0.75, blue: 0.48),
property: Color(red: 0.38, green: 0.69, blue: 0.94),
builtin: Color(red: 0.34, green: 0.71, blue: 0.76)
)
case "Nord":
return ThemePalette(
text: Color(red: 0.85, green: 0.87, blue: 0.91),
background: Color(red: 0.18, green: 0.20, blue: 0.25),
cursor: Color(red: 0.53, green: 0.75, blue: 0.82),
selection: Color(red: 0.26, green: 0.30, blue: 0.37),
keyword: Color(red: 0.51, green: 0.63, blue: 0.76),
string: Color(red: 0.64, green: 0.75, blue: 0.55),
number: Color(red: 0.71, green: 0.56, blue: 0.68),
comment: Color(red: 0.38, green: 0.43, blue: 0.53),
type: Color(red: 0.56, green: 0.74, blue: 0.73),
property: Color(red: 0.82, green: 0.53, blue: 0.44),
builtin: Color(red: 0.92, green: 0.80, blue: 0.55)
)
case "Tokyo Night":
return ThemePalette(
text: Color(red: 0.75, green: 0.79, blue: 0.96),
background: Color(red: 0.10, green: 0.11, blue: 0.15),
cursor: Color(red: 0.48, green: 0.64, blue: 0.97),
selection: Color(red: 0.20, green: 0.28, blue: 0.49),
keyword: Color(red: 0.73, green: 0.60, blue: 0.97),
string: Color(red: 0.62, green: 0.81, blue: 0.42),
number: Color(red: 1.00, green: 0.62, blue: 0.39),
comment: Color(red: 0.34, green: 0.37, blue: 0.54),
type: Color(red: 0.49, green: 0.81, blue: 1.00),
property: Color(red: 0.16, green: 0.77, blue: 0.87),
builtin: Color(red: 0.97, green: 0.46, blue: 0.56)
)
case "Gruvbox":
return ThemePalette(
text: Color(red: 0.92, green: 0.86, blue: 0.70),
background: Color(red: 0.16, green: 0.16, blue: 0.16),
cursor: Color(red: 0.98, green: 0.74, blue: 0.18),
selection: Color(red: 0.31, green: 0.29, blue: 0.27),
keyword: Color(red: 0.98, green: 0.29, blue: 0.20),
string: Color(red: 0.72, green: 0.73, blue: 0.15),
number: Color(red: 0.83, green: 0.53, blue: 0.61),
comment: Color(red: 0.57, green: 0.51, blue: 0.46),
type: Color(red: 0.56, green: 0.75, blue: 0.49),
property: Color(red: 0.51, green: 0.65, blue: 0.60),
builtin: Color(red: 1.00, green: 0.50, blue: 0.10)
)
case "Arc":
return ThemePalette(
text: Color(red: 0.95, green: 0.96, blue: 0.98),
background: Color(red: 0.12, green: 0.13, blue: 0.16),
cursor: Color(red: 0.35, green: 0.67, blue: 0.98),
selection: Color(red: 0.22, green: 0.29, blue: 0.40),
keyword: Color(red: 0.85, green: 0.48, blue: 1.0),
string: Color(red: 0.45, green: 0.88, blue: 0.72),
number: Color(red: 0.98, green: 0.75, blue: 0.37),
comment: Color(red: 0.56, green: 0.60, blue: 0.70),
type: Color(red: 0.44, green: 0.66, blue: 0.98),
property: Color(red: 0.94, green: 0.61, blue: 0.32),
builtin: Color(red: 0.99, green: 0.51, blue: 0.71)
)
case "Dusk":
return ThemePalette(
text: Color(red: 0.93, green: 0.92, blue: 0.95),
background: Color(red: 0.14, green: 0.10, blue: 0.20),
cursor: Color(red: 0.93, green: 0.54, blue: 0.94),
selection: Color(red: 0.28, green: 0.22, blue: 0.39),
keyword: Color(red: 0.98, green: 0.72, blue: 0.40),
string: Color(red: 0.48, green: 0.89, blue: 0.67),
number: Color(red: 0.98, green: 0.54, blue: 0.62),
comment: Color(red: 0.62, green: 0.57, blue: 0.70),
type: Color(red: 0.57, green: 0.75, blue: 1.0),
property: Color(red: 0.88, green: 0.64, blue: 0.98),
builtin: Color(red: 0.94, green: 0.44, blue: 0.76)
)
case "Aurora":
return ThemePalette(
text: Color(red: 0.92, green: 0.96, blue: 0.98),
background: Color(red: 0.08, green: 0.12, blue: 0.14),
cursor: Color(red: 0.35, green: 0.96, blue: 0.76),
selection: Color(red: 0.18, green: 0.28, blue: 0.30),
keyword: Color(red: 0.36, green: 0.92, blue: 0.98),
string: Color(red: 0.44, green: 0.98, blue: 0.62),
number: Color(red: 0.98, green: 0.76, blue: 0.38),
comment: Color(red: 0.70, green: 0.86, blue: 0.92),
type: Color(red: 0.52, green: 0.74, blue: 0.98),
property: Color(red: 0.90, green: 0.60, blue: 0.98),
builtin: Color(red: 0.98, green: 0.52, blue: 0.72)
)
case "Horizon":
return ThemePalette(
text: Color(red: 0.95, green: 0.94, blue: 0.92),
background: Color(red: 0.14, green: 0.10, blue: 0.09),
cursor: Color(red: 0.99, green: 0.62, blue: 0.36),
selection: Color(red: 0.30, green: 0.20, blue: 0.18),
keyword: Color(red: 0.99, green: 0.46, blue: 0.36),
string: Color(red: 0.98, green: 0.78, blue: 0.36),
number: Color(red: 0.98, green: 0.60, blue: 0.80),
comment: Color(red: 0.86, green: 0.72, blue: 0.64),
type: Color(red: 0.60, green: 0.78, blue: 0.98),
property: Color(red: 0.96, green: 0.56, blue: 0.45),
builtin: Color(red: 0.90, green: 0.72, blue: 0.36)
)
case "Midnight":
return ThemePalette(
text: Color(red: 0.90, green: 0.94, blue: 0.98),
background: Color(red: 0.08, green: 0.10, blue: 0.16),
cursor: Color(red: 0.25, green: 0.78, blue: 0.98),
selection: Color(red: 0.16, green: 0.22, blue: 0.32),
keyword: Color(red: 0.35, green: 0.86, blue: 0.96),
string: Color(red: 0.48, green: 0.94, blue: 0.62),
number: Color(red: 0.96, green: 0.77, blue: 0.31),
comment: Color(red: 0.55, green: 0.63, blue: 0.74),
type: Color(red: 0.40, green: 0.65, blue: 0.98),
property: Color(red: 0.96, green: 0.56, blue: 0.45),
builtin: Color(red: 0.86, green: 0.52, blue: 1.0)
)
case "Mono":
return ThemePalette(
text: Color(red: 0.88, green: 0.88, blue: 0.88),
background: Color(red: 0.12, green: 0.12, blue: 0.12),
cursor: Color.white,
selection: Color(red: 0.26, green: 0.26, blue: 0.26),
keyword: Color(red: 0.92, green: 0.92, blue: 0.92),
string: Color(red: 0.80, green: 0.80, blue: 0.80),
number: Color(red: 0.86, green: 0.86, blue: 0.86),
comment: Color(red: 0.55, green: 0.55, blue: 0.55),
type: Color(red: 0.90, green: 0.90, blue: 0.90),
property: Color(red: 0.84, green: 0.84, blue: 0.84),
builtin: Color(red: 0.78, green: 0.78, blue: 0.78)
)
case "Paper":
return ThemePalette(
text: Color(red: 0.12, green: 0.12, blue: 0.12),
background: Color(red: 0.98, green: 0.97, blue: 0.94),
cursor: Color(red: 0.16, green: 0.31, blue: 0.90),
selection: Color(red: 0.86, green: 0.90, blue: 0.98),
keyword: Color(red: 0.60, green: 0.18, blue: 0.82),
string: Color(red: 0.12, green: 0.54, blue: 0.39),
number: Color(red: 0.78, green: 0.37, blue: 0.09),
comment: Color(red: 0.46, green: 0.46, blue: 0.46),
type: Color(red: 0.12, green: 0.34, blue: 0.75),
property: Color(red: 0.67, green: 0.27, blue: 0.52),
builtin: Color(red: 0.74, green: 0.42, blue: 0.10)
)
case "Solar":
return ThemePalette(
text: Color(red: 0.98, green: 0.95, blue: 0.90),
background: Color(red: 0.19, green: 0.12, blue: 0.08),
cursor: Color(red: 0.99, green: 0.74, blue: 0.30),
selection: Color(red: 0.33, green: 0.20, blue: 0.14),
keyword: Color(red: 0.99, green: 0.64, blue: 0.24),
string: Color(red: 0.98, green: 0.84, blue: 0.34),
number: Color(red: 0.98, green: 0.52, blue: 0.74),
comment: Color(red: 0.92, green: 0.80, blue: 0.66),
type: Color(red: 0.52, green: 0.78, blue: 0.98),
property: Color(red: 0.98, green: 0.58, blue: 0.38),
builtin: Color(red: 0.94, green: 0.48, blue: 0.58)
)
case "Pulse":
return ThemePalette(
text: Color(red: 0.95, green: 0.96, blue: 0.98),
background: Color(red: 0.10, green: 0.10, blue: 0.14),
cursor: Color(red: 0.93, green: 0.45, blue: 0.57),
selection: Color(red: 0.24, green: 0.18, blue: 0.28),
keyword: Color(red: 0.98, green: 0.54, blue: 0.62),
string: Color(red: 0.46, green: 0.92, blue: 0.83),
number: Color(red: 0.96, green: 0.76, blue: 0.30),
comment: Color(red: 0.62, green: 0.63, blue: 0.72),
type: Color(red: 0.45, green: 0.72, blue: 0.98),
property: Color(red: 0.96, green: 0.59, blue: 0.32),
builtin: Color(red: 0.86, green: 0.52, blue: 1.0)
)
case "Mocha":
return ThemePalette(
text: Color(red: 0.95, green: 0.92, blue: 0.90),
background: Color(red: 0.12, green: 0.09, blue: 0.08),
cursor: Color(red: 0.82, green: 0.62, blue: 0.48),
selection: Color(red: 0.22, green: 0.17, blue: 0.15),
keyword: Color(red: 0.82, green: 0.60, blue: 0.98),
string: Color(red: 0.84, green: 0.72, blue: 0.46),
number: Color(red: 0.98, green: 0.70, blue: 0.46),
comment: Color(red: 0.78, green: 0.70, blue: 0.66),
type: Color(red: 0.52, green: 0.78, blue: 0.98),
property: Color(red: 0.94, green: 0.56, blue: 0.32),
builtin: Color(red: 0.90, green: 0.46, blue: 0.72)
)
case "Custom":
let text = colorFromHex(defaults.string(forKey: "SettingsThemeTextColor") ?? "#EDEDED", fallback: .white)
let background = colorFromHex(defaults.string(forKey: "SettingsThemeBackgroundColor") ?? "#0E1116", fallback: .black)
let cursor = colorFromHex(defaults.string(forKey: "SettingsThemeCursorColor") ?? "#4EA4FF", fallback: .blue)
let selection = colorFromHex(defaults.string(forKey: "SettingsThemeSelectionColor") ?? "#2A3340", fallback: .gray)
let keyword = colorFromHex(defaults.string(forKey: "SettingsThemeKeywordColor") ?? "#F5D90A", fallback: .yellow)
let string = colorFromHex(defaults.string(forKey: "SettingsThemeStringColor") ?? "#4EA4FF", fallback: .blue)
let number = colorFromHex(defaults.string(forKey: "SettingsThemeNumberColor") ?? "#FFB86C", fallback: .orange)
let comment = colorFromHex(defaults.string(forKey: "SettingsThemeCommentColor") ?? "#7F8C98", fallback: .gray)
let type = colorFromHex(defaults.string(forKey: "SettingsThemeTypeColor") ?? "#32D269", fallback: .green)
let builtin = colorFromHex(defaults.string(forKey: "SettingsThemeBuiltinColor") ?? "#EC7887", fallback: .red)
return ThemePalette(
text: text,
background: background,
cursor: cursor,
selection: selection,
keyword: keyword,
string: string,
number: number,
comment: comment,
type: type,
property: string,
builtin: builtin
)
default:
return ThemePalette(
text: Color(red: 0.93, green: 0.95, blue: 0.98),
background: Color(red: 0.10, green: 0.11, blue: 0.14),
cursor: Color(red: 0.31, green: 0.72, blue: 0.99),
selection: Color(red: 0.22, green: 0.30, blue: 0.43),
keyword: Color(red: 0.96, green: 0.84, blue: 0.23),
string: Color(red: 0.98, green: 0.48, blue: 0.82),
number: Color(red: 0.98, green: 0.72, blue: 0.33),
comment: Color(red: 0.60, green: 0.66, blue: 0.74),
type: Color(red: 0.41, green: 0.69, blue: 0.99),
property: Color(red: 0.39, green: 0.90, blue: 0.72),
builtin: Color(red: 0.94, green: 0.42, blue: 0.66)
)
}
}()
return ThemePalette(
text: palette.text,
background: palette.background,
cursor: palette.cursor,
selection: emphasizedSelectionColor(for: canonicalName, fallback: palette.selection),
keyword: palette.keyword,
string: palette.string,
number: palette.number,
comment: palette.comment,
type: palette.type,
property: palette.property,
builtin: palette.builtin
)
}
func themePaletteColors(for name: String, defaults: UserDefaults = .standard) -> ThemePaletteColors {
let palette = paletteForThemeName(canonicalThemeName(name), defaults: defaults)
return ThemePaletteColors(
text: palette.text,
background: palette.background,
cursor: palette.cursor,
selection: palette.selection,
keyword: palette.keyword,
string: palette.string,
number: palette.number,
comment: palette.comment,
type: palette.type,
property: palette.property,
builtin: palette.builtin
)
}
func currentEditorTheme(colorScheme: ColorScheme) -> EditorTheme {
let defaults = UserDefaults.standard
// Always respect the user's selected theme across iOS and macOS.
let name = canonicalThemeName(defaults.string(forKey: "SettingsThemeName") ?? "Neon Glow")
let boldKeywords = defaults.bool(forKey: "SettingsThemeBoldKeywords")
let italicComments = defaults.bool(forKey: "SettingsThemeItalicComments")
let underlineLinks = defaults.bool(forKey: "SettingsThemeUnderlineLinks")
let boldMarkdownHeadings = defaults.bool(forKey: "SettingsThemeBoldMarkdownHeadings")
let palette = paletteForThemeName(name, defaults: defaults)
// Keep base editor text legible and consistent across all themes.
// Neon Glow gets a slightly brighter dark-mode text tone.
let baseTextColor: Color = {
if name == "Neon Glow" {
return colorScheme == .light
? .black
: Color(red: 0.94, green: 0.94, blue: 0.94)
}
if colorScheme == .light { return .black }
return Color(red: 0.90, green: 0.90, blue: 0.90)
}()
let profile: SyntaxAdjustmentProfile = {
let vividNeonThemes: Set<String> = [
"Neon Glow",
"Neon Voltage",
"Laserwave",
"Cyber Lime",
"Plasma Storm",
"Inferno Neon",
"Ultraviolet Flux",
"Custom"
]
return vividNeonThemes.contains(name) ? .neonRaw : .standard
}()
let keyword = adjustedSyntaxColor(
palette.keyword,
colorScheme: colorScheme,
profile: profile,
darkenInLight: 0.30,
brightenInDark: 0.08
)
let string = adjustedSyntaxColor(
palette.string,
colorScheme: colorScheme,
profile: profile,
darkenInLight: 0.40,
brightenInDark: 0.10
)
let number = adjustedSyntaxColor(
palette.number,
colorScheme: colorScheme,
profile: profile,
darkenInLight: 0.28,
brightenInDark: 0.08
)
let comment = adjustedSyntaxColor(
palette.comment,
colorScheme: colorScheme,
profile: profile,
darkenInLight: 0.28,
brightenInDark: 0.20
)
let type = adjustedSyntaxColor(
palette.type,
colorScheme: colorScheme,
profile: profile,
darkenInLight: 0.30,
brightenInDark: 0.08
)
let property = adjustedSyntaxColor(
palette.property,
colorScheme: colorScheme,
profile: profile,
darkenInLight: 0.32,
brightenInDark: 0.08
)
let builtin = adjustedSyntaxColor(
palette.builtin,
colorScheme: colorScheme,
profile: profile,
darkenInLight: 0.30,
brightenInDark: 0.08
)
let syntax = SyntaxColors(
keyword: keyword,
string: string,
number: number,
comment: comment,
attribute: property,
variable: property,
def: type,
property: property,
meta: builtin,
tag: keyword,
atom: builtin,
builtin: builtin,
type: type
)
return EditorTheme(
text: baseTextColor,
background: modeAdjustedEditorBackground(palette.background, colorScheme: colorScheme),
cursor: palette.cursor,
selection: palette.selection,
syntax: syntax,
boldKeywords: boldKeywords,
italicComments: italicComments,
underlineLinks: underlineLinks,
boldMarkdownHeadings: boldMarkdownHeadings
)
}
func colorFromHex(_ hex: String, fallback: Color) -> Color {
let cleaned = hex.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: "#", with: "")
guard (cleaned.count == 6 || cleaned.count == 8), let intVal = UInt64(cleaned, radix: 16) else { return fallback }
if cleaned.count == 8 {
let r = Double((intVal >> 24) & 0xFF) / 255.0
let g = Double((intVal >> 16) & 0xFF) / 255.0
let b = Double((intVal >> 8) & 0xFF) / 255.0
let a = Double(intVal & 0xFF) / 255.0
return Color(red: r, green: g, blue: b, opacity: a)
}
let r = Double((intVal >> 16) & 0xFF) / 255.0
let g = Double((intVal >> 8) & 0xFF) / 255.0
let b = Double(intVal & 0xFF) / 255.0
return Color(red: r, green: g, blue: b)
}
func colorToHex(_ color: Color) -> String {
#if os(macOS)
let platform = PlatformColor(color)
guard let srgb = platform.usingColorSpace(.sRGB) else { return "#FFFFFF" }
let r = Int(round(srgb.redComponent * 255))
let g = Int(round(srgb.greenComponent * 255))
let b = Int(round(srgb.blueComponent * 255))
let a = Int(round(srgb.alphaComponent * 255))
#else
let platform = PlatformColor(color)
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
platform.getRed(&r, green: &g, blue: &b, alpha: &a)
let rInt = Int(round(r * 255))
let gInt = Int(round(g * 255))
let bInt = Int(round(b * 255))
let aInt = Int(round(a * 255))
#endif
#if os(macOS)
if a < 255 {
return String(format: "#%02X%02X%02X%02X", r, g, b, a)
}
return String(format: "#%02X%02X%02X", r, g, b)
#else
if aInt < 255 {
return String(format: "#%02X%02X%02X%02X", rInt, gInt, bInt, aInt)
}
return String(format: "#%02X%02X%02X", rInt, gInt, bInt)
#endif
}