use std::{f32::consts::PI, sync::OnceLock}; use gpui::{rems, Hsla, Rems, Rgba, Window, WindowAppearance}; #[derive(Clone, Copy, Debug, PartialEq)] pub struct SemanticColors { pub background: Hsla, pub foreground: Hsla, pub card: Hsla, pub card_foreground: Hsla, pub popover: Hsla, pub popover_foreground: Hsla, pub primary: Hsla, pub primary_foreground: Hsla, pub secondary: Hsla, pub secondary_foreground: Hsla, pub muted: Hsla, pub muted_foreground: Hsla, pub accent: Hsla, pub accent_foreground: Hsla, pub destructive: Hsla, pub border: Hsla, pub input: Hsla, pub ring: Hsla, pub chart_1: Hsla, pub chart_2: Hsla, pub chart_3: Hsla, pub chart_4: Hsla, pub chart_5: Hsla, pub sidebar: Hsla, pub sidebar_foreground: Hsla, pub sidebar_primary: Hsla, pub sidebar_primary_foreground: Hsla, pub sidebar_accent: Hsla, pub sidebar_accent_foreground: Hsla, pub sidebar_border: Hsla, pub sidebar_ring: Hsla, } #[derive(Clone, Copy, Debug, PartialEq)] pub struct Theme { pub colors: SemanticColors, pub radius: Rems, } static LIGHT_THEME: OnceLock = OnceLock::new(); static DARK_THEME: OnceLock = OnceLock::new(); impl Theme { pub fn for_appearance(appearance: WindowAppearance) -> &'static Self { match appearance { WindowAppearance::Light | WindowAppearance::VibrantLight => { LIGHT_THEME.get_or_init(Self::light) } WindowAppearance::Dark | WindowAppearance::VibrantDark => { DARK_THEME.get_or_init(Self::dark) } } } fn light() -> Self { Self { colors: SemanticColors { background: oklch(1.0, 0.0, 0.0), foreground: oklch(0.145, 0.0, 0.0), card: oklch(1.0, 0.0, 0.0), card_foreground: oklch(0.145, 0.0, 0.0), popover: oklch(1.0, 0.0, 0.0), popover_foreground: oklch(0.145, 0.0, 0.0), primary: oklch(0.205, 0.0, 0.0), primary_foreground: oklch(0.985, 0.0, 0.0), secondary: oklch(0.97, 0.0, 0.0), secondary_foreground: oklch(0.205, 0.0, 0.0), muted: oklch(0.97, 0.0, 0.0), muted_foreground: oklch(0.556, 0.0, 0.0), accent: oklch(0.97, 0.0, 0.0), accent_foreground: oklch(0.205, 0.0, 0.0), destructive: oklch(0.577, 0.245, 27.325), border: oklch(0.922, 0.0, 0.0), input: oklch(0.922, 0.0, 0.0), ring: oklch(0.708, 0.0, 0.0), chart_1: oklch(0.87, 0.0, 0.0), chart_2: oklch(0.556, 0.0, 0.0), chart_3: oklch(0.439, 0.0, 0.0), chart_4: oklch(0.371, 0.0, 0.0), chart_5: oklch(0.269, 0.0, 0.0), sidebar: oklch(0.985, 0.0, 0.0), sidebar_foreground: oklch(0.145, 0.0, 0.0), sidebar_primary: oklch(0.205, 0.0, 0.0), sidebar_primary_foreground: oklch(0.985, 0.0, 0.0), sidebar_accent: oklch(0.97, 0.0, 0.0), sidebar_accent_foreground: oklch(0.205, 0.0, 0.0), sidebar_border: oklch(0.922, 0.0, 0.0), sidebar_ring: oklch(0.708, 0.0, 0.0), }, radius: rems(0.625), } } fn dark() -> Self { Self { colors: SemanticColors { background: oklch(0.145, 0.0, 0.0), foreground: oklch(0.985, 0.0, 0.0), card: oklch(0.205, 0.0, 0.0), card_foreground: oklch(0.985, 0.0, 0.0), popover: oklch(0.205, 0.0, 0.0), popover_foreground: oklch(0.985, 0.0, 0.0), primary: oklch(0.922, 0.0, 0.0), primary_foreground: oklch(0.205, 0.0, 0.0), secondary: oklch(0.269, 0.0, 0.0), secondary_foreground: oklch(0.985, 0.0, 0.0), muted: oklch(0.269, 0.0, 0.0), muted_foreground: oklch(0.708, 0.0, 0.0), accent: oklch(0.269, 0.0, 0.0), accent_foreground: oklch(0.985, 0.0, 0.0), destructive: oklch(0.704, 0.191, 22.216), border: oklch_with_alpha(1.0, 0.0, 0.0, 0.1), input: oklch_with_alpha(1.0, 0.0, 0.0, 0.15), ring: oklch(0.556, 0.0, 0.0), chart_1: oklch(0.87, 0.0, 0.0), chart_2: oklch(0.556, 0.0, 0.0), chart_3: oklch(0.439, 0.0, 0.0), chart_4: oklch(0.371, 0.0, 0.0), chart_5: oklch(0.269, 0.0, 0.0), sidebar: oklch(0.205, 0.0, 0.0), sidebar_foreground: oklch(0.985, 0.0, 0.0), sidebar_primary: oklch(0.488, 0.243, 264.376), sidebar_primary_foreground: oklch(0.985, 0.0, 0.0), sidebar_accent: oklch(0.269, 0.0, 0.0), sidebar_accent_foreground: oklch(0.985, 0.0, 0.0), sidebar_border: oklch_with_alpha(1.0, 0.0, 0.0, 0.1), sidebar_ring: oklch(0.556, 0.0, 0.0), }, radius: rems(0.625), } } } pub trait ActiveTheme { fn theme(&self) -> &Theme; } impl ActiveTheme for Window { fn theme(&self) -> &Theme { Theme::for_appearance(self.appearance()) } } fn oklch(lightness: f32, chroma: f32, hue_degrees: f32) -> Hsla { oklch_with_alpha(lightness, chroma, hue_degrees, 1.0) } fn oklch_with_alpha(lightness: f32, chroma: f32, hue_degrees: f32, alpha: f32) -> Hsla { let hue_radians = hue_degrees * PI / 180.0; let a = chroma * hue_radians.cos(); let b = chroma * hue_radians.sin(); let l = (lightness + 0.396_337_78 * a + 0.215_803_76 * b).powi(3); let m = (lightness - 0.105_561_346 * a - 0.063_854_17 * b).powi(3); let s = (lightness - 0.089_484_18 * a - 1.291_485_5 * b).powi(3); let red = linear_srgb_to_srgb(4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s); let green = linear_srgb_to_srgb(-1.268_438 * l + 2.609_757_4 * m - 0.341_319_4 * s); let blue = linear_srgb_to_srgb(-0.004_196_086_3 * l - 0.703_418_6 * m + 1.707_614_7 * s); Hsla::from(Rgba { r: red.clamp(0.0, 1.0), g: green.clamp(0.0, 1.0), b: blue.clamp(0.0, 1.0), a: alpha.clamp(0.0, 1.0), }) } fn linear_srgb_to_srgb(channel: f32) -> f32 { if channel >= 0.003_130_8 { 1.055 * channel.powf(1.0 / 2.4) - 0.055 } else { 12.92 * channel } } #[cfg(test)] mod tests { use super::*; #[test] fn converts_neutral_oklch_endpoints() { let black = Rgba::from(oklch(0.0, 0.0, 0.0)); let white = Rgba::from(oklch(1.0, 0.0, 0.0)); assert_eq!( black, Rgba { r: 0.0, g: 0.0, b: 0.0, a: 1.0, } ); assert!((white.r - 1.0).abs() < f32::EPSILON); assert!((white.g - 1.0).abs() < f32::EPSILON); assert!((white.b - 1.0).abs() < f32::EPSILON); assert!((white.a - 1.0).abs() < f32::EPSILON); } #[test] fn keeps_translucent_tokens_translucent() { assert_eq!(Theme::dark().colors.border.a, 0.1); assert_eq!(Theme::dark().colors.input.a, 0.15); } #[test] fn selects_theme_for_system_appearance() { let light = Theme::for_appearance(WindowAppearance::Light); let dark = Theme::for_appearance(WindowAppearance::Dark); assert!(std::ptr::eq(light, Theme::for_appearance(WindowAppearance::VibrantLight))); assert!(std::ptr::eq(dark, Theme::for_appearance(WindowAppearance::VibrantDark))); assert_ne!(light.colors.background, dark.colors.background); } }