Skip to main content

gpui_component/theme/
schema.rs

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/// Represents a theme configuration.
22#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
23#[serde(default)]
24pub struct ThemeSet {
25    /// The name of the theme set.
26    pub name: SharedString,
27    /// The author of the theme.
28    pub author: Option<SharedString>,
29    /// The URL of the theme.
30    pub url: Option<SharedString>,
31    /// The theme list of the theme set.
32    #[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    /// Whether this theme is the default theme.
40    pub is_default: bool,
41    /// The name of the theme.
42    pub name: SharedString,
43    /// The mode of the theme, default is light.
44    pub mode: ThemeMode,
45
46    /// The base font size, default is 16.
47    #[serde(rename = "font.size")]
48    pub font_size: Option<f32>,
49    /// The base font family, default is system font: `.SystemUIFont`.
50    #[serde(rename = "font.family")]
51    pub font_family: Option<SharedString>,
52    /// The monospace font family, default is platform specific:
53    /// - macOS: `Menlo`
54    /// - Windows: `Consolas`
55    /// - Linux: `DejaVu Sans Mono`
56    ///
57    /// The default falls back to an installed monospace font, then to
58    /// `.SystemUIFont`, when the machine lacks it. A family named here is used
59    /// as-is, so name one that is installed or embedded with `add_fonts`.
60    #[serde(rename = "mono_font.family")]
61    pub mono_font_family: Option<SharedString>,
62    /// The monospace font size, default is 13.
63    #[serde(rename = "mono_font.size")]
64    pub mono_font_size: Option<f32>,
65
66    /// The border radius for general elements, default is 6.
67    #[serde(rename = "radius")]
68    pub radius: Option<usize>,
69    /// The border radius for large elements like Dialogs and Notifications, default is 8.
70    #[serde(rename = "radius.lg")]
71    pub radius_lg: Option<usize>,
72    /// Set shadows in the theme, for example the Input and Button, default is true.
73    #[serde(rename = "shadow")]
74    pub shadow: Option<bool>,
75
76    /// The colors of the theme.
77    pub colors: ThemeConfigColors,
78    /// The highlight theme, this part is combilbility with `style` section in Zed theme.
79    ///
80    /// https://github.com/zed-industries/zed/blob/f50041779dcfd7a76c8aec293361c60c53f02d51/assets/themes/ayu/ayu.json#L9
81    pub highlight: Option<HighlightThemeStyle>,
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
85#[serde(default)]
86pub struct SemanticThemeConfig {
87    pub colors: SemanticColorConfig,
88    pub radius: SemanticRadiusConfig,
89    pub spacing: SemanticSpacingConfig,
90    pub typography: SemanticTypographyConfig,
91    pub shadow: SemanticShadowConfig,
92}
93
94/// Standalone semantic theme configuration file.
95///
96/// This wrapper is intentionally separate from [`ThemeConfig`] so adding
97/// semantic tokens does not change the legacy public struct shape.
98#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
99#[serde(default)]
100pub struct SemanticThemeConfigFile {
101    pub tokens: SemanticThemeConfig,
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
105#[serde(default)]
106pub struct SemanticColorConfig {
107    pub background: Option<SharedString>,
108    pub foreground: Option<SharedString>,
109    pub surface: Option<SharedString>,
110    pub surface_foreground: Option<SharedString>,
111    pub primary: Option<SharedString>,
112    pub primary_foreground: Option<SharedString>,
113    pub secondary: Option<SharedString>,
114    pub secondary_foreground: Option<SharedString>,
115    pub muted: Option<SharedString>,
116    pub muted_foreground: Option<SharedString>,
117    pub accent: Option<SharedString>,
118    pub accent_foreground: Option<SharedString>,
119    pub destructive: Option<SharedString>,
120    pub destructive_foreground: Option<SharedString>,
121    pub border: Option<SharedString>,
122    pub input: Option<SharedString>,
123    pub ring: Option<SharedString>,
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
127#[serde(default)]
128pub struct SemanticRadiusConfig {
129    pub none: Option<f32>,
130    pub sm: Option<f32>,
131    pub md: Option<f32>,
132    pub lg: Option<f32>,
133    pub xl: Option<f32>,
134    pub full: Option<f32>,
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
138#[serde(default)]
139pub struct SemanticSpacingConfig {
140    pub xxs: Option<f32>,
141    pub xs: Option<f32>,
142    pub sm: Option<f32>,
143    pub md: Option<f32>,
144    pub lg: Option<f32>,
145    pub xl: Option<f32>,
146    pub xxl: Option<f32>,
147}
148
149#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
150#[serde(default)]
151pub struct SemanticTextStyleConfig {
152    pub size: Option<f32>,
153    pub line_height: Option<f32>,
154    pub weight: Option<FontWeight>,
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
158#[serde(default)]
159pub struct SemanticTypographyConfig {
160    pub sans: Option<SharedString>,
161    pub mono: Option<SharedString>,
162    pub xs: SemanticTextStyleConfig,
163    pub sm: SemanticTextStyleConfig,
164    pub md: SemanticTextStyleConfig,
165    pub lg: SemanticTextStyleConfig,
166    pub xl: SemanticTextStyleConfig,
167    pub mono_md: SemanticTextStyleConfig,
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
171#[serde(default)]
172pub struct SemanticShadowConfig {
173    pub sm: Option<Vec<BoxShadow>>,
174    pub md: Option<Vec<BoxShadow>>,
175    pub lg: Option<Vec<BoxShadow>>,
176}
177
178impl SemanticThemeConfig {
179    pub(crate) fn apply_to(&self, tokens: &mut SemanticThemeTokens) {
180        macro_rules! apply_color {
181            ($field:ident) => {
182                if let Some(value) = &self.colors.$field
183                    && let Ok(value) = try_parse_color(value)
184                {
185                    tokens.colors.$field = value;
186                }
187            };
188        }
189        apply_color!(background);
190        apply_color!(foreground);
191        apply_color!(surface);
192        apply_color!(surface_foreground);
193        apply_color!(primary);
194        apply_color!(primary_foreground);
195        apply_color!(secondary);
196        apply_color!(secondary_foreground);
197        apply_color!(muted);
198        apply_color!(muted_foreground);
199        apply_color!(accent);
200        apply_color!(accent_foreground);
201        apply_color!(destructive);
202        apply_color!(destructive_foreground);
203        apply_color!(border);
204        apply_color!(input);
205        apply_color!(ring);
206
207        macro_rules! apply_pixels {
208            ($config:expr, $tokens:expr, $($field:ident),+ $(,)?) => {
209                $(if let Some(value) = $config.$field { $tokens.$field = px(value); })+
210            };
211        }
212        apply_pixels!(self.radius, tokens.radius, none, sm, md, lg, xl, full);
213        apply_pixels!(self.spacing, tokens.spacing, xxs, xs, sm, md, lg, xl, xxl);
214
215        if let Some(value) = &self.typography.sans {
216            tokens.typography.sans = value.clone();
217        }
218        if let Some(value) = &self.typography.mono {
219            tokens.typography.mono = value.clone();
220        }
221        apply_text_style(&self.typography.xs, &mut tokens.typography.xs);
222        apply_text_style(&self.typography.sm, &mut tokens.typography.sm);
223        apply_text_style(&self.typography.md, &mut tokens.typography.md);
224        apply_text_style(&self.typography.lg, &mut tokens.typography.lg);
225        apply_text_style(&self.typography.xl, &mut tokens.typography.xl);
226        apply_text_style(&self.typography.mono_md, &mut tokens.typography.mono_md);
227
228        if let Some(value) = &self.shadow.sm {
229            tokens.shadow.sm = value.clone();
230        }
231        if let Some(value) = &self.shadow.md {
232            tokens.shadow.md = value.clone();
233        }
234        if let Some(value) = &self.shadow.lg {
235            tokens.shadow.lg = value.clone();
236        }
237    }
238}
239
240fn apply_text_style(config: &SemanticTextStyleConfig, token: &mut gpui_base::TextStyleToken) {
241    if let Some(value) = config.size {
242        token.size = px(value);
243    }
244    if let Some(value) = config.line_height {
245        token.line_height = px(value);
246    }
247    if let Some(value) = config.weight {
248        token.weight = value;
249    }
250}
251
252#[derive(Debug, Default, Clone, JsonSchema, Serialize, Deserialize)]
253pub struct ThemeConfigColors {
254    /// Used for accents such as hover background on MenuItem, ListItem, etc.
255    #[serde(rename = "accent.background")]
256    pub accent: Option<SharedString>,
257    /// Used for accent text color.
258    #[serde(rename = "accent.foreground")]
259    pub accent_foreground: Option<SharedString>,
260    /// Accordion background color.
261    #[serde(rename = "accordion.background")]
262    pub accordion: Option<SharedString>,
263    /// Default background color.
264    #[serde(rename = "background")]
265    pub background: Option<SharedString>,
266    /// Default border color
267    #[serde(rename = "border")]
268    pub border: Option<SharedString>,
269    /// Default Button background color.
270    #[serde(rename = "button.background")]
271    pub button: Option<SharedString>,
272    /// Default Button active background color.
273    #[serde(rename = "button.active.background")]
274    pub button_active: Option<SharedString>,
275    /// Default Button text color.
276    #[serde(rename = "button.foreground")]
277    pub button_foreground: Option<SharedString>,
278    /// Default Button hover background color.
279    #[serde(rename = "button.hover.background")]
280    pub button_hover: Option<SharedString>,
281    /// Button danger background color, fallback to `danger`.
282    #[serde(rename = "button.danger.background")]
283    pub button_danger: Option<SharedString>,
284    /// Button danger active background color, fallback to `danger_active`.
285    #[serde(rename = "button.danger.active.background")]
286    pub button_danger_active: Option<SharedString>,
287    /// Button danger text color, fallback to `danger_foreground`.
288    #[serde(rename = "button.danger.foreground")]
289    pub button_danger_foreground: Option<SharedString>,
290    /// Button danger hover background color, fallback to `danger_hover`.
291    #[serde(rename = "button.danger.hover.background")]
292    pub button_danger_hover: Option<SharedString>,
293    /// Button info background color, fallback to `info`.
294    #[serde(rename = "button.info.background")]
295    pub button_info: Option<SharedString>,
296    /// Button info active background color, fallback to `info_active`.
297    #[serde(rename = "button.info.active.background")]
298    pub button_info_active: Option<SharedString>,
299    /// Button info text color, fallback to `info_foreground`.
300    #[serde(rename = "button.info.foreground")]
301    pub button_info_foreground: Option<SharedString>,
302    /// Button info hover background color, fallback to `info_hover`.
303    #[serde(rename = "button.info.hover.background")]
304    pub button_info_hover: Option<SharedString>,
305    /// Button primary background color, fallback to `primary`.
306    #[serde(rename = "button.primary.background")]
307    pub button_primary: Option<SharedString>,
308    /// Button primary active background color, fallback to `primary_active`.
309    #[serde(rename = "button.primary.active.background")]
310    pub button_primary_active: Option<SharedString>,
311    /// Button primary text color, fallback to `primary_foreground`.
312    #[serde(rename = "button.primary.foreground")]
313    pub button_primary_foreground: Option<SharedString>,
314    /// Button primary hover background color, fallback to `primary_hover`.
315    #[serde(rename = "button.primary.hover.background")]
316    pub button_primary_hover: Option<SharedString>,
317    /// Button secondary background color, fallback to `secondary`.
318    #[serde(rename = "button.secondary.background")]
319    pub button_secondary: Option<SharedString>,
320    /// Button secondary active background color, fallback to `secondary_active`.
321    #[serde(rename = "button.secondary.active.background")]
322    pub button_secondary_active: Option<SharedString>,
323    /// Button secondary text color, fallback to `secondary_foreground`.
324    #[serde(rename = "button.secondary.foreground")]
325    pub button_secondary_foreground: Option<SharedString>,
326    /// Button secondary hover background color, fallback to `secondary_hover`.
327    #[serde(rename = "button.secondary.hover.background")]
328    pub button_secondary_hover: Option<SharedString>,
329    /// Button success background color, fallback to `success`.
330    #[serde(rename = "button.success.background")]
331    pub button_success: Option<SharedString>,
332    /// Button success active background color, fallback to `success_active`.
333    #[serde(rename = "button.success.active.background")]
334    pub button_success_active: Option<SharedString>,
335    /// Button success text color, fallback to `success_foreground`.
336    #[serde(rename = "button.success.foreground")]
337    pub button_success_foreground: Option<SharedString>,
338    /// Button success hover background color, fallback to `success_hover`.
339    #[serde(rename = "button.success.hover.background")]
340    pub button_success_hover: Option<SharedString>,
341    /// Button warning background color, fallback to `warning`.
342    #[serde(rename = "button.warning.background")]
343    pub button_warning: Option<SharedString>,
344    /// Button warning active background color, fallback to `warning_active`.
345    #[serde(rename = "button.warning.active.background")]
346    pub button_warning_active: Option<SharedString>,
347    /// Button warning text color, fallback to `warning_foreground`.
348    #[serde(rename = "button.warning.foreground")]
349    pub button_warning_foreground: Option<SharedString>,
350    /// Button warning hover background color, fallback to `warning_hover`.
351    #[serde(rename = "button.warning.hover.background")]
352    pub button_warning_hover: Option<SharedString>,
353    /// Background color for GroupBox.
354    #[serde(rename = "group_box.background")]
355    pub group_box: Option<SharedString>,
356    /// Text color for GroupBox.
357    #[serde(rename = "group_box.foreground")]
358    pub group_box_foreground: Option<SharedString>,
359    /// Title text color for GroupBox.
360    #[serde(rename = "group_box.title.foreground")]
361    pub group_box_title_foreground: Option<SharedString>,
362    /// Input caret color (Blinking cursor).
363    #[serde(rename = "caret")]
364    pub caret: Option<SharedString>,
365    /// Chart 1 color.
366    #[serde(rename = "chart.1")]
367    pub chart_1: Option<SharedString>,
368    /// Chart 2 color.
369    #[serde(rename = "chart.2")]
370    pub chart_2: Option<SharedString>,
371    /// Chart 3 color.
372    #[serde(rename = "chart.3")]
373    pub chart_3: Option<SharedString>,
374    /// Chart 4 color.
375    #[serde(rename = "chart.4")]
376    pub chart_4: Option<SharedString>,
377    /// Chart 5 color.
378    #[serde(rename = "chart.5")]
379    pub chart_5: Option<SharedString>,
380    /// Bullish color for candlestick charts (upward price movement).
381    #[serde(rename = "chart.bullish")]
382    pub chart_bullish: Option<SharedString>,
383    /// Bearish color for candlestick charts (downward price movement).
384    #[serde(rename = "chart.bearish")]
385    pub chart_bearish: Option<SharedString>,
386    /// Danger background color.
387    #[serde(rename = "danger.background")]
388    pub danger: Option<SharedString>,
389    /// Danger active background color.
390    #[serde(rename = "danger.active.background")]
391    pub danger_active: Option<SharedString>,
392    /// Danger text color.
393    #[serde(rename = "danger.foreground")]
394    pub danger_foreground: Option<SharedString>,
395    /// Danger hover background color.
396    #[serde(rename = "danger.hover.background")]
397    pub danger_hover: Option<SharedString>,
398    /// Description List label background color.
399    #[serde(rename = "description_list.label.background")]
400    pub description_list_label: Option<SharedString>,
401    /// Description List label foreground color.
402    #[serde(rename = "description_list.label.foreground")]
403    pub description_list_label_foreground: Option<SharedString>,
404    /// Drag border color.
405    #[serde(rename = "drag.border")]
406    pub drag_border: Option<SharedString>,
407    /// Drop target background color.
408    #[serde(rename = "drop_target.background")]
409    pub drop_target: Option<SharedString>,
410    /// Default text color.
411    #[serde(rename = "foreground")]
412    pub foreground: Option<SharedString>,
413    /// Info background color.
414    #[serde(rename = "info.background")]
415    pub info: Option<SharedString>,
416    /// Info active background color.
417    #[serde(rename = "info.active.background")]
418    pub info_active: Option<SharedString>,
419    /// Info text color.
420    #[serde(rename = "info.foreground")]
421    pub info_foreground: Option<SharedString>,
422    /// Info hover background color.
423    #[serde(rename = "info.hover.background")]
424    pub info_hover: Option<SharedString>,
425    /// Border color for inputs such as Input, Select, etc.
426    #[serde(rename = "input.border")]
427    pub input: Option<SharedString>,
428    /// Link text color.
429    #[serde(rename = "link")]
430    pub link: Option<SharedString>,
431    /// Active link text color.
432    #[serde(rename = "link.active")]
433    pub link_active: Option<SharedString>,
434    /// Hover link text color.
435    #[serde(rename = "link.hover")]
436    pub link_hover: Option<SharedString>,
437    /// Background color for List and ListItem.
438    #[serde(rename = "list.background")]
439    pub list: Option<SharedString>,
440    /// Background color for active ListItem.
441    #[serde(rename = "list.active.background")]
442    pub list_active: Option<SharedString>,
443    /// Border color for active ListItem.
444    #[serde(rename = "list.active.border")]
445    pub list_active_border: Option<SharedString>,
446    /// Stripe background color for even ListItem.
447    #[serde(rename = "list.even.background")]
448    pub list_even: Option<SharedString>,
449    /// Background color for List header.
450    #[serde(rename = "list.head.background")]
451    pub list_head: Option<SharedString>,
452    /// Hover background color for ListItem.
453    #[serde(rename = "list.hover.background")]
454    pub list_hover: Option<SharedString>,
455    /// Muted backgrounds such as Skeleton and Switch.
456    #[serde(rename = "muted.background")]
457    pub muted: Option<SharedString>,
458    /// Muted text color, as used in disabled text.
459    #[serde(rename = "muted.foreground")]
460    pub muted_foreground: Option<SharedString>,
461    /// Background color for Popover.
462    #[serde(rename = "popover.background")]
463    pub popover: Option<SharedString>,
464    /// Text color for Popover.
465    #[serde(rename = "popover.foreground")]
466    pub popover_foreground: Option<SharedString>,
467    /// Primary background color.
468    #[serde(rename = "primary.background")]
469    pub primary: Option<SharedString>,
470    /// Active primary background color.
471    #[serde(rename = "primary.active.background")]
472    pub primary_active: Option<SharedString>,
473    /// Primary text color.
474    #[serde(rename = "primary.foreground")]
475    pub primary_foreground: Option<SharedString>,
476    /// Hover primary background color.
477    #[serde(rename = "primary.hover.background")]
478    pub primary_hover: Option<SharedString>,
479    /// Progress bar background color.
480    #[serde(rename = "progress.bar.background")]
481    pub progress_bar: Option<SharedString>,
482    /// Used for focus ring.
483    #[serde(rename = "ring")]
484    pub ring: Option<SharedString>,
485    /// Scrollbar background color.
486    #[serde(rename = "scrollbar.background")]
487    pub scrollbar: Option<SharedString>,
488    /// Scrollbar thumb background color.
489    #[serde(rename = "scrollbar.thumb.background")]
490    pub scrollbar_thumb: Option<SharedString>,
491    /// Scrollbar thumb hover background color.
492    #[serde(rename = "scrollbar.thumb.hover.background")]
493    pub scrollbar_thumb_hover: Option<SharedString>,
494    /// Secondary background color.
495    #[serde(rename = "secondary.background")]
496    pub secondary: Option<SharedString>,
497    /// Active secondary background color.
498    #[serde(rename = "secondary.active.background")]
499    pub secondary_active: Option<SharedString>,
500    /// Secondary text color, used for secondary Button text color or secondary text.
501    #[serde(rename = "secondary.foreground")]
502    pub secondary_foreground: Option<SharedString>,
503    /// Hover secondary background color.
504    #[serde(rename = "secondary.hover.background")]
505    pub secondary_hover: Option<SharedString>,
506    /// Input selection background color.
507    #[serde(rename = "selection.background")]
508    pub selection: Option<SharedString>,
509    /// Sidebar background color.
510    #[serde(rename = "sidebar.background")]
511    pub sidebar: Option<SharedString>,
512    /// Sidebar accent background color.
513    #[serde(rename = "sidebar.accent.background")]
514    pub sidebar_accent: Option<SharedString>,
515    /// Sidebar accent text color.
516    #[serde(rename = "sidebar.accent.foreground")]
517    pub sidebar_accent_foreground: Option<SharedString>,
518    /// Sidebar border color.
519    #[serde(rename = "sidebar.border")]
520    pub sidebar_border: Option<SharedString>,
521    /// Sidebar text color.
522    #[serde(rename = "sidebar.foreground")]
523    pub sidebar_foreground: Option<SharedString>,
524    /// Sidebar primary background color.
525    #[serde(rename = "sidebar.primary.background")]
526    pub sidebar_primary: Option<SharedString>,
527    /// Sidebar primary text color.
528    #[serde(rename = "sidebar.primary.foreground")]
529    pub sidebar_primary_foreground: Option<SharedString>,
530    /// Skeleton background color.
531    #[serde(rename = "skeleton.background")]
532    pub skeleton: Option<SharedString>,
533    /// Slider bar background color.
534    #[serde(rename = "slider.background")]
535    pub slider_bar: Option<SharedString>,
536    /// Slider thumb background color.
537    #[serde(rename = "slider.thumb.background")]
538    pub slider_thumb: Option<SharedString>,
539    /// Success background color.
540    #[serde(rename = "success.background")]
541    pub success: Option<SharedString>,
542    /// Success text color.
543    #[serde(rename = "success.foreground")]
544    pub success_foreground: Option<SharedString>,
545    /// Success hover background color.
546    #[serde(rename = "success.hover.background")]
547    pub success_hover: Option<SharedString>,
548    /// Success active background color.
549    #[serde(rename = "success.active.background")]
550    pub success_active: Option<SharedString>,
551    /// Switch background color.
552    #[serde(rename = "switch.background")]
553    pub switch: Option<SharedString>,
554    /// Switch thumb background color.
555    #[serde(rename = "switch.thumb.background")]
556    pub switch_thumb: Option<SharedString>,
557    /// Tab background color.
558    #[serde(rename = "tab.background")]
559    pub tab: Option<SharedString>,
560    /// Tab active background color.
561    #[serde(rename = "tab.active.background")]
562    pub tab_active: Option<SharedString>,
563    /// Tab active text color.
564    #[serde(rename = "tab.active.foreground")]
565    pub tab_active_foreground: Option<SharedString>,
566    /// TabBar background color.
567    #[serde(rename = "tab_bar.background")]
568    pub tab_bar: Option<SharedString>,
569    /// TabBar segmented background color.
570    #[serde(rename = "tab_bar.segmented.background")]
571    pub tab_bar_segmented: Option<SharedString>,
572    /// Tab text color.
573    #[serde(rename = "tab.foreground")]
574    pub tab_foreground: Option<SharedString>,
575    /// Table background color.
576    #[serde(rename = "table.background")]
577    pub table: Option<SharedString>,
578    /// Table active item background color.
579    #[serde(rename = "table.active.background")]
580    pub table_active: Option<SharedString>,
581    /// Table active item border color.
582    #[serde(rename = "table.active.border")]
583    pub table_active_border: Option<SharedString>,
584    /// Stripe background color for even TableRow.
585    #[serde(rename = "table.even.background")]
586    pub table_even: Option<SharedString>,
587    /// Table header background color.
588    #[serde(rename = "table.head.background")]
589    pub table_head: Option<SharedString>,
590    /// Table header text color.
591    #[serde(rename = "table.head.foreground")]
592    pub table_head_foreground: Option<SharedString>,
593    /// Table footer background color.
594    #[serde(rename = "table.foot.background")]
595    pub table_foot: Option<SharedString>,
596    /// Table footer text color.
597    #[serde(rename = "table.foot.foreground")]
598    pub table_foot_foreground: Option<SharedString>,
599    /// Table item hover background color.
600    #[serde(rename = "table.hover.background")]
601    pub table_hover: Option<SharedString>,
602    /// Table row border color.
603    #[serde(rename = "table.row.border")]
604    pub table_row_border: Option<SharedString>,
605    /// TitleBar background color, use for Window title bar.
606    #[serde(rename = "title_bar.background")]
607    pub title_bar: Option<SharedString>,
608    /// TitleBar border color.
609    #[serde(rename = "title_bar.border")]
610    pub title_bar_border: Option<SharedString>,
611    /// StatusBar background color, use for the bottom status bar.
612    #[serde(rename = "status_bar.background")]
613    pub status_bar: Option<SharedString>,
614    /// StatusBar border color.
615    #[serde(rename = "status_bar.border")]
616    pub status_bar_border: Option<SharedString>,
617    /// Warning background color.
618    #[serde(rename = "warning.background")]
619    pub warning: Option<SharedString>,
620    /// Warning active background color.
621    #[serde(rename = "warning.active.background")]
622    pub warning_active: Option<SharedString>,
623    /// Warning hover background color.
624    #[serde(rename = "warning.hover.background")]
625    pub warning_hover: Option<SharedString>,
626    /// Warning foreground color.
627    #[serde(rename = "warning.foreground")]
628    pub warning_foreground: Option<SharedString>,
629    /// Overlay background color.
630    #[serde(rename = "overlay")]
631    pub overlay: Option<SharedString>,
632    /// Window border color.
633    ///
634    /// # Platform specific:
635    ///
636    /// This is only works on Linux, other platforms we can't change the window border color.
637    #[serde(rename = "window.border")]
638    pub window_border: Option<SharedString>,
639
640    /// Base blue color.
641    #[serde(rename = "base.blue")]
642    blue: Option<String>,
643    /// Base light blue color.
644    #[serde(rename = "base.blue.light")]
645    blue_light: Option<String>,
646    /// Base cyan color.
647    #[serde(rename = "base.cyan")]
648    cyan: Option<String>,
649    /// Base light cyan color.
650    #[serde(rename = "base.cyan.light")]
651    cyan_light: Option<String>,
652    /// Base green color.
653    #[serde(rename = "base.green")]
654    green: Option<String>,
655    /// Base light green color.
656    #[serde(rename = "base.green.light")]
657    green_light: Option<String>,
658    /// Base magenta color.
659    #[serde(rename = "base.magenta")]
660    magenta: Option<String>,
661    #[serde(rename = "base.magenta.light")]
662    magenta_light: Option<String>,
663    /// Base red color.
664    #[serde(rename = "base.red")]
665    red: Option<String>,
666    /// Base light red color.
667    #[serde(rename = "base.red.light")]
668    red_light: Option<String>,
669    /// Base yellow color.
670    #[serde(rename = "base.yellow")]
671    yellow: Option<String>,
672    /// Base light yellow color.
673    #[serde(rename = "base.yellow.light")]
674    yellow_light: Option<String>,
675}
676
677impl ThemeColor {
678    /// Create a new `ThemeColor` from a `ThemeConfig`.
679    pub(crate) fn apply_config(
680        &mut self,
681        config: &ThemeConfig,
682        default_theme: &ThemeColor,
683    ) -> ThemeTokens {
684        let colors = config.colors.clone();
685        let default_tokens = ThemeTokens::from(default_theme);
686        let mut tokens = default_tokens;
687
688        macro_rules! apply_color {
689            ($config_field:ident) => {
690                if let Some(value) = &colors.$config_field {
691                    self.$config_field =
692                        try_parse_color(value).unwrap_or(default_theme.$config_field);
693                } else {
694                    self.$config_field = default_theme.$config_field;
695                }
696                tokens.$config_field = self.$config_field.into();
697            };
698            // With fallback
699            ($config_field:ident, fallback = $fallback:expr) => {
700                let fallback: gpui::Hsla = ($fallback).into();
701                if let Some(value) = &colors.$config_field {
702                    self.$config_field = try_parse_color(value).unwrap_or(fallback);
703                } else {
704                    self.$config_field = fallback;
705                }
706                tokens.$config_field = self.$config_field.into();
707            };
708        }
709
710        macro_rules! apply_background_color {
711            ($config_field:ident) => {
712                let token = if let Some(value) = &colors.$config_field {
713                    if let Ok(token) = try_parse_theme_token(&value) {
714                        token
715                    } else {
716                        default_tokens.$config_field
717                    }
718                } else {
719                    default_tokens.$config_field
720                };
721                self.$config_field = token.color;
722                tokens.$config_field = token;
723            };
724            ($config_field:ident, fallback = $fallback:expr) => {
725                let fallback: ThemeToken = ($fallback).into();
726                let token = if let Some(value) = &colors.$config_field {
727                    if let Ok(token) = try_parse_theme_token(&value) {
728                        token
729                    } else {
730                        fallback
731                    }
732                } else {
733                    fallback
734                };
735                self.$config_field = token.color;
736                tokens.$config_field = token;
737            };
738        }
739
740        apply_background_color!(background);
741
742        // Base colors for fallback
743        apply_color!(red);
744        apply_color!(
745            red_light,
746            fallback = self.background.blend(self.red.opacity(0.8))
747        );
748        apply_color!(green);
749        apply_color!(
750            green_light,
751            fallback = self.background.blend(self.green.opacity(0.8))
752        );
753        apply_color!(blue);
754        apply_color!(
755            blue_light,
756            fallback = self.background.blend(self.blue.opacity(0.8))
757        );
758        apply_color!(magenta);
759        apply_color!(
760            magenta_light,
761            fallback = self.background.blend(self.magenta.opacity(0.8))
762        );
763        apply_color!(yellow);
764        apply_color!(
765            yellow_light,
766            fallback = self.background.blend(self.yellow.opacity(0.8))
767        );
768        apply_color!(cyan);
769        apply_color!(
770            cyan_light,
771            fallback = self.background.blend(self.cyan.opacity(0.8))
772        );
773
774        apply_color!(border);
775        apply_color!(foreground);
776        apply_color!(input, fallback = self.border);
777        apply_background_color!(muted);
778        apply_color!(
779            muted_foreground,
780            fallback = self.muted.blend(self.foreground.opacity(0.7))
781        );
782
783        // Button colors
784        let active_darken = if config.mode.is_dark() { 0.2 } else { 0.1 };
785        let hover_opacity = 0.9;
786        let transparent = gpui::transparent_black();
787        let button_background = if config.mode.is_dark() {
788            self.input.mix_oklab(transparent, 0.3)
789        } else {
790            self.background
791        };
792        apply_background_color!(button, fallback = button_background);
793        apply_color!(button_foreground, fallback = self.foreground);
794        apply_background_color!(
795            button_hover,
796            fallback = self.input.mix_oklab(transparent, 0.5)
797        );
798        apply_background_color!(
799            button_active,
800            fallback = self.input.mix_oklab(transparent, 0.7)
801        );
802        apply_background_color!(primary);
803        apply_color!(primary_foreground, fallback = self.foreground);
804        apply_background_color!(
805            primary_hover,
806            fallback = self.background.blend(self.primary.opacity(hover_opacity))
807        );
808        apply_background_color!(
809            primary_active,
810            fallback = self.primary.darken(active_darken)
811        );
812        apply_background_color!(button_primary, fallback = tokens.primary);
813        apply_color!(
814            button_primary_foreground,
815            fallback = self.primary_foreground
816        );
817        apply_background_color!(button_primary_hover, fallback = tokens.primary_hover);
818        apply_background_color!(button_primary_active, fallback = tokens.primary_active);
819        apply_background_color!(secondary);
820        apply_color!(secondary_foreground, fallback = self.foreground);
821        apply_background_color!(
822            secondary_hover,
823            fallback = self.background.blend(self.secondary.opacity(hover_opacity))
824        );
825        apply_background_color!(
826            secondary_active,
827            fallback = self.secondary.darken(active_darken)
828        );
829        apply_background_color!(button_secondary, fallback = tokens.secondary);
830        apply_color!(
831            button_secondary_foreground,
832            fallback = self.secondary_foreground
833        );
834        apply_background_color!(button_secondary_hover, fallback = tokens.secondary_hover);
835        apply_background_color!(button_secondary_active, fallback = tokens.secondary_active);
836        apply_background_color!(success, fallback = self.green);
837        apply_color!(success_foreground, fallback = self.primary_foreground);
838        apply_background_color!(
839            success_hover,
840            fallback = self.background.blend(self.success.opacity(hover_opacity))
841        );
842        apply_background_color!(
843            success_active,
844            fallback = self.success.darken(active_darken)
845        );
846        apply_background_color!(
847            button_success,
848            fallback = self.success.mix_oklab(transparent, 0.2)
849        );
850        apply_color!(button_success_foreground, fallback = self.success);
851        apply_background_color!(
852            button_success_hover,
853            fallback = self.success.mix_oklab(transparent, 0.3)
854        );
855        apply_background_color!(
856            button_success_active,
857            fallback = self.success.mix_oklab(transparent, 0.4)
858        );
859        apply_background_color!(info, fallback = self.cyan);
860        apply_color!(info_foreground, fallback = self.primary_foreground);
861        apply_background_color!(
862            info_hover,
863            fallback = self.background.blend(self.info.opacity(hover_opacity))
864        );
865        apply_background_color!(info_active, fallback = self.info.darken(active_darken));
866        apply_background_color!(
867            button_info,
868            fallback = self.info.mix_oklab(transparent, 0.2)
869        );
870        apply_color!(button_info_foreground, fallback = self.info);
871        apply_background_color!(
872            button_info_hover,
873            fallback = self.info.mix_oklab(transparent, 0.3)
874        );
875        apply_background_color!(
876            button_info_active,
877            fallback = self.info.mix_oklab(transparent, 0.4)
878        );
879        apply_background_color!(warning, fallback = self.yellow);
880        apply_color!(warning_foreground, fallback = self.primary_foreground);
881        apply_background_color!(
882            warning_hover,
883            fallback = self.background.blend(self.warning.opacity(0.9))
884        );
885        apply_background_color!(
886            warning_active,
887            fallback = self.background.blend(self.warning.darken(active_darken))
888        );
889        apply_background_color!(
890            button_warning,
891            fallback = self.warning.mix_oklab(transparent, 0.2)
892        );
893        apply_color!(button_warning_foreground, fallback = self.warning);
894        apply_background_color!(
895            button_warning_hover,
896            fallback = self.warning.mix_oklab(transparent, 0.3)
897        );
898        apply_background_color!(
899            button_warning_active,
900            fallback = self.warning.mix_oklab(transparent, 0.4)
901        );
902
903        // Other colors
904        apply_background_color!(accent, fallback = tokens.secondary);
905        apply_color!(accent_foreground, fallback = self.foreground);
906        apply_background_color!(accordion, fallback = tokens.background);
907        apply_background_color!(
908            group_box,
909            fallback = self
910                .background
911                .blend(
912                    self.secondary
913                        .opacity(if config.mode.is_dark() { 0.3 } else { 0.4 })
914                )
915        );
916        apply_color!(group_box_foreground, fallback = self.foreground);
917        apply_color!(caret, fallback = self.primary);
918        apply_color!(chart_1, fallback = self.blue.lighten(0.4));
919        apply_color!(chart_2, fallback = self.blue.lighten(0.2));
920        apply_color!(chart_3, fallback = self.blue);
921        apply_color!(chart_4, fallback = self.blue.darken(0.2));
922        apply_color!(chart_5, fallback = self.blue.darken(0.4));
923        apply_color!(chart_bullish, fallback = self.green);
924        apply_color!(chart_bearish, fallback = self.red);
925        apply_background_color!(danger, fallback = self.red);
926        apply_background_color!(danger_active, fallback = self.danger.darken(active_darken));
927        apply_color!(danger_foreground, fallback = self.primary_foreground);
928        apply_background_color!(
929            danger_hover,
930            fallback = self.background.blend(self.danger.opacity(0.9))
931        );
932        apply_background_color!(
933            button_danger,
934            fallback = self.danger.mix_oklab(transparent, 0.2)
935        );
936        apply_color!(button_danger_foreground, fallback = self.danger);
937        apply_background_color!(
938            button_danger_hover,
939            fallback = self.danger.mix_oklab(transparent, 0.3)
940        );
941        apply_background_color!(
942            button_danger_active,
943            fallback = self.danger.mix_oklab(transparent, 0.4)
944        );
945        apply_background_color!(
946            description_list_label,
947            fallback = self.background.blend(self.border.opacity(0.2))
948        );
949        apply_color!(
950            description_list_label_foreground,
951            fallback = self.muted_foreground
952        );
953        apply_color!(drag_border, fallback = self.primary.opacity(0.65));
954        apply_background_color!(drop_target, fallback = self.primary.opacity(0.2));
955        apply_color!(link, fallback = self.primary);
956        apply_color!(link_active, fallback = self.link);
957        apply_color!(link_hover, fallback = self.link);
958        apply_background_color!(list, fallback = tokens.background);
959        apply_background_color!(
960            list_active,
961            fallback = self.background.blend(self.primary.opacity(0.1))
962        );
963        apply_color!(
964            list_active_border,
965            fallback = self.background.blend(self.primary.opacity(0.6))
966        );
967        apply_background_color!(list_even, fallback = tokens.list);
968        apply_background_color!(list_head, fallback = tokens.list);
969        apply_background_color!(list_hover, fallback = self.accent.opacity(0.6));
970        apply_background_color!(popover, fallback = tokens.background);
971        apply_color!(popover_foreground, fallback = self.foreground);
972        apply_background_color!(progress_bar, fallback = tokens.primary);
973        apply_color!(ring, fallback = self.blue);
974        apply_background_color!(scrollbar, fallback = tokens.background);
975        apply_background_color!(scrollbar_thumb, fallback = tokens.accent);
976        apply_background_color!(scrollbar_thumb_hover, fallback = tokens.scrollbar_thumb);
977        apply_background_color!(selection, fallback = tokens.primary);
978        apply_background_color!(
979            sidebar,
980            fallback = self.background.blend(self.border.opacity(0.15))
981        );
982        apply_background_color!(sidebar_accent, fallback = tokens.accent);
983        apply_color!(sidebar_accent_foreground, fallback = self.accent_foreground);
984        apply_color!(sidebar_border, fallback = self.border);
985        apply_color!(sidebar_foreground, fallback = self.foreground);
986        apply_background_color!(sidebar_primary, fallback = tokens.primary);
987        apply_color!(
988            sidebar_primary_foreground,
989            fallback = self.primary_foreground
990        );
991        apply_background_color!(skeleton, fallback = tokens.secondary);
992        apply_background_color!(slider_bar, fallback = tokens.primary);
993        apply_background_color!(slider_thumb, fallback = self.primary_foreground);
994        apply_background_color!(switch, fallback = tokens.secondary_active);
995        apply_background_color!(switch_thumb, fallback = tokens.background);
996        apply_background_color!(tab, fallback = tokens.background);
997        apply_background_color!(tab_active, fallback = tokens.background);
998        apply_color!(tab_active_foreground, fallback = self.foreground);
999        apply_background_color!(tab_bar, fallback = tokens.background);
1000        apply_background_color!(tab_bar_segmented, fallback = tokens.secondary);
1001        apply_color!(tab_foreground, fallback = self.foreground);
1002        apply_background_color!(table, fallback = tokens.list);
1003        apply_background_color!(table_active, fallback = tokens.list_active);
1004        apply_color!(table_active_border, fallback = self.list_active_border);
1005        apply_background_color!(table_even, fallback = tokens.list_even);
1006        apply_background_color!(table_head, fallback = tokens.list_head);
1007        apply_color!(table_head_foreground, fallback = self.muted_foreground);
1008        apply_background_color!(table_foot, fallback = tokens.list_head);
1009        apply_color!(table_foot_foreground, fallback = self.muted_foreground);
1010        apply_background_color!(table_hover, fallback = tokens.list_hover);
1011        apply_color!(table_row_border, fallback = self.border);
1012        apply_background_color!(title_bar, fallback = tokens.background);
1013        apply_color!(title_bar_border, fallback = self.border);
1014        apply_background_color!(status_bar, fallback = tokens.title_bar);
1015        apply_color!(status_bar_border, fallback = self.title_bar_border);
1016        apply_background_color!(overlay);
1017        apply_color!(window_border, fallback = self.border);
1018
1019        // TODO: Apply default fallback colors to highlight.
1020
1021        // Ensure opacity for list_active, table_active, selection.
1022        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    /// Apply the given theme configuration to the current theme.
1060    pub fn apply_config(&mut self, config: &Rc<ThemeConfig>) {
1061        if config.mode.is_dark() {
1062            self.dark_theme = config.clone();
1063        } else {
1064            self.light_theme = config.clone();
1065        }
1066        if let Some(style) = &config.highlight {
1067            let highlight_theme = Arc::new(HighlightTheme {
1068                name: config.name.to_string(),
1069                appearance: config.mode,
1070                style: style.clone(),
1071            });
1072            self.highlight_theme = highlight_theme.clone();
1073        }
1074
1075        let default_colors = if config.mode.is_dark() {
1076            ThemeColor::dark()
1077        } else {
1078            ThemeColor::light()
1079        };
1080
1081        if let Some(font_size) = config.font_size {
1082            self.font_size = px(font_size);
1083        }
1084        if let Some(font_family) = &config.font_family {
1085            self.font_family = font_family.clone();
1086        }
1087        if let Some(mono_font_family) = &config.mono_font_family {
1088            self.mono_font_family = mono_font_family.clone();
1089        }
1090        if let Some(mono_font_size) = config.mono_font_size {
1091            self.mono_font_size = px(mono_font_size);
1092        }
1093        if let Some(radius) = config.radius {
1094            self.radius = px(radius as f32);
1095        }
1096        if let Some(radius_lg) = config.radius_lg {
1097            self.radius_lg = px(radius_lg as f32);
1098        }
1099        if let Some(shadow) = config.shadow {
1100            self.shadow = shadow;
1101        }
1102
1103        self.tokens = self.colors.apply_config(&config, &default_colors);
1104        self.mode = config.mode;
1105    }
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110    use gpui::{linear_color_stop, linear_gradient, px};
1111
1112    use crate::{Colorize as _, Theme, ThemeConfig, ThemeMode, ThemeSet, try_parse_color};
1113
1114    #[test]
1115    fn test_semantic_theme_config_parses_and_roundtrips() {
1116        let value = serde_json::json!({
1117            "name": "Semantic",
1118            "mode": "dark",
1119            "tokens": {
1120                "colors": {
1121                    "surface": "#111827",
1122                    "surface_foreground": "#f9fafb",
1123                    "primary": "#2563eb",
1124                    "destructive": "#dc2626"
1125                },
1126                "radius": { "sm": 2.0, "md": 6.0, "lg": 10.0 },
1127                "spacing": { "xs": 4.0, "md": 12.0, "xl": 24.0 },
1128                "typography": {
1129                    "sans": "Inter",
1130                    "md": { "size": 15.0, "line_height": 22.0 }
1131                }
1132            }
1133        });
1134        let config: super::SemanticThemeConfigFile = serde_json::from_value(value).unwrap();
1135        let serialized = serde_json::to_string(&config).unwrap();
1136        let reparsed: super::SemanticThemeConfigFile = serde_json::from_str(&serialized).unwrap();
1137        let semantic = reparsed.tokens;
1138
1139        assert_eq!(semantic.colors.surface.as_deref(), Some("#111827"));
1140        assert_eq!(semantic.colors.destructive.as_deref(), Some("#dc2626"));
1141        assert_eq!(semantic.radius.lg, Some(10.0));
1142        assert_eq!(semantic.spacing.xl, Some(24.0));
1143        assert_eq!(semantic.typography.sans.as_deref(), Some("Inter"));
1144        assert_eq!(semantic.typography.md.line_height, Some(22.0));
1145
1146        let mut theme = Theme::default();
1147        let resolved = theme.apply_semantic_config_str(&serialized).unwrap();
1148        assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1149        assert_eq!(resolved.spacing.xl, px(24.));
1150        assert_eq!(resolved.typography.md.line_height, px(22.));
1151    }
1152
1153    #[test]
1154    fn test_semantic_tokens_override_legacy_generic_fields_only() {
1155        let config = serde_json::from_value::<super::SemanticThemeConfigFile>(serde_json::json!({
1156            "tokens": {
1157                "colors": { "primary": "#2563eb", "destructive": "#b91c1c" },
1158                "spacing": { "md": 14.0 },
1159                "typography": { "md": { "size": 15.0 } }
1160            }
1161        }))
1162        .unwrap();
1163        let mut theme = Theme::default();
1164        let component_color = theme.button_primary;
1165        let resolved = theme.apply_semantic_config(&config.tokens);
1166
1167        assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1168        assert_eq!(theme.danger, try_parse_color("#b91c1c").unwrap());
1169        assert_eq!(theme.button_primary, component_color);
1170        assert_eq!(resolved.spacing.md, px(14.));
1171        assert_eq!(resolved.typography.md.size, px(15.));
1172    }
1173
1174    #[test]
1175    fn test_legacy_config_without_semantic_tokens_is_unchanged() {
1176        let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1177            "name": "Legacy",
1178            "mode": "light",
1179            "radius": 7,
1180            "colors": { "primary.background": "#7c3aed" }
1181        }))
1182        .unwrap();
1183        let mut theme = Theme::default();
1184        theme.apply_config(&std::rc::Rc::new(config));
1185        assert_eq!(theme.primary, try_parse_color("#7c3aed").unwrap());
1186        assert_eq!(theme.radius, px(7.));
1187        assert_eq!(theme.semantic_tokens().spacing, Default::default());
1188    }
1189
1190    #[test]
1191    fn test_apply_config_reads_the_chart_colors() {
1192        let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1193            "name": "Palette",
1194            "mode": "light",
1195            "colors": {
1196                "chart.1": "#111111",
1197                "chart.2": "not a color",
1198                "chart.3": "#333333",
1199                "chart.bullish": "#00ff00",
1200                "chart.bearish": "#ff0000"
1201            }
1202        }))
1203        .unwrap();
1204
1205        let mut theme = Theme::default();
1206        theme.apply_config(&std::rc::Rc::new(config));
1207
1208        // The keys match the fields; an unparsable or missing one falls back
1209        // to the ramp of the base blue.
1210        assert_eq!(theme.chart_1, try_parse_color("#111111").unwrap());
1211        assert_eq!(theme.chart_2, theme.blue.lighten(0.2));
1212        assert_eq!(theme.chart_3, try_parse_color("#333333").unwrap());
1213        assert_eq!(theme.chart_4, theme.blue.darken(0.2));
1214        assert_eq!(theme.chart_5, theme.blue.darken(0.4));
1215        assert_eq!(theme.tokens.chart_3.color, theme.chart_3);
1216        assert_eq!(theme.chart_bullish, try_parse_color("#00ff00").unwrap());
1217        assert_eq!(theme.chart_bearish, try_parse_color("#ff0000").unwrap());
1218    }
1219
1220    #[test]
1221    fn test_apply_config_preserves_gradient_background_and_solid_color_fallback() {
1222        let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1223            "name": "Gradient",
1224            "mode": "light",
1225            "colors": {
1226                "primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)",
1227                "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)"
1228            }
1229        }))
1230        .unwrap();
1231
1232        let mut theme = Theme::default();
1233        theme.apply_config(&std::rc::Rc::new(config));
1234
1235        let primary_from = try_parse_color("#4F46E5").unwrap();
1236        let primary_to = try_parse_color("#06B6D4").unwrap();
1237        assert_eq!(theme.primary, primary_from);
1238        assert_eq!(theme.tokens.primary.color, primary_from);
1239        assert_eq!(
1240            theme.tokens.primary.background,
1241            linear_gradient(
1242                135.,
1243                linear_color_stop(primary_from, 0.),
1244                linear_color_stop(primary_to, 1.)
1245            )
1246        );
1247        assert_eq!(
1248            theme.tokens.button_primary.background,
1249            theme.tokens.primary.background
1250        );
1251        assert_eq!(
1252            theme.tokens.button_primary_hover.background,
1253            linear_gradient(
1254                90.,
1255                linear_color_stop(crate::red_500(), 0.25),
1256                linear_color_stop(crate::blue_600(), 0.75)
1257            )
1258        );
1259        assert_eq!(theme.mode, ThemeMode::Light);
1260    }
1261
1262    #[test]
1263    fn test_aurora_theme_parses_gradient_backgrounds() {
1264        let theme_set =
1265            serde_json::from_str::<ThemeSet>(include_str!("../../../../themes/aurora.json"))
1266                .unwrap();
1267        assert_eq!(theme_set.themes.len(), 1);
1268        assert!(theme_set.themes.iter().all(|theme| !theme.mode.is_dark()));
1269
1270        let light = theme_set
1271            .themes
1272            .iter()
1273            .find(|theme| theme.name.as_ref() == "Aurora Light")
1274            .unwrap();
1275        let mut theme = Theme::default();
1276        theme.apply_config(&std::rc::Rc::new(light.clone()));
1277
1278        assert_ne!(
1279            theme.tokens.button_primary.background,
1280            theme.button_primary.into()
1281        );
1282        assert_eq!(theme.tokens.background.background, theme.background.into());
1283        assert_eq!(theme.button_primary, try_parse_color("#1E293B").unwrap());
1284        assert_eq!(theme.background, try_parse_color("#FFFFFF").unwrap());
1285        assert_ne!(
1286            theme.tokens.progress_bar.background,
1287            theme.progress_bar.into()
1288        );
1289        assert_ne!(
1290            theme.tokens.scrollbar_thumb.background,
1291            theme.scrollbar_thumb.into()
1292        );
1293        assert_ne!(theme.tokens.switch.background, theme.switch.into());
1294        assert_ne!(
1295            theme.tokens.switch_thumb.background,
1296            theme.switch_thumb.into()
1297        );
1298        assert_ne!(theme.tokens.title_bar.background, theme.title_bar.into());
1299        assert_ne!(theme.tokens.status_bar.background, theme.status_bar.into());
1300    }
1301
1302    #[test]
1303    fn test_apply_config_clamps_highlight_alpha_per_gradient_stop() {
1304        let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1305            "name": "Highlight",
1306            "mode": "light",
1307            "colors": {
1308                // Solid above the cap: must be capped to 0.2, not attenuated twice.
1309                "list.active.background": "#3b82f6",
1310                // Gradient with a faint `from` stop and an opaque `to` stop: the
1311                // `to` stop must be clamped independently, not left at full alpha.
1312                "table.active.background": "linear-gradient(#bfdbfe33, #3b82f6)",
1313                // Gradient with a transparent `from` stop: the opaque `to` stop
1314                // must still be clamped (the `base == 0` factor fallback used to
1315                // leave it untouched).
1316                "selection.background": "linear-gradient(#3b82f600, #3b82f6)",
1317            }
1318        }))
1319        .unwrap();
1320
1321        let mut theme = Theme::default();
1322        theme.apply_config(&std::rc::Rc::new(config));
1323
1324        // Solid: representative color and rendered background both capped at 0.2.
1325        let blue = try_parse_color("#3b82f6").unwrap();
1326        assert_eq!(theme.list_active, blue.alpha(0.2));
1327        assert_eq!(theme.tokens.list_active.background, blue.alpha(0.2).into());
1328
1329        // Gradient: the opaque `to` stop is clamped to 0.2, not left fully opaque.
1330        let faint = try_parse_color("#bfdbfe33").unwrap();
1331        assert_eq!(
1332            theme.tokens.table_active.background,
1333            linear_gradient(
1334                180.,
1335                linear_color_stop(faint.alpha(faint.a.min(0.2)), 0.),
1336                linear_color_stop(blue.alpha(0.2), 1.),
1337            )
1338        );
1339
1340        // Gradient: a transparent `from` stop stays transparent while the opaque
1341        // `to` stop is still clamped to 0.3 (selection cap).
1342        let clear = try_parse_color("#3b82f600").unwrap();
1343        assert_eq!(
1344            theme.tokens.selection.background,
1345            linear_gradient(
1346                180.,
1347                linear_color_stop(clear.alpha(clear.a.min(0.3)), 0.),
1348                linear_color_stop(blue.alpha(0.3), 1.),
1349            )
1350        );
1351    }
1352}