Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 134 additions & 8 deletions ora/Common/Constants/Theme.swift
Original file line number Diff line number Diff line change
@@ -1,28 +1,107 @@
import AppKit
import SwiftUI

// swiftlint:disable identifier_name
extension Notification.Name {
static let colorThemeChanged = Notification.Name("colorThemeChanged")
static let customColorChanged = Notification.Name("customColorChanged")
}

struct ThemeConstants {
static let colorTransitionDuration: Double = 0.3
static let colorThemeUserDefaultsKey = "ColorTheme"
static let customColorLightKey = "CustomColorLight"
static let customColorDarkKey = "CustomColorDark"
}

enum ColorTheme: String, CaseIterable, Identifiable {
case orange = "Orange"
case blue = "Blue"
case green = "Green"
case purple = "Purple"
case red = "Red"
case teal = "Teal"
case gray = "Gray"
case custom = "Custom"

var id: String { rawValue }

var primaryLight: Color {
switch self {
case .orange: return Color(hex: "f3e5d6")
case .blue: return Color(hex: "d6e5f3")
case .green: return Color(hex: "d6f3e5")
case .purple: return Color(hex: "e5d6f3")
case .red: return Color(hex: "f3d6d6")
case .teal: return Color(hex: "d6f3f0")
case .gray: return Color(hex: "e5e5e5")
case .custom:
// Load custom color from UserDefaults and soften it for light mode
if let hexString = UserDefaults.standard.string(forKey: ThemeConstants.customColorLightKey) {
let originalColor = Color(hex: hexString)
// Soften the color: increase brightness, reduce saturation
return originalColor.adjusted(brightness: 1.3, saturation: 0.6)
}
return Color(hex: "f3e5d6") // fallback to orange
}
}

var primaryDark: Color {
switch self {
case .orange: return Color(hex: "63411D")
case .blue: return Color(hex: "1D4163")
case .green: return Color(hex: "1D6341")
case .purple: return Color(hex: "411D63")
case .red: return Color(hex: "631D1D")
case .teal: return Color(hex: "1D6360")
case .gray: return Color(hex: "414141")
case .custom:
// Load custom color from UserDefaults
if let hexString = UserDefaults.standard.string(forKey: ThemeConstants.customColorDarkKey) {
return Color(hex: hexString)
}
return Color(hex: "63411D") // fallback to orange
}
}

/// Returns the appropriate foreground color for the primary light background
var primaryLightForeground: Color {
primaryLight.adaptiveForeground
}

/// Returns the appropriate foreground color for the primary dark background
var primaryDarkForeground: Color {
primaryDark.adaptiveForeground
}
}

