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 = "warning.background")]
619 pub warning: Option<SharedString>,
620 #[serde(rename = "warning.active.background")]
622 pub warning_active: Option<SharedString>,
623 #[serde(rename = "warning.hover.background")]
625 pub warning_hover: Option<SharedString>,
626 #[serde(rename = "warning.foreground")]
628 pub warning_foreground: Option<SharedString>,
629 #[serde(rename = "overlay")]
631 pub overlay: Option<SharedString>,
632 #[serde(rename = "window.border")]
638 pub window_border: Option<SharedString>,
639
640 #[serde(rename = "base.blue")]
642 blue: Option<String>,
643 #[serde(rename = "base.blue.light")]
645 blue_light: Option<String>,
646 #[serde(rename = "base.cyan")]
648 cyan: Option<String>,
649 #[serde(rename = "base.cyan.light")]
651 cyan_light: Option<String>,
652 #[serde(rename = "base.green")]
654 green: Option<String>,
655 #[serde(rename = "base.green.light")]
657 green_light: Option<String>,
658 #[serde(rename = "base.magenta")]
660 magenta: Option<String>,
661 #[serde(rename = "base.magenta.light")]
662 magenta_light: Option<String>,
663 #[serde(rename = "base.red")]
665 red: Option<String>,
666 #[serde(rename = "base.red.light")]
668 red_light: Option<String>,
669 #[serde(rename = "base.yellow")]
671 yellow: Option<String>,
672 #[serde(rename = "base.yellow.light")]
674 yellow_light: Option<String>,
675}
676
677impl ThemeColor {
678 pub(crate) fn apply_config(
680 &mut self,
681 config: &ThemeConfig,
682 default_theme: &ThemeColor,
683 ) -> ThemeTokens {
684 let colors = config.colors.clone();
685 let default_tokens = ThemeTokens::from(default_theme);
686 let mut tokens = default_tokens;
687
688 macro_rules! apply_color {
689 ($config_field:ident) => {
690 if let Some(value) = &colors.$config_field {
691 self.$config_field =
692 try_parse_color(value).unwrap_or(default_theme.$config_field);
693 } else {
694 self.$config_field = default_theme.$config_field;
695 }
696 tokens.$config_field = self.$config_field.into();
697 };
698 ($config_field:ident, fallback = $fallback:expr) => {
700 let fallback: gpui::Hsla = ($fallback).into();
701 if let Some(value) = &colors.$config_field {
702 self.$config_field = try_parse_color(value).unwrap_or(fallback);
703 } else {
704 self.$config_field = fallback;
705 }
706 tokens.$config_field = self.$config_field.into();
707 };
708 }
709
710 macro_rules! apply_background_color {
711 ($config_field:ident) => {
712 let token = if let Some(value) = &colors.$config_field {
713 if let Ok(token) = try_parse_theme_token(&value) {
714 token
715 } else {
716 default_tokens.$config_field
717 }
718 } else {
719 default_tokens.$config_field
720 };
721 self.$config_field = token.color;
722 tokens.$config_field = token;
723 };
724 ($config_field:ident, fallback = $fallback:expr) => {
725 let fallback: ThemeToken = ($fallback).into();
726 let token = if let Some(value) = &colors.$config_field {
727 if let Ok(token) = try_parse_theme_token(&value) {
728 token
729 } else {
730 fallback
731 }
732 } else {
733 fallback
734 };
735 self.$config_field = token.color;
736 tokens.$config_field = token;
737 };
738 }
739
740 apply_background_color!(background);
741
742 apply_color!(red);
744 apply_color!(
745 red_light,
746 fallback = self.background.blend(self.red.opacity(0.8))
747 );
748 apply_color!(green);
749 apply_color!(
750 green_light,
751 fallback = self.background.blend(self.green.opacity(0.8))
752 );
753 apply_color!(blue);
754 apply_color!(
755 blue_light,
756 fallback = self.background.blend(self.blue.opacity(0.8))
757 );
758 apply_color!(magenta);
759 apply_color!(
760 magenta_light,
761 fallback = self.background.blend(self.magenta.opacity(0.8))
762 );
763 apply_color!(yellow);
764 apply_color!(
765 yellow_light,
766 fallback = self.background.blend(self.yellow.opacity(0.8))
767 );
768 apply_color!(cyan);
769 apply_color!(
770 cyan_light,
771 fallback = self.background.blend(self.cyan.opacity(0.8))
772 );
773
774 apply_color!(border);
775 apply_color!(foreground);
776 apply_color!(input, fallback = self.border);
777 apply_background_color!(muted);
778 apply_color!(
779 muted_foreground,
780 fallback = self.muted.blend(self.foreground.opacity(0.7))
781 );
782
783 let active_darken = if config.mode.is_dark() { 0.2 } else { 0.1 };
785 let hover_opacity = 0.9;
786 let transparent = gpui::transparent_black();
787 let button_background = if config.mode.is_dark() {
788 self.input.mix_oklab(transparent, 0.3)
789 } else {
790 self.background
791 };
792 apply_background_color!(button, fallback = button_background);
793 apply_color!(button_foreground, fallback = self.foreground);
794 apply_background_color!(
795 button_hover,
796 fallback = self.input.mix_oklab(transparent, 0.5)
797 );
798 apply_background_color!(
799 button_active,
800 fallback = self.input.mix_oklab(transparent, 0.7)
801 );
802 apply_background_color!(primary);
803 apply_color!(primary_foreground, fallback = self.foreground);
804 apply_background_color!(
805 primary_hover,
806 fallback = self.background.blend(self.primary.opacity(hover_opacity))
807 );
808 apply_background_color!(
809 primary_active,
810 fallback = self.primary.darken(active_darken)
811 );
812 apply_background_color!(button_primary, fallback = tokens.primary);
813 apply_color!(
814 button_primary_foreground,
815 fallback = self.primary_foreground
816 );
817 apply_background_color!(button_primary_hover, fallback = tokens.primary_hover);
818 apply_background_color!(button_primary_active, fallback = tokens.primary_active);
819 apply_background_color!(secondary);
820 apply_color!(secondary_foreground, fallback = self.foreground);
821 apply_background_color!(
822 secondary_hover,
823 fallback = self.background.blend(self.secondary.opacity(hover_opacity))
824 );
825 apply_background_color!(
826 secondary_active,
827 fallback = self.secondary.darken(active_darken)
828 );
829 apply_background_color!(button_secondary, fallback = tokens.secondary);
830 apply_color!(
831 button_secondary_foreground,
832 fallback = self.secondary_foreground
833 );
834 apply_background_color!(button_secondary_hover, fallback = tokens.secondary_hover);
835 apply_background_color!(button_secondary_active, fallback = tokens.secondary_active);
836 apply_background_color!(success, fallback = self.green);
837 apply_color!(success_foreground, fallback = self.primary_foreground);
838 apply_background_color!(
839 success_hover,
840 fallback = self.background.blend(self.success.opacity(hover_opacity))
841 );
842 apply_background_color!(
843 success_active,
844 fallback = self.success.darken(active_darken)
845 );
846 apply_background_color!(
847 button_success,
848 fallback = self.success.mix_oklab(transparent, 0.2)
849 );
850 apply_color!(button_success_foreground, fallback = self.success);
851 apply_background_color!(
852 button_success_hover,
853 fallback = self.success.mix_oklab(transparent, 0.3)
854 );
855 apply_background_color!(
856 button_success_active,
857 fallback = self.success.mix_oklab(transparent, 0.4)
858 );
859 apply_background_color!(info, fallback = self.cyan);
860 apply_color!(info_foreground, fallback = self.primary_foreground);
861 apply_background_color!(
862 info_hover,
863 fallback = self.background.blend(self.info.opacity(hover_opacity))
864 );
865 apply_background_color!(info_active, fallback = self.info.darken(active_darken));
866 apply_background_color!(
867 button_info,
868 fallback = self.info.mix_oklab(transparent, 0.2)
869 );
870 apply_color!(button_info_foreground, fallback = self.info);
871 apply_background_color!(
872 button_info_hover,
873 fallback = self.info.mix_oklab(transparent, 0.3)
874 );
875 apply_background_color!(
876 button_info_active,
877 fallback = self.info.mix_oklab(transparent, 0.4)
878 );
879 apply_background_color!(warning, fallback = self.yellow);
880 apply_color!(warning_foreground, fallback = self.primary_foreground);
881 apply_background_color!(
882 warning_hover,
883 fallback = self.background.blend(self.warning.opacity(0.9))
884 );
885 apply_background_color!(
886 warning_active,
887 fallback = self.background.blend(self.warning.darken(active_darken))
888 );
889 apply_background_color!(
890 button_warning,
891 fallback = self.warning.mix_oklab(transparent, 0.2)
892 );
893 apply_color!(button_warning_foreground, fallback = self.warning);
894 apply_background_color!(
895 button_warning_hover,
896 fallback = self.warning.mix_oklab(transparent, 0.3)
897 );
898 apply_background_color!(
899 button_warning_active,
900 fallback = self.warning.mix_oklab(transparent, 0.4)
901 );
902
903 apply_background_color!(accent, fallback = tokens.secondary);
905 apply_color!(accent_foreground, fallback = self.foreground);
906 apply_background_color!(accordion, fallback = tokens.background);
907 apply_background_color!(
908 group_box,
909 fallback = self
910 .background
911 .blend(
912 self.secondary
913 .opacity(if config.mode.is_dark() { 0.3 } else { 0.4 })
914 )
915 );
916 apply_color!(group_box_foreground, fallback = self.foreground);
917 apply_color!(caret, fallback = self.primary);
918 apply_color!(chart_1, fallback = self.blue.lighten(0.4));
919 apply_color!(chart_2, fallback = self.blue.lighten(0.2));
920 apply_color!(chart_3, fallback = self.blue);
921 apply_color!(chart_4, fallback = self.blue.darken(0.2));
922 apply_color!(chart_5, fallback = self.blue.darken(0.4));
923 apply_color!(chart_bullish, fallback = self.green);
924 apply_color!(chart_bearish, fallback = self.red);
925 apply_background_color!(danger, fallback = self.red);
926 apply_background_color!(danger_active, fallback = self.danger.darken(active_darken));
927 apply_color!(danger_foreground, fallback = self.primary_foreground);
928 apply_background_color!(
929 danger_hover,
930 fallback = self.background.blend(self.danger.opacity(0.9))
931 );
932 apply_background_color!(
933 button_danger,
934 fallback = self.danger.mix_oklab(transparent, 0.2)
935 );
936 apply_color!(button_danger_foreground, fallback = self.danger);
937 apply_background_color!(
938 button_danger_hover,
939 fallback = self.danger.mix_oklab(transparent, 0.3)
940 );
941 apply_background_color!(
942 button_danger_active,
943 fallback = self.danger.mix_oklab(transparent, 0.4)
944 );
945 apply_background_color!(
946 description_list_label,
947 fallback = self.background.blend(self.border.opacity(0.2))
948 );
949 apply_color!(
950 description_list_label_foreground,
951 fallback = self.muted_foreground
952 );
953 apply_color!(drag_border, fallback = self.primary.opacity(0.65));
954 apply_background_color!(drop_target, fallback = self.primary.opacity(0.2));
955 apply_color!(link, fallback = self.primary);
956 apply_color!(link_active, fallback = self.link);
957 apply_color!(link_hover, fallback = self.link);
958 apply_background_color!(list, fallback = tokens.background);
959 apply_background_color!(
960 list_active,
961 fallback = self.background.blend(self.primary.opacity(0.1))
962 );
963 apply_color!(
964 list_active_border,
965 fallback = self.background.blend(self.primary.opacity(0.6))
966 );
967 apply_background_color!(list_even, fallback = tokens.list);
968 apply_background_color!(list_head, fallback = tokens.list);
969 apply_background_color!(list_hover, fallback = self.accent.opacity(0.6));
970 apply_background_color!(popover, fallback = tokens.background);
971 apply_color!(popover_foreground, fallback = self.foreground);
972 apply_background_color!(progress_bar, fallback = tokens.primary);
973 apply_color!(ring, fallback = self.blue);
974 apply_background_color!(scrollbar, fallback = tokens.background);
975 apply_background_color!(scrollbar_thumb, fallback = tokens.accent);
976 apply_background_color!(scrollbar_thumb_hover, fallback = tokens.scrollbar_thumb);
977 apply_background_color!(selection, fallback = tokens.primary);
978 apply_background_color!(
979 sidebar,
980 fallback = self.background.blend(self.border.opacity(0.15))
981 );
982 apply_background_color!(sidebar_accent, fallback = tokens.accent);
983 apply_color!(sidebar_accent_foreground, fallback = self.accent_foreground);
984 apply_color!(sidebar_border, fallback = self.border);
985 apply_color!(sidebar_foreground, fallback = self.foreground);
986 apply_background_color!(sidebar_primary, fallback = tokens.primary);
987 apply_color!(
988 sidebar_primary_foreground,
989 fallback = self.primary_foreground
990 );
991 apply_background_color!(skeleton, fallback = tokens.secondary);
992 apply_background_color!(slider_bar, fallback = tokens.primary);
993 apply_background_color!(slider_thumb, fallback = self.primary_foreground);
994 apply_background_color!(switch, fallback = tokens.secondary_active);
995 apply_background_color!(switch_thumb, fallback = tokens.background);
996 apply_background_color!(tab, fallback = tokens.background);
997 apply_background_color!(tab_active, fallback = tokens.background);
998 apply_color!(tab_active_foreground, fallback = self.foreground);
999 apply_background_color!(tab_bar, fallback = tokens.background);
1000 apply_background_color!(tab_bar_segmented, fallback = tokens.secondary);
1001 apply_color!(tab_foreground, fallback = self.foreground);
1002 apply_background_color!(table, fallback = tokens.list);
1003 apply_background_color!(table_active, fallback = tokens.list_active);
1004 apply_color!(table_active_border, fallback = self.list_active_border);
1005 apply_background_color!(table_even, fallback = tokens.list_even);
1006 apply_background_color!(table_head, fallback = tokens.list_head);
1007 apply_color!(table_head_foreground, fallback = self.muted_foreground);
1008 apply_background_color!(table_foot, fallback = tokens.list_head);
1009 apply_color!(table_foot_foreground, fallback = self.muted_foreground);
1010 apply_background_color!(table_hover, fallback = tokens.list_hover);
1011 apply_color!(table_row_border, fallback = self.border);
1012 apply_background_color!(title_bar, fallback = tokens.background);
1013 apply_color!(title_bar_border, fallback = self.border);
1014 apply_background_color!(status_bar, fallback = tokens.title_bar);
1015 apply_color!(status_bar_border, fallback = self.title_bar_border);
1016 apply_background_color!(overlay);
1017 apply_color!(window_border, fallback = self.border);
1018
1019 let clamp_alpha = |raw: Option<&str>, color: Hsla, background: Background, max: f32| {
1023 let base = color.a;
1024 let target = base.min(max);
1025 let color = color.alpha(target);
1026 let background = raw
1027 .and_then(|value| try_parse_background_clamped(value, max).ok())
1028 .unwrap_or_else(|| {
1029 let factor = if base > 0. { target / base } else { 1. };
1030 background.opacity(factor)
1031 });
1032 (color, ThemeToken::new(color, background))
1033 };
1034
1035 (self.list_active, tokens.list_active) = clamp_alpha(
1036 colors.list_active.as_deref(),
1037 self.list_active,
1038 tokens.list_active.background,
1039 0.2,
1040 );
1041 (self.table_active, tokens.table_active) = clamp_alpha(
1042 colors.table_active.as_deref(),
1043 self.table_active,
1044 tokens.table_active.background,
1045 0.2,
1046 );
1047 (self.selection, tokens.selection) = clamp_alpha(
1048 colors.selection.as_deref(),
1049 self.selection,
1050 tokens.selection.background,
1051 0.3,
1052 );
1053
1054 tokens
1055 }
1056}
1057
1058impl Theme {
1059 pub fn apply_config(&mut self, config: &Rc<ThemeConfig>) {
1061 if config.mode.is_dark() {
1062 self.dark_theme = config.clone();
1063 } else {
1064 self.light_theme = config.clone();
1065 }
1066 if let Some(style) = &config.highlight {
1067 let highlight_theme = Arc::new(HighlightTheme {
1068 name: config.name.to_string(),
1069 appearance: config.mode,
1070 style: style.clone(),
1071 });
1072 self.highlight_theme = highlight_theme.clone();
1073 }
1074
1075 let default_colors = if config.mode.is_dark() {
1076 ThemeColor::dark()
1077 } else {
1078 ThemeColor::light()
1079 };
1080
1081 if let Some(font_size) = config.font_size {
1082 self.font_size = px(font_size);
1083 }
1084 if let Some(font_family) = &config.font_family {
1085 self.font_family = font_family.clone();
1086 }
1087 if let Some(mono_font_family) = &config.mono_font_family {
1088 self.mono_font_family = mono_font_family.clone();
1089 }
1090 if let Some(mono_font_size) = config.mono_font_size {
1091 self.mono_font_size = px(mono_font_size);
1092 }
1093 if let Some(radius) = config.radius {
1094 self.radius = px(radius as f32);
1095 }
1096 if let Some(radius_lg) = config.radius_lg {
1097 self.radius_lg = px(radius_lg as f32);
1098 }
1099 if let Some(shadow) = config.shadow {
1100 self.shadow = shadow;
1101 }
1102
1103 self.tokens = self.colors.apply_config(&config, &default_colors);
1104 self.mode = config.mode;
1105 }
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110 use gpui::{linear_color_stop, linear_gradient, px};
1111
1112 use crate::{Colorize as _, Theme, ThemeConfig, ThemeMode, ThemeSet, try_parse_color};
1113
1114 #[test]
1115 fn test_semantic_theme_config_parses_and_roundtrips() {
1116 let value = serde_json::json!({
1117 "name": "Semantic",
1118 "mode": "dark",
1119 "tokens": {
1120 "colors": {
1121 "surface": "#111827",
1122 "surface_foreground": "#f9fafb",
1123 "primary": "#2563eb",
1124 "destructive": "#dc2626"
1125 },
1126 "radius": { "sm": 2.0, "md": 6.0, "lg": 10.0 },
1127 "spacing": { "xs": 4.0, "md": 12.0, "xl": 24.0 },
1128 "typography": {
1129 "sans": "Inter",
1130 "md": { "size": 15.0, "line_height": 22.0 }
1131 }
1132 }
1133 });
1134 let config: super::SemanticThemeConfigFile = serde_json::from_value(value).unwrap();
1135 let serialized = serde_json::to_string(&config).unwrap();
1136 let reparsed: super::SemanticThemeConfigFile = serde_json::from_str(&serialized).unwrap();
1137 let semantic = reparsed.tokens;
1138
1139 assert_eq!(semantic.colors.surface.as_deref(), Some("#111827"));
1140 assert_eq!(semantic.colors.destructive.as_deref(), Some("#dc2626"));
1141 assert_eq!(semantic.radius.lg, Some(10.0));
1142 assert_eq!(semantic.spacing.xl, Some(24.0));
1143 assert_eq!(semantic.typography.sans.as_deref(), Some("Inter"));
1144 assert_eq!(semantic.typography.md.line_height, Some(22.0));
1145
1146 let mut theme = Theme::default();
1147 let resolved = theme.apply_semantic_config_str(&serialized).unwrap();
1148 assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1149 assert_eq!(resolved.spacing.xl, px(24.));
1150 assert_eq!(resolved.typography.md.line_height, px(22.));
1151 }
1152
1153 #[test]
1154 fn test_semantic_tokens_override_legacy_generic_fields_only() {
1155 let config = serde_json::from_value::<super::SemanticThemeConfigFile>(serde_json::json!({
1156 "tokens": {
1157 "colors": { "primary": "#2563eb", "destructive": "#b91c1c" },
1158 "spacing": { "md": 14.0 },
1159 "typography": { "md": { "size": 15.0 } }
1160 }
1161 }))
1162 .unwrap();
1163 let mut theme = Theme::default();
1164 let component_color = theme.button_primary;
1165 let resolved = theme.apply_semantic_config(&config.tokens);
1166
1167 assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1168 assert_eq!(theme.danger, try_parse_color("#b91c1c").unwrap());
1169 assert_eq!(theme.button_primary, component_color);
1170 assert_eq!(resolved.spacing.md, px(14.));
1171 assert_eq!(resolved.typography.md.size, px(15.));
1172 }
1173
1174 #[test]
1175 fn test_legacy_config_without_semantic_tokens_is_unchanged() {
1176 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1177 "name": "Legacy",
1178 "mode": "light",
1179 "radius": 7,
1180 "colors": { "primary.background": "#7c3aed" }
1181 }))
1182 .unwrap();
1183 let mut theme = Theme::default();
1184 theme.apply_config(&std::rc::Rc::new(config));
1185 assert_eq!(theme.primary, try_parse_color("#7c3aed").unwrap());
1186 assert_eq!(theme.radius, px(7.));
1187 assert_eq!(theme.semantic_tokens().spacing, Default::default());
1188 }
1189
1190 #[test]
1191 fn test_apply_config_reads_the_chart_colors() {
1192 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1193 "name": "Palette",
1194 "mode": "light",
1195 "colors": {
1196 "chart.1": "#111111",
1197 "chart.2": "not a color",
1198 "chart.3": "#333333",
1199 "chart.bullish": "#00ff00",
1200 "chart.bearish": "#ff0000"
1201 }
1202 }))
1203 .unwrap();
1204
1205 let mut theme = Theme::default();
1206 theme.apply_config(&std::rc::Rc::new(config));
1207
1208 assert_eq!(theme.chart_1, try_parse_color("#111111").unwrap());
1211 assert_eq!(theme.chart_2, theme.blue.lighten(0.2));
1212 assert_eq!(theme.chart_3, try_parse_color("#333333").unwrap());
1213 assert_eq!(theme.chart_4, theme.blue.darken(0.2));
1214 assert_eq!(theme.chart_5, theme.blue.darken(0.4));
1215 assert_eq!(theme.tokens.chart_3.color, theme.chart_3);
1216 assert_eq!(theme.chart_bullish, try_parse_color("#00ff00").unwrap());
1217 assert_eq!(theme.chart_bearish, try_parse_color("#ff0000").unwrap());
1218 }
1219
1220 #[test]
1221 fn test_apply_config_preserves_gradient_background_and_solid_color_fallback() {
1222 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1223 "name": "Gradient",
1224 "mode": "light",
1225 "colors": {
1226 "primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)",
1227 "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)"
1228 }
1229 }))
1230 .unwrap();
1231
1232 let mut theme = Theme::default();
1233 theme.apply_config(&std::rc::Rc::new(config));
1234
1235 let primary_from = try_parse_color("#4F46E5").unwrap();
1236 let primary_to = try_parse_color("#06B6D4").unwrap();
1237 assert_eq!(theme.primary, primary_from);
1238 assert_eq!(theme.tokens.primary.color, primary_from);
1239 assert_eq!(
1240 theme.tokens.primary.background,
1241 linear_gradient(
1242 135.,
1243 linear_color_stop(primary_from, 0.),
1244 linear_color_stop(primary_to, 1.)
1245 )
1246 );
1247 assert_eq!(
1248 theme.tokens.button_primary.background,
1249 theme.tokens.primary.background
1250 );
1251 assert_eq!(
1252 theme.tokens.button_primary_hover.background,
1253 linear_gradient(
1254 90.,
1255 linear_color_stop(crate::red_500(), 0.25),
1256 linear_color_stop(crate::blue_600(), 0.75)
1257 )
1258 );
1259 assert_eq!(theme.mode, ThemeMode::Light);
1260 }
1261
1262 #[test]
1263 fn test_aurora_theme_parses_gradient_backgrounds() {
1264 let theme_set =
1265 serde_json::from_str::<ThemeSet>(include_str!("../../../../themes/aurora.json"))
1266 .unwrap();
1267 assert_eq!(theme_set.themes.len(), 1);
1268 assert!(theme_set.themes.iter().all(|theme| !theme.mode.is_dark()));
1269
1270 let light = theme_set
1271 .themes
1272 .iter()
1273 .find(|theme| theme.name.as_ref() == "Aurora Light")
1274 .unwrap();
1275 let mut theme = Theme::default();
1276 theme.apply_config(&std::rc::Rc::new(light.clone()));
1277
1278 assert_ne!(
1279 theme.tokens.button_primary.background,
1280 theme.button_primary.into()
1281 );
1282 assert_eq!(theme.tokens.background.background, theme.background.into());
1283 assert_eq!(theme.button_primary, try_parse_color("#1E293B").unwrap());
1284 assert_eq!(theme.background, try_parse_color("#FFFFFF").unwrap());
1285 assert_ne!(
1286 theme.tokens.progress_bar.background,
1287 theme.progress_bar.into()
1288 );
1289 assert_ne!(
1290 theme.tokens.scrollbar_thumb.background,
1291 theme.scrollbar_thumb.into()
1292 );
1293 assert_ne!(theme.tokens.switch.background, theme.switch.into());
1294 assert_ne!(
1295 theme.tokens.switch_thumb.background,
1296 theme.switch_thumb.into()
1297 );
1298 assert_ne!(theme.tokens.title_bar.background, theme.title_bar.into());
1299 assert_ne!(theme.tokens.status_bar.background, theme.status_bar.into());
1300 }
1301
1302 #[test]
1303 fn test_apply_config_clamps_highlight_alpha_per_gradient_stop() {
1304 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1305 "name": "Highlight",
1306 "mode": "light",
1307 "colors": {
1308 "list.active.background": "#3b82f6",
1310 "table.active.background": "linear-gradient(#bfdbfe33, #3b82f6)",
1313 "selection.background": "linear-gradient(#3b82f600, #3b82f6)",
1317 }
1318 }))
1319 .unwrap();
1320
1321 let mut theme = Theme::default();
1322 theme.apply_config(&std::rc::Rc::new(config));
1323
1324 let blue = try_parse_color("#3b82f6").unwrap();
1326 assert_eq!(theme.list_active, blue.alpha(0.2));
1327 assert_eq!(theme.tokens.list_active.background, blue.alpha(0.2).into());
1328
1329 let faint = try_parse_color("#bfdbfe33").unwrap();
1331 assert_eq!(
1332 theme.tokens.table_active.background,
1333 linear_gradient(
1334 180.,
1335 linear_color_stop(faint.alpha(faint.a.min(0.2)), 0.),
1336 linear_color_stop(blue.alpha(0.2), 1.),
1337 )
1338 );
1339
1340 let clear = try_parse_color("#3b82f600").unwrap();
1343 assert_eq!(
1344 theme.tokens.selection.background,
1345 linear_gradient(
1346 180.,
1347 linear_color_stop(clear.alpha(clear.a.min(0.3)), 0.),
1348 linear_color_stop(blue.alpha(0.3), 1.),
1349 )
1350 );
1351 }
1352}