OpenCut/apps/desktop/src/theme.rs

223 lines
7.7 KiB
Rust

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<Theme> = OnceLock::new();
static DARK_THEME: OnceLock<Theme> = 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);
}
}