1use std::{rc::Rc, sync::Arc};
2
3use gpui::{Background, BoxShadow, FontWeight, Hsla, SharedString, px};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use crate::highlighter::{HighlightTheme, HighlightThemeStyle};
8
9use super::color::{
10 try_parse_background, try_parse_background_clamped, try_parse_color, try_parse_theme_color,
11};
12use super::{Colorize, SemanticThemeTokens, Theme, ThemeColor, ThemeMode, ThemeToken, ThemeTokens};
13
14fn try_parse_theme_token(value: &str) -> anyhow::Result<ThemeToken> {
15 Ok(ThemeToken::new(
16 try_parse_theme_color(value)?,
17 try_parse_background(value)?,
18 ))
19}
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
23#[serde(default)]
24pub struct ThemeSet {
25 pub name: SharedString,
27 pub author: Option<SharedString>,
29 pub url: Option<SharedString>,
31 #[serde(rename = "themes")]
33 pub themes: Vec<ThemeConfig>,
34}
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
37#[serde(default)]
38pub struct ThemeConfig {
39 pub is_default: bool,
41 pub name: SharedString,
43 pub mode: ThemeMode,
45
46 #[serde(rename = "font.size")]
48 pub font_size: Option<f32>,
49 #[serde(rename = "font.family")]
51 pub font_family: Option<SharedString>,
52 #[serde(rename = "mono_font.family")]
61 pub mono_font_family: Option<SharedString>,
62 #[serde(rename = "mono_font.size")]
64 pub mono_font_size: Option<f32>,
65
66 #[serde(rename = "radius")]
68 pub radius: Option<usize>,
69 #[serde(rename = "radius.lg")]
71 pub radius_lg: Option<usize>,
72 #[serde(rename = "shadow")]
74 pub shadow: Option<bool>,
75
76 pub colors: ThemeConfigColors,
78 pub highlight: Option<HighlightThemeStyle>,
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
85#[serde(default)]
86pub struct SemanticThemeConfig {
87 pub colors: SemanticColorConfig,
88 pub radius: SemanticRadiusConfig,
89 pub spacing: SemanticSpacingConfig,
90 pub typography: SemanticTypographyConfig,
91 pub shadow: SemanticShadowConfig,
92}
93
94#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
99#[serde(default)]
100pub struct SemanticThemeConfigFile {
101 pub tokens: SemanticThemeConfig,
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
105#[serde(default)]
106pub struct SemanticColorConfig {
107 pub background: Option<SharedString>,
108 pub foreground: Option<SharedString>,
109 pub surface: Option<SharedString>,
110 pub surface_foreground: Option<SharedString>,
111 pub primary: Option<SharedString>,
112 pub primary_foreground: Option<SharedString>,
113 pub secondary: Option<SharedString>,
114 pub secondary_foreground: Option<SharedString>,
115 pub muted: Option<SharedString>,
116 pub muted_foreground: Option<SharedString>,
117 pub accent: Option<SharedString>,
118 pub accent_foreground: Option<SharedString>,
119 pub destructive: Option<SharedString>,
120 pub destructive_foreground: Option<SharedString>,
121 pub border: Option<SharedString>,
122 pub input: Option<SharedString>,
123 pub ring: Option<SharedString>,
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
127#[serde(default)]
128pub struct SemanticRadiusConfig {
129 pub none: Option<f32>,
130 pub sm: Option<f32>,
131 pub md: Option<f32>,
132 pub lg: Option<f32>,
133 pub xl: Option<f32>,
134 pub full: Option<f32>,
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
138#[serde(default)]
139pub struct SemanticSpacingConfig {
140 pub xxs: Option<f32>,
141 pub xs: Option<f32>,
142 pub sm: Option<f32>,
143 pub md: Option<f32>,
144 pub lg: Option<f32>,
145 pub xl: Option<f32>,
146 pub xxl: Option<f32>,
147}
148
149#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
150#[serde(default)]
151pub struct SemanticTextStyleConfig {
152 pub size: Option<f32>,
153 pub line_height: Option<f32>,
154 pub weight: Option<FontWeight>,
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
158#[serde(default)]
159pub struct SemanticTypographyConfig {
160 pub sans: Option<SharedString>,
161 pub mono: Option<SharedString>,
162 pub xs: SemanticTextStyleConfig,
163 pub sm: SemanticTextStyleConfig,
164 pub md: SemanticTextStyleConfig,
165 pub lg: SemanticTextStyleConfig,
166 pub xl: SemanticTextStyleConfig,
167 pub mono_md: SemanticTextStyleConfig,
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
171#[serde(default)]
172pub struct SemanticShadowConfig {
173 pub sm: Option<Vec<BoxShadow>>,
174 pub md: Option<Vec<BoxShadow>>,
175 pub lg: Option<Vec<BoxShadow>>,
176}
177
178impl SemanticThemeConfig {
179 pub(crate) fn apply_to(&self, tokens: &mut SemanticThemeTokens) {
180 macro_rules! apply_color {
181 ($field:ident) => {
182 if let Some(value) = &self.colors.$field
183 && let Ok(value) = try_parse_color(value)
184 {
185 tokens.colors.$field = value;
186 }
187 };
188 }
189 apply_color!(background);
190 apply_color!(foreground);
191 apply_color!(surface);
192 apply_color!(surface_foreground);
193 apply_color!(primary);
194 apply_color!(primary_foreground);
195 apply_color!(secondary);
196 apply_color!(secondary_foreground);
197 apply_color!(muted);
198 apply_color!(muted_foreground);
199 apply_color!(accent);
200 apply_color!(accent_foreground);
201 apply_color!(destructive);
202 apply_color!(destructive_foreground);
203 apply_color!(border);
204 apply_color!(input);
205 apply_color!(ring);
206
207 macro_rules! apply_pixels {
208 ($config:expr, $tokens:expr, $($field:ident),+ $(,)?) => {
209 $(if let Some(value) = $config.$field { $tokens.$field = px(value); })+
210 };
211 }
212 apply_pixels!(self.radius, tokens.radius, none, sm, md, lg, xl, full);
213 apply_pixels!(self.spacing, tokens.spacing, xxs, xs, sm, md, lg, xl, xxl);
214
215 if let Some(value) = &self.typography.sans {
216 tokens.typography.sans = value.clone();
217 }
218 if let Some(value) = &self.typography.mono {
219 tokens.typography.mono = value.clone();
220 }
221 apply_text_style(&self.typography.xs, &mut tokens.typography.xs);
222 apply_text_style(&self.typography.sm, &mut tokens.typography.sm);
223 apply_text_style(&self.typography.md, &mut tokens.typography.md);
224 apply_text_style(&self.typography.lg, &mut tokens.typography.lg);
225 apply_text_style(&self.typography.xl, &mut tokens.typography.xl);
226 apply_text_style(&self.typography.mono_md, &mut tokens.typography.mono_md);
227
228 if let Some(value) = &self.shadow.sm {
229 tokens.shadow.sm = value.clone();
230 }
231 if let Some(value) = &self.shadow.md {
232 tokens.shadow.md = value.clone();
233 }
234 if let Some(value) = &self.shadow.lg {
235 tokens.shadow.lg = value.clone();
236 }
237 }
238}
239
240fn apply_text_style(config: &SemanticTextStyleConfig, token: &mut gpui_base::TextStyleToken) {
241 if let Some(value) = config.size {
242 token.size = px(value);
243 }
244 if let Some(value) = config.line_height {
245 token.line_height = px(value);
246 }
247 if let Some(value) = config.weight {
248 token.weight = value;
249 }
250}
251
252#[derive(Debug, Default, Clone, JsonSchema, Serialize, Deserialize)]
253pub struct ThemeConfigColors {
254 #[serde(rename = "accent.background")]
256 pub accent: Option<SharedString>,
257 #[serde(rename = "accent.foreground")]
259 pub accent_foreground: Option<SharedString>,
260 #[serde(rename = "accordion.background")]
262 pub accordion: Option<SharedString>,
263 #[serde(rename = "background")]
265 pub background: Option<SharedString>,
266 #[serde(rename = "border")]
268 pub border: Option<SharedString>,
269 #[serde(rename = "button.background")]
271 pub button: Option<SharedString>,
272 #[serde(rename = "button.active.background")]
274 pub button_active: Option<SharedString>,
275 #[serde(rename = "button.foreground")]
277 pub button_foreground: Option<SharedString>,
278 #[serde(rename = "button.hover.background")]
280 pub button_hover: Option<SharedString>,
281 #[serde(rename = "button.danger.background")]
283 pub button_danger: Option<SharedString>,
284 #[serde(rename = "button.danger.active.background")]
286 pub button_danger_active: Option<SharedString>,
287 #[serde(rename = "button.danger.foreground")]
289 pub button_danger_foreground: Option<SharedString>,
290 #[serde(rename = "button.danger.hover.background")]
292 pub button_danger_hover: Option<SharedString>,
293 #[serde(rename = "button.info.background")]
295 pub button_info: Option<SharedString>,
296 #[serde(rename = "button.info.active.background")]
298 pub button_info_active: Option<SharedString>,
299 #[serde(rename = "button.info.foreground")]
301 pub button_info_foreground: Option<SharedString>,
302 #[serde(rename = "button.info.hover.background")]
304 pub button_info_hover: Option<SharedString>,
305 #[serde(rename = "button.primary.background")]
307 pub button_primary: Option<SharedString>,
308 #[serde(rename = "button.primary.active.background")]
310 pub button_primary_active: Option<SharedString>,
311 #[serde(rename = "button.primary.foreground")]
313 pub button_primary_foreground: Option<SharedString>,
314 #[serde(rename = "button.primary.hover.background")]
316 pub button_primary_hover: Option<SharedString>,
317 #[serde(rename = "button.secondary.background")]
319 pub button_secondary: Option<SharedString>,
320 #[serde(rename = "button.secondary.active.background")]
322 pub button_secondary_active: Option<SharedString>,
323 #[serde(rename = "button.secondary.foreground")]
325 pub button_secondary_foreground: Option<SharedString>,
326 #[serde(rename = "button.secondary.hover.background")]
328 pub button_secondary_hover: Option<SharedString>,
329 #[serde(rename = "button.success.background")]
331 pub button_success: Option<SharedString>,
332 #[serde(rename = "button.success.active.background")]
334 pub button_success_active: Option<SharedString>,
335 #[serde(rename = "button.success.foreground")]
337 pub button_success_foreground: Option<SharedString>,
338 #[serde(rename = "button.success.hover.background")]
340 pub button_success_hover: Option<SharedString>,
341 #[serde(rename = "button.warning.background")]
343 pub button_warning: Option<SharedString>,
344 #[serde(rename = "button.warning.active.background")]
346 pub button_warning_active: Option<SharedString>,
347 #[serde(rename = "button.warning.foreground")]
349 pub button_warning_foreground: Option<SharedString>,
350 #[serde(rename = "button.warning.hover.background")]
352 pub button_warning_hover: Option<SharedString>,
353 #[serde(rename = "group_box.background")]
355 pub group_box: Option<SharedString>,
356 #[serde(rename = "group_box.foreground")]
358 pub group_box_foreground: Option<SharedString>,
359 #[serde(rename = "group_box.title.foreground")]
361 pub group_box_title_foreground: Option<SharedString>,
362 #[serde(rename = "caret")]
364 pub caret: Option<SharedString>,
365 #[serde(rename = "chart.1")]
367 pub chart_1: Option<SharedString>,
368 #[serde(rename = "chart.2")]
370 pub chart_2: Option<SharedString>,
371 #[serde(rename = "chart.3")]
373 pub chart_3: Option<SharedString>,
374 #[serde(rename = "chart.4")]
376 pub chart_4: Option<SharedString>,
377 #[serde(rename = "chart.5")]
379 pub chart_5: Option<SharedString>,
380 #[serde(rename = "chart_bullish")]
382 pub chart_bullish: Option<SharedString>,
383 #[serde(rename = "chart_bearish")]
385 pub chart_bearish: Option<SharedString>,
386 #[serde(rename = "danger.background")]
388 pub danger: Option<SharedString>,
389 #[serde(rename = "danger.active.background")]
391 pub danger_active: Option<SharedString>,
392 #[serde(rename = "danger.foreground")]
394 pub danger_foreground: Option<SharedString>,
395 #[serde(rename = "danger.hover.background")]
397 pub danger_hover: Option<SharedString>,
398 #[serde(rename = "description_list.label.background")]
400 pub description_list_label: Option<SharedString>,
401 #[serde(rename = "description_list.label.foreground")]
403 pub description_list_label_foreground: Option<SharedString>,
404 #[serde(rename = "drag.border")]
406 pub drag_border: Option<SharedString>,
407 #[serde(rename = "drop_target.background")]
409 pub drop_target: Option<SharedString>,
410 #[serde(rename = "foreground")]
412 pub foreground: Option<SharedString>,
413 #[serde(rename = "info.background")]
415 pub info: Option<SharedString>,
416 #[serde(rename = "info.active.background")]
418 pub info_active: Option<SharedString>,
419 #[serde(rename = "info.foreground")]
421 pub info_foreground: Option<SharedString>,
422 #[serde(rename = "info.hover.background")]
424 pub info_hover: Option<SharedString>,
425 #[serde(rename = "input.border")]
427 pub input: Option<SharedString>,
428 #[serde(rename = "link")]
430 pub link: Option<SharedString>,
431 #[serde(rename = "link.active")]
433 pub link_active: Option<SharedString>,
434 #[serde(rename = "link.hover")]
436 pub link_hover: Option<SharedString>,
437 #[serde(rename = "list.background")]
439 pub list: Option<SharedString>,
440 #[serde(rename = "list.active.background")]
442 pub list_active: Option<SharedString>,
443 #[serde(rename = "list.active.border")]
445 pub list_active_border: Option<SharedString>,
446 #[serde(rename = "list.even.background")]
448 pub list_even: Option<SharedString>,
449 #[serde(rename = "list.head.background")]
451 pub list_head: Option<SharedString>,
452 #[serde(rename = "list.hover.background")]
454 pub list_hover: Option<SharedString>,
455 #[serde(rename = "muted.background")]
457 pub muted: Option<SharedString>,
458 #[serde(rename = "muted.foreground")]
460 pub muted_foreground: Option<SharedString>,
461 #[serde(rename = "popover.background")]
463 pub popover: Option<SharedString>,
464 #[serde(rename = "popover.foreground")]
466 pub popover_foreground: Option<SharedString>,
467 #[serde(rename = "primary.background")]
469 pub primary: Option<SharedString>,
470 #[serde(rename = "primary.active.background")]
472 pub primary_active: Option<SharedString>,
473 #[serde(rename = "primary.foreground")]
475 pub primary_foreground: Option<SharedString>,
476 #[serde(rename = "primary.hover.background")]
478 pub primary_hover: Option<SharedString>,
479 #[serde(rename = "progress.bar.background")]
481 pub progress_bar: Option<SharedString>,
482 #[serde(rename = "ring")]
484 pub ring: Option<SharedString>,
485 #[serde(rename = "scrollbar.background")]
487 pub scrollbar: Option<SharedString>,
488 #[serde(rename = "scrollbar.thumb.background")]
490 pub scrollbar_thumb: Option<SharedString>,
491 #[serde(rename = "scrollbar.thumb.hover.background")]
493 pub scrollbar_thumb_hover: Option<SharedString>,
494 #[serde(rename = "secondary.background")]
496 pub secondary: Option<SharedString>,
497 #[serde(rename = "secondary.active.background")]
499 pub secondary_active: Option<SharedString>,
500 #[serde(rename = "secondary.foreground")]
502 pub secondary_foreground: Option<SharedString>,
503 #[serde(rename = "secondary.hover.background")]
505 pub secondary_hover: Option<SharedString>,
506 #[serde(rename = "selection.background")]
508 pub selection: Option<SharedString>,
509 #[serde(rename = "sidebar.background")]
511 pub sidebar: Option<SharedString>,
512 #[serde(rename = "sidebar.accent.background")]
514 pub sidebar_accent: Option<SharedString>,
515 #[serde(rename = "sidebar.accent.foreground")]
517 pub sidebar_accent_foreground: Option<SharedString>,
518 #[serde(rename = "sidebar.border")]
520 pub sidebar_border: Option<SharedString>,
521 #[serde(rename = "sidebar.foreground")]
523 pub sidebar_foreground: Option<SharedString>,
524 #[serde(rename = "sidebar.primary.background")]
526 pub sidebar_primary: Option<SharedString>,
527 #[serde(rename = "sidebar.primary.foreground")]
529 pub sidebar_primary_foreground: Option<SharedString>,
530 #[serde(rename = "skeleton.background")]
532 pub skeleton: Option<SharedString>,
533 #[serde(rename = "slider.background")]
535 pub slider_bar: Option<SharedString>,
536 #[serde(rename = "slider.thumb.background")]
538 pub slider_thumb: Option<SharedString>,
539 #[serde(rename = "success.background")]
541 pub success: Option<SharedString>,
542 #[serde(rename = "success.foreground")]
544 pub success_foreground: Option<SharedString>,
545 #[serde(rename = "success.hover.background")]
547 pub success_hover: Option<SharedString>,
548 #[serde(rename = "success.active.background")]
550 pub success_active: Option<SharedString>,
551 #[serde(rename = "switch.background")]
553 pub switch: Option<SharedString>,
554 #[serde(rename = "switch.thumb.background")]
556 pub switch_thumb: Option<SharedString>,
557 #[serde(rename = "tab.background")]
559 pub tab: Option<SharedString>,
560 #[serde(rename = "tab.active.background")]
562 pub tab_active: Option<SharedString>,
563 #[serde(rename = "tab.active.foreground")]
565 pub tab_active_foreground: Option<SharedString>,
566 #[serde(rename = "tab_bar.background")]
568 pub tab_bar: Option<SharedString>,
569 #[serde(rename = "tab_bar.segmented.background")]
571 pub tab_bar_segmented: Option<SharedString>,
572 #[serde(rename = "tab.foreground")]
574 pub tab_foreground: Option<SharedString>,
575 #[serde(rename = "table.background")]
577 pub table: Option<SharedString>,
578 #[serde(rename = "table.active.background")]
580 pub table_active: Option<SharedString>,
581 #[serde(rename = "table.active.border")]
583 pub table_active_border: Option<SharedString>,
584 #[serde(rename = "table.even.background")]
586 pub table_even: Option<SharedString>,
587 #[serde(rename = "table.head.background")]
589 pub table_head: Option<SharedString>,
590 #[serde(rename = "table.head.foreground")]
592 pub table_head_foreground: Option<SharedString>,
593 #[serde(rename = "table.foot.background")]
595 pub table_foot: Option<SharedString>,
596 #[serde(rename = "table.foot.foreground")]
598 pub table_foot_foreground: Option<SharedString>,
599 #[serde(rename = "table.hover.background")]
601 pub table_hover: Option<SharedString>,
602 #[serde(rename = "table.row.border")]
604 pub table_row_border: Option<SharedString>,
605 #[serde(rename = "title_bar.background")]
607 pub title_bar: Option<SharedString>,
608 #[serde(rename = "title_bar.border")]
610 pub title_bar_border: Option<SharedString>,
611 #[serde(rename = "status_bar.background")]
613 pub status_bar: Option<SharedString>,
614 #[serde(rename = "status_bar.border")]
616 pub status_bar_border: Option<SharedString>,
617 #[serde(rename = "tiles.background")]
619 pub tiles: Option<SharedString>,
620 #[serde(rename = "warning.background")]
622 pub warning: Option<SharedString>,
623 #[serde(rename = "warning.active.background")]
625 pub warning_active: Option<SharedString>,
626 #[serde(rename = "warning.hover.background")]
628 pub warning_hover: Option<SharedString>,
629 #[serde(rename = "warning.foreground")]
631 pub warning_foreground: Option<SharedString>,
632 #[serde(rename = "overlay")]
634 pub overlay: Option<SharedString>,
635 #[serde(rename = "window.border")]
641 pub window_border: Option<SharedString>,
642
643 #[serde(rename = "base.blue")]
645 blue: Option<String>,
646 #[serde(rename = "base.blue.light")]
648 blue_light: Option<String>,
649 #[serde(rename = "base.cyan")]
651 cyan: Option<String>,
652 #[serde(rename = "base.cyan.light")]
654 cyan_light: Option<String>,
655 #[serde(rename = "base.green")]
657 green: Option<String>,
658 #[serde(rename = "base.green.light")]
660 green_light: Option<String>,
661 #[serde(rename = "base.magenta")]
663 magenta: Option<String>,
664 #[serde(rename = "base.magenta.light")]
665 magenta_light: Option<String>,
666 #[serde(rename = "base.red")]
668 red: Option<String>,
669 #[serde(rename = "base.red.light")]
671 red_light: Option<String>,
672 #[serde(rename = "base.yellow")]
674 yellow: Option<String>,
675 #[serde(rename = "base.yellow.light")]
677 yellow_light: Option<String>,
678}
679
680impl ThemeColor {
681 pub(crate) fn apply_config(
683 &mut self,
684 config: &ThemeConfig,
685 default_theme: &ThemeColor,
686 ) -> ThemeTokens {
687 let colors = config.colors.clone();
688 let default_tokens = ThemeTokens::from(default_theme);
689 let mut tokens = default_tokens;
690
691 macro_rules! apply_color {
692 ($config_field:ident) => {
693 if let Some(value) = &colors.$config_field {
694 self.$config_field =
695 try_parse_color(value).unwrap_or(default_theme.$config_field);
696 } else {
697 self.$config_field = default_theme.$config_field;
698 }
699 tokens.$config_field = self.$config_field.into();
700 };
701 ($config_field:ident, fallback = $fallback:expr) => {
703 let fallback: gpui::Hsla = ($fallback).into();
704 if let Some(value) = &colors.$config_field {
705 self.$config_field = try_parse_color(value).unwrap_or(fallback);
706 } else {
707 self.$config_field = fallback;
708 }
709 tokens.$config_field = self.$config_field.into();
710 };
711 }
712
713 macro_rules! apply_background_color {
714 ($config_field:ident) => {
715 let token = if let Some(value) = &colors.$config_field {
716 if let Ok(token) = try_parse_theme_token(&value) {
717 token
718 } else {
719 default_tokens.$config_field
720 }
721 } else {
722 default_tokens.$config_field
723 };
724 self.$config_field = token.color;
725 tokens.$config_field = token;
726 };
727 ($config_field:ident, fallback = $fallback:expr) => {
728 let fallback: ThemeToken = ($fallback).into();
729 let token = if let Some(value) = &colors.$config_field {
730 if let Ok(token) = try_parse_theme_token(&value) {
731 token
732 } else {
733 fallback
734 }
735 } else {
736 fallback
737 };
738 self.$config_field = token.color;
739 tokens.$config_field = token;
740 };
741 }
742
743 apply_background_color!(background);
744
745 apply_color!(red);
747 apply_color!(
748 red_light,
749 fallback = self.background.blend(self.red.opacity(0.8))
750 );
751 apply_color!(green);
752 apply_color!(
753 green_light,
754 fallback = self.background.blend(self.green.opacity(0.8))
755 );
756 apply_color!(blue);
757 apply_color!(
758 blue_light,
759 fallback = self.background.blend(self.blue.opacity(0.8))
760 );
761 apply_color!(magenta);
762 apply_color!(
763 magenta_light,
764 fallback = self.background.blend(self.magenta.opacity(0.8))
765 );
766 apply_color!(yellow);
767 apply_color!(
768 yellow_light,
769 fallback = self.background.blend(self.yellow.opacity(0.8))
770 );
771 apply_color!(cyan);
772 apply_color!(
773 cyan_light,
774 fallback = self.background.blend(self.cyan.opacity(0.8))
775 );
776
777 apply_color!(border);
778 apply_color!(foreground);
779 apply_color!(input, fallback = self.border);
780 apply_background_color!(muted);
781 apply_color!(
782 muted_foreground,
783 fallback = self.muted.blend(self.foreground.opacity(0.7))
784 );
785
786 let active_darken = if config.mode.is_dark() { 0.2 } else { 0.1 };
788 let hover_opacity = 0.9;
789 let transparent = gpui::transparent_black();
790 let button_background = if config.mode.is_dark() {
791 self.input.mix_oklab(transparent, 0.3)
792 } else {
793 self.background
794 };
795 apply_background_color!(button, fallback = button_background);
796 apply_color!(button_foreground, fallback = self.foreground);
797 apply_background_color!(
798 button_hover,
799 fallback = self.input.mix_oklab(transparent, 0.5)
800 );
801 apply_background_color!(
802 button_active,
803 fallback = self.input.mix_oklab(transparent, 0.7)
804 );
805 apply_background_color!(primary);
806 apply_color!(primary_foreground, fallback = self.foreground);
807 apply_background_color!(
808 primary_hover,
809 fallback = self.background.blend(self.primary.opacity(hover_opacity))
810 );
811 apply_background_color!(
812 primary_active,
813 fallback = self.primary.darken(active_darken)
814 );
815 apply_background_color!(button_primary, fallback = tokens.primary);
816 apply_color!(
817 button_primary_foreground,
818 fallback = self.primary_foreground
819 );
820 apply_background_color!(button_primary_hover, fallback = tokens.primary_hover);
821 apply_background_color!(button_primary_active, fallback = tokens.primary_active);
822 apply_background_color!(secondary);
823 apply_color!(secondary_foreground, fallback = self.foreground);
824 apply_background_color!(
825 secondary_hover,
826 fallback = self.background.blend(self.secondary.opacity(hover_opacity))
827 );
828 apply_background_color!(
829 secondary_active,
830 fallback = self.secondary.darken(active_darken)
831 );
832 apply_background_color!(button_secondary, fallback = tokens.secondary);
833 apply_color!(
834 button_secondary_foreground,
835 fallback = self.secondary_foreground
836 );
837 apply_background_color!(button_secondary_hover, fallback = tokens.secondary_hover);
838 apply_background_color!(button_secondary_active, fallback = tokens.secondary_active);
839 apply_background_color!(success, fallback = self.green);
840 apply_color!(success_foreground, fallback = self.primary_foreground);
841 apply_background_color!(
842 success_hover,
843 fallback = self.background.blend(self.success.opacity(hover_opacity))
844 );
845 apply_background_color!(
846 success_active,
847 fallback = self.success.darken(active_darken)
848 );
849 apply_background_color!(
850 button_success,
851 fallback = self.success.mix_oklab(transparent, 0.2)
852 );
853 apply_color!(button_success_foreground, fallback = self.success);
854 apply_background_color!(
855 button_success_hover,
856 fallback = self.success.mix_oklab(transparent, 0.3)
857 );
858 apply_background_color!(
859 button_success_active,
860 fallback = self.success.mix_oklab(transparent, 0.4)
861 );
862 apply_background_color!(info, fallback = self.cyan);
863 apply_color!(info_foreground, fallback = self.primary_foreground);
864 apply_background_color!(
865 info_hover,
866 fallback = self.background.blend(self.info.opacity(hover_opacity))
867 );
868 apply_background_color!(info_active, fallback = self.info.darken(active_darken));
869 apply_background_color!(
870 button_info,
871 fallback = self.info.mix_oklab(transparent, 0.2)
872 );
873 apply_color!(button_info_foreground, fallback = self.info);
874 apply_background_color!(
875 button_info_hover,
876 fallback = self.info.mix_oklab(transparent, 0.3)
877 );
878 apply_background_color!(
879 button_info_active,
880 fallback = self.info.mix_oklab(transparent, 0.4)
881 );
882 apply_background_color!(warning, fallback = self.yellow);
883 apply_color!(warning_foreground, fallback = self.primary_foreground);
884 apply_background_color!(
885 warning_hover,
886 fallback = self.background.blend(self.warning.opacity(0.9))
887 );
888 apply_background_color!(
889 warning_active,
890 fallback = self.background.blend(self.warning.darken(active_darken))
891 );
892 apply_background_color!(
893 button_warning,
894 fallback = self.warning.mix_oklab(transparent, 0.2)
895 );
896 apply_color!(button_warning_foreground, fallback = self.warning);
897 apply_background_color!(
898 button_warning_hover,
899 fallback = self.warning.mix_oklab(transparent, 0.3)
900 );
901 apply_background_color!(
902 button_warning_active,
903 fallback = self.warning.mix_oklab(transparent, 0.4)
904 );
905
906 apply_background_color!(accent, fallback = tokens.secondary);
908 apply_color!(accent_foreground, fallback = self.foreground);
909 apply_background_color!(accordion, fallback = tokens.background);
910 apply_background_color!(
911 group_box,
912 fallback = self
913 .background
914 .blend(
915 self.secondary
916 .opacity(if config.mode.is_dark() { 0.3 } else { 0.4 })
917 )
918 );
919 apply_color!(group_box_foreground, fallback = self.foreground);
920 apply_color!(caret, fallback = self.primary);
921 apply_color!(chart_1, fallback = self.blue.lighten(0.4));
922 apply_color!(chart_2, fallback = self.blue.lighten(0.2));
923 apply_color!(chart_3, fallback = self.blue);
924 apply_color!(chart_4, fallback = self.blue.darken(0.2));
925 apply_color!(chart_5, fallback = self.blue.darken(0.4));
926 apply_color!(chart_bullish, fallback = self.green);
927 apply_color!(chart_bearish, fallback = self.red);
928 apply_background_color!(danger, fallback = self.red);
929 apply_background_color!(danger_active, fallback = self.danger.darken(active_darken));
930 apply_color!(danger_foreground, fallback = self.primary_foreground);
931 apply_background_color!(
932 danger_hover,
933 fallback = self.background.blend(self.danger.opacity(0.9))
934 );
935 apply_background_color!(
936 button_danger,
937 fallback = self.danger.mix_oklab(transparent, 0.2)
938 );
939 apply_color!(button_danger_foreground, fallback = self.danger);
940 apply_background_color!(
941 button_danger_hover,
942 fallback = self.danger.mix_oklab(transparent, 0.3)
943 );
944 apply_background_color!(
945 button_danger_active,
946 fallback = self.danger.mix_oklab(transparent, 0.4)
947 );
948 apply_background_color!(
949 description_list_label,
950 fallback = self.background.blend(self.border.opacity(0.2))
951 );
952 apply_color!(
953 description_list_label_foreground,
954 fallback = self.muted_foreground
955 );
956 apply_color!(drag_border, fallback = self.primary.opacity(0.65));
957 apply_background_color!(drop_target, fallback = self.primary.opacity(0.2));
958 apply_color!(link, fallback = self.primary);
959 apply_color!(link_active, fallback = self.link);
960 apply_color!(link_hover, fallback = self.link);
961 apply_background_color!(list, fallback = tokens.background);
962 apply_background_color!(
963 list_active,
964 fallback = self.background.blend(self.primary.opacity(0.1))
965 );
966 apply_color!(
967 list_active_border,
968 fallback = self.background.blend(self.primary.opacity(0.6))
969 );
970 apply_background_color!(list_even, fallback = tokens.list);
971 apply_background_color!(list_head, fallback = tokens.list);
972 apply_background_color!(list_hover, fallback = self.accent.opacity(0.6));
973 apply_background_color!(popover, fallback = tokens.background);
974 apply_color!(popover_foreground, fallback = self.foreground);
975 apply_background_color!(progress_bar, fallback = tokens.primary);
976 apply_color!(ring, fallback = self.blue);
977 apply_background_color!(scrollbar, fallback = tokens.background);
978 apply_background_color!(scrollbar_thumb, fallback = tokens.accent);
979 apply_background_color!(scrollbar_thumb_hover, fallback = tokens.scrollbar_thumb);
980 apply_background_color!(selection, fallback = tokens.primary);
981 apply_background_color!(
982 sidebar,
983 fallback = self.background.blend(self.border.opacity(0.15))
984 );
985 apply_background_color!(sidebar_accent, fallback = tokens.accent);
986 apply_color!(sidebar_accent_foreground, fallback = self.accent_foreground);
987 apply_color!(sidebar_border, fallback = self.border);
988 apply_color!(sidebar_foreground, fallback = self.foreground);
989 apply_background_color!(sidebar_primary, fallback = tokens.primary);
990 apply_color!(
991 sidebar_primary_foreground,
992 fallback = self.primary_foreground
993 );
994 apply_background_color!(skeleton, fallback = tokens.secondary);
995 apply_background_color!(slider_bar, fallback = tokens.primary);
996 apply_background_color!(slider_thumb, fallback = self.primary_foreground);
997 apply_background_color!(switch, fallback = tokens.secondary_active);
998 apply_background_color!(switch_thumb, fallback = tokens.background);
999 apply_background_color!(tab, fallback = tokens.background);
1000 apply_background_color!(tab_active, fallback = tokens.background);
1001 apply_color!(tab_active_foreground, fallback = self.foreground);
1002 apply_background_color!(tab_bar, fallback = tokens.background);
1003 apply_background_color!(tab_bar_segmented, fallback = tokens.secondary);
1004 apply_color!(tab_foreground, fallback = self.foreground);
1005 apply_background_color!(table, fallback = tokens.list);
1006 apply_background_color!(table_active, fallback = tokens.list_active);
1007 apply_color!(table_active_border, fallback = self.list_active_border);
1008 apply_background_color!(table_even, fallback = tokens.list_even);
1009 apply_background_color!(table_head, fallback = tokens.list_head);
1010 apply_color!(table_head_foreground, fallback = self.muted_foreground);
1011 apply_background_color!(table_foot, fallback = tokens.list_head);
1012 apply_color!(table_foot_foreground, fallback = self.muted_foreground);
1013 apply_background_color!(table_hover, fallback = tokens.list_hover);
1014 apply_color!(table_row_border, fallback = self.border);
1015 apply_background_color!(title_bar, fallback = tokens.background);
1016 apply_color!(title_bar_border, fallback = self.border);
1017 apply_background_color!(status_bar, fallback = tokens.title_bar);
1018 apply_color!(status_bar_border, fallback = self.title_bar_border);
1019 apply_background_color!(tiles, fallback = tokens.background);
1020 apply_background_color!(overlay);
1021 apply_color!(window_border, fallback = self.border);
1022
1023 let clamp_alpha = |raw: Option<&str>, color: Hsla, background: Background, max: f32| {
1027 let base = color.a;
1028 let target = base.min(max);
1029 let color = color.alpha(target);
1030 let background = raw
1031 .and_then(|value| try_parse_background_clamped(value, max).ok())
1032 .unwrap_or_else(|| {
1033 let factor = if base > 0. { target / base } else { 1. };
1034 background.opacity(factor)
1035 });
1036 (color, ThemeToken::new(color, background))
1037 };
1038
1039 (self.list_active, tokens.list_active) = clamp_alpha(
1040 colors.list_active.as_deref(),
1041 self.list_active,
1042 tokens.list_active.background,
1043 0.2,
1044 );
1045 (self.table_active, tokens.table_active) = clamp_alpha(
1046 colors.table_active.as_deref(),
1047 self.table_active,
1048 tokens.table_active.background,
1049 0.2,
1050 );
1051 (self.selection, tokens.selection) = clamp_alpha(
1052 colors.selection.as_deref(),
1053 self.selection,
1054 tokens.selection.background,
1055 0.3,
1056 );
1057
1058 tokens
1059 }
1060}
1061
1062impl Theme {
1063 pub fn apply_config(&mut self, config: &Rc<ThemeConfig>) {
1065 if config.mode.is_dark() {
1066 self.dark_theme = config.clone();
1067 } else {
1068 self.light_theme = config.clone();
1069 }
1070 if let Some(style) = &config.highlight {
1071 let highlight_theme = Arc::new(HighlightTheme {
1072 name: config.name.to_string(),
1073 appearance: config.mode,
1074 style: style.clone(),
1075 });
1076 self.highlight_theme = highlight_theme.clone();
1077 }
1078
1079 let default_colors = if config.mode.is_dark() {
1080 ThemeColor::dark()
1081 } else {
1082 ThemeColor::light()
1083 };
1084
1085 if let Some(font_size) = config.font_size {
1086 self.font_size = px(font_size);
1087 }
1088 if let Some(font_family) = &config.font_family {
1089 self.font_family = font_family.clone();
1090 }
1091 if let Some(mono_font_family) = &config.mono_font_family {
1092 self.mono_font_family = mono_font_family.clone();
1093 }
1094 if let Some(mono_font_size) = config.mono_font_size {
1095 self.mono_font_size = px(mono_font_size);
1096 }
1097 if let Some(radius) = config.radius {
1098 self.radius = px(radius as f32);
1099 }
1100 if let Some(radius_lg) = config.radius_lg {
1101 self.radius_lg = px(radius_lg as f32);
1102 }
1103 if let Some(shadow) = config.shadow {
1104 self.shadow = shadow;
1105 }
1106
1107 self.tokens = self.colors.apply_config(&config, &default_colors);
1108 self.mode = config.mode;
1109 }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114 use gpui::{linear_color_stop, linear_gradient, px};
1115
1116 use crate::{Theme, ThemeConfig, ThemeMode, ThemeSet, try_parse_color};
1117
1118 #[test]
1119 fn test_semantic_theme_config_parses_and_roundtrips() {
1120 let value = serde_json::json!({
1121 "name": "Semantic",
1122 "mode": "dark",
1123 "tokens": {
1124 "colors": {
1125 "surface": "#111827",
1126 "surface_foreground": "#f9fafb",
1127 "primary": "#2563eb",
1128 "destructive": "#dc2626"
1129 },
1130 "radius": { "sm": 2.0, "md": 6.0, "lg": 10.0 },
1131 "spacing": { "xs": 4.0, "md": 12.0, "xl": 24.0 },
1132 "typography": {
1133 "sans": "Inter",
1134 "md": { "size": 15.0, "line_height": 22.0 }
1135 }
1136 }
1137 });
1138 let config: super::SemanticThemeConfigFile = serde_json::from_value(value).unwrap();
1139 let serialized = serde_json::to_string(&config).unwrap();
1140 let reparsed: super::SemanticThemeConfigFile = serde_json::from_str(&serialized).unwrap();
1141 let semantic = reparsed.tokens;
1142
1143 assert_eq!(semantic.colors.surface.as_deref(), Some("#111827"));
1144 assert_eq!(semantic.colors.destructive.as_deref(), Some("#dc2626"));
1145 assert_eq!(semantic.radius.lg, Some(10.0));
1146 assert_eq!(semantic.spacing.xl, Some(24.0));
1147 assert_eq!(semantic.typography.sans.as_deref(), Some("Inter"));
1148 assert_eq!(semantic.typography.md.line_height, Some(22.0));
1149
1150 let mut theme = Theme::default();
1151 let resolved = theme.apply_semantic_config_str(&serialized).unwrap();
1152 assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1153 assert_eq!(resolved.spacing.xl, px(24.));
1154 assert_eq!(resolved.typography.md.line_height, px(22.));
1155 }
1156
1157 #[test]
1158 fn test_semantic_tokens_override_legacy_generic_fields_only() {
1159 let config = serde_json::from_value::<super::SemanticThemeConfigFile>(serde_json::json!({
1160 "tokens": {
1161 "colors": { "primary": "#2563eb", "destructive": "#b91c1c" },
1162 "spacing": { "md": 14.0 },
1163 "typography": { "md": { "size": 15.0 } }
1164 }
1165 }))
1166 .unwrap();
1167 let mut theme = Theme::default();
1168 let component_color = theme.button_primary;
1169 let resolved = theme.apply_semantic_config(&config.tokens);
1170
1171 assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1172 assert_eq!(theme.danger, try_parse_color("#b91c1c").unwrap());
1173 assert_eq!(theme.button_primary, component_color);
1174 assert_eq!(resolved.spacing.md, px(14.));
1175 assert_eq!(resolved.typography.md.size, px(15.));
1176 }
1177
1178 #[test]
1179 fn test_legacy_config_without_semantic_tokens_is_unchanged() {
1180 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1181 "name": "Legacy",
1182 "mode": "light",
1183 "radius": 7,
1184 "colors": { "primary.background": "#7c3aed" }
1185 }))
1186 .unwrap();
1187 let mut theme = Theme::default();
1188 theme.apply_config(&std::rc::Rc::new(config));
1189 assert_eq!(theme.primary, try_parse_color("#7c3aed").unwrap());
1190 assert_eq!(theme.radius, px(7.));
1191 assert_eq!(theme.semantic_tokens().spacing, Default::default());
1192 }
1193
1194 #[test]
1195 fn test_apply_config_preserves_gradient_background_and_solid_color_fallback() {
1196 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1197 "name": "Gradient",
1198 "mode": "light",
1199 "colors": {
1200 "primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)",
1201 "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)"
1202 }
1203 }))
1204 .unwrap();
1205
1206 let mut theme = Theme::default();
1207 theme.apply_config(&std::rc::Rc::new(config));
1208
1209 let primary_from = try_parse_color("#4F46E5").unwrap();
1210 let primary_to = try_parse_color("#06B6D4").unwrap();
1211 assert_eq!(theme.primary, primary_from);
1212 assert_eq!(theme.tokens.primary.color, primary_from);
1213 assert_eq!(
1214 theme.tokens.primary.background,
1215 linear_gradient(
1216 135.,
1217 linear_color_stop(primary_from, 0.),
1218 linear_color_stop(primary_to, 1.)
1219 )
1220 );
1221 assert_eq!(
1222 theme.tokens.button_primary.background,
1223 theme.tokens.primary.background
1224 );
1225 assert_eq!(
1226 theme.tokens.button_primary_hover.background,
1227 linear_gradient(
1228 90.,
1229 linear_color_stop(crate::red_500(), 0.25),
1230 linear_color_stop(crate::blue_600(), 0.75)
1231 )
1232 );
1233 assert_eq!(theme.mode, ThemeMode::Light);
1234 }
1235
1236 #[test]
1237 fn test_aurora_theme_parses_gradient_backgrounds() {
1238 let theme_set =
1239 serde_json::from_str::<ThemeSet>(include_str!("../../../../themes/aurora.json"))
1240 .unwrap();
1241 assert_eq!(theme_set.themes.len(), 1);
1242 assert!(theme_set.themes.iter().all(|theme| !theme.mode.is_dark()));
1243
1244 let light = theme_set
1245 .themes
1246 .iter()
1247 .find(|theme| theme.name.as_ref() == "Aurora Light")
1248 .unwrap();
1249 let mut theme = Theme::default();
1250 theme.apply_config(&std::rc::Rc::new(light.clone()));
1251
1252 assert_ne!(
1253 theme.tokens.button_primary.background,
1254 theme.button_primary.into()
1255 );
1256 assert_eq!(theme.tokens.background.background, theme.background.into());
1257 assert_eq!(theme.button_primary, try_parse_color("#1E293B").unwrap());
1258 assert_eq!(theme.background, try_parse_color("#FFFFFF").unwrap());
1259 assert_ne!(
1260 theme.tokens.progress_bar.background,
1261 theme.progress_bar.into()
1262 );
1263 assert_ne!(
1264 theme.tokens.scrollbar_thumb.background,
1265 theme.scrollbar_thumb.into()
1266 );
1267 assert_ne!(theme.tokens.switch.background, theme.switch.into());
1268 assert_ne!(
1269 theme.tokens.switch_thumb.background,
1270 theme.switch_thumb.into()
1271 );
1272 assert_ne!(theme.tokens.title_bar.background, theme.title_bar.into());
1273 assert_ne!(theme.tokens.status_bar.background, theme.status_bar.into());
1274 }
1275
1276 #[test]
1277 fn test_apply_config_clamps_highlight_alpha_per_gradient_stop() {
1278 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1279 "name": "Highlight",
1280 "mode": "light",
1281 "colors": {
1282 "list.active.background": "#3b82f6",
1284 "table.active.background": "linear-gradient(#bfdbfe33, #3b82f6)",
1287 "selection.background": "linear-gradient(#3b82f600, #3b82f6)",
1291 }
1292 }))
1293 .unwrap();
1294
1295 let mut theme = Theme::default();
1296 theme.apply_config(&std::rc::Rc::new(config));
1297
1298 let blue = try_parse_color("#3b82f6").unwrap();
1300 assert_eq!(theme.list_active, blue.alpha(0.2));
1301 assert_eq!(theme.tokens.list_active.background, blue.alpha(0.2).into());
1302
1303 let faint = try_parse_color("#bfdbfe33").unwrap();
1305 assert_eq!(
1306 theme.tokens.table_active.background,
1307 linear_gradient(
1308 180.,
1309 linear_color_stop(faint.alpha(faint.a.min(0.2)), 0.),
1310 linear_color_stop(blue.alpha(0.2), 1.),
1311 )
1312 );
1313
1314 let clear = try_parse_color("#3b82f600").unwrap();
1317 assert_eq!(
1318 theme.tokens.selection.background,
1319 linear_gradient(
1320 180.,
1321 linear_color_stop(clear.alpha(clear.a.min(0.3)), 0.),
1322 linear_color_stop(blue.alpha(0.3), 1.),
1323 )
1324 );
1325 }
1326}