krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/Orgstar/ThemeColors.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Sources/Orgstar/ThemeColors.swift history · blame · raw

61 lines · 2670 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", "agenda-scheduled", "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}