1use 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 #[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 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 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 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 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}