~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
251 lines · 11.8 KB Raw History
import SwiftUI
import UIKit

/// The colours a theme draws the app in: a `dev.knotless.theme#palette`, each
/// named for what it colours, with what it leaves out filled in as the pages do
/// (`web/src/tokens.css`).
struct Palette: Equatable, Sendable {
    var dark: Bool
    var bg: UInt32
    var panel: UInt32
    var soft: UInt32
    var line: UInt32
    var fg: UInt32
    var mute: UInt32
    var faint: UInt32
    var primary: UInt32
    var onPrimary: UInt32
    var accent: UInt32
    var success: UInt32
    var danger: UInt32
    var done: UInt32
    var attention: UInt32
    var neutral: UInt32
    /// Code's colours, by the names `Highlight` paints: comment, string, keyword…
    var syntax: [String: UInt32]

    var scheme: ColorScheme { dark ? .dark : .light }
}

extension Palette {
    /// Light, as the pages draw it: what a light theme of someone's own falls back on.
    static let light = Palette(
        dark: false, bg: 0xffffff, panel: 0xfafafa, soft: 0xf4f4f4, line: 0xe8e8e8,
        fg: 0x171717, mute: 0x525252, faint: 0x6e6e6e, primary: 0x2563eb, onPrimary: 0xffffff,
        accent: 0x2f5fb3, success: 0x1a7f37, danger: 0xcf222e, done: 0x7048b5, attention: 0x9a6700, neutral: 0x636363,
        syntax: ["comment": 0x59636e, "string": 0x0a3069, "keyword": 0xcf222e, "number": 0x0550ae,
                 "function": 0x6639ba, "type": 0x953800, "tag": 0x116329, "attr": 0x0550ae]
    )

    /// Dark, as the pages draw it.
    static let dark = Palette(
        dark: true, bg: 0x111111, panel: 0x161616, soft: 0x1a1a1a, line: 0x262626,
        fg: 0xededed, mute: 0xa1a1a1, faint: 0x848484, primary: 0x2563eb, onPrimary: 0xffffff,
        accent: 0x7aa7f0, success: 0x3fb950, danger: 0xf85149, done: 0xb39af0, attention: 0xd29922, neutral: 0x9198a1,
        syntax: ["comment": 0x9198a1, "string": 0xa5d6ff, "keyword": 0xff7b72, "number": 0x79c0ff,
                 "function": 0xd2a8ff, "type": 0xffa657, "tag": 0x7ee787, "attr": 0x79c0ff]
    )

