feat(desktop): follow system appearance with semantic themes
This commit is contained in:
parent
0b5310a17b
commit
be605d8479
|
|
@ -1,14 +1,20 @@
|
|||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString,
|
||||
TitlebarOptions, Window, WindowBounds, WindowOptions,
|
||||
App, Application, Bounds, Context, SharedString, TitlebarOptions, Window, WindowBounds,
|
||||
WindowOptions, div, prelude::*, px, size,
|
||||
};
|
||||
|
||||
mod theme;
|
||||
|
||||
use theme::ActiveTheme;
|
||||
|
||||
struct Root {
|
||||
status: SharedString,
|
||||
}
|
||||
|
||||
impl Render for Root {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = window.theme().colors;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
|
|
@ -16,13 +22,13 @@ impl Render for Root {
|
|||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.bg(rgb(0x111111))
|
||||
.text_color(rgb(0xffffff))
|
||||
.bg(colors.background)
|
||||
.text_color(colors.foreground)
|
||||
.child(div().text_xl().child("OpenCut"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(0x888888))
|
||||
.text_color(colors.muted_foreground)
|
||||
.child(self.status.clone()),
|
||||
)
|
||||
}
|
||||
|
|
@ -40,9 +46,16 @@ fn main() {
|
|||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| Root {
|
||||
status: "desktop shell scaffold".into(),
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
cx.observe_window_appearance(window, |_, window, _| {
|
||||
window.refresh();
|
||||
})
|
||||
.detach();
|
||||
|
||||
Root {
|
||||
status: "desktop shell scaffold".into(),
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
use std::{f32::consts::PI, sync::OnceLock};
|
||||
|
||||
use gpui::{Hsla, Rems, Rgba, Window, WindowAppearance, rems};
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue