Skip to main content

gpui_base/
theme_tokens.rs

1//! Semantic design tokens shared by application-owned components.
2//!
3//! These tokens describe visual roles and scales. They intentionally do not
4//! contain component names such as `button`, `table`, or `sidebar`.
5
6use gpui::{BoxShadow, FontWeight, Hsla, Pixels, SharedString, hsla, point, px, rgb};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
11pub struct SemanticThemeTokens {
12    pub colors: ColorTokens,
13    pub radius: RadiusTokens,
14    pub spacing: SpacingTokens,
15    pub typography: TypographyTokens,
16    pub shadow: ShadowTokens,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
20pub struct ColorTokens {
21    pub background: Hsla,
22    pub foreground: Hsla,
23    pub surface: Hsla,
24    pub surface_foreground: Hsla,
25    pub primary: Hsla,
26    pub primary_foreground: Hsla,
27    pub secondary: Hsla,
28    pub secondary_foreground: Hsla,
29    pub muted: Hsla,
30    pub muted_foreground: Hsla,
31    pub accent: Hsla,
32    pub accent_foreground: Hsla,
33    pub destructive: Hsla,
34    pub destructive_foreground: Hsla,
35    pub border: Hsla,
36    pub input: Hsla,
37    pub ring: Hsla,
38    /// Background painted behind selected text.
39    ///
40    /// Selection quads are painted under the glyphs, so this is a translucent
41    /// wash that leaves the text legible. It carries a serde default so
42    /// palettes written before the token existed still load.
43    #[serde(default = "ColorTokens::default_selection")]
44    pub selection: Hsla,
45}
46
47impl Default for ColorTokens {
48    fn default() -> Self {
49        Self::light()
50    }
51}
52
53impl ColorTokens {
54    /// Default light palette, aligned with gpui-component's Default Light theme.
55    pub fn light() -> Self {
56        Self {
57            background: hsla(0., 0., 1., 1.),
58            foreground: hsla(0., 0., 0.039, 1.),
59            surface: hsla(0., 0., 1., 1.),
60            surface_foreground: hsla(0., 0., 0.039, 1.),
61            primary: hsla(0., 0., 0.09, 1.),
62            primary_foreground: hsla(0., 0., 0.98, 1.),
63            secondary: hsla(0., 0., 0.898, 1.),
64            secondary_foreground: hsla(0., 0., 0.09, 1.),
65            muted: hsla(0., 0., 0.961, 1.),
66            muted_foreground: hsla(0., 0., 0.451, 1.),
67            accent: hsla(0., 0., 0.961, 1.),
68            accent_foreground: hsla(0., 0., 0.09, 1.),
69            destructive: hsla(0., 0.842, 0.602, 1.),
70            destructive_foreground: hsla(0., 0., 0.98, 1.),
71            border: hsla(0., 0., 0.898, 1.),
72            input: hsla(0., 0., 0.898, 1.),
73            ring: hsla(0., 0., 0.639, 1.),
74            selection: Hsla::from(rgb(0x55a0fc)).alpha(0.3),
75        }
76    }
77
78    /// Default dark palette, aligned with gpui-component's Default Dark theme.
79    pub fn dark() -> Self {
80        Self {
81            background: hsla(0., 0., 0.039, 1.),
82            foreground: hsla(0., 0., 0.98, 1.),
83            surface: hsla(0., 0., 0.039, 1.),
84            surface_foreground: hsla(0., 0., 0.98, 1.),
85            primary: hsla(0., 0., 0.98, 1.),
86            primary_foreground: hsla(0., 0., 0.09, 1.),
87            secondary: hsla(0., 0., 0.149, 1.),
88            secondary_foreground: hsla(0., 0., 0.98, 1.),
89            muted: hsla(0., 0., 0.149, 1.),
90            muted_foreground: hsla(0., 0., 0.639, 1.),
91            accent: hsla(0., 0., 0.149, 1.),
92            accent_foreground: hsla(0., 0., 0.98, 1.),
93            destructive: hsla(0., 0.906, 0.708, 1.),
94            destructive_foreground: hsla(0., 0.722, 0.506, 1.),
95            border: hsla(0., 0., 0.149, 1.),
96            input: hsla(0., 0., 47. / 255., 1.),
97            ring: hsla(0., 0., 0.451, 1.),
98            selection: Hsla::from(rgb(0x1d4ed8)).alpha(0.3),
99        }
100    }
101
102    /// The selection color a palette falls back to when it predates the token.
103    fn default_selection() -> Hsla {
104        Self::light().selection
105    }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
109pub struct RadiusTokens {
110    pub none: Pixels,
111    pub sm: Pixels,
112    pub md: Pixels,
113    pub lg: Pixels,
114    pub xl: Pixels,
115    pub full: Pixels,
116}
117
118impl Default for RadiusTokens {
119    fn default() -> Self {
120        Self {
121            none: px(0.),
122            sm: px(3.),
123            md: px(6.),
124            lg: px(8.),
125            xl: px(12.),
126            full: px(9999.),
127        }
128    }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
132pub struct SpacingTokens {
133    pub xxs: Pixels,
134    pub xs: Pixels,
135    pub sm: Pixels,
136    pub md: Pixels,
137    pub lg: Pixels,
138    pub xl: Pixels,
139    pub xxl: Pixels,
140}
141
142impl Default for SpacingTokens {
143    fn default() -> Self {
144        Self {
145            xxs: px(2.),
146            xs: px(4.),
147            sm: px(8.),
148            md: px(12.),
149            lg: px(16.),
150            xl: px(24.),
151            xxl: px(32.),
152        }
153    }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
157pub struct TextStyleToken {
158    pub size: Pixels,
159    pub line_height: Pixels,
160    pub weight: FontWeight,
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
164pub struct TypographyTokens {
165    pub sans: SharedString,
166    pub mono: SharedString,
167    pub xs: TextStyleToken,
168    pub sm: TextStyleToken,
169    pub md: TextStyleToken,
170    pub lg: TextStyleToken,
171    pub xl: TextStyleToken,
172    pub mono_md: TextStyleToken,
173}
174
175impl Default for TypographyTokens {
176    fn default() -> Self {
177        Self {
178            sans: ".SystemUIFont".into(),
179            mono: default_mono_font_family(),
180            xs: text_style(12., 16.),
181            sm: text_style(14., 20.),
182            md: text_style(16., 24.),
183            lg: text_style(18., 28.),
184            xl: text_style(20., 28.),
185            mono_md: text_style(13., 20.),
186        }
187    }
188}
189
190#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
191pub struct ShadowTokens {
192    pub sm: Vec<BoxShadow>,
193    pub md: Vec<BoxShadow>,
194    pub lg: Vec<BoxShadow>,
195}
196
197impl ShadowTokens {
198    pub fn elevations(color: Hsla) -> Self {
199        Self {
200            sm: vec![box_shadow(0., 1., 2., 0., color)],
201            md: vec![box_shadow(0., 4., 8., -2., color)],
202            lg: vec![box_shadow(0., 12., 24., -4., color)],
203        }
204    }
205}
206
207fn text_style(size: f32, line_height: f32) -> TextStyleToken {
208    TextStyleToken {
209        size: px(size),
210        line_height: px(line_height),
211        weight: FontWeight::NORMAL,
212    }
213}
214
215fn default_mono_font_family() -> SharedString {
216    if cfg!(target_os = "macos") {
217        "Menlo".into()
218    } else if cfg!(target_os = "windows") {
219        "Consolas".into()
220    } else {
221        "DejaVu Sans Mono".into()
222    }
223}
224
225fn box_shadow(x: f32, y: f32, blur: f32, spread: f32, color: Hsla) -> BoxShadow {
226    BoxShadow {
227        color,
228        offset: point(px(x), px(y)),
229        blur_radius: px(blur),
230        spread_radius: px(spread),
231        inset: false,
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::ColorTokens;
238
239    #[test]
240    fn default_colors_are_the_light_palette_and_both_palettes_are_readable() {
241        let light = ColorTokens::light();
242        let dark = ColorTokens::dark();
243
244        assert_eq!(ColorTokens::default(), light);
245        assert_eq!(light.background.l, 1.);
246        assert!(light.foreground.l < light.background.l);
247        assert!(dark.background.l < dark.foreground.l);
248        assert_eq!(light.primary.a, 1.);
249        assert_eq!(dark.primary.a, 1.);
250    }
251}