Files
onyx/Packages/OnyxKit/Sources/OnyxDesign/OnyxTheme.swift
Scarriffle 2aa60eedf9 Farben einstellbar — und erst mal überhaupt eine Farbe
Das Grün, das überall auftauchte, war die System-Akzentfarbe: `.tint` und
`.accentColor` werden ohne eigene Angabe dorthin durchgereicht, und das
betraf alle Popovers und das ganze Einstellungsfenster. Die Notch-Widgets
dagegen benutzten Onyx' eigenes Blau. Zwei Farben, ungewollt.

Jetzt eine, und die ist einstellbar. Ein neuer Reiter „Farben": Akzentfarbe
mit sechs Vorschlägen und einem Knopf, der die Systemfarbe übernimmt, dazu
das Paar für Herunter- und Hochladen samt Tauschknopf.

Die Signalfarben bleiben fest und stehen nur zur Ansicht dort. Gelb heißt
Warnung, Rot heißt kritisch, Grün heißt in Ordnung — bei Temperatur, Akku,
Lüfter und Fehlern. Wer sie umstellen kann, kann ihre Bedeutung zerstören;
ein rotes „alles gut" liest niemand richtig. Sichtbar sind sie trotzdem,
sonst sucht man die Warnfarbe im Reiter und findet sie nirgends.

Umgesetzt über die Tokens statt über vierzig Fundstellen: `Onyx.Color.accent`
ist keine Konstante mehr, sondern liest bei jedem Zugriff aus `OnyxTheme`.
Damit merkt SwiftUI die Abhängigkeit und zeichnet neu, sobald die Farbe sich
ändert. Dazu ein `.tint` an den vier Wurzeln — Panel, Popovers, Einstellungen,
Einrichtung —, damit auch Haken, Regler und Auswahlfelder mitziehen.

Nebenbei die Beschriftung: „Je Programm" heißt jetzt „Programme". Großgesetzt
las sich das als englisches „J-E".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:24:40 +02:00

107 lines
4.1 KiB
Swift

import SwiftUI
import AppKit
/// Die einstellbaren Farben.
///
/// **Nicht alle Farben sind einstellbar, und das ist Absicht.** Gelb heißt
/// Warnung, Rot heißt kritisch, Grün heißt in Ordnung bei Temperatur, Akku,
/// Lüfter und Fehlermeldungen. Wer sie umstellen kann, kann ihre Bedeutung
/// zerstören; ein rotes alles gut" liest niemand richtig. Einstellbar ist
/// deshalb die Akzentfarbe, mit der Onyx sich selbst zeichnet, und das Paar für
/// Herunter- und Hochladen dort ist die Zuordnung willkürlich und nur eine
/// Gewohnheit.
@MainActor
@Observable
public final class OnyxTheme {
/// Die Instanz, aus der `Onyx.Color` liest.
public static let shared = OnyxTheme()
/// Kühl und entsättigt Farbe ist in Onyx ein Signal, keine Dekoration.
public static let defaultAccent = Color(red: 0.498, green: 0.659, blue: 1.0)
/// Herunterladen grün, Hochladen rosa: die Zuordnung ist reine Gewohnheit,
/// aber eine verbreitete. Wer sie anders kennt, dreht sie um.
public static let defaultDownload = Color(red: 0.435, green: 0.812, blue: 0.592)
public static let defaultUpload = Color(red: 0.937, green: 0.427, blue: 0.612)
public var accent: Color {
didSet { store(accent, as: Keys.accent) }
}
public var download: Color {
didSet { store(download, as: Keys.download) }
}
public var upload: Color {
didSet { store(upload, as: Keys.upload) }
}
private let defaults: UserDefaults
private enum Keys {
static let accent = "onyx.theme.accent"
static let download = "onyx.theme.download"
static let upload = "onyx.theme.upload"
}
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
accent = Color(onyxHex: defaults.string(forKey: Keys.accent)) ?? Self.defaultAccent
download = Color(onyxHex: defaults.string(forKey: Keys.download)) ?? Self.defaultDownload
upload = Color(onyxHex: defaults.string(forKey: Keys.upload)) ?? Self.defaultUpload
}
/// Ob alles auf den Werkseinstellungen steht für den Zurücksetzen-Knopf,
/// der sonst auch dann anböte, etwas zu tun, wenn es nichts zu tun gibt.
public var isDefault: Bool {
accent.onyxHex == Self.defaultAccent.onyxHex
&& download.onyxHex == Self.defaultDownload.onyxHex
&& upload.onyxHex == Self.defaultUpload.onyxHex
}
public func reset() {
accent = Self.defaultAccent
download = Self.defaultDownload
upload = Self.defaultUpload
}
/// Übernimmt die Akzentfarbe des Systems.
///
/// Genau die stand hier vorher an vielen Stellen unfreiwillig, weil
/// `.tint` und `.accentColor` ohne eigene Angabe dorthin durchgereicht
/// werden. Wem das gefiel, soll es behalten können.
public func adoptSystemAccent() {
accent = Color(nsColor: .controlAccentColor)
}
private func store(_ color: Color, as key: String) {
defaults.set(color.onyxHex, forKey: key)
}
}
public extension Color {
/// `#RRGGBB`, immer in sRGB.
///
/// Der Umweg über `NSColor` ist nötig, weil eine `Color` aus dem Farbwähler
/// in einem beliebigen Farbraum liegen kann ohne Umrechnung ergäbe die
/// Zerlegung in Komponenten je nach Bildschirm etwas anderes.
var onyxHex: String {
guard let srgb = NSColor(self).usingColorSpace(.sRGB) else { return "#7FA8FF" }
let red = Int((srgb.redComponent * 255).rounded())
let green = Int((srgb.greenComponent * 255).rounded())
let blue = Int((srgb.blueComponent * 255).rounded())
return String(format: "#%02X%02X%02X", red, green, blue)
}
/// Liest `#RRGGBB` und `RRGGBB`.
init?(onyxHex: String?) {
guard let onyxHex else { return nil }
var text = onyxHex.trimmingCharacters(in: .whitespaces)
if text.hasPrefix("#") { text.removeFirst() }
guard text.count == 6, let value = UInt32(text, radix: 16) else { return nil }
self.init(.sRGB,
red: Double((value >> 16) & 0xFF) / 255,
green: Double((value >> 8) & 0xFF) / 255,
blue: Double(value & 0xFF) / 255)
}
}