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