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")]
57 pub mono_font_family: Option<SharedString>,
58 #[serde(rename = "mono_font.size")]
60 pub mono_font_size: Option<f32>,
61
62 #[serde(rename = "radius")]
64 pub radius: Option<usize>,
65 #[serde(rename = "radius.lg")]
67 pub radius_lg: Option<usize>,
68 #[serde(rename = "shadow")]
70 pub shadow: Option<bool>,
71
72 pub colors: ThemeConfigColors,
74 pub highlight: Option<HighlightThemeStyle>,
78}
79
80#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
81#[serde(default)]
82pub struct SemanticThemeConfig {
83 pub colors: SemanticColorConfig,
84 pub radius: SemanticRadiusConfig,
85 pub spacing: SemanticSpacingConfig,
86 pub typography: SemanticTypographyConfig,
87 pub shadow: SemanticShadowConfig,
88}
89
90#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
95#[serde(default)]
96pub struct SemanticThemeConfigFile {
97 pub tokens: SemanticThemeConfig,
98}
99
100#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
101#[serde(default)]
102pub struct SemanticColorConfig {
103 pub background: Option<SharedString>,
104 pub foreground: Option<SharedString>,
105 pub surface: Option<SharedString>,
106 pub surface_foreground: Option<SharedString>,
107 pub primary: Option<SharedString>,
108 pub primary_foreground: Option<SharedString>,
109 pub secondary: Option<SharedString>,
110 pub secondary_foreground: Option<SharedString>,
111 pub muted: Option<SharedString>,
112 pub muted_foreground: Option<SharedString>,
113 pub accent: Option<SharedString>,
114 pub accent_foreground: Option<SharedString>,
115 pub destructive: Option<SharedString>,
116 pub destructive_foreground: Option<SharedString>,
117 pub border: Option<SharedString>,
118 pub input: Option<SharedString>,
119 pub ring: Option<SharedString>,
120}
121
122#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
123#[serde(default)]
124pub struct SemanticRadiusConfig {
125 pub none: Option<f32>,
126 pub sm: Option<f32>,
127 pub md: Option<f32>,
128 pub lg: Option<f32>,
129 pub xl: Option<f32>,
130 pub full: Option<f32>,
131}
132
133#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
134#[serde(default)]
135pub struct SemanticSpacingConfig {
136 pub xxs: Option<f32>,
137 pub xs: Option<f32>,
138 pub sm: Option<f32>,
139 pub md: Option<f32>,
140 pub lg: Option<f32>,
141 pub xl: Option<f32>,
142 pub xxl: Option<f32>,
143}
144
145#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
146#[serde(default)]
147pub struct SemanticTextStyleConfig {
148 pub size: Option<f32>,
149 pub line_height: Option<f32>,
150 pub weight: Option<FontWeight>,
151}
152
153#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
154#[serde(default)]
155pub struct SemanticTypographyConfig {
156 pub sans: Option<SharedString>,
157 pub mono: Option<SharedString>,
158 pub xs: SemanticTextStyleConfig,
159 pub sm: SemanticTextStyleConfig,
160 pub md: SemanticTextStyleConfig,
161 pub lg: SemanticTextStyleConfig,
162 pub xl: SemanticTextStyleConfig,
163 pub mono_md: SemanticTextStyleConfig,
164}
165
166#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
167#[serde(default)]
168pub struct SemanticShadowConfig {
169 pub sm: Option<Vec<BoxShadow>>,
170 pub md: Option<Vec<BoxShadow>>,
171 pub lg: Option<Vec<BoxShadow>>,
172}
173
174impl SemanticThemeConfig {
175 pub(crate) fn apply_to(&self, tokens: &mut SemanticThemeTokens) {
176 macro_rules! apply_color {
177 ($field:ident) => {
178 if let Some(value) = &self.colors.$field
179 && let Ok(value) = try_parse_color(value)
180 {
181 tokens.colors.$field = value;
182 }
183 };
184 }
185 apply_color!(background);
186 apply_color!(foreground);
187 apply_color!(surface);
188 apply_color!(surface_foreground);
189 apply_color!(primary);
190 apply_color!(primary_foreground);
191 apply_color!(secondary);
192 apply_color!(secondary_foreground);
193 apply_color!(muted);
194 apply_color!(muted_foreground);
195 apply_color!(accent);
196 apply_color!(accent_foreground);
197 apply_color!(destructive);
198 apply_color!(destructive_foreground);
199 apply_color!(border);
200 apply_color!(input);
201 apply_color!(ring);
202
203 macro_rules! apply_pixels {
204 ($config:expr, $tokens:expr, $($field:ident),+ $(,)?) => {
205 $(if let Some(value) = $config.$field { $tokens.$field = px(value); })+
206 };
207 }
208 apply_pixels!(self.radius, tokens.radius, none, sm, md, lg, xl, full);
209 apply_pixels!(self.spacing, tokens.spacing, xxs, xs, sm, md, lg, xl, xxl);
210
211 if let Some(value) = &self.typography.sans {
212 tokens.typography.sans = value.clone();
213 }
214 if let Some(value) = &self.typography.mono {
215 tokens.typography.mono = value.clone();
216 }
217 apply_text_style(&self.typography.xs, &mut tokens.typography.xs);
218 apply_text_style(&self.typography.sm, &mut tokens.typography.sm);
219 apply_text_style(&self.typography.md, &mut tokens.typography.md);
220 apply_text_style(&self.typography.lg, &mut tokens.typography.lg);
221 apply_text_style(&self.typography.xl, &mut tokens.typography.xl);
222 apply_text_style(&self.typography.mono_md, &mut tokens.typography.mono_md);
223
224 if let Some(value) = &self.shadow.sm {
225 tokens.shadow.sm = value.clone();
226 }
227 if let Some(value) = &self.shadow.md {
228 tokens.shadow.md = value.clone();
229 }
230 if let Some(value) = &self.shadow.lg {
231 tokens.shadow.lg = value.clone();
232 }
233 }
234}
235
236fn apply_text_style(config: &SemanticTextStyleConfig, token: &mut gpui_base::TextStyleToken) {
237 if let Some(value) = config.size {
238 token.size = px(value);
239 }
240 if let Some(value) = config.line_height {
241 token.line_height = px(value);
242 }
243 if let Some(value) = config.weight {
244 token.weight = value;
245 }
246}
247
248#[derive(Debug, Default, Clone, JsonSchema, Serialize, Deserialize)]
249pub struct ThemeConfigColors {
250 #[serde(rename = "accent.background")]
252 pub accent: Option<SharedString>,
253 #[serde(rename = "accent.foreground")]
255 pub accent_foreground: Option<SharedString>,
256 #[serde(rename = "accordion.background")]
258 pub accordion: Option<SharedString>,
259 #[serde(rename = "background")]
261 pub background: Option<SharedString>,
262 #[serde(rename = "border")]
264 pub border: Option<SharedString>,
265 #[serde(rename = "button.background")]
267 pub button: Option<SharedString>,
268 #[serde(rename = "button.active.background")]
270 pub button_active: Option<SharedString>,
271 #[serde(rename = "button.foreground")]
273 pub button_foreground: Option<SharedString>,
274 #[serde(rename = "button.hover.background")]
276 pub button_hover: Option<SharedString>,
277 #[serde(rename = "button.danger.background")]
279 pub button_danger: Option<SharedString>,
280 #[serde(rename = "button.danger.active.background")]
282 pub button_danger_active: Option<SharedString>,
283 #[serde(rename = "button.danger.foreground")]
285 pub button_danger_foreground: Option<SharedString>,
286 #[serde(rename = "button.danger.hover.background")]
288 pub button_danger_hover: Option<SharedString>,
289 #[serde(rename = "button.info.background")]
291 pub button_info: Option<SharedString>,
292 #[serde(rename = "button.info.active.background")]
294 pub button_info_active: Option<SharedString>,
295 #[serde(rename = "button.info.foreground")]
297 pub button_info_foreground: Option<SharedString>,
298 #[serde(rename = "button.info.hover.background")]
300 pub button_info_hover: Option<SharedString>,
301 #[serde(rename = "button.primary.background")]
303 pub button_primary: Option<SharedString>,
304 #[serde(rename = "button.primary.active.background")]
306 pub button_primary_active: Option<SharedString>,
307 #[serde(rename = "button.primary.foreground")]
309 pub button_primary_foreground: Option<SharedString>,
310 #[serde(rename = "button.primary.hover.background")]
312 pub button_primary_hover: Option<SharedString>,
313 #[serde(rename = "button.secondary.background")]
315 pub button_secondary: Option<SharedString>,
316 #[serde(rename = "button.secondary.active.background")]
318 pub button_secondary_active: Option<SharedString>,
319 #[serde(rename = "button.secondary.foreground")]
321 pub button_secondary_foreground: Option<SharedString>,
322 #[serde(rename = "button.secondary.hover.background")]
324 pub button_secondary_hover: Option<SharedString>,
325 #[serde(rename = "button.success.background")]
327 pub button_success: Option<SharedString>,
328 #[serde(rename = "button.success.active.background")]
330 pub button_success_active: Option<SharedString>,
331 #[serde(rename = "button.success.foreground")]
333 pub button_success_foreground: Option<SharedString>,
334 #[serde(rename = "button.success.hover.background")]
336 pub button_success_hover: Option<SharedString>,
337 #[serde(rename = "button.warning.background")]
339 pub button_warning: Option<SharedString>,
340 #[serde(rename = "button.warning.active.background")]
342 pub button_warning_active: Option<SharedString>,
343 #[serde(rename = "button.warning.foreground")]
345 pub button_warning_foreground: Option<SharedString>,
346 #[serde(rename = "button.warning.hover.background")]
348 pub button_warning_hover: Option<SharedString>,
349 #[serde(rename = "group_box.background")]
351 pub group_box: Option<SharedString>,
352 #[serde(rename = "group_box.foreground")]
354 pub group_box_foreground: Option<SharedString>,
355 #[serde(rename = "group_box.title.foreground")]
357 pub group_box_title_foreground: Option<SharedString>,
358 #[serde(rename = "caret")]
360 pub caret: Option<SharedString>,
361 #[serde(rename = "chart.1")]
363 pub chart_1: Option<SharedString>,
364 #[serde(rename = "chart.2")]
366 pub chart_2: Option<SharedString>,
367 #[serde(rename = "chart.3")]
369 pub chart_3: Option<SharedString>,
370 #[serde(rename = "chart.4")]
372 pub chart_4: Option<SharedString>,
373 #[serde(rename = "chart.5")]
375 pub chart_5: Option<SharedString>,
376 #[serde(rename = "chart_bullish")]
378 pub chart_bullish: Option<SharedString>,
379 #[serde(rename = "chart_bearish")]
381 pub chart_bearish: Option<SharedString>,
382 #[serde(rename = "danger.background")]
384 pub danger: Option<SharedString>,
385 #[serde(rename = "danger.active.background")]
387 pub danger_active: Option<SharedString>,
388 #[serde(rename = "danger.foreground")]
390 pub danger_foreground: Option<SharedString>,
391 #[serde(rename = "danger.hover.background")]
393 pub danger_hover: Option<SharedString>,
394 #[serde(rename = "description_list.label.background")]
396 pub description_list_label: Option<SharedString>,
397 #[serde(rename = "description_list.label.foreground")]
399 pub description_list_label_foreground: Option<SharedString>,
400 #[serde(rename = "drag.border")]
402 pub drag_border: Option<SharedString>,
403 #[serde(rename = "drop_target.background")]
405 pub drop_target: Option<SharedString>,
406 #[serde(rename = "foreground")]
408 pub foreground: Option<SharedString>,
409 #[serde(rename = "info.background")]
411 pub info: Option<SharedString>,
412 #[serde(rename = "info.active.background")]
414 pub info_active: Option<SharedString>,
415 #[serde(rename = "info.foreground")]
417 pub info_foreground: Option<SharedString>,
418 #[serde(rename = "info.hover.background")]
420 pub info_hover: Option<SharedString>,
421 #[serde(rename = "input.border")]
423 pub input: Option<SharedString>,
424 #[serde(rename = "link")]
426 pub link: Option<SharedString>,
427 #[serde(rename = "link.active")]
429 pub link_active: Option<SharedString>,
430 #[serde(rename = "link.hover")]
432 pub link_hover: Option<SharedString>,
433 #[serde(rename = "list.background")]
435 pub list: Option<SharedString>,
436 #[serde(rename = "list.active.background")]
438 pub list_active: Option<SharedString>,
439 #[serde(rename = "list.active.border")]
441 pub list_active_border: Option<SharedString>,
442 #[serde(rename = "list.even.background")]
444 pub list_even: Option<SharedString>,
445 #[serde(rename = "list.head.background")]
447 pub list_head: Option<SharedString>,
448 #[serde(rename = "list.hover.background")]
450 pub list_hover: Option<SharedString>,
451 #[serde(rename = "muted.background")]
453 pub muted: Option<SharedString>,
454 #[serde(rename = "muted.foreground")]
456 pub muted_foreground: Option<SharedString>,
457 #[serde(rename = "popover.background")]
459 pub popover: Option<SharedString>,
460 #[serde(rename = "popover.foreground")]
462 pub popover_foreground: Option<SharedString>,
463 #[serde(rename = "primary.background")]
465 pub primary: Option<SharedString>,
466 #[serde(rename = "primary.active.background")]
468 pub primary_active: Option<SharedString>,
469 #[serde(rename = "primary.foreground")]
471 pub primary_foreground: Option<SharedString>,
472 #[serde(rename = "primary.hover.background")]
474 pub primary_hover: Option<SharedString>,
475 #[serde(rename = "progress.bar.background")]
477 pub progress_bar: Option<SharedString>,
478 #[serde(rename = "ring")]
480 pub ring: Option<SharedString>,
481 #[serde(rename = "scrollbar.background")]
483 pub scrollbar: Option<SharedString>,
484 #[serde(rename = "scrollbar.thumb.background")]
486 pub scrollbar_thumb: Option<SharedString>,
487 #[serde(rename = "scrollbar.thumb.hover.background")]
489 pub scrollbar_thumb_hover: Option<SharedString>,
490 #[serde(rename = "secondary.background")]
492 pub secondary: Option<SharedString>,
493 #[serde(rename = "secondary.active.background")]
495 pub secondary_active: Option<SharedString>,
496 #[serde(rename = "secondary.foreground")]
498 pub secondary_foreground: Option<SharedString>,
499 #[serde(rename = "secondary.hover.background")]
501 pub secondary_hover: Option<SharedString>,
502 #[serde(rename = "selection.background")]
504 pub selection: Option<SharedString>,
505 #[serde(rename = "sidebar.background")]
507 pub sidebar: Option<SharedString>,
508 #[serde(rename = "sidebar.accent.background")]
510 pub sidebar_accent: Option<SharedString>,
511 #[serde(rename = "sidebar.accent.foreground")]
513 pub sidebar_accent_foreground: Option<SharedString>,
514 #[serde(rename = "sidebar.border")]
516 pub sidebar_border: Option<SharedString>,
517 #[serde(rename = "sidebar.foreground")]
519 pub sidebar_foreground: Option<SharedString>,
520 #[serde(rename = "sidebar.primary.background")]
522 pub sidebar_primary: Option<SharedString>,
523 #[serde(rename = "sidebar.primary.foreground")]
525 pub sidebar_primary_foreground: Option<SharedString>,
526 #[serde(rename = "skeleton.background")]
528 pub skeleton: Option<SharedString>,
529 #[serde(rename = "slider.background")]
531 pub slider_bar: Option<SharedString>,
532 #[serde(rename = "slider.thumb.background")]
534 pub slider_thumb: Option<SharedString>,
535 #[serde(rename = "success.background")]
537 pub success: Option<SharedString>,
538 #[serde(rename = "success.foreground")]
540 pub success_foreground: Option<SharedString>,
541 #[serde(rename = "success.hover.background")]
543 pub success_hover: Option<SharedString>,
544 #[serde(rename = "success.active.background")]
546 pub success_active: Option<SharedString>,
547 #[serde(rename = "switch.background")]
549 pub switch: Option<SharedString>,
550 #[serde(rename = "switch.thumb.background")]
552 pub switch_thumb: Option<SharedString>,
553 #[serde(rename = "tab.background")]
555 pub tab: Option<SharedString>,
556 #[serde(rename = "tab.active.background")]
558 pub tab_active: Option<SharedString>,
559 #[serde(rename = "tab.active.foreground")]
561 pub tab_active_foreground: Option<SharedString>,
562 #[serde(rename = "tab_bar.background")]
564 pub tab_bar: Option<SharedString>,
565 #[serde(rename = "tab_bar.segmented.background")]
567 pub tab_bar_segmented: Option<SharedString>,
568 #[serde(rename = "tab.foreground")]
570 pub tab_foreground: Option<SharedString>,
571 #[serde(rename = "table.background")]
573 pub table: Option<SharedString>,
574 #[serde(rename = "table.active.background")]
576 pub table_active: Option<SharedString>,
577 #[serde(rename = "table.active.border")]
579 pub table_active_border: Option<SharedString>,
580 #[serde(rename = "table.even.background")]
582 pub table_even: Option<SharedString>,
583 #[serde(rename = "table.head.background")]
585 pub table_head: Option<SharedString>,
586 #[serde(rename = "table.head.foreground")]
588 pub table_head_foreground: Option<SharedString>,
589 #[serde(rename = "table.foot.background")]
591 pub table_foot: Option<SharedString>,
592 #[serde(rename = "table.foot.foreground")]
594 pub table_foot_foreground: Option<SharedString>,
595 #[serde(rename = "table.hover.background")]
597 pub table_hover: Option<SharedString>,
598 #[serde(rename = "table.row.border")]
600 pub table_row_border: Option<SharedString>,
601 #[serde(rename = "title_bar.background")]
603 pub title_bar: Option<SharedString>,
604 #[serde(rename = "title_bar.border")]
606 pub title_bar_border: Option<SharedString>,
607 #[serde(rename = "status_bar.background")]
609 pub status_bar: Option<SharedString>,
610 #[serde(rename = "status_bar.border")]
612 pub status_bar_border: Option<SharedString>,
613 #[serde(rename = "tiles.background")]
615 pub tiles: Option<SharedString>,
616 #[serde(rename = "warning.background")]
618 pub warning: Option<SharedString>,
619 #[serde(rename = "warning.active.background")]
621 pub warning_active: Option<SharedString>,
622 #[serde(rename = "warning.hover.background")]
624 pub warning_hover: Option<SharedString>,
625 #[serde(rename = "warning.foreground")]
627 pub warning_foreground: Option<SharedString>,
628 #[serde(rename = "overlay")]
630 pub overlay: Option<SharedString>,
631 #[serde(rename = "window.border")]
637 pub window_border: Option<SharedString>,
638
639 #[serde(rename = "base.blue")]
641 blue: Option<String>,
642 #[serde(rename = "base.blue.light")]
644 blue_light: Option<String>,
645 #[serde(rename = "base.cyan")]
647 cyan: Option<String>,
648 #[serde(rename = "base.cyan.light")]
650 cyan_light: Option<String>,
651 #[serde(rename = "base.green")]
653 green: Option<String>,
654 #[serde(rename = "base.green.light")]
656 green_light: Option<String>,
657 #[serde(rename = "base.magenta")]
659 magenta: Option<String>,
660 #[serde(rename = "base.magenta.light")]
661 magenta_light: Option<String>,
662 #[serde(rename = "base.red")]
664 red: Option<String>,
665 #[serde(rename = "base.red.light")]
667 red_light: Option<String>,
668 #[serde(rename = "base.yellow")]
670 yellow: Option<String>,
671 #[serde(rename = "base.yellow.light")]
673 yellow_light: Option<String>,
674}
675
676impl ThemeColor {
677 pub(crate) fn apply_config(
679 &mut self,
680 config: &ThemeConfig,
681 default_theme: &ThemeColor,
682 ) -> ThemeTokens {
683 let colors = config.colors.clone();
684 let default_tokens = ThemeTokens::from(default_theme);
685 let mut tokens = default_tokens;
686
687 macro_rules! apply_color {
688 ($config_field:ident) => {
689 if let Some(value) = &colors.$config_field {
690 self.$config_field =
691 try_parse_color(value).unwrap_or(default_theme.$config_field);
692 } else {
693 self.$config_field = default_theme.$config_field;
694 }
695 tokens.$config_field = self.$config_field.into();
696 };
697 ($config_field:ident, fallback = $fallback:expr) => {
699 let fallback: gpui::Hsla = ($fallback).into();
700 if let Some(value) = &colors.$config_field {
701 self.$config_field = try_parse_color(value).unwrap_or(fallback);
702 } else {
703 self.$config_field = fallback;
704 }
705 tokens.$config_field = self.$config_field.into();
706 };
707 }
708
709 macro_rules! apply_background_color {
710 ($config_field:ident) => {
711 let token = if let Some(value) = &colors.$config_field {
712 if let Ok(token) = try_parse_theme_token(&value) {
713 token
714 } else {
715 default_tokens.$config_field
716 }
717 } else {
718 default_tokens.$config_field
719 };
720 self.$config_field = token.color;
721 tokens.$config_field = token;
722 };
723 ($config_field:ident, fallback = $fallback:expr) => {
724 let fallback: ThemeToken = ($fallback).into();
725 let token = if let Some(value) = &colors.$config_field {
726 if let Ok(token) = try_parse_theme_token(&value) {
727 token
728 } else {
729 fallback
730 }
731 } else {
732 fallback
733 };
734 self.$config_field = token.color;
735 tokens.$config_field = token;
736 };
737 }
738
739 apply_background_color!(background);
740
741 apply_color!(red);
743 apply_color!(
744 red_light,
745 fallback = self.background.blend(self.red.opacity(0.8))
746 );
747 apply_color!(green);
748 apply_color!(
749 green_light,
750 fallback = self.background.blend(self.green.opacity(0.8))
751 );
752 apply_color!(blue);
753 apply_color!(
754 blue_light,
755 fallback = self.background.blend(self.blue.opacity(0.8))
756 );
757 apply_color!(magenta);
758 apply_color!(
759 magenta_light,
760 fallback = self.background.blend(self.magenta.opacity(0.8))
761 );
762 apply_color!(yellow);
763 apply_color!(
764 yellow_light,
765 fallback = self.background.blend(self.yellow.opacity(0.8))
766 );
767 apply_color!(cyan);
768 apply_color!(
769 cyan_light,
770 fallback = self.background.blend(self.cyan.opacity(0.8))
771 );
772
773 apply_color!(border);
774 apply_color!(foreground);
775 apply_color!(input, fallback = self.border);
776 apply_background_color!(muted);
777 apply_color!(
778 muted_foreground,
779 fallback = self.muted.blend(self.foreground.opacity(0.7))
780 );
781
782 let active_darken = if config.mode.is_dark() { 0.2 } else { 0.1 };
784 let hover_opacity = 0.9;
785 let transparent = gpui::transparent_black();
786 let button_background = if config.mode.is_dark() {
787 self.input.mix_oklab(transparent, 0.3)
788 } else {
789 self.background
790 };
791 apply_background_color!(button, fallback = button_background);
792 apply_color!(button_foreground, fallback = self.foreground);
793 apply_background_color!(
794 button_hover,
795 fallback = self.input.mix_oklab(transparent, 0.5)
796 );
797 apply_background_color!(
798 button_active,
799 fallback = self.input.mix_oklab(transparent, 0.7)
800 );
801 apply_background_color!(primary);
802 apply_color!(primary_foreground, fallback = self.foreground);
803 apply_background_color!(
804 primary_hover,
805 fallback = self.background.blend(self.primary.opacity(hover_opacity))
806 );
807 apply_background_color!(
808 primary_active,
809 fallback = self.primary.darken(active_darken)
810 );
811 apply_background_color!(button_primary, fallback = tokens.primary);
812 apply_color!(
813 button_primary_foreground,
814 fallback = self.primary_foreground
815 );
816 apply_background_color!(button_primary_hover, fallback = tokens.primary_hover);
817 apply_background_color!(button_primary_active, fallback = tokens.primary_active);
818 apply_background_color!(secondary);
819 apply_color!(secondary_foreground, fallback = self.foreground);
820 apply_background_color!(
821 secondary_hover,
822 fallback = self.background.blend(self.secondary.opacity(hover_opacity))
823 );
824 apply_background_color!(
825 secondary_active,
826 fallback = self.secondary.darken(active_darken)
827 );
828 apply_background_color!(button_secondary, fallback = tokens.secondary);
829 apply_color!(
830 button_secondary_foreground,
831 fallback = self.secondary_foreground
832 );
833 apply_background_color!(button_secondary_hover, fallback = tokens.secondary_hover);
834 apply_background_color!(button_secondary_active, fallback = tokens.secondary_active);
835 apply_background_color!(success, fallback = self.green);
836 apply_color!(success_foreground, fallback = self.primary_foreground);
837 apply_background_color!(
838 success_hover,
839 fallback = self.background.blend(self.success.opacity(hover_opacity))
840 );
841 apply_background_color!(
842 success_active,
843 fallback = self.success.darken(active_darken)
844 );
845 apply_background_color!(
846 button_success,
847 fallback = self.success.mix_oklab(transparent, 0.2)
848 );
849 apply_color!(button_success_foreground, fallback = self.success);
850 apply_background_color!(
851 button_success_hover,
852 fallback = self.success.mix_oklab(transparent, 0.3)
853 );
854 apply_background_color!(
855 button_success_active,
856 fallback = self.success.mix_oklab(transparent, 0.4)
857 );
858 apply_background_color!(info, fallback = self.cyan);
859 apply_color!(info_foreground, fallback = self.primary_foreground);
860 apply_background_color!(
861 info_hover,
862 fallback = self.background.blend(self.info.opacity(hover_opacity))
863 );
864 apply_background_color!(info_active, fallback = self.info.darken(active_darken));
865 apply_background_color!(
866 button_info,
867 fallback = self.info.mix_oklab(transparent, 0.2)
868 );
869 apply_color!(button_info_foreground, fallback = self.info);
870 apply_background_color!(
871 button_info_hover,
872 fallback = self.info.mix_oklab(transparent, 0.3)
873 );
874 apply_background_color!(
875 button_info_active,
876 fallback = self.info.mix_oklab(transparent, 0.4)
877 );
878 apply_background_color!(warning, fallback = self.yellow);
879 apply_color!(warning_foreground, fallback = self.primary_foreground);
880 apply_background_color!(
881 warning_hover,
882 fallback = self.background.blend(self.warning.opacity(0.9))
883 );
884 apply_background_color!(
885 warning_active,
886 fallback = self.background.blend(self.warning.darken(active_darken))
887 );
888 apply_background_color!(
889 button_warning,
890 fallback = self.warning.mix_oklab(transparent, 0.2)
891 );
892 apply_color!(button_warning_foreground, fallback = self.warning);
893 apply_background_color!(
894 button_warning_hover,
895 fallback = self.warning.mix_oklab(transparent, 0.3)
896 );
897 apply_background_color!(
898 button_warning_active,
899 fallback = self.warning.mix_oklab(transparent, 0.4)
900 );
901
902 apply_background_color!(accent, fallback = tokens.secondary);
904 apply_color!(accent_foreground, fallback = self.foreground);
905 apply_background_color!(accordion, fallback = tokens.background);
906 apply_background_color!(
907 group_box,
908 fallback = self
909 .background
910 .blend(
911 self.secondary
912 .opacity(if config.mode.is_dark() { 0.3 } else { 0.4 })
913 )
914 );
915 apply_color!(group_box_foreground, fallback = self.foreground);
916 apply_color!(caret, fallback = self.primary);
917 apply_color!(chart_1, fallback = self.blue.lighten(0.4));
918 apply_color!(chart_2, fallback = self.blue.lighten(0.2));
919 apply_color!(chart_3, fallback = self.blue);
920 apply_color!(chart_4, fallback = self.blue.darken(0.2));
921 apply_color!(chart_5, fallback = self.blue.darken(0.4));
922 apply_color!(chart_bullish, fallback = self.green);
923 apply_color!(chart_bearish, fallback = self.red);
924 apply_background_color!(danger, fallback = self.red);
925 apply_background_color!(danger_active, fallback = self.danger.darken(active_darken));
926 apply_color!(danger_foreground, fallback = self.primary_foreground);
927 apply_background_color!(
928 danger_hover,
929 fallback = self.background.blend(self.danger.opacity(0.9))
930 );
931 apply_background_color!(
932 button_danger,
933 fallback = self.danger.mix_oklab(transparent, 0.2)
934 );
935 apply_color!(button_danger_foreground, fallback = self.danger);
936 apply_background_color!(
937 button_danger_hover,
938 fallback = self.danger.mix_oklab(transparent, 0.3)
939 );
940 apply_background_color!(
941 button_danger_active,
942 fallback = self.danger.mix_oklab(transparent, 0.4)
943 );
944 apply_background_color!(
945 description_list_label,
946 fallback = self.background.blend(self.border.opacity(0.2))
947 );
948 apply_color!(
949 description_list_label_foreground,
950 fallback = self.muted_foreground
951 );
952 apply_color!(drag_border, fallback = self.primary.opacity(0.65));
953 apply_background_color!(drop_target, fallback = self.primary.opacity(0.2));
954 apply_color!(link, fallback = self.primary);
955 apply_color!(link_active, fallback = self.link);
956 apply_color!(link_hover, fallback = self.link);
957 apply_background_color!(list, fallback = tokens.background);
958 apply_background_color!(
959 list_active,
960 fallback = self.background.blend(self.primary.opacity(0.1))
961 );
962 apply_color!(
963 list_active_border,
964 fallback = self.background.blend(self.primary.opacity(0.6))
965 );
966 apply_background_color!(list_even, fallback = tokens.list);
967 apply_background_color!(list_head, fallback = tokens.list);
968 apply_background_color!(list_hover, fallback = self.accent.opacity(0.6));
969 apply_background_color!(popover, fallback = tokens.background);
970 apply_color!(popover_foreground, fallback = self.foreground);
971 apply_background_color!(progress_bar, fallback = tokens.primary);
972 apply_color!(ring, fallback = self.blue);
973 apply_background_color!(scrollbar, fallback = tokens.background);
974 apply_background_color!(scrollbar_thumb, fallback = tokens.accent);
975 apply_background_color!(scrollbar_thumb_hover, fallback = tokens.scrollbar_thumb);
976 apply_background_color!(selection, fallback = tokens.primary);
977 apply_background_color!(
978 sidebar,
979 fallback = self.background.blend(self.border.opacity(0.15))
980 );
981 apply_background_color!(sidebar_accent, fallback = tokens.accent);
982 apply_color!(sidebar_accent_foreground, fallback = self.accent_foreground);
983 apply_color!(sidebar_border, fallback = self.border);
984 apply_color!(sidebar_foreground, fallback = self.foreground);
985 apply_background_color!(sidebar_primary, fallback = tokens.primary);
986 apply_color!(
987 sidebar_primary_foreground,
988 fallback = self.primary_foreground
989 );
990 apply_background_color!(skeleton, fallback = tokens.secondary);
991 apply_background_color!(slider_bar, fallback = tokens.primary);
992 apply_background_color!(slider_thumb, fallback = self.primary_foreground);
993 apply_background_color!(switch, fallback = tokens.secondary_active);
994 apply_background_color!(switch_thumb, fallback = tokens.background);
995 apply_background_color!(tab, fallback = tokens.background);
996 apply_background_color!(tab_active, fallback = tokens.background);
997 apply_color!(tab_active_foreground, fallback = self.foreground);
998 apply_background_color!(tab_bar, fallback = tokens.background);
999 apply_background_color!(tab_bar_segmented, fallback = tokens.secondary);
1000 apply_color!(tab_foreground, fallback = self.foreground);
1001 apply_background_color!(table, fallback = tokens.list);
1002 apply_background_color!(table_active, fallback = tokens.list_active);
1003 apply_color!(table_active_border, fallback = self.list_active_border);
1004 apply_background_color!(table_even, fallback = tokens.list_even);
1005 apply_background_color!(table_head, fallback = tokens.list_head);
1006 apply_color!(table_head_foreground, fallback = self.muted_foreground);
1007 apply_background_color!(table_foot, fallback = tokens.list_head);
1008 apply_color!(table_foot_foreground, fallback = self.muted_foreground);
1009 apply_background_color!(table_hover, fallback = tokens.list_hover);
1010 apply_color!(table_row_border, fallback = self.border);
1011 apply_background_color!(title_bar, fallback = tokens.background);
1012 apply_color!(title_bar_border, fallback = self.border);
1013 apply_background_color!(status_bar, fallback = tokens.title_bar);
1014 apply_color!(status_bar_border, fallback = self.title_bar_border);
1015 apply_background_color!(tiles, fallback = tokens.background);
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::{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_preserves_gradient_background_and_solid_color_fallback() {
1192 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1193 "name": "Gradient",
1194 "mode": "light",
1195 "colors": {
1196 "primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)",
1197 "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)"
1198 }
1199 }))
1200 .unwrap();
1201
1202 let mut theme = Theme::default();
1203 theme.apply_config(&std::rc::Rc::new(config));
1204
1205 let primary_from = try_parse_color("#4F46E5").unwrap();
1206 let primary_to = try_parse_color("#06B6D4").unwrap();
1207 assert_eq!(theme.primary, primary_from);
1208 assert_eq!(theme.tokens.primary.color, primary_from);
1209 assert_eq!(
1210 theme.tokens.primary.background,
1211 linear_gradient(
1212 135.,
1213 linear_color_stop(primary_from, 0.),
1214 linear_color_stop(primary_to, 1.)
1215 )
1216 );
1217 assert_eq!(
1218 theme.tokens.button_primary.background,
1219 theme.tokens.primary.background
1220 );
1221 assert_eq!(
1222 theme.tokens.button_primary_hover.background,
1223 linear_gradient(
1224 90.,
1225 linear_color_stop(crate::red_500(), 0.25),
1226 linear_color_stop(crate::blue_600(), 0.75)
1227 )
1228 );
1229 assert_eq!(theme.mode, ThemeMode::Light);
1230 }
1231
1232 #[test]
1233 fn test_aurora_theme_parses_gradient_backgrounds() {
1234 let theme_set =
1235 serde_json::from_str::<ThemeSet>(include_str!("../../../../themes/aurora.json"))
1236 .unwrap();
1237 assert_eq!(theme_set.themes.len(), 1);
1238 assert!(theme_set.themes.iter().all(|theme| !theme.mode.is_dark()));
1239
1240 let light = theme_set
1241 .themes
1242 .iter()
1243 .find(|theme| theme.name.as_ref() == "Aurora Light")
1244 .unwrap();
1245 let mut theme = Theme::default();
1246 theme.apply_config(&std::rc::Rc::new(light.clone()));
1247
1248 assert_ne!(
1249 theme.tokens.button_primary.background,
1250 theme.button_primary.into()
1251 );
1252 assert_eq!(theme.tokens.background.background, theme.background.into());
1253 assert_eq!(theme.button_primary, try_parse_color("#1E293B").unwrap());
1254 assert_eq!(theme.background, try_parse_color("#FFFFFF").unwrap());
1255 assert_ne!(
1256 theme.tokens.progress_bar.background,
1257 theme.progress_bar.into()
1258 );
1259 assert_ne!(
1260 theme.tokens.scrollbar_thumb.background,
1261 theme.scrollbar_thumb.into()
1262 );
1263 assert_ne!(theme.tokens.switch.background, theme.switch.into());
1264 assert_ne!(
1265 theme.tokens.switch_thumb.background,
1266 theme.switch_thumb.into()
1267 );
1268 assert_ne!(theme.tokens.title_bar.background, theme.title_bar.into());
1269 assert_ne!(theme.tokens.status_bar.background, theme.status_bar.into());
1270 }
1271
1272 #[test]
1273 fn test_apply_config_clamps_highlight_alpha_per_gradient_stop() {
1274 let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1275 "name": "Highlight",
1276 "mode": "light",
1277 "colors": {
1278 "list.active.background": "#3b82f6",
1280 "table.active.background": "linear-gradient(#bfdbfe33, #3b82f6)",
1283 "selection.background": "linear-gradient(#3b82f600, #3b82f6)",
1287 }
1288 }))
1289 .unwrap();
1290
1291 let mut theme = Theme::default();
1292 theme.apply_config(&std::rc::Rc::new(config));
1293
1294 let blue = try_parse_color("#3b82f6").unwrap();
1296 assert_eq!(theme.list_active, blue.alpha(0.2));
1297 assert_eq!(theme.tokens.list_active.background, blue.alpha(0.2).into());
1298
1299 let faint = try_parse_color("#bfdbfe33").unwrap();
1301 assert_eq!(
1302 theme.tokens.table_active.background,
1303 linear_gradient(
1304 180.,
1305 linear_color_stop(faint.alpha(faint.a.min(0.2)), 0.),
1306 linear_color_stop(blue.alpha(0.2), 1.),
1307 )
1308 );
1309
1310 let clear = try_parse_color("#3b82f600").unwrap();
1313 assert_eq!(
1314 theme.tokens.selection.background,
1315 linear_gradient(
1316 180.,
1317 linear_color_stop(clear.alpha(clear.a.min(0.3)), 0.),
1318 linear_color_stop(blue.alpha(0.3), 1.),
1319 )
1320 );
1321 }
1322}