Sources/Orgstar/ThemeColors.swift
61 lines · 2650 bytes
1import AppKit
2import OrgPresentation
3import SwiftUI
4
5extension EnvironmentValues {
6 /// The theme's colors, for the views around the editor.
7 @Entry var orgTheme: ThemeSpec = .default
8}
9
10extension ThemeSpec {
11 /// A color that follows the appearance, or nil when the theme leaves `key` to the system.
12 /// A key set for one appearance only leaves the other to the system.
13 func color(_ key: String) -> Color? {
14 guard light[key] != nil || dark[key] != nil else { return nil }
15 let (light, dark) = (light[key], dark[key])
16 let fallback = Self.systemColor(key)
17 return Color(nsColor: NSColor(name: nil) { appearance in
18 let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
19 guard let c = isDark ? dark : light else { return fallback }
20 return NSColor(srgbRed: c.red, green: c.green, blue: c.blue, alpha: c.alpha)
21 })
22 }
23
24 /// A color that follows the appearance from a helper taking `dark:` (`todoColor`,
25 /// `statusColor`, …), or `fallback` where it gives none.
26 func dynamic(_ fallback: NSColor = .labelColor, _ pick: @escaping @Sendable (_ dark: Bool) -> RGBA?) -> Color {
27 Color(nsColor: NSColor(name: nil) { appearance in
28 let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
29 guard let c = pick(isDark) else { return fallback }
30 return NSColor(srgbRed: c.red, green: c.green, blue: c.blue, alpha: c.alpha)
31 })
32 }
33
34 /// What the system shows for a key the theme leaves unset in one appearance.
35 static func systemColor(_ key: String) -> NSColor {
36 switch key {
37 case "sidebar-background", "modeline-background": .windowBackgroundColor
38 case "agenda-background", "background": .textBackgroundColor
39 case "sidebar-header", "modeline-foreground", "agenda-time", "agenda-category", "tags": .secondaryLabelColor
40 default: .labelColor
41 }
42 }
43
44 /// A TODO keyword's color: its own from `[theme.todo]`, else the theme's TODO or DONE.
45 func keywordColor(_ keyword: String, done: Bool) -> Color {
46 if let c = todo[keyword] { return Color(.sRGB, red: c.red, green: c.green, blue: c.blue, opacity: c.alpha) }
47 return color(done ? "done" : "todo") ?? (done ? .green : .red)
48 }
49}
50
51extension View {
52 /// A list's background from the theme, or the system's when the theme has none.
53 @ViewBuilder
54 func themedListBackground(_ color: Color?) -> some View {
55 if let color {
56 scrollContentBackground(.hidden).background(color)
57 } else {
58 self
59 }
60 }
61}