1use gpui_kit::base::motion::{Easing, Transition};
3use gpui_kit::base::{ColorTokens, PlotMotion, PlotTheme, RadiusTokens, Spring, ThemeAppearance};
4use gpui_kit::{App, BoxShadow, Global, Hsla, SharedString, WindowAppearance, px, rgb};
5use std::time::Duration;
6
7#[derive(Clone, Debug, PartialEq)]
9pub struct Theme {
10 pub name: SharedString,
11 pub appearance: ThemeAppearance,
12 pub background: Hsla,
13 pub surface: Hsla,
14 pub inset: Hsla,
15 pub foreground: Hsla,
16 pub secondary: Hsla,
17 pub bright: Hsla,
18 pub accent: Hsla,
19 pub on_accent: Hsla,
20 pub selection: Hsla,
21 pub border: Hsla,
22 pub danger: Hsla,
23 pub warning: Hsla,
24 pub success: Hsla,
25 pub chart: [Hsla; 5],
26 pub font: SharedString,
27 pub mono_font: SharedString,
28 pub glass_tint: f32,
30 pub reduce_transparency: bool,
32 pub increased_contrast: bool,
34}
35impl Global for Theme {}
36impl Default for Theme {
37 fn default() -> Self {
38 Self::light()
39 }
40}
41
42impl Theme {
43 pub fn light() -> Self {
44 Self {
45 name: "macOS Light".into(),
46 appearance: ThemeAppearance::Light,
47 background: rgb(0xffffff).into(),
48 surface: rgb(0xffffff).into(),
49 inset: rgb(0xf7f7f7).into(),
50 foreground: Hsla::black().opacity(216. / 255.),
51 secondary: Hsla::black().opacity(127. / 255.),
52 bright: rgb(0).into(),
53 accent: rgb(0x007aff).into(),
54 on_accent: rgb(0xffffff).into(),
55 selection: rgb(0xb3d7ff).into(),
56 border: Hsla::from(rgb(0)).opacity(0.12),
57 danger: rgb(0xff3b30).into(),
58 warning: rgb(0xa46100).into(),
59 success: rgb(0x34c759).into(),
60 chart: [
61 rgb(0x007aff).into(),
62 rgb(0xaf52de).into(),
63 rgb(0x30b0c7).into(),
64 rgb(0xff9500).into(),
65 rgb(0x34c759).into(),
66 ],
67 font: ".SystemUIFont".into(),
68 mono_font: default_mono_font(),
69 glass_tint: 0.65,
70 reduce_transparency: false,
71 increased_contrast: false,
72 }
73 }
74 pub fn dark() -> Self {
75 Self {
76 name: "macOS Dark".into(),
77 appearance: ThemeAppearance::Dark,
78 background: rgb(0x1e1e1e).into(),
79 surface: rgb(0x1e1e1e).into(),
80 inset: rgb(0x262626).into(),
81 foreground: Hsla::white().opacity(216. / 255.),
82 secondary: Hsla::white().opacity(140. / 255.),
83 bright: rgb(0xffffff).into(),
84 accent: rgb(0x007aff).into(),
85 on_accent: rgb(0xffffff).into(),
86 selection: rgb(0x3f638b).into(),
87 border: Hsla::from(rgb(0xffffff)).opacity(0.13),
88 danger: rgb(0xff453a).into(),
89 warning: rgb(0xffd60a).into(),
90 success: rgb(0x30d158).into(),
91 chart: [
92 rgb(0x007aff).into(),
93 rgb(0xbf5af2).into(),
94 rgb(0x64d2ff).into(),
95 rgb(0xff9f0a).into(),
96 rgb(0x30d158).into(),
97 ],
98 font: ".SystemUIFont".into(),
99 mono_font: default_mono_font(),
100 glass_tint: 0.65,
101 reduce_transparency: false,
102 increased_contrast: false,
103 }
104 }
105 pub fn from_appearance(appearance: WindowAppearance) -> Self {
106 let mut theme = match appearance {
107 WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(),
108 WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(),
109 };
110 theme.read_accessibility();
111 theme
112 }
113 pub fn system(cx: &App) -> Self {
115 Self::from_appearance(cx.window_appearance())
116 }
117 pub fn is_dark(&self) -> bool {
118 self.appearance == ThemeAppearance::Dark
119 }
120 pub fn with_accent(mut self, accent: Hsla) -> Self {
121 self.accent = accent;
122 self.selection = accent.opacity(if self.is_dark() { 0.32 } else { 0.3 });
123 self.chart[0] = accent;
124 self
125 }
126 pub fn with_glass_tint(mut self, tint: f32) -> Self {
127 self.glass_tint = if tint.is_finite() {
128 tint.clamp(0., 1.)
129 } else {
130 0.65
131 };
132 self
133 }
134 pub fn with_reduce_transparency(mut self, reduce: bool) -> Self {
135 self.reduce_transparency = reduce;
136 self
137 }
138 pub fn with_increased_contrast(mut self, increase: bool) -> Self {
139 self.increased_contrast = increase;
140 self
141 }
142 pub fn normal_fill(&self) -> Hsla {
143 self.foreground.opacity(0.04)
144 }
145 pub fn hover_fill(&self) -> Hsla {
146 self.foreground.opacity(0.07)
147 }
148 pub fn selected_fill(&self) -> Hsla {
149 self.accent
150 .opacity(if self.is_dark() { 0.25 } else { 0.12 })
151 }
152 pub fn pressed_fill(&self) -> Hsla {
153 self.foreground.opacity(0.12)
154 }
155 pub fn control_fill(&self) -> Hsla {
156 self.background.blend(self.foreground.opacity(0.11))
159 }
160 pub fn control_border(&self) -> Hsla {
161 self.foreground.opacity(if self.increased_contrast {
162 0.55
163 } else if self.is_dark() {
164 0.1
165 } else {
166 0.08
167 })
168 }
169 pub fn focus_border(&self) -> Hsla {
170 self.accent
171 }
172 pub fn divider(&self) -> Hsla {
173 self.foreground.opacity(if self.increased_contrast {
174 0.4
175 } else {
176 25. / 255.
177 })
178 }
179 pub fn glass_fill(&self) -> Hsla {
182 if self.reduce_transparency {
183 self.surface
184 } else {
185 self.surface
186 .opacity(0.42 + 0.5 * self.glass_tint.clamp(0., 1.))
187 }
188 }
189 pub fn glass_border(&self) -> Hsla {
190 if self.increased_contrast {
191 self.control_border()
192 } else if self.is_dark() {
193 Hsla::from(rgb(0xffffff)).opacity(0.18)
194 } else {
195 Hsla::from(rgb(0)).opacity(0.1)
196 }
197 }
198 pub fn control_shadow(&self) -> Vec<BoxShadow> {
199 vec![
200 BoxShadow::new(
201 px(0.),
202 px(1.),
203 Hsla::from(rgb(0)).opacity(if self.is_dark() { 0.2 } else { 0.08 }),
204 )
205 .blur_radius(px(2.)),
206 ]
207 }
208 pub fn focus_shadow(&self) -> Vec<BoxShadow> {
209 vec![BoxShadow::new(px(0.), px(0.), self.accent.opacity(0.35)).spread_radius(px(3.))]
210 }
211 pub fn popover_shadow(&self) -> Vec<BoxShadow> {
212 vec![
213 BoxShadow::new(
214 px(0.),
215 px(8.),
216 Hsla::from(rgb(0)).opacity(if self.is_dark() { 0.4 } else { 0.16 }),
217 )
218 .blur_radius(px(28.)),
219 BoxShadow::new(px(0.), px(1.), Hsla::from(rgb(0)).opacity(0.09)).blur_radius(px(4.)),
220 ]
221 }
222 pub fn input_style(&self) -> gpui_kit::base::input::InputEditorStyle {
223 gpui_kit::base::input::InputEditorStyle {
224 foreground: self.foreground,
225 muted_foreground: self.secondary,
226 background: self.surface,
227 border: self.control_border(),
228 selection: self.selection,
229 caret: self.accent,
230 ..Default::default()
231 }
232 }
233 pub fn tokens(&self) -> ColorTokens {
234 ColorTokens {
235 background: self.background,
236 foreground: self.foreground,
237 surface: self.surface,
238 surface_foreground: self.foreground,
239 primary: self.accent,
240 primary_foreground: self.on_accent,
241 secondary: self.surface,
242 secondary_foreground: self.foreground,
243 muted: self.inset,
244 muted_foreground: self.secondary,
245 accent: self.selected_fill(),
246 accent_foreground: self.foreground,
247 destructive: self.danger,
248 destructive_foreground: self.on_accent,
249 border: self.control_border(),
250 input: self.control_border(),
251 ring: self.accent,
252 selection: self.selection,
253 }
254 }
255 pub fn apply(self, cx: &mut App) {
257 crate::system_theme::stop_following(cx);
258 cx.set_window_appearance(Some(if self.is_dark() {
259 WindowAppearance::Dark
260 } else {
261 WindowAppearance::Light
262 }));
263 self.apply_palette(cx);
264 }
265 pub(crate) fn read_accessibility(&mut self) {
266 #[cfg(target_os = "macos")]
267 {
268 let workspace = objc2_app_kit::NSWorkspace::sharedWorkspace();
269 self.reduce_transparency = workspace.accessibilityDisplayShouldReduceTransparency();
270 self.increased_contrast = workspace.accessibilityDisplayShouldIncreaseContrast();
271 }
272 }
273 pub(crate) fn apply_palette(self, cx: &mut App) {
274 self.sync_kit(cx);
275 let base = gpui_kit::base::Theme::global_mut(cx);
276 base.appearance = self.appearance;
277 base.tokens.colors = self.tokens();
278 base.tokens.radius = RadiusTokens {
279 none: px(0.),
280 sm: px(4.),
281 md: px(6.),
282 lg: px(10.),
283 xl: px(16.),
284 full: px(9999.),
285 };
286 base.tokens.shadow.sm = self.control_shadow();
287 base.tokens.shadow.md = self.popover_shadow();
288 base.tokens.shadow.lg = self.popover_shadow();
289 let type_scale = &mut base.tokens.typography;
290 type_scale.sans = self.font.clone();
291 type_scale.mono = self.mono_font.clone();
292 for (token, size) in [
293 (&mut type_scale.xs, 10.),
294 (&mut type_scale.sm, 11.),
295 (&mut type_scale.md, 13.),
296 (&mut type_scale.lg, 15.),
297 (&mut type_scale.xl, 20.),
298 (&mut type_scale.mono_md, 12.),
299 ] {
300 token.size = px(size);
301 token.line_height = px(size * 1.4);
302 }
303 let duration = Duration::from_millis(180);
304 base.plot = PlotTheme::new().with_motion(
305 PlotMotion::default()
306 .with_pointer(Spring::new(duration).with_epsilon(0.1))
307 .with_enter(
308 Transition::new(duration)
309 .easing(Easing::cubic_bezier(0.16, 1., 0.3, 1.).expect("static curve")),
310 )
311 .with_exit(Transition::new(Duration::from_millis(120))),
312 );
313 cx.set_global(self);
314 cx.refresh_windows();
315 }
316 fn sync_kit(&self, cx: &mut App) {
317 use gpui_kit::component::theme::{Theme as KitTheme, ThemeMode};
318 let mode = if self.is_dark() {
319 ThemeMode::Dark
320 } else {
321 ThemeMode::Light
322 };
323 if KitTheme::global(cx).mode != mode {
324 KitTheme::change(mode, None, cx);
325 }
326 KitTheme::update(cx, |kit| {
327 kit.font_family = self.font.clone();
328 kit.mono_font_family = self.mono_font.clone();
329 kit.font_size = px(16.);
331 kit.mono_font_size = px(12.);
332 kit.radius = px(6.);
333 kit.radius_lg = px(16.);
334 kit.shadow = true;
335 kit.focus_ring = true;
336 let c = &mut kit.colors;
337 c.background = self.background;
338 c.foreground = self.foreground;
339 c.border = self.control_border();
340 c.input = self.control_border();
341 c.accent = self.selected_fill();
342 c.accent_foreground = self.foreground;
343 c.primary = self.accent;
344 c.primary_foreground = self.on_accent;
345 c.primary_hover = self.accent.opacity(0.85);
346 c.primary_active = self.accent.opacity(0.7);
347 c.secondary = self.control_fill();
348 c.secondary_foreground = self.foreground;
349 c.secondary_hover = self.hover_fill();
350 c.secondary_active = self.pressed_fill();
351 c.button = self.control_fill();
352 c.button_foreground = self.foreground;
353 c.button_hover = self.hover_fill();
354 c.button_active = self.pressed_fill();
355 c.button_primary = c.primary;
356 c.button_primary_foreground = c.primary_foreground;
357 c.button_primary_hover = c.primary_hover;
358 c.button_primary_active = c.primary_active;
359 c.button_secondary = c.secondary;
360 c.button_secondary_foreground = c.secondary_foreground;
361 c.button_secondary_hover = c.secondary_hover;
362 c.button_secondary_active = c.secondary_active;
363 c.danger = self.danger;
364 c.danger_foreground = self.on_accent;
365 c.danger_hover = self.danger.opacity(0.85);
366 c.danger_active = self.danger.opacity(0.7);
367 c.button_danger = c.danger;
368 c.button_danger_foreground = c.danger_foreground;
369 c.button_danger_hover = c.danger_hover;
370 c.button_danger_active = c.danger_active;
371 c.success = self.success;
372 c.success_foreground = Hsla::from(rgb(0x003b11));
373 c.success_hover = self.success.opacity(0.85);
374 c.success_active = self.success.opacity(0.7);
375 c.button_success = c.success;
376 c.button_success_foreground = c.success_foreground;
377 c.button_success_hover = c.success_hover;
378 c.button_success_active = c.success_active;
379 c.warning = self.warning;
380 c.warning_foreground = if self.is_dark() {
381 self.background
382 } else {
383 self.on_accent
384 };
385 c.warning_hover = self.warning.opacity(0.85);
386 c.warning_active = self.warning.opacity(0.7);
387 c.button_warning = c.warning;
388 c.button_warning_foreground = c.warning_foreground;
389 c.button_warning_hover = c.warning_hover;
390 c.button_warning_active = c.warning_active;
391 c.info = self.accent;
392 c.info_foreground = self.on_accent;
393 c.info_hover = c.primary_hover;
394 c.info_active = c.primary_active;
395 c.button_info = c.info;
396 c.button_info_foreground = c.info_foreground;
397 c.button_info_hover = c.info_hover;
398 c.button_info_active = c.info_active;
399 c.muted = self.inset;
400 c.muted_foreground = self.secondary;
401 c.popover = self.glass_fill();
402 c.popover_foreground = self.foreground;
403 c.group_box = self.inset;
404 c.group_box_foreground = self.foreground;
405 c.accordion = self.surface;
406 c.description_list_label = self.background;
407 c.description_list_label_foreground = self.secondary;
408 c.caret = self.accent;
409 c.selection = self.selection;
410 c.ring = self.accent.opacity(0.4);
411 c.link = self.accent;
412 c.link_hover = self.accent;
413 c.link_active = self.accent.opacity(0.75);
414 c.drag_border = self.accent;
415 c.drop_target = self.selected_fill();
416 c.sidebar = self.glass_fill();
417 c.sidebar_foreground = self.foreground;
418 c.sidebar_accent = self.selected_fill();
419 c.sidebar_accent_foreground = self.foreground;
420 c.sidebar_primary = self.accent;
421 c.sidebar_primary_foreground = self.on_accent;
422 c.sidebar_border = self.divider();
423 c.list = self.surface;
424 c.list_active = self.selected_fill();
425 c.list_active_border = self.accent;
426 c.list_hover = self.hover_fill();
427 c.list_even = self.normal_fill();
428 c.list_head = self.background;
429 c.table = self.surface;
430 c.table_active = self.selected_fill();
431 c.table_active_border = self.accent;
432 c.table_even = self.normal_fill();
433 c.table_head = self.background;
434 c.table_head_foreground = self.secondary;
435 c.table_foot = self.background;
436 c.table_foot_foreground = self.secondary;
437 c.table_hover = self.hover_fill();
438 c.table_row_border = self.divider();
439 c.tab = self.background;
440 c.tab_foreground = self.secondary;
441 c.tab_active = self.surface;
442 c.tab_active_foreground = self.foreground;
443 c.tab_bar = self.background;
444 c.tab_bar_segmented = self.inset;
445 c.title_bar = self.glass_fill();
446 c.title_bar_border = self.divider();
447 c.status_bar = self.background;
448 c.status_bar_border = self.divider();
449 c.skeleton = self.inset;
450 c.progress_bar = self.accent;
451 c.slider_bar = self.accent;
452 c.slider_thumb = Hsla::from(rgb(0xffffff));
453 c.switch = self.control_fill();
454 c.switch_thumb = Hsla::from(rgb(0xffffff));
455 c.scrollbar = self.foreground.opacity(0.);
456 c.scrollbar_thumb = self.foreground.opacity(0.28);
457 c.scrollbar_thumb_hover = self.foreground.opacity(0.48);
458 c.overlay = Hsla::from(rgb(0)).opacity(0.24);
459 c.window_border = self.border;
460 c.chart_1 = self.chart[0];
461 c.chart_2 = self.chart[1];
462 c.chart_3 = self.chart[2];
463 c.chart_4 = self.chart[3];
464 c.chart_5 = self.chart[4];
465 c.chart_bullish = self.success;
466 c.chart_bearish = self.danger;
467 c.chart_grid = self.divider();
468 c.red = self.danger;
469 c.red_light = self.danger.opacity(0.14);
470 c.green = self.success;
471 c.green_light = self.success.opacity(0.14);
472 c.blue = self.accent;
473 c.blue_light = self.selected_fill();
474 c.yellow = self.warning;
475 c.yellow_light = self.warning.opacity(0.14);
476 c.magenta = self.chart[1];
477 c.magenta_light = self.chart[1].opacity(0.14);
478 c.cyan = self.chart[2];
479 c.cyan_light = self.chart[2].opacity(0.14);
480 });
481 }
482}
483pub(crate) fn default_mono_font() -> SharedString {
484 if cfg!(target_os = "macos") {
485 "Menlo".into()
486 } else if cfg!(target_os = "windows") {
487 "Consolas".into()
488 } else {
489 "monospace".into()
490 }
491}
492pub trait ActiveTheme {
493 fn macos(&self) -> &Theme;
494}
495impl ActiveTheme for App {
496 fn macos(&self) -> &Theme {
497 self.global::<Theme>()
498 }
499}
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use gpui_kit::TestAppContext;
504 #[test]
505 fn material_tint_is_bounded_and_accessibility_is_opaque() {
506 let clear = Theme::light().with_glass_tint(-1.);
507 let tinted = Theme::light().with_glass_tint(2.);
508 assert!(clear.glass_fill().a < tinted.glass_fill().a);
509 assert_eq!(tinted.with_reduce_transparency(true).glass_fill().a, 1.);
510 assert_eq!(Theme::dark().with_glass_tint(f32::NAN).glass_tint, 0.65);
511 }
512 #[gpui_kit::test]
513 fn all_three_theme_layers_change_together(cx: &mut TestAppContext) {
514 cx.update(crate::init);
515 for theme in [Theme::dark(), Theme::light()] {
516 cx.update(|cx| {
517 theme.clone().apply(cx);
518 let base = gpui_kit::base::Theme::global(cx);
519 let kit = gpui_kit::component::theme::Theme::global(cx);
520 assert_eq!(base.tokens.colors.primary, theme.accent);
521 assert_eq!(kit.primary, theme.accent);
522 assert_eq!(kit.tokens.primary.color, theme.accent);
523 assert_eq!(base.tokens.radius.md, px(6.));
524 assert_eq!(cx.macos(), &theme);
525 });
526 }
527 }
528}