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