    /// The themes the pages offer besides light and dark, by the name
    /// `appearancePref` gives each, as `web/src/tokens.css` draws them.
    static let named: [String: Palette] = [
        "latte": Palette(
            dark: false, bg: 0xeff1f5, panel: 0xe6e9ef, soft: 0xdce0e8, line: 0xccd0da,
            fg: 0x3c3f58, mute: 0x53566c, faint: 0x60626e, primary: 0x8839ef, onPrimary: 0xffffff,
            accent: 0x1955cc, success: 0x317c21, danger: 0xd20f39, done: 0x8839ef, attention: 0x996214, neutral: 0x696c81,
            syntax: ["comment": 0x5d5f6b, "string": 0x2d701e, "keyword": 0x7b33d7, "number": 0xa64108,
                     "function": 0x1a58d5, "type": 0x865511, "tag": 0xc40e35, "attr": 0xa64108]
        ),
        "mocha": Palette(
            dark: true, bg: 0x1e1e2e, panel: 0x24243a, soft: 0x313244, line: 0x363749,
            fg: 0xcdd6f4, mute: 0xa6adc8, faint: 0x989caf, primary: 0xcba6f7, onPrimary: 0x1e1e2e,
            accent: 0x89b4fa, success: 0xa6e3a1, danger: 0xf38ba8, done: 0xcba6f7, attention: 0xf9e2af, neutral: 0x9399b2,
            syntax: ["comment": 0x9a9eb1, "string": 0xa6e3a1, "keyword": 0xcba6f7, "number": 0xfab387,
                     "function": 0x89b4fa, "type": 0xf9e2af, "tag": 0xf38ba8, "attr": 0xfab387]
        ),
        "dracula": Palette(
            dark: true, bg: 0x282a36, panel: 0x2d2f3d, soft: 0x363849, line: 0x3c3f52,
            fg: 0xf8f8f2, mute: 0xb9bcd6, faint: 0x98a1c4, primary: 0xbd93f9, onPrimary: 0x282a36,
            accent: 0xff79c6, success: 0x50fa7b, danger: 0xff5555, done: 0xbd93f9, attention: 0xf1fa8c, neutral: 0xa9adc9,
            syntax: ["comment": 0xa1aac9, "string": 0xf1fa8c, "keyword": 0xff92d1, "number": 0xc9a7fa,
                     "function": 0x50fa7b, "type": 0x8be9fd, "tag": 0x50fa7b, "attr": 0x50fa7b]
        ),
        "pckt": Palette(
            dark: true, bg: 0x101828, panel: 0x172133, soft: 0x1e2939, line: 0x2b3648,
            fg: 0xf9fafb, mute: 0xb4bac5, faint: 0x8a92a0, primary: 0x58a7ff, onPrimary: 0x101828,
            accent: 0x58a7ff, success: 0x21dc66, danger: 0xff5d5b, done: 0xb39dfa, attention: 0xffca46, neutral: 0x99a1af,
            syntax: ["comment": 0x8a92a0, "string": 0x21dc66, "keyword": 0xff5d5b, "number": 0xffca46,
                     "function": 0x58a7ff, "type": 0xffca46, "tag": 0xff5d5b, "attr": 0x58a7ff]
        ),
        "plyr": Palette(
            dark: true, bg: 0x0a0a0a, panel: 0x141414, soft: 0x1a1a1a, line: 0x282828,
            fg: 0xe8e8e8, mute: 0xb0b0b0, faint: 0x848484, primary: 0x6a9fff, onPrimary: 0x0a0a0a,
            accent: 0x6a9fff, success: 0x4ade80, danger: 0xef4444, done: 0xa78bfa, attention: 0xfbbf24, neutral: 0x808080,
            syntax: ["comment": 0x888888, "string": 0x4ade80, "keyword": 0x6a9fff, "number": 0xfbbf24,
                     "function": 0x8ab3ff, "type": 0xa78bfa, "tag": 0xf16060, "attr": 0xfbbf24]
        ),
        "blacksky": Palette(
            dark: true, bg: 0x070c0c, panel: 0x0e1212, soft: 0x1f2020, line: 0x262828,
            fg: 0xf8faf9, mute: 0xc8cac9, faint: 0x9c9e9e, primary: 0x8686ff, onPrimary: 0x070c0c,
            accent: 0x8686ff, success: 0xd2fc51, danger: 0xff4b78, done: 0xb0b3f0, attention: 0xffd166, neutral: 0x9c9e9e,
            syntax: ["comment": 0x9c9e9e, "string": 0xd2fc51, "keyword": 0x8686ff, "number": 0xff7396,
                     "function": 0xb0b3f0, "type": 0xaefa59, "tag": 0xff7396, "attr": 0xb0b3f0]
        ),
    ]

    /// A palette of someone's own: light's or dark's colours, as its scheme says,
    /// under its own; the surfaces and texts it leaves out mixed from its
    /// background and text, as `[data-theme^='custom-']` mixes them.
    init(record: PaletteRecord) {
        let base: Palette = record.colorScheme == "dark" ? .dark : .light
        let bg = Self.hex(record.background) ?? base.bg
        let fg = Self.hex(record.text) ?? base.fg
        let mixed = { (part: Double) in Self.mix(fg, part, bg) }
        self.init(
            dark: base.dark, bg: bg,
            panel: Self.hex(record.panel) ?? mixed(0.04),
            soft: Self.hex(record.soft) ?? mixed(0.08),
            line: Self.hex(record.border) ?? mixed(0.18),
            fg: fg,
            mute: Self.hex(record.textMuted) ?? mixed(0.80),
            faint: Self.hex(record.textFaint) ?? mixed(0.70),
            primary: Self.hex(record.primary) ?? base.primary,
            onPrimary: Self.hex(record.onPrimary) ?? bg,
            accent: Self.hex(record.accent) ?? base.accent,
            success: Self.hex(record.success) ?? base.success,
            danger: Self.hex(record.danger) ?? base.danger,
            done: Self.hex(record.done) ?? base.done,
            attention: Self.hex(record.attention) ?? base.attention,
            neutral: Self.hex(record.neutral) ?? base.neutral,
            syntax: base.syntax
        )
        let given: [(String, String?)] = [
            ("comment", record.syntax?.comments), ("string", record.syntax?.strings),
            ("keyword", record.syntax?.keywords), ("number", record.syntax?.numbers),
            ("function", record.syntax?.functions), ("type", record.syntax?.types), ("tag", record.syntax?.tags),
        ]
        for (name, value) in given { if let v = Self.hex(value) { syntax[name] = v } }
        syntax["attr"] = syntax["number"]
    }

    /// `#rrggbb` as a number; anything else, nil, which readers pass over.
    static func hex(_ string: String?) -> UInt32? {
        guard var s = string, s.hasPrefix("#") else { return nil }
        s.removeFirst()
        guard s.count == 6 else { return nil }
        return UInt32(s, radix: 16)
    }

