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)]
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 #[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 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 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 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}