struct Theme: Equatable {
let colorScheme: ColorScheme
let colorTheme: ColorTheme
let updateTrigger: Int

var primary: Color {
Color(hex: "f3e5d6")
colorTheme.primaryLight
}

var primaryDark: Color {
Color(hex: "63411D")
colorTheme.primaryDark
}

var accent: Color {
Color(hex: "FF5F57")
if colorTheme == .custom {
// Use a subtle version of the custom color as accent
let customColor = colorTheme.primaryLight
return customColor.adjusted(brightness: 1.1, saturation: 0.9)
}
return Color(hex: "FF5F57")
}

var background: Color {
colorScheme == .dark ? Color(hex: "#141414") : .white
}

var foreground: Color {
colorScheme == .dark ? .white : .black
background.adaptiveForeground
}

var subtleWindowBackgroundColor: Color {
Expand Down Expand Up @@ -77,6 +156,28 @@ struct Theme: Equatable {
Color(hex: "#93DA97")
}

// MARK: - Adaptive Foreground Colors

/// Foreground color that adapts to the solid window background
var solidWindowForeground: Color {
solidWindowBackgroundColor.adaptiveForeground
}

/// Foreground color that adapts to the inverted solid window background
var invertedSolidWindowForeground: Color {
invertedSolidWindowBackgroundColor.adaptiveForeground
}

/// Foreground color that adapts to the active tab background
var activeTabForeground: Color {
activeTabBackground.adaptiveForeground
}

/// Foreground color that adapts to the muted background
var mutedBackgroundForeground: Color {
mutedBackground.adaptiveForeground
}

var warning: Color {
Color(hex: "#FFBF78")
}
Expand Down Expand Up @@ -110,7 +211,7 @@ struct Theme: Equatable {
Color(hex: "#FF4500")
}

var x: Color {
var xPlatform: Color {
colorScheme == .dark ? .white : .black
}

Expand All @@ -128,7 +229,7 @@ struct Theme: Equatable {
}

private struct ThemeKey: EnvironmentKey {
static let defaultValue = Theme(colorScheme: .light) // fallback
static let defaultValue = Theme(colorScheme: .light, colorTheme: .orange, updateTrigger: 0) // fallback
}

extension EnvironmentValues {
Expand All @@ -140,8 +241,33 @@ extension EnvironmentValues {

struct ThemeProvider: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
@State private var colorTheme: ColorTheme = {
// Load initial value synchronously
let saved = UserDefaults.standard.string(forKey: ThemeConstants.colorThemeUserDefaultsKey)
return ColorTheme(rawValue: saved ?? "") ?? .orange
}()
@State private var customColorUpdateTrigger = 0

func body(content: Content) -> some View {
content.environment(\.theme, Theme(colorScheme: colorScheme))
content
.environment(
\.theme,
Theme(colorScheme: colorScheme, colorTheme: colorTheme, updateTrigger: customColorUpdateTrigger)
)
.onReceive(NotificationCenter.default.publisher(for: .colorThemeChanged)) { notification in
if let newTheme = notification.object as? ColorTheme {
withAnimation(.easeInOut(duration: ThemeConstants.colorTransitionDuration)) {
colorTheme = newTheme
}
}
}
.onReceive(NotificationCenter.default.publisher(for: .customColorChanged)) { _ in
// Trigger theme update when custom colors change
if colorTheme == .custom {
withAnimation(.easeInOut(duration: ThemeConstants.colorTransitionDuration)) {
customColorUpdateTrigger += 1
}
}
}
}
}
62 changes: 60 additions & 2 deletions ora/Common/Extensions/Color+Hex.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI

// swiftlint:disable identifier_name
extension Color {
// swiftlint:disable identifier_name
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Expand All @@ -26,21 +26,79 @@ extension Color {
opacity: Double(a) / 255
)
}
// swiftlint:enable identifier_name

/// Converts the Color to a hex string in `#RRGGBB` or `#RRGGBBAA` format
func toHex(includeAlpha: Bool = false) -> String? {
let nsColor = NSColor(self)
guard let rgbColor = nsColor.usingColorSpace(.sRGB) else {
return nil
}

// swiftlint:disable identifier_name
let r = Int(rgbColor.redComponent * 255)
let g = Int(rgbColor.greenComponent * 255)
let b = Int(rgbColor.blueComponent * 255)
let a = Int(rgbColor.alphaComponent * 255)
// swiftlint:enable identifier_name

return includeAlpha
? String(format: "#%02X%02X%02X%02X", r, g, b, a)
: String(format: "#%02X%02X%02X", r, g, b)
}

/// Adjusts the brightness and saturation of a color
func adjusted(brightness: Double = 1.0, saturation: Double = 1.0) -> Color {
let nsColor = NSColor(self)
guard let rgbColor = nsColor.usingColorSpace(.sRGB) else {
return self
}
// swiftlint:disable identifier_name
var h: CGFloat = 0
var s: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
// swiftlint:enable identifier_name
rgbColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a)

return Color(
hue: Double(h),
saturation: min(1.0, Double(s) * saturation),
brightness: min(1.0, Double(b) * brightness)
).opacity(Double(a))
}

/// Calculates the relative luminance of a color using the WCAG formula
var luminance: Double {
let nsColor = NSColor(self)
guard let rgbColor = nsColor.usingColorSpace(.sRGB) else {
return 0.5 // fallback to medium luminance
}

// Convert to linear RGB
func linearize(_ component: Double) -> Double {
if component <= 0.03928 {
return component / 12.92
} else {
return pow((component + 0.055) / 1.055, 2.4)
}
}
// swiftlint:disable identifier_name
let r = linearize(rgbColor.redComponent)
let g = linearize(rgbColor.greenComponent)
let b = linearize(rgbColor.blueComponent)
// swiftlint:enable identifier_name

// WCAG luminance formula
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}

/// Returns an appropriate foreground color (black or white) based on the background's luminance
var adaptiveForeground: Color {
return luminance > 0.5 ? .black : .white
}

/// Returns a contrasting foreground color with optional opacity
func contrastingForeground(opacity: Double = 1.0) -> Color {
return adaptiveForeground.opacity(opacity)
}
}
2 changes: 1 addition & 1 deletion ora/Models/SearchEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ extension SearchEngine {
return LauncherMain.Match(
text: name,
color: color,
foregroundColor: foregroundColor ?? .white,
foregroundColor: foregroundColor ?? color.adaptiveForeground,
icon: icon,
originalAlias: originalAlias,
searchURL: searchURL,
Expand Down
Loading