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