mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
617 lines
26 KiB
Swift
617 lines
26 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
)
|
|
#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))
|
|
#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)
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
///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",
|
|
"Dracula",
|
|
"One Dark Pro",
|
|
"Nord",
|
|
"Tokyo Night",
|
|
"Gruvbox",
|
|
"Arc",
|
|
"Dusk",
|
|
"Aurora",
|
|
"Horizon",
|
|
"Midnight",
|
|
"Mono",
|
|
"Paper",
|
|
"Solar",
|
|
"Pulse",
|
|
"Mocha",
|
|
"Custom"
|
|
]
|
|
|
|
// 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.00, green: 0.00, blue: 0.00),
|
|
background: Color(red: 1.00, green: 1.00, blue: 1.00),
|
|
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 "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 palette
|
|
}
|
|
|
|
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 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 colorScheme == .light { return .black }
|
|
if name == "Neon Glow" {
|
|
return Color(red: 0.94, green: 0.94, blue: 0.94)
|
|
}
|
|
return Color(red: 0.90, green: 0.90, blue: 0.90)
|
|
}()
|
|
|
|
let profile: SyntaxAdjustmentProfile = (name == "Neon Glow") ? .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
|
|
)
|
|
}
|
|
|
|
func colorFromHex(_ hex: String, fallback: Color) -> Color {
|
|
let cleaned = hex.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: "#", with: "")
|
|
guard cleaned.count == 6, let intVal = Int(cleaned, radix: 16) else { return fallback }
|
|
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))
|
|
#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))
|
|
#endif
|
|
#if os(macOS)
|
|
return String(format: "#%02X%02X%02X", r, g, b)
|
|
#else
|
|
return String(format: "#%02X%02X%02X", rInt, gInt, bInt)
|
|
#endif
|
|
}
|