| 1 | import AppKit |
| 2 | |
| 3 | extension Notification.Name { |
| 4 | static let themeChanged = Notification.Name("themeChanged") |
| 5 | } |
| 6 | |
| 7 | /// App-wide light/dark theme — follows the SYSTEM appearance (no setting). |
| 8 | /// Custom views draw from these; system controls come along for free. |
| 9 | enum Theme { |
| 10 | static var light: Bool { |
| 11 | NSApp.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .aqua |
| 12 | } |
| 13 | |
| 14 | private static var observation: NSKeyValueObservation? |
| 15 | |
| 16 | /// Start following the system appearance; custom views redraw on change. |
| 17 | static func startObserving() { |
| 18 | observation = NSApp.observe(\.effectiveAppearance) { _, _ in |
| 19 | DispatchQueue.main.async { |
| 20 | NotificationCenter.default.post(name: .themeChanged, object: nil) |
| 21 | NotificationCenter.default.post(name: .viewOptionsChanged, object: nil) |
| 22 | } |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | private static func pick(_ dark: NSColor, _ lightC: NSColor) -> NSColor { |
| 27 | light ? lightC : dark |
| 28 | } |
| 29 | |
| 30 | /// Selected-clip outline: white reads well on dark, the accent color on |
| 31 | /// light (white vanishes against light lanes). |
| 32 | static var selection: NSColor { |
| 33 | pick(.white, .controlAccentColor) |
| 34 | } |
| 35 | |
| 36 | // Surfaces |
| 37 | static var timelineBg: NSColor { |
| 38 | pick(NSColor(calibratedWhite: 0.10, alpha: 1), NSColor(calibratedWhite: 0.91, alpha: 1)) |
| 39 | } |
| 40 | static var laneBg: NSColor { |
| 41 | pick(NSColor(calibratedWhite: 0.145, alpha: 1), NSColor(calibratedWhite: 0.85, alpha: 1)) |
| 42 | } |
| 43 | /// Storyboard track lane — a cool tint so it reads as its own strip apart |
| 44 | /// from the neutral video/audio lanes. |
| 45 | static var storyboardLaneBg: NSColor { |
| 46 | pick(NSColor(calibratedHue: 0.60, saturation: 0.28, brightness: 0.20, alpha: 1), |
| 47 | NSColor(calibratedHue: 0.60, saturation: 0.14, brightness: 0.78, alpha: 1)) |
| 48 | } |
| 49 | static var rulerBg: NSColor { |
| 50 | pick(NSColor(calibratedWhite: 0.13, alpha: 1), NSColor(calibratedWhite: 0.88, alpha: 1)) |
| 51 | } |
| 52 | static var barBg: NSColor { |
| 53 | pick(NSColor(calibratedWhite: 0.13, alpha: 1), NSColor(calibratedWhite: 0.93, alpha: 1)) |
| 54 | } |
| 55 | static var viewerBg: NSColor { |
| 56 | pick(NSColor(calibratedWhite: 0.06, alpha: 1), NSColor(calibratedWhite: 0.80, alpha: 1)) |
| 57 | } |
| 58 | static var canvasBg: NSColor { |
| 59 | pick(NSColor(calibratedWhite: 0.12, alpha: 1), NSColor(calibratedWhite: 0.82, alpha: 1)) |
| 60 | } |
| 61 | |
| 62 | // Lines & text |
| 63 | static var rulerLine: NSColor { |
| 64 | pick(NSColor(calibratedWhite: 0.22, alpha: 1), NSColor(calibratedWhite: 0.62, alpha: 1)) |
| 65 | } |
| 66 | static var tickMajor: NSColor { |
| 67 | pick(NSColor(calibratedWhite: 0.35, alpha: 1), NSColor(calibratedWhite: 0.45, alpha: 1)) |
| 68 | } |
| 69 | static var tickMinor: NSColor { |
| 70 | pick(NSColor(calibratedWhite: 0.24, alpha: 1), NSColor(calibratedWhite: 0.68, alpha: 1)) |
| 71 | } |
| 72 | static var label: NSColor { |
| 73 | pick(NSColor(calibratedWhite: 0.9, alpha: 1), NSColor(calibratedWhite: 0.12, alpha: 1)) |
| 74 | } |
| 75 | static var subtleLabel: NSColor { |
| 76 | pick(NSColor(calibratedWhite: 0.55, alpha: 1), NSColor(calibratedWhite: 0.40, alpha: 1)) |
| 77 | } |
| 78 | static var faintLabel: NSColor { |
| 79 | pick(NSColor(calibratedWhite: 0.5, alpha: 1), NSColor(calibratedWhite: 0.45, alpha: 1)) |
| 80 | } |
| 81 | static var clipTitle: NSColor { |
| 82 | NSColor(calibratedWhite: 0.92, alpha: 1) // titles sit on dark strips in both modes |
| 83 | } |
| 84 | static var dragHint: NSColor { |
| 85 | pick(NSColor(calibratedWhite: 0.3, alpha: 1), NSColor(calibratedWhite: 0.55, alpha: 1)) |
| 86 | } |
| 87 | |
| 88 | // Optimization strip (the thin band under the ruler). Muted so a mostly |
| 89 | // red/yellow strip informs rather than alarms. |
| 90 | /// Proxy built at the full preview-quality target. |
| 91 | static var stripFull: NSColor { |
| 92 | pick(NSColor(calibratedHue: 0.36, saturation: 0.70, brightness: 0.62, alpha: 1), |
| 93 | NSColor(calibratedHue: 0.36, saturation: 0.65, brightness: 0.60, alpha: 1)) |
| 94 | } |
| 95 | /// Proxy built below target — usable now, an upgrade is still queued. |
| 96 | static var stripUsable: NSColor { |
| 97 | pick(NSColor(calibratedHue: 0.36, saturation: 0.50, brightness: 0.42, alpha: 1), |
| 98 | NSColor(calibratedHue: 0.36, saturation: 0.40, brightness: 0.72, alpha: 1)) |
| 99 | } |
| 100 | /// Building right now (or a rescue slice standing in). |
| 101 | static var stripBuilding: NSColor { |
| 102 | pick(NSColor(calibratedHue: 0.13, saturation: 0.80, brightness: 0.72, alpha: 1), |
| 103 | NSColor(calibratedHue: 0.13, saturation: 0.85, brightness: 0.80, alpha: 1)) |
| 104 | } |
| 105 | /// No proxy on disk (never built, evicted, or failed). |
| 106 | static var stripMissing: NSColor { |
| 107 | pick(NSColor(calibratedHue: 0.01, saturation: 0.65, brightness: 0.48, alpha: 1), |
| 108 | NSColor(calibratedHue: 0.01, saturation: 0.55, brightness: 0.75, alpha: 1)) |
| 109 | } |
| 110 | } |