    /// `part` of `a` over `b`, as CSS's `color-mix(in srgb, a part, b)`.
    static func mix(_ a: UInt32, _ part: Double, _ b: UInt32) -> UInt32 {
        func channel(_ shift: UInt32) -> UInt32 {
            let x = Double(a >> shift & 255), y = Double(b >> shift & 255)
            return UInt32((x * part + y * (1 - part)).rounded()) << shift
        }
        return channel(16) | channel(8) | channel(0)
    }
}

extension ThemeHalf {
    /// Its colours: none for light, dark, or one the app doesn't know, which
    /// iOS's own colours draw.
    var resolved: Palette? {
        switch theme {
        case "custom": palette.map(Palette.init(record:))
        case let name?: Palette.named[name]
        case nil: nil
        }
    }

    /// The light or dark it holds the app in, if it holds it in one.
    var scheme: ColorScheme? {
        switch theme {
        case "light": .light
        case "dark": .dark
        default: resolved?.scheme
        }
    }
}

extension ThemeChoice {
    func palette(dark: Bool) -> Palette? {
        (dark ? night ?? day : day).resolved
    }

    /// A pair follows the system, which picks its half.
    var scheme: ColorScheme? { night == nil ? day.scheme : nil }
}

/// The theme the app is in now, for colours to read as they are drawn.
/// `generation` changes with it, carried to them as a trait, so each is drawn again.
enum Themes {
    nonisolated(unsafe) private(set) static var current: ThemeChoice?
    nonisolated(unsafe) private(set) static var generation = 0

    @MainActor static func set(_ choice: ThemeChoice?) {
        guard choice != current else { return }
        current = choice
        generation += 1
    }

    static func palette(_ traits: UITraitCollection) -> Palette? {
        _ = traits[ThemeTrait.self]
        return current?.palette(dark: traits.userInterfaceStyle == .dark)
    }
}

/// Which theme a view is drawn in, so a colour drawn again when it changes asks anew.
struct ThemeTrait: UITraitDefinition {
    static let defaultValue = 0
    static let affectsColorAppearance = true
}

private struct ThemeKey: EnvironmentKey {
    static let defaultValue = 0
}

extension ThemeKey: UITraitBridgedEnvironmentKey {
    static func read(from traits: UITraitCollection) -> Int { traits[ThemeTrait.self] }
    static func write(to traits: inout any UIMutableTraits, value: Int) { traits[ThemeTrait.self] = value }
}

extension EnvironmentValues {
    var themeGeneration: Int {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

extension Color {
    /// A colour of the theme, or iOS's own where there is none.
    init(themed: @escaping @Sendable (Palette) -> UInt32, otherwise: UIColor) {
        self.init(uiColor: UIColor { traits in
            Themes.palette(traits).map { UIColor(hex: themed($0)) } ?? otherwise.resolvedColor(with: traits)
        })
    }

    /// A colour of the theme, or its light and dark values where there is none.
    init(themed: @escaping @Sendable (Palette) -> UInt32, light: UInt32, dark: UInt32) {
        self.init(themed: themed, otherwise: UIColor { $0.userInterfaceStyle == .dark ? UIColor(hex: dark) : UIColor(hex: light) })
    }

    // Surfaces, from the page up, each standing for the iOS colour drawn where there is no theme.
    /// A screen's own: `systemBackground`.
    static let page = Color(themed: \.bg, otherwise: .systemBackground)
    /// A screen of cards: `systemGroupedBackground`.
    static let groupedPage = Color(themed: \.bg, otherwise: .systemGroupedBackground)
    /// A card on a screen of them: `secondarySystemGroupedBackground`.
    static let card = Color(themed: \.panel, otherwise: .secondarySystemGroupedBackground)
    /// Something set apart on a page, as a field or a file's header: `secondarySystemBackground`.
    static let raised = Color(themed: \.soft, otherwise: .secondarySystemBackground)
    /// A chip's or a box's fill: `tertiarySystemFill`.
    static let fill = Color(themed: \.soft, otherwise: .tertiarySystemFill)
    /// Within a card: `tertiarySystemGroupedBackground`.
    static let inset = Color(themed: \.soft, otherwise: .tertiarySystemGroupedBackground)
    static let line = Color(themed: \.line, otherwise: .separator)

    // Text.
    static let fg = Color(themed: \.fg, otherwise: .label)
    static let mute = Color(themed: \.mute, otherwise: .secondaryLabel)
    static let faint = Color(themed: \.faint, otherwise: .tertiaryLabel)
}