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)]
24#[non_exhaustive]
25pub struct ThemeSet {
26 pub name: SharedString,
28 pub author: Option<SharedString>,
30 pub url: Option<SharedString>,
32 #[serde(rename = "themes")]
34 pub themes: Vec<ThemeConfig>,
35}
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
38#[serde(default)]
39#[non_exhaustive]
40pub struct ThemeConfig {
41 pub is_default: bool,
43 pub name: SharedString,
45 pub mode: ThemeMode,
47
48 #[serde(rename = "font.size")]
50 pub font_size: Option<f32>,
51 #[serde(rename = "font.family")]
53 pub font_family: Option<SharedString>,
54 #[serde(rename = "mono_font.family")]
63 pub mono_font_family: Option<SharedString>,
64 #[serde(rename = "mono_font.size")]
66 pub mono_font_size: Option<f32>,
67
68 #[serde(rename = "radius")]
70 pub radius: Option<usize>,
71 #[serde(rename = "radius.lg")]
73 pub radius_lg: Option<usize>,
74 #[serde(rename = "shadow")]
76 pub shadow: Option<bool>,
77
78 pub colors: ThemeConfigColors,
80 pub highlight: Option<HighlightThemeStyle>,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
87#[serde(default)]
88#[non_exhaustive]
89pub struct SemanticThemeConfig {
90 pub colors: SemanticColorConfig,
91 pub radius: SemanticRadiusConfig,
92 pub spacing: SemanticSpacingConfig,
93 pub typography: SemanticTypographyConfig,
94 pub shadow: SemanticShadowConfig,
95}
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
102#[serde(default)]
103#[non_exhaustive]
104pub struct SemanticThemeConfigFile {
105 pub tokens: SemanticThemeConfig,
106}
107
108#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
109#[serde(default)]
110#[non_exhaustive]
111pub struct SemanticColorConfig {
112 pub background: Option<SharedString>,
113 pub foreground: Option<SharedString>,
114 pub surface: Option<SharedString>,
115 pub surface_foreground: Option<SharedString>,
116 pub primary: Option<SharedString>,
117 pub primary_foreground: Option<SharedString>,
118 pub secondary: Option<SharedString>,
119 pub secondary_foreground: Option<SharedString>,
120 pub muted: Option<SharedString>,
121 pub muted_foreground: Option<SharedString>,
122 pub accent: Option<SharedString>,
123 pub accent_foreground: Option<SharedString>,
124 pub destructive: Option<SharedString>,
125 pub destructive_foreground: Option<SharedString>,
126 pub border: Option<SharedString>,
127 pub input: Option<SharedString>,
128 pub ring: Option<SharedString>,
129}
130
131#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
132#[serde(default)]
133#[non_exhaustive]
134pub struct SemanticRadiusConfig {
135 pub none: Option<f32>,
136 pub sm: Option<f32>,
137 pub md: Option<f32>,
138 pub lg: Option<f32>,
139 pub xl: Option<f32>,
140 pub full: Option<f32>,
141}
142
143#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
144#[serde(default)]
145#[non_exhaustive]
146pub struct SemanticSpacingConfig {
147 pub xxs: Option<f32>,
148 pub xs: Option<f32>,
149 pub sm: Option<f32>,
150 pub md: Option<f32>,
151 pub lg: Option<f32>,
152 pub xl: Option<f32>,
153 pub xxl: Option<f32>,
154}
155
156#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
157#[serde(default)]
158#[non_exhaustive]
159pub struct SemanticTextStyleConfig {
160 pub size: Option<f32>,
161 pub line_height: Option<f32>,
162 pub weight: Option<FontWeight>,
163}
164
165#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
166#[serde(default)]
167#[non_exhaustive]
168pub struct SemanticTypographyConfig {
169 pub sans: Option<SharedString>,
170 pub mono: Option<SharedString>,
171 pub xs: SemanticTextStyleConfig,
172 pub sm: SemanticTextStyleConfig,
173 pub md: SemanticTextStyleConfig,
174 pub lg: SemanticTextStyleConfig,
175 pub xl: SemanticTextStyleConfig,
176 pub mono_md: SemanticTextStyleConfig,
177}
178
179#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
180#[serde(default)]
181#[non_exhaustive]
182pub struct SemanticShadowConfig {
183 pub sm: Option<Vec<BoxShadow>>,
184 pub md: Option<Vec<BoxShadow>>,
185 pub lg: Option<Vec<BoxShadow>>,
186}
187
188impl SemanticThemeConfig {
189 pub(crate) fn apply_to(&self, tokens: &mut SemanticThemeTokens) {
190 macro_rules! apply_color {
191 ($field:ident) => {
192 if let Some(value) = &self.colors.$field
193 && let Ok(value) = try_parse_color(value)
194 {
195 tokens.colors.$field = value;
196 }
197 };
198 }
199 apply_color!(background);
200 apply_color!(foreground);
201 apply_color!(surface);
202 apply_color!(surface_foreground);
203 apply_color!(primary);
204 apply_color!(primary_foreground);
205 apply_color!(secondary);
206 apply_color!(secondary_foreground);
207 apply_color!(muted);
208 apply_color!(muted_foreground);
209 apply_color!(accent);
210 apply_color!(accent_foreground);
211 apply_color!(destructive);
212 apply_color!(destructive_foreground);
213 apply_color!(border);
214 apply_color!(input);
215 apply_color!(ring);
216
217 macro_rules! apply_pixels {
218 ($config:expr, $tokens:expr, $($field:ident),+ $(,)?) => {
219 $(if let Some(value) = $config.$field { $tokens.$field = px(value); })+
220 };
221 }
222 apply_pixels!(self.radius, tokens.radius, none, sm, md, lg, xl, full);
223 apply_pixels!(self.spacing, tokens.spacing, xxs, xs, sm, md, lg, xl, xxl);
224
225 if let Some(value) = &self.typography.sans {
226 tokens.typography.sans = value.clone();
227 }
228 if let Some(value) = &self.typography.mono {
229 tokens.typography.mono = value.clone();
230 }
231 apply_text_style(&self.typography.xs, &mut tokens.typography.xs);
232 apply_text_style(&self.typography.sm, &mut tokens.typography.sm);
233 apply_text_style(&self.typography.md, &mut tokens.typography.md);
234 apply_text_style(&self.typography.lg, &mut tokens.typography.lg);
235 apply_text_style(&self.typography.xl, &mut tokens.typography.xl);
236 apply_text_style(&self.typography.mono_md, &mut tokens.typography.mono_md);
237
238 if let Some(value) = &self.shadow.sm {
239 tokens.shadow.sm = value.clone();
240 }
241 if let Some(value) = &self.shadow.md {
242 tokens.shadow.md = value.clone();
243 }
244 if let Some(value) = &self.shadow.lg {
245 tokens.shadow.lg = value.clone();
246 }
247 }
248}
249
250fn apply_text_style(config: &SemanticTextStyleConfig, token: &mut gpui_base::TextStyleToken) {
251 if let Some(value) = config.size {
252 token.size = px(value);
253 }
254 if let Some(value) = config.line_height {
255 token.line_height = px(value);
256 }
257 if let Some(value) = config.weight {
258 token.weight = value;
259 }
260}
261
262#[derive(Debug, Default, Clone, JsonSchema, Serialize, Deserialize)]
263#[non_exhaustive]
264pub struct ThemeConfigColors {
265 #[serde(rename = "accent.background")]
267 pub accent: Option<SharedString>,
268 #[serde(rename = "accent.foreground")]
270 pub accent_foreground: Option<SharedString>,
271 #[serde(rename = "accordion.background")]
273 pub accordion: Option<SharedString>,
274 #[serde(rename = "background")]
276 pub background: Option<SharedString>,
277 #[serde(rename = "border")]
279 pub border: Option<SharedString>,
280 #[serde(rename = "button.background")]
282 pub button: Option<SharedString>,
283 #[serde(rename = "button.active.background")]
285 pub button_active: Option<SharedString>,
286 #[serde(rename = "button.foreground")]
288 pub button_foreground: Option<SharedString>,
289 #[serde(rename = "button.hover.background")]
291 pub button_hover: Option<SharedString>,
292 #[serde(rename = "button.danger.background")]
294 pub button_danger: Option<SharedString>,
295 #[serde(rename = "button.danger.active.background")]
297 pub button_danger_active: Option<SharedString>,
298 #[serde(rename = "button.danger.foreground")]
300 pub button_danger_foreground: Option<SharedString>,
301 #[serde(rename = "button.danger.hover.background")]
303 pub button_danger_hover: Option<SharedString>,
304 #[serde(rename = "button.info.background")]
306 pub button_info: Option<SharedString>,
307 #[serde(rename = "button.info.active.background")]
309 pub button_info_active: Option<SharedString>,
310 #[serde(rename = "button.info.foreground")]
312 pub button_info_foreground: Option<SharedString>,
313 #[serde(rename = "button.info.hover.background")]
315 pub button_info_hover: Option<SharedString>,
316 #[serde(rename = "button.primary.background")]
318 pub button_primary: Option<SharedString>,
319 #[serde(rename = "button.primary.active.background")]
321 pub button_primary_active: Option<SharedString>,
322 #[serde(rename = "button.primary.foreground")]
324 pub button_primary_foreground: Option<SharedString>,
325 #[serde(rename = "button.primary.hover.background")]
327 pub button_primary_hover: Option<SharedString>,
328 #[serde(rename = "button.secondary.background")]
330 pub button_secondary: Option<SharedString>,
331 #[serde(rename = "button.secondary.active.background")]
333 pub button_secondary_active: Option<SharedString>,
334 #[serde(rename = "button.secondary.foreground")]
336 pub button_secondary_foreground: Option<SharedString>,
337 #[serde(rename = "button.secondary.hover.background")]
339 pub button_secondary_hover: Option<SharedString>,
340 #[serde(rename = "button.success.background")]
342 pub button_success: Option<SharedString>,
343 #[serde(rename = "button.success.active.background")]
345 pub button_success_active: Option<SharedString>,
346 #[serde(rename = "button.success.foreground")]
348 pub button_success_foreground: Option<SharedString>,
349 #[serde(rename = "button.success.hover.background")]
351 pub button_success_hover: Option<SharedString>,
352 #[serde(rename = "button.warning.background")]
354 pub button_warning: Option<SharedString>,
355 #[serde(rename = "button.warning.active.background")]
357 pub button_warning_active: Option<SharedString>,
358 #[serde(rename = "button.warning.foreground")]
360 pub button_warning_foreground: Option<SharedString>,
361 #[serde(rename = "button.warning.hover.background")]
363 pub button_warning_hover: Option<SharedString>,
364 #[serde(rename = "group_box.background")]
366 pub group_box: Option<SharedString>,
367 #[serde(rename = "group_box.foreground")]
369 pub group_box_foreground: Option<SharedString>,
370 #[serde(rename = "group_box.title.foreground")]
372 pub group_box_title_foreground: Option<SharedString>,
373 #[serde(rename = "caret")]
375 pub caret: Option<SharedString>,
376 #[serde(rename = "chart.1")]
378 pub chart_1: Option<SharedString>,
379 #[serde(rename = "chart.2")]
381 pub chart_2: Option<SharedString>,
382 #[serde(rename = "chart.3")]
384 pub chart_3: Option<SharedString>,
385 #[serde(rename = "chart.4")]
387 pub chart_4: Option<SharedString>,
388 #[serde(rename = "chart.5")]
390 pub chart_5: Option<SharedString>,
391 #[serde(rename = "chart.bullish")]
393 pub chart_bullish: Option<SharedString>,
394 #[serde(rename = "chart.bearish")]
396 pub chart_bearish: Option<SharedString>,
397 #[serde(rename = "chart.grid")]
399 pub chart_grid: Option<SharedString>,
400 #[serde(rename = "danger.background")]
402 pub danger: Option<SharedString>,
403 #[serde(rename = "danger.active.background")]
405 pub danger_active: Option<SharedString>,
406 #[serde(rename = "danger.foreground")]
408 pub danger_foreground: Option<SharedString>,
409 #[serde(rename = "danger.hover.background")]
411 pub danger_hover: Option<SharedString>,
412 #[serde(rename = "description_list.label.background")]
414 pub description_list_label: Option<SharedString>,
415 #[serde(rename = "description_list.label.foreground")]
417 pub description_list_label_foreground: Option<SharedString>,
418 #[serde(rename = "drag.border")]
420 pub drag_border: Option<SharedString>,
421 #[serde(rename = "drop_target.background")]
423 pub drop_target: Option<SharedString>,
424 #[serde(rename = "foreground")]
426 pub foreground: Option<SharedString>,
427 #[serde(rename = "info.background")]
429 pub info: Option<SharedString>,
430 #[serde(rename = "info.active.background")]
432 pub info_active: Option<SharedString>,
433 #[serde(rename = "info.foreground")]
435 pub info_foreground: Option<SharedString>,
436 #[serde(rename = "info.hover.background")]
438 pub info_hover: Option<SharedString>,
439 #[serde(rename = "input.border")]
441 pub input: Option<SharedString>,
442 #[serde(rename = "link")]
444 pub link: Option<SharedString>,
445 #[serde(rename = "link.active")]
447 pub link_active: Option<SharedString>,
448 #[serde(rename = "link.hover")]
450 pub link_hover: Option<SharedString>,
451 #[serde(rename = "list.background")]
453 pub list: Option<SharedString>,
454 #[serde(rename = "list.active.background")]
456 pub list_active: Option<SharedString>,
457 #[serde(rename = "list.active.border")]
459 pub list_active_border: Option<SharedString>,
460 #[serde(rename = "list.even.background")]
462 pub list_even: Option<SharedString>,
463 #[serde(rename = "list.head.background")]
465 pub list_head: Option<SharedString>,
466 #[serde(rename = "list.hover.background")]
468 pub list_hover: Option<SharedString>,
469 #[serde(rename = "muted.background")]
471 pub muted: Option<SharedString>,
472 #[serde(rename = "muted.foreground")]
474 pub muted_foreground: Option<SharedString>,
475 #[serde(rename = "popover.background")]
477 pub popover: Option<SharedString>,
478 #[serde(rename = "popover.foreground")]
480 pub popover_foreground: Option<SharedString>,
481 #[serde(rename = "primary.background")]
483 pub primary: Option<SharedString>,
484 #[serde(rename = "primary.active.background")]
486 pub primary_active: Option<SharedString>,
487 #[serde(rename = "primary.foreground")]
489 pub primary_foreground: Option<SharedString>,
490 #[serde(rename = "primary.hover.background")]
492 pub primary_hover: Option<SharedString>,
493 #[serde(rename = "progress.bar.background")]
495 pub progress_bar: Option<SharedString>,
496 #[serde(rename = "ring")]
498 pub ring: Option<SharedString>,
499 #[serde(rename = "scrollbar.background")]
501 pub scrollbar: Option<SharedString>,
502 #[serde(rename = "scrollbar.thumb.background")]
504 pub scrollbar_thumb: Option<SharedString>,
505 #[serde(rename = "scrollbar.thumb.hover.background")]
507 pub scrollbar_thumb_hover: Option<SharedString>,
508 #[serde(rename = "secondary.background")]
510 pub secondary: Option<SharedString>,
511 #[serde(rename = "secondary.active.background")]
513 pub secondary_active: Option<SharedString>,
514 #[serde(rename = "secondary.foreground")]
516 pub secondary_foreground: Option<SharedString>,
517 #[serde(rename = "secondary.hover.background")]
519 pub secondary_hover: Option<SharedString>,
520 #[serde(rename = "selection.background")]
522 pub selection: Option<SharedString>,
523 #[serde(rename = "sidebar.background")]
525 pub sidebar: Option<SharedString>,
526 #[serde(rename = "sidebar.accent.background")]
528 pub sidebar_accent: Option<SharedString>,
529 #[serde(rename = "sidebar.accent.foreground")]
531 pub sidebar_accent_foreground: Option<SharedString>,
532 #[serde(rename = "sidebar.border")]
534 pub sidebar_border: Option<SharedString>,
535 #[serde(rename = "sidebar.foreground")]
537 pub sidebar_foreground: Option<SharedString>,
538 #[serde(rename = "sidebar.primary.background")]
540 pub sidebar_primary: Option<SharedString>,
541 #[serde(rename = "sidebar.primary.foreground")]
543 pub sidebar_primary_foreground: Option<SharedString>,
544 #[serde(rename = "skeleton.background")]
546 pub skeleton: Option<SharedString>,
547 #[serde(rename = "slider.background")]
549 pub slider_bar: Option<SharedString>,
550 #[serde(rename = "slider.thumb.background")]
552 pub slider_thumb: Option<SharedString>,
553 #[serde(rename = "success.background")]
555 pub success: Option<SharedString>,
556 #[serde(rename = "success.foreground")]
558 pub success_foreground: Option<SharedString>,
559 #[serde(rename = "success.hover.background")]
561 pub success_hover: Option<SharedString>,
562 #[serde(rename = "success.active.background")]
564 pub success_active: Option<SharedString>,
565 #[serde(rename = "switch.background")]
567 pub switch: Option<SharedString>,
568 #[serde(rename = "switch.thumb.background")]
570 pub switch_thumb: Option<SharedString>,
571 #[serde(rename = "tab.background")]
573 pub tab: Option<SharedString>,
574 #[serde(rename = "tab.active.background")]
576 pub tab_active: Option<SharedString>,
577 #[serde(rename = "tab.active.foreground")]
579 pub tab_active_foreground: Option<SharedString>,
580 #[serde(rename = "tab_bar.background")]
582 pub tab_bar: Option<SharedString>,
583 #[serde(rename = "tab_bar.segmented.background")]
585 pub tab_bar_segmented: Option<SharedString>,
586 #[serde(rename = "tab.foreground")]
588 pub tab_foreground: Option<SharedString>,
589 #[serde(rename = "table.background")]
591 pub table: Option<SharedString>,
592 #[serde(rename = "table.active.background")]
594 pub table_active: Option<SharedString>,
595 #[serde(rename = "table.active.border")]
597 pub table_active_border: Option<SharedString>,
598 #[serde(rename = "table.even.background")]
600 pub table_even: Option<SharedString>,
601 #[serde(rename = "table.head.background")]
603 pub table_head: Option<SharedString>,
604 #[serde(rename = "table.head.foreground")]
606 pub table_head_foreground: Option<SharedString>,
607 #[serde(rename = "table.foot.background")]
609 pub table_foot: Option<SharedString>,
610 #[serde(rename = "table.foot.foreground")]
612 pub table_foot_foreground: Option<SharedString>,
613 #[serde(rename = "table.hover.background")]
615 pub table_hover: Option<SharedString>,
616 #[serde(rename = "table.row.border")]
618 pub table_row_border: Option<SharedString>,
619 #[serde(rename = "title_bar.background")]
621 pub title_bar: Option<SharedString>,
622 #[serde(rename = "title_bar.border")]
624 pub title_bar_border: Option<SharedString>,
625 #[serde(rename = "status_bar.background")]
627 pub status_bar: Option<SharedString>,
628 #[serde(rename = "status_bar.border")]
630 pub status_bar_border: Option<SharedString>,
631 #[serde(rename = "warning.background")]
633 pub warning: Option<SharedString>,
634 #[serde(rename = "warning.active.background")]
636 pub warning_active: Option<SharedString>,
637 #[serde(rename = "warning.hover.background")]
639 pub warning_hover: Option<SharedString>,
640 #[serde(rename = "warning.foreground")]
642 pub warning_foreground: Option<SharedString>,
643 #[serde(rename = "overlay")]
645 pub overlay: Option<SharedString>,
646 #[serde(rename = "window.border")]
652 pub window_border: Option<SharedString>,
653
654 #[serde(rename = "base.blue")]
656 blue: Option<String>,
657 #[serde(rename = "base.blue.light")]
659 blue_light: Option<String>,
660 #[serde(rename = "base.cyan")]
662 cyan: Option<String>,
663 #[serde(rename = "base.cyan.light")]
665 cyan_light: Option<String>,
666 #[serde(rename = "base.green")]
668 green: Option<String>,
669 #[serde(rename = "base.green.light")]
671 green_light: Option<String>,
672 #[serde(rename = "base.magenta")]
674 magenta: Option<String>,
675 #[serde(rename = "base.magenta.light")]
676 magenta_light: Option<String>,
677 #[serde(rename = "base.red")]
679 red: Option<String>,
680 #[serde(rename = "base.red.light")]
682 red_light: Option<String>,
683 #[serde(rename = "base.yellow")]
685 yellow: Option<String>,
686 #[serde(rename = "base.yellow.light")]
688 yellow_light: Option<String>,
689}
690
691impl ThemeColor {
692 pub(crate) fn apply_config(
694 &mut self,
695 config: &ThemeConfig,
696 default_theme: &ThemeColor,
697 ) -> ThemeTokens {
698 let colors = config.colors.clone();
699 let default_tokens = ThemeTokens::from(default_theme);
700 let mut tokens = default_tokens;
701
702 macro_rules! apply_color {
703 ($config_field:ident) => {
704 if let Some(value) = &colors.$config_field {
705 self.$config_field =
706 try_parse_color(value).unwrap_or(default_theme.$config_field);
707 } else {
708 self.$config_field = default_theme.$config_field;
709 }
710 tokens.$config_field = self.$config_field.into();
711 };
712 ($config_field:ident, fallback = $fallback:expr) => {
714 let fallback: gpui::Hsla = ($fallback).into();
715 if let Some(value) = &colors.$config_field {
716 self.$config_field = try_parse_color(value).unwrap_or(fallback);
717 } else {
718 self.$config_field = fallback;
719 }
720 tokens.$config_field = self.$config_field.into();
721 };
722 }
723
724 macro_rules! apply_background_color {
725 ($config_field:ident) => {
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 default_tokens.$config_field
731 }
732 } else {
733 default_tokens.$config_field
734 };
735 self.$config_field = token.color;
736 tokens.$config_field = token;
737 };
738 ($config_field:ident, fallback = $fallback:expr) => {
739 let fallback: ThemeToken = ($fallback).into();
740 let token = if let Some(value) = &colors.$config_field {
741 if let Ok(token) = try_parse_theme_token(&value) {
742 token
743 } else {
744 fallback
745 }
746 } else {
747 fallback
748 };
749 self.$config_field = token.color;
750 tokens.$config_field = token;
751 };
752 }
753
754 apply_background_color!(background);
755
756 apply_color!(red);
758 apply_color!(
759 red_light,
760 fallback = self.background.blend(self.red.opacity(0.8))
761 );
762 apply_color!(green);
763 apply_color!(
764 green_light,
765 fallback = self.background.blend(self.green.opacity(0.8))
766 );
767 apply_color!(blue);
768 apply_color!(
769 blue_light,
770 fallback = self.background.blend(self.blue.opacity(0.8))
771 );
772 apply_color!(magenta);
773 apply_color!(
774 magenta_light,
775 fallback = self.background.blend(self.magenta.opacity(0.8))
776 );
777 apply_color!(yellow);
778 apply_color!(
779 yellow_light,
780 fallback = self.background.blend(self.yellow.opacity(0.8))
781 );
782 apply_color!(cyan);
783 apply_color!(
784 cyan_light,
785 fallback = self.background.blend(self.cyan.opacity(0.8))
786 );
787
788 apply_color!(border);
789 apply_color!(foreground);
790 apply_color!(input, fallback = self.border);
791 apply_background_color!(muted);
792 apply_color!(
793 muted_foreground,
794 fallback = self.muted.blend(self.foreground.opacity(0.7))
795 );
796
797 let active_darken = if config.mode.is_dark() { 0.2 } else { 0.1 };
799 let hover_opacity = 0.9;
800 let transparent = gpui::transparent_black();
801 let button_background = if config.mode.is_dark() {
802 self.input.mix_oklab(transparent, 0.3)
803 } else {
804 self.background
805 };
806 apply_background_color!(button, fallback = button_background);
807 apply_color!(button_foreground, fallback = self.foreground);
808 apply_background_color!(
809 button_hover,
810 fallback = self.input.mix_oklab(transparent, 0.5)
811 );
812 apply_background_color!(
813 button_active,
814 fallback = self.input.mix_oklab(transparent, 0.7)
815 );
816 apply_background_color!(primary);
817 apply_color!(primary_foreground, fallback = self.foreground);
818 apply_background_color!(
819 primary_hover,
820 fallback = self.background.blend(self.primary.opacity(hover_opacity))
821 );
822 apply_background_color!(
823 primary_active,
824 fallback = self.primary.darken(active_darken)
825 );
826 apply_background_color!(button_primary, fallback = tokens.primary);
827 apply_color!(
828 button_primary_foreground,
829 fallback = self.primary_foreground
830 );
831 apply_background_color!(button_primary_hover, fallback = tokens.primary_hover);
832 apply_background_color!(button_primary_active, fallback = tokens.primary_active);
833 apply_background_color!(secondary);
834 apply_color!(secondary_foreground, fallback = self.foreground);
835 apply_background_color!(
836 secondary_hover,
837 fallback = self.background.blend(self.secondary.opacity(hover_opacity))
838 );
839 apply_background_color!(
840 secondary_active,
841 fallback = self.secondary.darken(active_darken)
842 );
843 apply_background_color!(button_secondary, fallback = tokens.secondary);
844 apply_color!(
845 button_secondary_foreground,
846 fallback = self.secondary_foreground
847 );
848 apply_background_color!(button_secondary_hover, fallback = tokens.secondary_hover);
849 apply_background_color!(button_secondary_active, fallback = tokens.secondary_active);
850 apply_background_color!(success, fallback = self.green);
851 apply_color!(success_foreground, fallback = self.primary_foreground);
852 apply_background_color!(
853 success_hover,
854 fallback = self.background.blend(self.success.opacity(hover_opacity))
855 );
856 apply_background_color!(
857 success_active,
858 fallback = self.success.darken(active_darken)
859 );
860 apply_background_color!(
861 button_success,
862 fallback = self.success.mix_oklab(transparent, 0.2)
863 );
864 apply_color!(button_success_foreground, fallback = self.success);
865 apply_background_color!(
866 button_success_hover,
867 fallback = self.success.mix_oklab(transparent, 0.3)
868 );
869 apply_background_color!(
870 button_success_active,
871 fallback = self.success.mix_oklab(transparent, 0.4)
872 );
873 apply_background_color!(info, fallback = self.cyan);
874 apply_color!(info_foreground, fallback = self.primary_foreground);
875 apply_background_color!(
876 info_hover,
877 fallback = self.background.blend(self.info.opacity(hover_opacity))
878 );
879 apply_background_color!(info_active, fallback = self.info.darken(active_darken));
880 apply_background_color!(
881 button_info,
882 fallback = self.info.mix_oklab(transparent, 0.2)
883 );
884 apply_color!(button_info_foreground, fallback = self.info);
885 apply_background_color!(
886 button_info_hover,
887 fallback = self.info.mix_oklab(transparent, 0.3)
888 );
889 apply_background_color!(
890 button_info_active,
891 fallback = self.info.mix_oklab(transparent, 0.4)
892 );
893 apply_background_color!(warning, fallback = self.yellow);
894 apply_color!(warning_foreground, fallback = self.primary_foreground);
895 apply_background_color!(
896 warning_hover,
897 fallback = self.background.blend(self.warning.opacity(0.9))
898 );
899 apply_background_color!(
900 warning_active,
901 fallback = self.background.blend(self.warning.darken(active_darken))
902 );
903 apply_background_color!(
904 button_warning,
905 fallback = self.warning.mix_oklab(transparent, 0.2)
906 );
907 apply_color!(button_warning_foreground, fallback = self.warning);
908 apply_background_color!(
909 button_warning_hover,
910 fallback = self.warning.mix_oklab(transparent, 0.3)
911 );
912 apply_background_color!(
913 button_warning_active,
914 fallback = self.warning.mix_oklab(transparent, 0.4)
915 );
916
917 apply_background_color!(accent, fallback = tokens.secondary);
919 apply_color!(accent_foreground, fallback = self.foreground);
920 apply_background_color!(accordion, fallback = tokens.background);
921 apply_background_color!(
922 group_box,
923 fallback = self
924 .background
925 .blend(
926 self.secondary
927 .opacity(if config.mode.is_dark() { 0.3 } else { 0.4 })
928 )
929 );
930 apply_color!(group_box_foreground, fallback = self.foreground);
931 apply_color!(caret, fallback = self.primary);
932 apply_color!(chart_1, fallback = self.blue.lighten(0.4));
933 apply_color!(chart_2, fallback = self.blue.lighten(0.2));
934 apply_color!(chart_3, fallback = self.blue);
935 apply_color!(chart_4, fallback = self.blue.darken(0.2));
936 apply_color!(chart_5, fallback = self.blue.darken(0.4));
937 apply_color!(chart_bullish, fallback = self.green);
938 apply_color!(chart_bearish, fallback = self.red);
939 apply_color!(chart_grid, fallback = self.border.opacity(0.6));
940 apply_background_color!(danger, fallback = self.red);
941 apply_background_color!(danger_active, fallback = self.danger.darken(active_darken));
942 apply_color!(danger_foreground, fallback = self.primary_foreground);
943 apply_background_color!(
944 danger_hover,
945 fallback = self.background.blend(self.danger.opacity(0.9))
946 );
947 apply_background_color!(
948 button_danger,
949 fallback = self.danger.mix_oklab(transparent, 0.2)
950 );
951 apply_color!(button_danger_foreground, fallback = self.danger);
952 apply_background_color!(
953 button_danger_hover,
954 fallback = self.danger.mix_oklab(transparent, 0.3)
955 );
956 apply_background_color!(
957 button_danger_active,
958 fallback = self.danger.mix_oklab(transparent, 0.4)
959 );
960 apply_background_color!(
961 description_list_label,
962 fallback = self.background.blend(self.border.opacity(0.2))
963 );
964 apply_color!(
965 description_list_label_foreground,
966 fallback = self.muted_foreground
967 );
968 apply_color!(drag_border, fallback = self.primary.opacity(0.65));
969 apply_background_color!(drop_target, fallback = self.primary.opacity(0.2));
970 apply_color!(link, fallback = self.primary);
971 apply_color!(link_active, fallback = self.link);
972 apply_color!(link_hover, fallback = self.link);
973 apply_background_color!(list, fallback = tokens.background);
974 apply_background_color!(
975 list_active,
976 fallback = self.background.blend(self.primary.opacity(0.1))
977 );
978 apply_color!(
979 list_active_border,
980 fallback = self.background.blend(self.primary.opacity(0.6))
981 );
982 apply_background_color!(list_even, fallback = tokens.list);
983 apply_background_color!(list_head, fallback = tokens.list);
984 apply_background_color!(list_hover, fallback = self.accent.opacity(0.6));
985 apply_background_color!(popover, fallback = tokens.background);
986 apply_color!(popover_foreground, fallback = self.foreground);
987 apply_background_color!(progress_bar, fallback = tokens.primary);
988 apply_color!(ring, fallback = self.blue);
989 apply_background_color!(scrollbar, fallback = tokens.background);
990 apply_background_color!(scrollbar_thumb, fallback = tokens.accent);
991 apply_background_color!(scrollbar_thumb_hover, fallback = tokens.scrollbar_thumb);
992 apply_background_color!(selection, fallback = tokens.primary);
993 apply_background_color!(
994 sidebar,
995 fallback = self.background.blend(self.border.opacity(0.15))
996 );
997 apply_background_color!(sidebar_accent, fallback = tokens.accent);
998 apply_color!(sidebar_accent_foreground, fallback = self.accent_foreground);
999 apply_color!(sidebar_border, fallback = self.border);
1000 apply_color!(sidebar_foreground, fallback = self.foreground);
1001 apply_background_color!(sidebar_primary, fallback = tokens.primary);
1002 apply_color!(
1003 sidebar_primary_foreground,
1004 fallback = self.primary_foreground
1005 );
1006 apply_background_color!(skeleton, fallback = tokens.secondary);
1007 apply_background_color!(slider_bar, fallback = tokens.primary);
1008 apply_background_color!(slider_thumb, fallback = self.primary_foreground);
1009 apply_background_color!(switch, fallback = tokens.secondary_active);
1010 apply_background_color!(switch_thumb, fallback = tokens.background);
1011 apply_background_color!(tab, fallback = tokens.background);
1012 apply_background_color!(tab_active, fallback = tokens.background);
1013 apply_color!(tab_active_foreground, fallback = self.foreground);
1014 apply_background_color!(tab_bar, fallback = tokens.background);
1015 apply_background_color!(tab_bar_segmented, fallback = tokens.secondary);
1016 apply_color!(tab_foreground, fallback = self.foreground);
1017 apply_background_color!(table, fallback = tokens.list);
1018 apply_background_color!(table_active, fallback = tokens.list_active);
1019 apply_color!(table_active_border, fallback = self.list_active_border);
1020 apply_background_color!(table_even, fallback = tokens.list_even);
1021 apply_background_color!(table_head, fallback = tokens.list_head);
1022 apply_color!(table_head_foreground, fallback = self.muted_foreground);
1023 apply_background_color!(table_foot, fallback = tokens.list_head);
1024 apply_color!(table_foot_foreground, fallback = self.muted_foreground);
1025 apply_background_color!(table_hover, fallback = tokens.list_hover);
1026 apply_color!(table_row_border, fallback = self.border);
1027 apply_background_color!(title_bar, fallback = tokens.background);
1028 apply_color!(title_bar_border, fallback = self.border);
1029 apply_background_color!(status_bar, fallback = tokens.title_bar);
1030 apply_color!(status_bar_border, fallback = self.title_bar_border);
1031 apply_background_color!(overlay);
1032 apply_color!(window_border, fallback = self.border);
1033
1034 let clamp_alpha = |raw: Option<&str>, color: Hsla, background: Background, max: f32| {
1038 let base = color.a;
1039 let target = base.min(max);
1040 let color = color.alpha(target);
1041 let background = raw
1042 .and_then(|value| try_parse_background_clamped(value, max).ok())
1043 .unwrap_or_else(|| {
1044 let factor = if base > 0. { target / base } else { 1. };
1045 background.opacity(factor)
1046 });
1047 (color, ThemeToken::new(color, background))
1048 };
1049
1050 (self.list_active, tokens.list_active) = clamp_alpha(
1051 colors.list_active.as_deref(),
1052 self.list_active,
1053 tokens.list_active.background,
1054 0.2,
1055 );
1056 (self.table_active, tokens.table_active) = clamp_alpha(
1057 colors.table_active.as_deref(),
1058 self.table_active,
1059 tokens.table_active.background,
1060 0.2,
1061 );
1062 (self.selection, tokens.selection) = clamp_alpha(
1063 colors.selection.as_deref(),
1064 self.selection,
1065 tokens.selection.background,
1066 0.3,
1067 );
1068
1069 tokens
1070 }
1071}
1072
1073impl Theme {
1074 pub fn apply_config(&mut self, config: &Rc<ThemeConfig>) {
1076 if config.mode.is_dark() {
1077 self.dark_theme = config.clone();
1078 } else {
1079 self.light_theme = config.clone();
1080 }
1081 if let Some(style) = &config.highlight {
1082 let highlight_theme = Arc::new(HighlightTheme {
1083 name: config.name.to_string(),
1084 appearance: config.mode,
1085 style: style.clone(),
1086 });
1087 self.highlight_theme = highlight_theme.clone();
1088 }
1089
1090 let default_colors = if config.mode.is_dark() {
1091 ThemeColor::dark()
1092 } else {
1093 ThemeColor::light()
1094 };
1095
1096 if let Some(font_size) = config.font_size {
1097 self.font_size = px(font_size);
1098 }
1099 if let Some(font_family) = &config.font_family {
1100 self.font_family = font_family.clone();
1101 }
1102 if let Some(mono_font_family) = &config.mono_font_family {
1103 self.mono_font_family = mono_font_family.clone();
1104 }
1105 if let Some(mono_font_size) = config.mono_font_size {
1106 self.mono_font_size = px(mono_font_size);
1107 }
1108 if let Some(radius) = config.radius {
1109 self.radius = px(radius as f32);
1110 }
1111 if let Some(radius_lg) = config.radius_lg {
1112 self.radius_lg = px(radius_lg as f32);
1113 }
1114 if let Some(shadow) = config.shadow {
1115 self.shadow = shadow;
1116 }
1117
1118 self.tokens = self.colors.apply_config(&config, &default_colors);
1119 self.mode = config.mode;
1120 }
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125 use gpui::{linear_color_stop, linear_gradient, px};
1126
1127 use crate::{Colorize as _, Theme, ThemeConfig, ThemeMode, ThemeSet, try_parse_color};
1128
1129 #[test]
1130 fn test_semantic_theme_config_parses_and_roundtrips() {
1131 let value = serde_json::json!({
1132 "name": "Semantic",
1133 "mode": "dark",
1134 "tokens": {
1135 "colors": {
1136 "surface": "#111827",
1137 "surface_foreground": "#f9fafb",
1138 "primary": "#2563eb",
1139 "destructive": "#dc2626"
1140 },
1141 "radius": { "sm": 2.0, "md": 6.0, "lg": 10.0 },
1142 "spacing": { "xs": 4.0, "md": 12.0, "xl": 24.0 },
1143 "typography": {
1144 "sans": "Inter",
1145 "md": { "size": 15.0, "line_height": 22.0 }
1146 }
1147 }
1148 });
1149 let config: super::SemanticThemeConfigFile = serde_json::from_value(value).unwrap();
1150 let serialized = serde_json::to_string(&config).unwrap();
1151 let reparsed: super::SemanticThemeConfigFile = serde_json::from_str(&serialized).unwrap();
1152 let semantic = reparsed.tokens;
1153
1154 assert_eq!(semantic.colors.surface.as_deref(), Some("#111827"));
1155 assert_eq!(semantic.colors.destructive.as_deref(), Some("#dc2626"));
1156 assert_eq!(semantic.radius.lg, Some(10.0));
1157 assert_eq!(semantic.spacing.xl, Some(24.0));
1158 assert_eq!(semantic.typography.sans.as_deref(), Some("Inter"));
1159 assert_eq!(semantic.typography.md.line_height, Some(22.0));
1160
1161 let mut theme = Theme::default();
1162 let resolved = theme.apply_semantic_config_str(&serialized).unwrap();
1163 assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1164 assert_eq!(resolved.spacing.xl, px(24.));
1165 assert_eq!(resolved.typography.md.line_height, px(22.));
1166 }
1167
1168 #[test]
1169 fn test_semantic_tokens_override_legacy_generic_fields_only() {
1170 let config = serde_json::from_value::<super::SemanticThemeConfigFile>(serde_json::json!({
1171 "tokens": {
1172 "colors": { "primary": "#2563eb", "destructive": "#b91c1c" },
1173 "spacing": { "md": 14.0 },
1174 "typography": { "md": { "size": 15.0 } }
1175 }
1176 }))
1177 .unwrap();
1178 let mut theme = Theme::default();
1179 let component_color = theme.button_primary;
1180 let resolved = theme.apply_semantic_config(&config.tokens);
1181
1182 assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1183 assert_eq!(theme.danger, try_parse_color("#b91c1c").unwrap());
1184 assert_eq!(theme.button_primary, component_color);
1185 assert_eq!(resolved.spacing.md, px(14.));
1186 assert_eq!(resolved.typography.md.size, px(15.));
1187 }
1188
1189 #[test]
1190 fn test_legacy_config_without_semantic_tokens_is_unchanged() {
1191 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1192 "name": "Legacy",
1193 "mode": "light",
1194 "radius": 7,
1195 "colors": { "primary.background": "#7c3aed" }
1196 }))
1197 .unwrap();
1198 let mut theme = Theme::default();
1199 theme.apply_config(&std::rc::Rc::new(config));
1200 assert_eq!(theme.primary, try_parse_color("#7c3aed").unwrap());
1201 assert_eq!(theme.radius, px(7.));
1202 assert_eq!(theme.semantic_tokens().spacing, Default::default());
1203 }
1204
1205 #[test]
1206 fn test_apply_config_reads_the_chart_colors() {
1207 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1208 "name": "Palette",
1209 "mode": "light",
1210 "colors": {
1211 "chart.1": "#111111",
1212 "chart.2": "not a color",
1213 "chart.3": "#333333",
1214 "chart.bullish": "#00ff00",
1215 "chart.bearish": "#ff0000",
1216 "chart.grid": "#eeeeee"
1217 }
1218 }))
1219 .unwrap();
1220
1221 let mut theme = Theme::default();
1222 theme.apply_config(&std::rc::Rc::new(config));
1223
1224 assert_eq!(theme.chart_1, try_parse_color("#111111").unwrap());
1227 assert_eq!(theme.chart_2, theme.blue.lighten(0.2));
1228 assert_eq!(theme.chart_3, try_parse_color("#333333").unwrap());
1229 assert_eq!(theme.chart_4, theme.blue.darken(0.2));
1230 assert_eq!(theme.chart_5, theme.blue.darken(0.4));
1231 assert_eq!(theme.tokens.chart_3.color, theme.chart_3);
1232 assert_eq!(theme.chart_bullish, try_parse_color("#00ff00").unwrap());
1233 assert_eq!(theme.chart_bearish, try_parse_color("#ff0000").unwrap());
1234 assert_eq!(theme.chart_grid, try_parse_color("#eeeeee").unwrap());
1235
1236 theme.apply_config(&std::rc::Rc::new(ThemeConfig::default()));
1237 assert_eq!(theme.chart_grid, theme.border.opacity(0.6));
1238 }
1239
1240 #[test]
1241 fn test_apply_config_preserves_gradient_background_and_solid_color_fallback() {
1242 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1243 "name": "Gradient",
1244 "mode": "light",
1245 "colors": {
1246 "primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)",
1247 "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)"
1248 }
1249 }))
1250 .unwrap();
1251
1252 let mut theme = Theme::default();
1253 theme.apply_config(&std::rc::Rc::new(config));
1254
1255 let primary_from = try_parse_color("#4F46E5").unwrap();
1256 let primary_to = try_parse_color("#06B6D4").unwrap();
1257 assert_eq!(theme.primary, primary_from);
1258 assert_eq!(theme.tokens.primary.color, primary_from);
1259 assert_eq!(
1260 theme.tokens.primary.background,
1261 linear_gradient(
1262 135.,
1263 linear_color_stop(primary_from, 0.),
1264 linear_color_stop(primary_to, 1.)
1265 )
1266 );
1267 assert_eq!(
1268 theme.tokens.button_primary.background,
1269 theme.tokens.primary.background
1270 );
1271 assert_eq!(
1272 theme.tokens.button_primary_hover.background,
1273 linear_gradient(
1274 90.,
1275 linear_color_stop(crate::red_500(), 0.25),
1276 linear_color_stop(crate::blue_600(), 0.75)
1277 )
1278 );
1279 assert_eq!(theme.mode, ThemeMode::Light);
1280 }
1281
1282 #[test]
1283 fn test_aurora_theme_parses_gradient_backgrounds() {
1284 let theme_set =
1285 serde_json::from_str::<ThemeSet>(include_str!("../../../../themes/aurora.json"))
1286 .unwrap();
1287 assert_eq!(theme_set.themes.len(), 1);
1288 assert!(theme_set.themes.iter().all(|theme| !theme.mode.is_dark()));
1289
1290 let light = theme_set
1291 .themes
1292 .iter()
1293 .find(|theme| theme.name.as_ref() == "Aurora Light")
1294 .unwrap();
1295 let mut theme = Theme::default();
1296 theme.apply_config(&std::rc::Rc::new(light.clone()));
1297
1298 assert_ne!(
1299 theme.tokens.button_primary.background,
1300 theme.button_primary.into()
1301 );
1302 assert_eq!(theme.tokens.background.background, theme.background.into());
1303 assert_eq!(theme.button_primary, try_parse_color("#1E293B").unwrap());
1304 assert_eq!(theme.background, try_parse_color("#FFFFFF").unwrap());
1305 assert_ne!(
1306 theme.tokens.progress_bar.background,
1307 theme.progress_bar.into()
1308 );
1309 assert_ne!(
1310 theme.tokens.scrollbar_thumb.background,
1311 theme.scrollbar_thumb.into()
1312 );
1313 assert_ne!(theme.tokens.switch.background, theme.switch.into());
1314 assert_ne!(
1315 theme.tokens.switch_thumb.background,
1316 theme.switch_thumb.into()
1317 );
1318 assert_ne!(theme.tokens.title_bar.background, theme.title_bar.into());
1319 assert_ne!(theme.tokens.status_bar.background, theme.status_bar.into());
1320 }
1321
1322 #[test]
1323 fn test_apply_config_clamps_highlight_alpha_per_gradient_stop() {
1324 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1325 "name": "Highlight",
1326 "mode": "light",
1327 "colors": {
1328 "list.active.background": "#3b82f6",
1330 "table.active.background": "linear-gradient(#bfdbfe33, #3b82f6)",
1333 "selection.background": "linear-gradient(#3b82f600, #3b82f6)",
1337 }
1338 }))
1339 .unwrap();
1340
1341 let mut theme = Theme::default();
1342 theme.apply_config(&std::rc::Rc::new(config));
1343
1344 let blue = try_parse_color("#3b82f6").unwrap();
1346 assert_eq!(theme.list_active, blue.alpha(0.2));
1347 assert_eq!(theme.tokens.list_active.background, blue.alpha(0.2).into());
1348
1349 let faint = try_parse_color("#bfdbfe33").unwrap();
1351 assert_eq!(
1352 theme.tokens.table_active.background,
1353 linear_gradient(
1354 180.,
1355 linear_color_stop(faint.alpha(faint.a.min(0.2)), 0.),
1356 linear_color_stop(blue.alpha(0.2), 1.),
1357 )
1358 );
1359
1360 let clear = try_parse_color("#3b82f600").unwrap();
1363 assert_eq!(
1364 theme.tokens.selection.background,
1365 linear_gradient(
1366 180.,
1367 linear_color_stop(clear.alpha(clear.a.min(0.3)), 0.),
1368 linear_color_stop(blue.alpha(0.3), 1.),
1369 )
1370 );
1371 }
1372}