1use std::{rc::Rc, sync::Arc};
2
3use anyhow::Result;
4use gpui::{Hsla, SharedString, px};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 Colorize, Theme, ThemeColor, ThemeMode,
10 highlighter::{HighlightTheme, HighlightThemeStyle},
11};
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
15#[serde(default)]
16pub struct ThemeSet {
17 pub name: SharedString,
19 pub author: Option<SharedString>,
21 pub url: Option<SharedString>,
23 #[serde(rename = "themes")]
25 pub themes: Vec<ThemeConfig>,
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
29#[serde(default)]
30pub struct ThemeConfig {
31 pub is_default: bool,
33 pub name: SharedString,
35 pub mode: ThemeMode,
37
38 #[serde(rename = "font.size")]
40 pub font_size: Option<f32>,
41 #[serde(rename = "font.family")]
43 pub font_family: Option<SharedString>,
44 #[serde(rename = "mono_font.family")]
49 pub mono_font_family: Option<SharedString>,
50 #[serde(rename = "mono_font.size")]
52 pub mono_font_size: Option<f32>,
53
54 #[serde(rename = "radius")]
56 pub radius: Option<usize>,
57 #[serde(rename = "radius.lg")]
59 pub radius_lg: Option<usize>,
60 #[serde(rename = "shadow")]
62 pub shadow: Option<bool>,
63
64 pub colors: ThemeConfigColors,
66 pub highlight: Option<HighlightThemeStyle>,
70}
71
72#[derive(Debug, Default, Clone, JsonSchema, Serialize, Deserialize)]
73pub struct ThemeConfigColors {
74 #[serde(rename = "accent.background")]
76 pub accent: Option<SharedString>,
77 #[serde(rename = "accent.foreground")]
79 pub accent_foreground: Option<SharedString>,
80 #[serde(rename = "accordion.background")]
82 pub accordion: Option<SharedString>,
83 #[serde(rename = "accordion.hover.background")]
85 pub accordion_hover: Option<SharedString>,
86 #[serde(rename = "background")]
88 pub background: Option<SharedString>,
89 #[serde(rename = "border")]
91 pub border: Option<SharedString>,
92 #[serde(rename = "group_box.background")]
94 pub group_box: Option<SharedString>,
95 #[serde(rename = "group_box.foreground")]
97 pub group_box_foreground: Option<SharedString>,
98 #[serde(rename = "group_box.title.foreground")]
100 pub group_box_title_foreground: Option<SharedString>,
101 #[serde(rename = "caret")]
103 pub caret: Option<SharedString>,
104 #[serde(rename = "chart.1")]
106 pub chart_1: Option<SharedString>,
107 #[serde(rename = "chart.2")]
109 pub chart_2: Option<SharedString>,
110 #[serde(rename = "chart.3")]
112 pub chart_3: Option<SharedString>,
113 #[serde(rename = "chart.4")]
115 pub chart_4: Option<SharedString>,
116 #[serde(rename = "chart.5")]
118 pub chart_5: Option<SharedString>,
119 #[serde(rename = "danger.background")]
121 pub danger: Option<SharedString>,
122 #[serde(rename = "danger.active.background")]
124 pub danger_active: Option<SharedString>,
125 #[serde(rename = "danger.foreground")]
127 pub danger_foreground: Option<SharedString>,
128 #[serde(rename = "danger.hover.background")]
130 pub danger_hover: Option<SharedString>,
131 #[serde(rename = "description_list.label.background")]
133 pub description_list_label: Option<SharedString>,
134 #[serde(rename = "description_list.label.foreground")]
136 pub description_list_label_foreground: Option<SharedString>,
137 #[serde(rename = "drag.border")]
139 pub drag_border: Option<SharedString>,
140 #[serde(rename = "drop_target.background")]
142 pub drop_target: Option<SharedString>,
143 #[serde(rename = "foreground")]
145 pub foreground: Option<SharedString>,
146 #[serde(rename = "info.background")]
148 pub info: Option<SharedString>,
149 #[serde(rename = "info.active.background")]
151 pub info_active: Option<SharedString>,
152 #[serde(rename = "info.foreground")]
154 pub info_foreground: Option<SharedString>,
155 #[serde(rename = "info.hover.background")]
157 pub info_hover: Option<SharedString>,
158 #[serde(rename = "input.border")]
160 pub input: Option<SharedString>,
161 #[serde(rename = "link")]
163 pub link: Option<SharedString>,
164 #[serde(rename = "link.active")]
166 pub link_active: Option<SharedString>,
167 #[serde(rename = "link.hover")]
169 pub link_hover: Option<SharedString>,
170 #[serde(rename = "list.background")]
172 pub list: Option<SharedString>,
173 #[serde(rename = "list.active.background")]
175 pub list_active: Option<SharedString>,
176 #[serde(rename = "list.active.border")]
178 pub list_active_border: Option<SharedString>,
179 #[serde(rename = "list.even.background")]
181 pub list_even: Option<SharedString>,
182 #[serde(rename = "list.head.background")]
184 pub list_head: Option<SharedString>,
185 #[serde(rename = "list.hover.background")]
187 pub list_hover: Option<SharedString>,
188 #[serde(rename = "muted.background")]
190 pub muted: Option<SharedString>,
191 #[serde(rename = "muted.foreground")]
193 pub muted_foreground: Option<SharedString>,
194 #[serde(rename = "popover.background")]
196 pub popover: Option<SharedString>,
197 #[serde(rename = "popover.foreground")]
199 pub popover_foreground: Option<SharedString>,
200 #[serde(rename = "primary.background")]
202 pub primary: Option<SharedString>,
203 #[serde(rename = "primary.active.background")]
205 pub primary_active: Option<SharedString>,
206 #[serde(rename = "primary.foreground")]
208 pub primary_foreground: Option<SharedString>,
209 #[serde(rename = "primary.hover.background")]
211 pub primary_hover: Option<SharedString>,
212 #[serde(rename = "progress.bar.background")]
214 pub progress_bar: Option<SharedString>,
215 #[serde(rename = "ring")]
217 pub ring: Option<SharedString>,
218 #[serde(rename = "scrollbar.background")]
220 pub scrollbar: Option<SharedString>,
221 #[serde(rename = "scrollbar.thumb.background")]
223 pub scrollbar_thumb: Option<SharedString>,
224 #[serde(rename = "scrollbar.thumb.hover.background")]
226 pub scrollbar_thumb_hover: Option<SharedString>,
227 #[serde(rename = "secondary.background")]
229 pub secondary: Option<SharedString>,
230 #[serde(rename = "secondary.active.background")]
232 pub secondary_active: Option<SharedString>,
233 #[serde(rename = "secondary.foreground")]
235 pub secondary_foreground: Option<SharedString>,
236 #[serde(rename = "secondary.hover.background")]
238 pub secondary_hover: Option<SharedString>,
239 #[serde(rename = "selection.background")]
241 pub selection: Option<SharedString>,
242 #[serde(rename = "sidebar.background")]
244 pub sidebar: Option<SharedString>,
245 #[serde(rename = "sidebar.accent.background")]
247 pub sidebar_accent: Option<SharedString>,
248 #[serde(rename = "sidebar.accent.foreground")]
250 pub sidebar_accent_foreground: Option<SharedString>,
251 #[serde(rename = "sidebar.border")]
253 pub sidebar_border: Option<SharedString>,
254 #[serde(rename = "sidebar.foreground")]
256 pub sidebar_foreground: Option<SharedString>,
257 #[serde(rename = "sidebar.primary.background")]
259 pub sidebar_primary: Option<SharedString>,
260 #[serde(rename = "sidebar.primary.foreground")]
262 pub sidebar_primary_foreground: Option<SharedString>,
263 #[serde(rename = "skeleton.background")]
265 pub skeleton: Option<SharedString>,
266 #[serde(rename = "slider.background")]
268 pub slider_bar: Option<SharedString>,
269 #[serde(rename = "slider.thumb.background")]
271 pub slider_thumb: Option<SharedString>,
272 #[serde(rename = "success.background")]
274 pub success: Option<SharedString>,
275 #[serde(rename = "success.foreground")]
277 pub success_foreground: Option<SharedString>,
278 #[serde(rename = "success.hover.background")]
280 pub success_hover: Option<SharedString>,
281 #[serde(rename = "success.active.background")]
283 pub success_active: Option<SharedString>,
284 #[serde(rename = "bullish.background")]
286 pub bullish: Option<SharedString>,
287 #[serde(rename = "bearish.background")]
289 pub bearish: Option<SharedString>,
290 #[serde(rename = "switch.background")]
292 pub switch: Option<SharedString>,
293 #[serde(rename = "switch.thumb.background")]
295 pub switch_thumb: Option<SharedString>,
296 #[serde(rename = "tab.background")]
298 pub tab: Option<SharedString>,
299 #[serde(rename = "tab.active.background")]
301 pub tab_active: Option<SharedString>,
302 #[serde(rename = "tab.active.foreground")]
304 pub tab_active_foreground: Option<SharedString>,
305 #[serde(rename = "tab_bar.background")]
307 pub tab_bar: Option<SharedString>,
308 #[serde(rename = "tab_bar.segmented.background")]
310 pub tab_bar_segmented: Option<SharedString>,
311 #[serde(rename = "tab.foreground")]
313 pub tab_foreground: Option<SharedString>,
314 #[serde(rename = "table.background")]
316 pub table: Option<SharedString>,
317 #[serde(rename = "table.active.background")]
319 pub table_active: Option<SharedString>,
320 #[serde(rename = "table.active.border")]
322 pub table_active_border: Option<SharedString>,
323 #[serde(rename = "table.even.background")]
325 pub table_even: Option<SharedString>,
326 #[serde(rename = "table.head.background")]
328 pub table_head: Option<SharedString>,
329 #[serde(rename = "table.head.foreground")]
331 pub table_head_foreground: Option<SharedString>,
332 #[serde(rename = "table.hover.background")]
334 pub table_hover: Option<SharedString>,
335 #[serde(rename = "table.row.border")]
337 pub table_row_border: Option<SharedString>,
338 #[serde(rename = "title_bar.background")]
340 pub title_bar: Option<SharedString>,
341 #[serde(rename = "title_bar.border")]
343 pub title_bar_border: Option<SharedString>,
344 #[serde(rename = "tiles.background")]
346 pub tiles: Option<SharedString>,
347 #[serde(rename = "warning.background")]
349 pub warning: Option<SharedString>,
350 #[serde(rename = "warning.active.background")]
352 pub warning_active: Option<SharedString>,
353 #[serde(rename = "warning.hover.background")]
355 pub warning_hover: Option<SharedString>,
356 #[serde(rename = "warning.foreground")]
358 pub warning_foreground: Option<SharedString>,
359 #[serde(rename = "overlay")]
361 pub overlay: Option<SharedString>,
362 #[serde(rename = "window.border")]
368 pub window_border: Option<SharedString>,
369
370 #[serde(rename = "base.blue")]
372 blue: Option<String>,
373 #[serde(rename = "base.blue.light")]
375 blue_light: Option<String>,
376 #[serde(rename = "base.cyan")]
378 cyan: Option<String>,
379 #[serde(rename = "base.cyan.light")]
381 cyan_light: Option<String>,
382 #[serde(rename = "base.green")]
384 green: Option<String>,
385 #[serde(rename = "base.green.light")]
387 green_light: Option<String>,
388 #[serde(rename = "base.magenta")]
390 magenta: Option<String>,
391 #[serde(rename = "base.magenta.light")]
392 magenta_light: Option<String>,
393 #[serde(rename = "base.red")]
395 red: Option<String>,
396 #[serde(rename = "base.red.light")]
398 red_light: Option<String>,
399 #[serde(rename = "base.yellow")]
401 yellow: Option<String>,
402 #[serde(rename = "base.yellow.light")]
404 yellow_light: Option<String>,
405}
406
407fn try_parse_color(color: &str) -> Result<Hsla> {
409 let rgba = gpui::Rgba::try_from(color)?;
410 Ok(rgba.into())
411}
412
413impl ThemeColor {
414 pub(crate) fn apply_config(&mut self, config: &ThemeConfig, default_theme: &ThemeColor) {
416 let colors = config.colors.clone();
417
418 macro_rules! apply_color {
419 ($config_field:ident) => {
420 if let Some(value) = colors.$config_field {
421 if let Ok(color) = try_parse_color(&value) {
422 self.$config_field = color;
423 } else {
424 self.$config_field = default_theme.$config_field;
425 }
426 } else {
427 self.$config_field = default_theme.$config_field;
428 }
429 };
430 ($config_field:ident, fallback = $fallback:expr) => {
432 if let Some(value) = colors.$config_field {
433 if let Ok(color) = try_parse_color(&value) {
434 self.$config_field = color;
435 }
436 } else {
437 self.$config_field = $fallback;
438 }
439 };
440 }
441
442 apply_color!(background);
443
444 apply_color!(red);
446 apply_color!(
447 red_light,
448 fallback = self.background.blend(self.red.opacity(0.8))
449 );
450 apply_color!(green);
451 apply_color!(
452 green_light,
453 fallback = self.background.blend(self.green.opacity(0.8))
454 );
455 apply_color!(blue);
456 apply_color!(
457 blue_light,
458 fallback = self.background.blend(self.blue.opacity(0.8))
459 );
460 apply_color!(magenta);
461 apply_color!(
462 magenta_light,
463 fallback = self.background.blend(self.magenta.opacity(0.8))
464 );
465 apply_color!(yellow);
466 apply_color!(
467 yellow_light,
468 fallback = self.background.blend(self.yellow.opacity(0.8))
469 );
470 apply_color!(cyan);
471 apply_color!(
472 cyan_light,
473 fallback = self.background.blend(self.cyan.opacity(0.8))
474 );
475
476 apply_color!(border);
477 apply_color!(foreground);
478 apply_color!(muted);
479 apply_color!(
480 muted_foreground,
481 fallback = self.muted.blend(self.foreground.opacity(0.7))
482 );
483
484 let active_darken = if config.mode.is_dark() { 0.2 } else { 0.1 };
486 let hover_opacity = 0.9;
487 apply_color!(primary);
488 apply_color!(primary_foreground, fallback = self.foreground);
489 apply_color!(
490 primary_hover,
491 fallback = self.background.blend(self.primary.opacity(hover_opacity))
492 );
493 apply_color!(
494 primary_active,
495 fallback = self.primary.darken(active_darken)
496 );
497 apply_color!(secondary);
498 apply_color!(secondary_foreground, fallback = self.foreground);
499 apply_color!(
500 secondary_hover,
501 fallback = self.background.blend(self.secondary.opacity(hover_opacity))
502 );
503 apply_color!(
504 secondary_active,
505 fallback = self.secondary.darken(active_darken)
506 );
507 apply_color!(success, fallback = self.green);
508 apply_color!(success_foreground, fallback = self.primary_foreground);
509 apply_color!(
510 success_hover,
511 fallback = self.background.blend(self.success.opacity(hover_opacity))
512 );
513 apply_color!(
514 success_active,
515 fallback = self.success.darken(active_darken)
516 );
517 apply_color!(bullish, fallback = self.green);
518 apply_color!(bearish, fallback = self.red);
519 apply_color!(info, fallback = self.cyan);
520 apply_color!(info_foreground, fallback = self.primary_foreground);
521 apply_color!(
522 info_hover,
523 fallback = self.background.blend(self.info.opacity(hover_opacity))
524 );
525 apply_color!(info_active, fallback = self.info.darken(active_darken));
526 apply_color!(warning, fallback = self.yellow);
527 apply_color!(warning_foreground, fallback = self.primary_foreground);
528 apply_color!(
529 warning_hover,
530 fallback = self.background.blend(self.warning.opacity(0.9))
531 );
532 apply_color!(
533 warning_active,
534 fallback = self.background.blend(self.warning.darken(active_darken))
535 );
536
537 apply_color!(accent, fallback = self.secondary);
539 apply_color!(accent_foreground, fallback = self.foreground);
540 apply_color!(accordion, fallback = self.background);
541 apply_color!(accordion_hover, fallback = self.accent.opacity(0.8));
542 apply_color!(
543 group_box,
544 fallback = self
545 .background
546 .blend(
547 self.secondary
548 .opacity(if config.mode.is_dark() { 0.3 } else { 0.4 })
549 )
550 );
551 apply_color!(group_box_foreground, fallback = self.foreground);
552 apply_color!(caret, fallback = self.primary);
553 apply_color!(chart_1, fallback = self.blue.lighten(0.4));
554 apply_color!(chart_2, fallback = self.blue.lighten(0.2));
555 apply_color!(chart_3, fallback = self.blue);
556 apply_color!(chart_4, fallback = self.blue.darken(0.2));
557 apply_color!(chart_5, fallback = self.blue.darken(0.4));
558 apply_color!(danger, fallback = self.red);
559 apply_color!(danger_active, fallback = self.danger.darken(active_darken));
560 apply_color!(danger_foreground, fallback = self.primary_foreground);
561 apply_color!(
562 danger_hover,
563 fallback = self.background.blend(self.danger.opacity(0.9))
564 );
565 apply_color!(
566 description_list_label,
567 fallback = self.background.blend(self.border.opacity(0.2))
568 );
569 apply_color!(
570 description_list_label_foreground,
571 fallback = self.muted_foreground
572 );
573 apply_color!(drag_border, fallback = self.primary.opacity(0.65));
574 apply_color!(drop_target, fallback = self.primary.opacity(0.2));
575 apply_color!(input, fallback = self.border);
576 apply_color!(link, fallback = self.primary);
577 apply_color!(link_active, fallback = self.link);
578 apply_color!(link_hover, fallback = self.link);
579 apply_color!(list, fallback = self.background);
580 apply_color!(
581 list_active,
582 fallback = self.background.blend(self.primary.opacity(0.1))
583 );
584 apply_color!(
585 list_active_border,
586 fallback = self.background.blend(self.primary.opacity(0.6))
587 );
588 apply_color!(list_even, fallback = self.list);
589 apply_color!(list_head, fallback = self.list);
590 apply_color!(list_hover, fallback = self.secondary_hover);
591 apply_color!(popover, fallback = self.background);
592 apply_color!(popover_foreground, fallback = self.foreground);
593 apply_color!(progress_bar, fallback = self.primary);
594 apply_color!(ring, fallback = self.blue);
595 apply_color!(scrollbar, fallback = self.background);
596 apply_color!(scrollbar_thumb, fallback = self.accent);
597 apply_color!(scrollbar_thumb_hover, fallback = self.scrollbar_thumb);
598 apply_color!(selection, fallback = self.primary);
599 apply_color!(sidebar, fallback = self.background);
600 apply_color!(sidebar_accent, fallback = self.accent);
601 apply_color!(sidebar_accent_foreground, fallback = self.accent_foreground);
602 apply_color!(sidebar_border, fallback = self.border);
603 apply_color!(sidebar_foreground, fallback = self.foreground);
604 apply_color!(sidebar_primary, fallback = self.primary);
605 apply_color!(
606 sidebar_primary_foreground,
607 fallback = self.primary_foreground
608 );
609 apply_color!(skeleton, fallback = self.secondary);
610 apply_color!(slider_bar, fallback = self.primary);
611 apply_color!(slider_thumb, fallback = self.primary_foreground);
612 apply_color!(switch, fallback = self.secondary);
613 apply_color!(switch_thumb, fallback = self.background);
614 apply_color!(tab, fallback = self.background);
615 apply_color!(tab_active, fallback = self.background);
616 apply_color!(tab_active_foreground, fallback = self.foreground);
617 apply_color!(tab_bar, fallback = self.background);
618 apply_color!(tab_bar_segmented, fallback = self.secondary);
619 apply_color!(tab_foreground, fallback = self.foreground);
620 apply_color!(table, fallback = self.list);
621 apply_color!(table_active, fallback = self.list_active);
622 apply_color!(table_active_border, fallback = self.list_active_border);
623 apply_color!(table_even, fallback = self.list_even);
624 apply_color!(table_head, fallback = self.list_head);
625 apply_color!(table_head_foreground, fallback = self.muted_foreground);
626 apply_color!(table_hover, fallback = self.list_hover);
627 apply_color!(table_row_border, fallback = self.border);
628 apply_color!(title_bar, fallback = self.background);
629 apply_color!(title_bar_border, fallback = self.border);
630 apply_color!(tiles, fallback = self.background);
631 apply_color!(overlay);
632 apply_color!(window_border, fallback = self.border);
633
634 self.list_active = self.list_active.alpha(self.list_active.a.min(0.2));
638 self.table_active = self.table_active.alpha(self.table_active.a.min(0.2));
639 self.selection = self.selection.alpha(self.selection.a.min(0.3));
640 }
641}
642
643impl Theme {
644 pub fn apply_config(&mut self, config: &Rc<ThemeConfig>) {
646 if config.mode.is_dark() {
647 self.dark_theme = config.clone();
648 } else {
649 self.light_theme = config.clone();
650 }
651 if let Some(style) = &config.highlight {
652 let highlight_theme = Arc::new(HighlightTheme {
653 name: config.name.to_string(),
654 appearance: config.mode,
655 style: style.clone(),
656 });
657 self.highlight_theme = highlight_theme.clone();
658 }
659
660 let default_theme = if config.mode.is_dark() {
661 Self::from(ThemeColor::dark().as_ref())
662 } else {
663 Self::from(ThemeColor::light().as_ref())
664 };
665
666 if let Some(font_size) = config.font_size {
667 self.font_size = px(font_size);
668 } else {
669 self.font_size = default_theme.font_size;
670 }
671 if let Some(font_family) = &config.font_family {
672 self.font_family = font_family.clone();
673 } else {
674 self.font_family = default_theme.font_family.clone();
675 }
676 if let Some(mono_font_family) = &config.mono_font_family {
677 self.mono_font_family = mono_font_family.clone();
678 } else {
679 self.mono_font_family = default_theme.mono_font_family.clone();
680 }
681 if let Some(mono_font_size) = config.mono_font_size {
682 self.mono_font_size = px(mono_font_size);
683 } else {
684 self.mono_font_size = default_theme.mono_font_size;
685 }
686 if let Some(radius) = config.radius {
687 self.radius = px(radius as f32);
688 } else {
689 self.radius = default_theme.radius;
690 }
691 if let Some(radius_lg) = config.radius_lg {
692 self.radius_lg = px(radius_lg as f32);
693 } else {
694 self.radius_lg = default_theme.radius_lg;
695 }
696 if let Some(shadow) = config.shadow {
697 self.shadow = shadow;
698 } else {
699 self.shadow = default_theme.shadow;
700 }
701
702 self.colors.apply_config(&config, &default_theme.colors);
703 self.mode = config.mode;
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::try_parse_color;
710 use gpui::hsla;
711
712 #[test]
713 fn test_try_parse_color() {
714 assert_eq!(
715 try_parse_color("#F2F200").ok(),
716 Some(hsla(0.16666667, 1., 0.4745098, 1.0))
717 );
718 assert_eq!(
719 try_parse_color("#00f21888").ok(),
720 Some(hsla(0.34986225, 1.0, 0.4745098, 0.53333336))
721 );
722 }
723}