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    /// Background color for Tiles.
618    #[serde(rename = "tiles.background")]
619    pub tiles: Option<SharedString>,
620    /// Warning background color.
621    #[serde(rename = "warning.background")]
622    pub warning: Option<SharedString>,
623    /// Warning active background color.
624    #[serde(rename = "warning.active.background")]
625    pub warning_active: Option<SharedString>,
626    /// Warning hover background color.
627    #[serde(rename = "warning.hover.background")]
628    pub warning_hover: Option<SharedString>,
629    /// Warning foreground color.
630    #[serde(rename = "warning.foreground")]
631    pub warning_foreground: Option<SharedString>,
632    /// Overlay background color.
633    #[serde(rename = "overlay")]
634    pub overlay: Option<SharedString>,
635    /// Window border color.
636    ///
637    /// # Platform specific:
638    ///
639    /// This is only works on Linux, other platforms we can't change the window border color.
640    #[serde(rename = "window.border")]
641    pub window_border: Option<SharedString>,
642
643    /// Base blue color.
644    #[serde(rename = "base.blue")]
645    blue: Option<String>,
646    /// Base light blue color.
647    #[serde(rename = "base.blue.light")]
648    blue_light: Option<String>,
649    /// Base cyan color.
650    #[serde(rename = "base.cyan")]
651    cyan: Option<String>,
652    /// Base light cyan color.
653    #[serde(rename = "base.cyan.light")]
654    cyan_light: Option<String>,
655    /// Base green color.
656    #[serde(rename = "base.green")]
657    green: Option<String>,
658    /// Base light green color.
659    #[serde(rename = "base.green.light")]
660    green_light: Option<String>,
661    /// Base magenta color.
662    #[serde(rename = "base.magenta")]
663    magenta: Option<String>,
664    #[serde(rename = "base.magenta.light")]
665    magenta_light: Option<String>,
666    /// Base red color.
667    #[serde(rename = "base.red")]
668    red: Option<String>,
669    /// Base light red color.
670    #[serde(rename = "base.red.light")]
671    red_light: Option<String>,
672    /// Base yellow color.
673    #[serde(rename = "base.yellow")]
674    yellow: Option<String>,
675    /// Base light yellow color.
676    #[serde(rename = "base.yellow.light")]
677    yellow_light: Option<String>,
678}
679
680impl ThemeColor {
681    /// Create a new `ThemeColor` from a `ThemeConfig`.
682    pub(crate) fn apply_config(
683        &mut self,
684        config: &ThemeConfig,
685        default_theme: &ThemeColor,
686    ) -> ThemeTokens {
687        let colors = config.colors.clone();
688        let default_tokens = ThemeTokens::from(default_theme);
689        let mut tokens = default_tokens;
690
691        macro_rules! apply_color {
692            ($config_field:ident) => {
693                if let Some(value) = &colors.$config_field {
694                    self.$config_field =
695                        try_parse_color(value).unwrap_or(default_theme.$config_field);
696                } else {
697                    self.$config_field = default_theme.$config_field;
698                }
699                tokens.$config_field = self.$config_field.into();
700            };
701            // With fallback
702            ($config_field:ident, fallback = $fallback:expr) => {
703                let fallback: gpui::Hsla = ($fallback).into();
704                if let Some(value) = &colors.$config_field {
705                    self.$config_field = try_parse_color(value).unwrap_or(fallback);
706                } else {
707                    self.$config_field = fallback;
708                }
709                tokens.$config_field = self.$config_field.into();
710            };
711        }
712
713        macro_rules! apply_background_color {
714            ($config_field:ident) => {
715                let token = if let Some(value) = &colors.$config_field {
716                    if let Ok(token) = try_parse_theme_token(&value) {
717                        token
718                    } else {
719                        default_tokens.$config_field
720                    }
721                } else {
722                    default_tokens.$config_field
723                };
724                self.$config_field = token.color;
725                tokens.$config_field = token;
726            };
727            ($config_field:ident, fallback = $fallback:expr) => {
728                let fallback: ThemeToken = ($fallback).into();
729                let token = if let Some(value) = &colors.$config_field {
730                    if let Ok(token) = try_parse_theme_token(&value) {
731                        token
732                    } else {
733                        fallback
734                    }
735                } else {
736                    fallback
737                };
738                self.$config_field = token.color;
739                tokens.$config_field = token;
740            };
741        }
742
743        apply_background_color!(background);
744
745        // Base colors for fallback
746        apply_color!(red);
747        apply_color!(
748            red_light,
749            fallback = self.background.blend(self.red.opacity(0.8))
750        );
751        apply_color!(green);
752        apply_color!(
753            green_light,
754            fallback = self.background.blend(self.green.opacity(0.8))
755        );
756        apply_color!(blue);
757        apply_color!(
758            blue_light,
759            fallback = self.background.blend(self.blue.opacity(0.8))
760        );
761        apply_color!(magenta);
762        apply_color!(
763            magenta_light,
764            fallback = self.background.blend(self.magenta.opacity(0.8))
765        );
766        apply_color!(yellow);
767        apply_color!(
768            yellow_light,
769            fallback = self.background.blend(self.yellow.opacity(0.8))
770        );
771        apply_color!(cyan);
772        apply_color!(
773            cyan_light,
774            fallback = self.background.blend(self.cyan.opacity(0.8))
775        );
776
777        apply_color!(border);
778        apply_color!(foreground);
779        apply_color!(input, fallback = self.border);
780        apply_background_color!(muted);
781        apply_color!(
782            muted_foreground,
783            fallback = self.muted.blend(self.foreground.opacity(0.7))
784        );
785
786        // Button colors
787        let active_darken = if config.mode.is_dark() { 0.2 } else { 0.1 };
788        let hover_opacity = 0.9;
789        let transparent = gpui::transparent_black();
790        let button_background = if config.mode.is_dark() {
791            self.input.mix_oklab(transparent, 0.3)
792        } else {
793            self.background
794        };
795        apply_background_color!(button, fallback = button_background);
796        apply_color!(button_foreground, fallback = self.foreground);
797        apply_background_color!(
798            button_hover,
799            fallback = self.input.mix_oklab(transparent, 0.5)
800        );
801        apply_background_color!(
802            button_active,
803            fallback = self.input.mix_oklab(transparent, 0.7)
804        );
805        apply_background_color!(primary);
806        apply_color!(primary_foreground, fallback = self.foreground);
807        apply_background_color!(
808            primary_hover,
809            fallback = self.background.blend(self.primary.opacity(hover_opacity))
810        );
811        apply_background_color!(
812            primary_active,
813            fallback = self.primary.darken(active_darken)
814        );
815        apply_background_color!(button_primary, fallback = tokens.primary);
816        apply_color!(
817            button_primary_foreground,
818            fallback = self.primary_foreground
819        );
820        apply_background_color!(button_primary_hover, fallback = tokens.primary_hover);
821        apply_background_color!(button_primary_active, fallback = tokens.primary_active);
822        apply_background_color!(secondary);
823        apply_color!(secondary_foreground, fallback = self.foreground);
824        apply_background_color!(
825            secondary_hover,
826            fallback = self.background.blend(self.secondary.opacity(hover_opacity))
827        );
828        apply_background_color!(
829            secondary_active,
830            fallback = self.secondary.darken(active_darken)
831        );
832        apply_background_color!(button_secondary, fallback = tokens.secondary);
833        apply_color!(
834            button_secondary_foreground,
835            fallback = self.secondary_foreground
836        );
837        apply_background_color!(button_secondary_hover, fallback = tokens.secondary_hover);
838        apply_background_color!(button_secondary_active, fallback = tokens.secondary_active);
839        apply_background_color!(success, fallback = self.green);
840        apply_color!(success_foreground, fallback = self.primary_foreground);
841        apply_background_color!(
842            success_hover,
843            fallback = self.background.blend(self.success.opacity(hover_opacity))
844        );
845        apply_background_color!(
846            success_active,
847            fallback = self.success.darken(active_darken)
848        );
849        apply_background_color!(
850            button_success,
851            fallback = self.success.mix_oklab(transparent, 0.2)
852        );
853        apply_color!(button_success_foreground, fallback = self.success);
854        apply_background_color!(
855            button_success_hover,
856            fallback = self.success.mix_oklab(transparent, 0.3)
857        );
858        apply_background_color!(
859            button_success_active,
860            fallback = self.success.mix_oklab(transparent, 0.4)
861        );
862        apply_background_color!(info, fallback = self.cyan);
863        apply_color!(info_foreground, fallback = self.primary_foreground);
864        apply_background_color!(
865            info_hover,
866            fallback = self.background.blend(self.info.opacity(hover_opacity))
867        );
868        apply_background_color!(info_active, fallback = self.info.darken(active_darken));
869        apply_background_color!(
870            button_info,
871            fallback = self.info.mix_oklab(transparent, 0.2)
872        );
873        apply_color!(button_info_foreground, fallback = self.info);
874        apply_background_color!(
875            button_info_hover,
876            fallback = self.info.mix_oklab(transparent, 0.3)
877        );
878        apply_background_color!(
879            button_info_active,
880            fallback = self.info.mix_oklab(transparent, 0.4)
881        );
882        apply_background_color!(warning, fallback = self.yellow);
883        apply_color!(warning_foreground, fallback = self.primary_foreground);
884        apply_background_color!(
885            warning_hover,
886            fallback = self.background.blend(self.warning.opacity(0.9))
887        );
888        apply_background_color!(
889            warning_active,
890            fallback = self.background.blend(self.warning.darken(active_darken))
891        );
892        apply_background_color!(
893            button_warning,
894            fallback = self.warning.mix_oklab(transparent, 0.2)
895        );
896        apply_color!(button_warning_foreground, fallback = self.warning);
897        apply_background_color!(
898            button_warning_hover,
899            fallback = self.warning.mix_oklab(transparent, 0.3)
900        );
901        apply_background_color!(
902            button_warning_active,
903            fallback = self.warning.mix_oklab(transparent, 0.4)
904        );
905
906        // Other colors
907        apply_background_color!(accent, fallback = tokens.secondary);
908        apply_color!(accent_foreground, fallback = self.foreground);
909        apply_background_color!(accordion, fallback = tokens.background);
910        apply_background_color!(
911            group_box,
912            fallback = self
913                .background
914                .blend(
915                    self.secondary
916                        .opacity(if config.mode.is_dark() { 0.3 } else { 0.4 })
917                )
918        );
919        apply_color!(group_box_foreground, fallback = self.foreground);
920        apply_color!(caret, fallback = self.primary);
921        apply_color!(chart_1, fallback = self.blue.lighten(0.4));
922        apply_color!(chart_2, fallback = self.blue.lighten(0.2));
923        apply_color!(chart_3, fallback = self.blue);
924        apply_color!(chart_4, fallback = self.blue.darken(0.2));
925        apply_color!(chart_5, fallback = self.blue.darken(0.4));
926        apply_color!(chart_bullish, fallback = self.green);
927        apply_color!(chart_bearish, fallback = self.red);
928        apply_background_color!(danger, fallback = self.red);
929        apply_background_color!(danger_active, fallback = self.danger.darken(active_darken));
930        apply_color!(danger_foreground, fallback = self.primary_foreground);
931        apply_background_color!(
932            danger_hover,
933            fallback = self.background.blend(self.danger.opacity(0.9))
934        );
935        apply_background_color!(
936            button_danger,
937            fallback = self.danger.mix_oklab(transparent, 0.2)
938        );
939        apply_color!(button_danger_foreground, fallback = self.danger);
940        apply_background_color!(
941            button_danger_hover,
942            fallback = self.danger.mix_oklab(transparent, 0.3)
943        );
944        apply_background_color!(
945            button_danger_active,
946            fallback = self.danger.mix_oklab(transparent, 0.4)
947        );
948        apply_background_color!(
949            description_list_label,
950            fallback = self.background.blend(self.border.opacity(0.2))
951        );
952        apply_color!(
953            description_list_label_foreground,
954            fallback = self.muted_foreground
955        );
956        apply_color!(drag_border, fallback = self.primary.opacity(0.65));
957        apply_background_color!(drop_target, fallback = self.primary.opacity(0.2));
958        apply_color!(link, fallback = self.primary);
959        apply_color!(link_active, fallback = self.link);
960        apply_color!(link_hover, fallback = self.link);
961        apply_background_color!(list, fallback = tokens.background);
962        apply_background_color!(
963            list_active,
964            fallback = self.background.blend(self.primary.opacity(0.1))
965        );
966        apply_color!(
967            list_active_border,
968            fallback = self.background.blend(self.primary.opacity(0.6))
969        );
970        apply_background_color!(list_even, fallback = tokens.list);
971        apply_background_color!(list_head, fallback = tokens.list);
972        apply_background_color!(list_hover, fallback = self.accent.opacity(0.6));
973        apply_background_color!(popover, fallback = tokens.background);
974        apply_color!(popover_foreground, fallback = self.foreground);
975        apply_background_color!(progress_bar, fallback = tokens.primary);
976        apply_color!(ring, fallback = self.blue);
977        apply_background_color!(scrollbar, fallback = tokens.background);
978        apply_background_color!(scrollbar_thumb, fallback = tokens.accent);
979        apply_background_color!(scrollbar_thumb_hover, fallback = tokens.scrollbar_thumb);
980        apply_background_color!(selection, fallback = tokens.primary);
981        apply_background_color!(
982            sidebar,
983            fallback = self.background.blend(self.border.opacity(0.15))
984        );
985        apply_background_color!(sidebar_accent, fallback = tokens.accent);
986        apply_color!(sidebar_accent_foreground, fallback = self.accent_foreground);
987        apply_color!(sidebar_border, fallback = self.border);
988        apply_color!(sidebar_foreground, fallback = self.foreground);
989        apply_background_color!(sidebar_primary, fallback = tokens.primary);
990        apply_color!(
991            sidebar_primary_foreground,
992            fallback = self.primary_foreground
993        );
994        apply_background_color!(skeleton, fallback = tokens.secondary);
995        apply_background_color!(slider_bar, fallback = tokens.primary);
996        apply_background_color!(slider_thumb, fallback = self.primary_foreground);
997        apply_background_color!(switch, fallback = tokens.secondary_active);
998        apply_background_color!(switch_thumb, fallback = tokens.background);
999        apply_background_color!(tab, fallback = tokens.background);
1000        apply_background_color!(tab_active, fallback = tokens.background);
1001        apply_color!(tab_active_foreground, fallback = self.foreground);
1002        apply_background_color!(tab_bar, fallback = tokens.background);
1003        apply_background_color!(tab_bar_segmented, fallback = tokens.secondary);
1004        apply_color!(tab_foreground, fallback = self.foreground);
1005        apply_background_color!(table, fallback = tokens.list);
1006        apply_background_color!(table_active, fallback = tokens.list_active);
1007        apply_color!(table_active_border, fallback = self.list_active_border);
1008        apply_background_color!(table_even, fallback = tokens.list_even);
1009        apply_background_color!(table_head, fallback = tokens.list_head);
1010        apply_color!(table_head_foreground, fallback = self.muted_foreground);
1011        apply_background_color!(table_foot, fallback = tokens.list_head);
1012        apply_color!(table_foot_foreground, fallback = self.muted_foreground);
1013        apply_background_color!(table_hover, fallback = tokens.list_hover);
1014        apply_color!(table_row_border, fallback = self.border);
1015        apply_background_color!(title_bar, fallback = tokens.background);
1016        apply_color!(title_bar_border, fallback = self.border);
1017        apply_background_color!(status_bar, fallback = tokens.title_bar);
1018        apply_color!(status_bar_border, fallback = self.title_bar_border);
1019        apply_background_color!(tiles, fallback = tokens.background);
1020        apply_background_color!(overlay);
1021        apply_color!(window_border, fallback = self.border);
1022
1023        // TODO: Apply default fallback colors to highlight.
1024
1025        // Ensure opacity for list_active, table_active, selection.
1026        let clamp_alpha = |raw: Option<&str>, color: Hsla, background: Background, max: f32| {
1027            let base = color.a;
1028            let target = base.min(max);
1029            let color = color.alpha(target);
1030            let background = raw
1031                .and_then(|value| try_parse_background_clamped(value, max).ok())
1032                .unwrap_or_else(|| {
1033                    let factor = if base > 0. { target / base } else { 1. };
1034                    background.opacity(factor)
1035                });
1036            (color, ThemeToken::new(color, background))
1037        };
1038
1039        (self.list_active, tokens.list_active) = clamp_alpha(
1040            colors.list_active.as_deref(),
1041            self.list_active,
1042            tokens.list_active.background,
1043            0.2,
1044        );
1045        (self.table_active, tokens.table_active) = clamp_alpha(
1046            colors.table_active.as_deref(),
1047            self.table_active,
1048            tokens.table_active.background,
1049            0.2,
1050        );
1051        (self.selection, tokens.selection) = clamp_alpha(
1052            colors.selection.as_deref(),
1053            self.selection,
1054            tokens.selection.background,
1055            0.3,
1056        );
1057
1058        tokens
1059    }
1060}
1061
1062impl Theme {
1063    /// Apply the given theme configuration to the current theme.
1064    pub fn apply_config(&mut self, config: &Rc<ThemeConfig>) {
1065        if config.mode.is_dark() {
1066            self.dark_theme = config.clone();
1067        } else {
1068            self.light_theme = config.clone();
1069        }
1070        if let Some(style) = &config.highlight {
1071            let highlight_theme = Arc::new(HighlightTheme {
1072                name: config.name.to_string(),
1073                appearance: config.mode,
1074                style: style.clone(),
1075            });
1076            self.highlight_theme = highlight_theme.clone();
1077        }
1078
1079        let default_colors = if config.mode.is_dark() {
1080            ThemeColor::dark()
1081        } else {
1082            ThemeColor::light()
1083        };
1084
1085        if let Some(font_size) = config.font_size {
1086            self.font_size = px(font_size);
1087        }
1088        if let Some(font_family) = &config.font_family {
1089            self.font_family = font_family.clone();
1090        }
1091        if let Some(mono_font_family) = &config.mono_font_family {
1092            self.mono_font_family = mono_font_family.clone();
1093        }
1094        if let Some(mono_font_size) = config.mono_font_size {
1095            self.mono_font_size = px(mono_font_size);
1096        }
1097        if let Some(radius) = config.radius {
1098            self.radius = px(radius as f32);
1099        }
1100        if let Some(radius_lg) = config.radius_lg {
1101            self.radius_lg = px(radius_lg as f32);
1102        }
1103        if let Some(shadow) = config.shadow {
1104            self.shadow = shadow;
1105        }
1106
1107        self.tokens = self.colors.apply_config(&config, &default_colors);
1108        self.mode = config.mode;
1109    }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use gpui::{linear_color_stop, linear_gradient, px};
1115
1116    use crate::{Theme, ThemeConfig, ThemeMode, ThemeSet, try_parse_color};
1117
1118    #[test]
1119    fn test_semantic_theme_config_parses_and_roundtrips() {
1120        let value = serde_json::json!({
1121            "name": "Semantic",
1122            "mode": "dark",
1123            "tokens": {
1124                "colors": {
1125                    "surface": "#111827",
1126                    "surface_foreground": "#f9fafb",
1127                    "primary": "#2563eb",
1128                    "destructive": "#dc2626"
1129                },
1130                "radius": { "sm": 2.0, "md": 6.0, "lg": 10.0 },
1131                "spacing": { "xs": 4.0, "md": 12.0, "xl": 24.0 },
1132                "typography": {
1133                    "sans": "Inter",
1134                    "md": { "size": 15.0, "line_height": 22.0 }
1135                }
1136            }
1137        });
1138        let config: super::SemanticThemeConfigFile = serde_json::from_value(value).unwrap();
1139        let serialized = serde_json::to_string(&config).unwrap();
1140        let reparsed: super::SemanticThemeConfigFile = serde_json::from_str(&serialized).unwrap();
1141        let semantic = reparsed.tokens;
1142
1143        assert_eq!(semantic.colors.surface.as_deref(), Some("#111827"));
1144        assert_eq!(semantic.colors.destructive.as_deref(), Some("#dc2626"));
1145        assert_eq!(semantic.radius.lg, Some(10.0));
1146        assert_eq!(semantic.spacing.xl, Some(24.0));
1147        assert_eq!(semantic.typography.sans.as_deref(), Some("Inter"));
1148        assert_eq!(semantic.typography.md.line_height, Some(22.0));
1149
1150        let mut theme = Theme::default();
1151        let resolved = theme.apply_semantic_config_str(&serialized).unwrap();
1152        assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1153        assert_eq!(resolved.spacing.xl, px(24.));
1154        assert_eq!(resolved.typography.md.line_height, px(22.));
1155    }
1156
1157    #[test]
1158    fn test_semantic_tokens_override_legacy_generic_fields_only() {
1159        let config = serde_json::from_value::<super::SemanticThemeConfigFile>(serde_json::json!({
1160            "tokens": {
1161                "colors": { "primary": "#2563eb", "destructive": "#b91c1c" },
1162                "spacing": { "md": 14.0 },
1163                "typography": { "md": { "size": 15.0 } }
1164            }
1165        }))
1166        .unwrap();
1167        let mut theme = Theme::default();
1168        let component_color = theme.button_primary;
1169        let resolved = theme.apply_semantic_config(&config.tokens);
1170
1171        assert_eq!(theme.primary, try_parse_color("#2563eb").unwrap());
1172        assert_eq!(theme.danger, try_parse_color("#b91c1c").unwrap());
1173        assert_eq!(theme.button_primary, component_color);
1174        assert_eq!(resolved.spacing.md, px(14.));
1175        assert_eq!(resolved.typography.md.size, px(15.));
1176    }
1177
1178    #[test]
1179    fn test_legacy_config_without_semantic_tokens_is_unchanged() {
1180        let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1181            "name": "Legacy",
1182            "mode": "light",
1183            "radius": 7,
1184            "colors": { "primary.background": "#7c3aed" }
1185        }))
1186        .unwrap();
1187        let mut theme = Theme::default();
1188        theme.apply_config(&std::rc::Rc::new(config));
1189        assert_eq!(theme.primary, try_parse_color("#7c3aed").unwrap());
1190        assert_eq!(theme.radius, px(7.));
1191        assert_eq!(theme.semantic_tokens().spacing, Default::default());
1192    }
1193
1194    #[test]
1195    fn test_apply_config_preserves_gradient_background_and_solid_color_fallback() {
1196        let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1197            "name": "Gradient",
1198            "mode": "light",
1199            "colors": {
1200                "primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)",
1201                "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)"
1202            }
1203        }))
1204        .unwrap();
1205
1206        let mut theme = Theme::default();
1207        theme.apply_config(&std::rc::Rc::new(config));
1208
1209        let primary_from = try_parse_color("#4F46E5").unwrap();
1210        let primary_to = try_parse_color("#06B6D4").unwrap();
1211        assert_eq!(theme.primary, primary_from);
1212        assert_eq!(theme.tokens.primary.color, primary_from);
1213        assert_eq!(
1214            theme.tokens.primary.background,
1215            linear_gradient(
1216                135.,
1217                linear_color_stop(primary_from, 0.),
1218                linear_color_stop(primary_to, 1.)
1219            )
1220        );
1221        assert_eq!(
1222            theme.tokens.button_primary.background,
1223            theme.tokens.primary.background
1224        );
1225        assert_eq!(
1226            theme.tokens.button_primary_hover.background,
1227            linear_gradient(
1228                90.,
1229                linear_color_stop(crate::red_500(), 0.25),
1230                linear_color_stop(crate::blue_600(), 0.75)
1231            )
1232        );
1233        assert_eq!(theme.mode, ThemeMode::Light);
1234    }
1235
1236    #[test]
1237    fn test_aurora_theme_parses_gradient_backgrounds() {
1238        let theme_set =
1239            serde_json::from_str::<ThemeSet>(include_str!("../../../../themes/aurora.json"))
1240                .unwrap();
1241        assert_eq!(theme_set.themes.len(), 1);
1242        assert!(theme_set.themes.iter().all(|theme| !theme.mode.is_dark()));
1243
1244        let light = theme_set
1245            .themes
1246            .iter()
1247            .find(|theme| theme.name.as_ref() == "Aurora Light")
1248            .unwrap();
1249        let mut theme = Theme::default();
1250        theme.apply_config(&std::rc::Rc::new(light.clone()));
1251
1252        assert_ne!(
1253            theme.tokens.button_primary.background,
1254            theme.button_primary.into()
1255        );
1256        assert_eq!(theme.tokens.background.background, theme.background.into());
1257        assert_eq!(theme.button_primary, try_parse_color("#1E293B").unwrap());
1258        assert_eq!(theme.background, try_parse_color("#FFFFFF").unwrap());
1259        assert_ne!(
1260            theme.tokens.progress_bar.background,
1261            theme.progress_bar.into()
1262        );
1263        assert_ne!(
1264            theme.tokens.scrollbar_thumb.background,
1265            theme.scrollbar_thumb.into()
1266        );
1267        assert_ne!(theme.tokens.switch.background, theme.switch.into());
1268        assert_ne!(
1269            theme.tokens.switch_thumb.background,
1270            theme.switch_thumb.into()
1271        );
1272        assert_ne!(theme.tokens.title_bar.background, theme.title_bar.into());
1273        assert_ne!(theme.tokens.status_bar.background, theme.status_bar.into());
1274    }
1275
1276    #[test]
1277    fn test_apply_config_clamps_highlight_alpha_per_gradient_stop() {
1278        let config = serde_json::from_value::<ThemeConfig>(serde_json::json!({
1279            "name": "Highlight",
1280            "mode": "light",
1281            "colors": {
1282                // Solid above the cap: must be capped to 0.2, not attenuated twice.
1283                "list.active.background": "#3b82f6",
1284                // Gradient with a faint `from` stop and an opaque `to` stop: the
1285                // `to` stop must be clamped independently, not left at full alpha.
1286                "table.active.background": "linear-gradient(#bfdbfe33, #3b82f6)",
1287                // Gradient with a transparent `from` stop: the opaque `to` stop
1288                // must still be clamped (the `base == 0` factor fallback used to
1289                // leave it untouched).
1290                "selection.background": "linear-gradient(#3b82f600, #3b82f6)",
1291            }
1292        }))
1293        .unwrap();
1294
1295        let mut theme = Theme::default();
1296        theme.apply_config(&std::rc::Rc::new(config));
1297
1298        // Solid: representative color and rendered background both capped at 0.2.
1299        let blue = try_parse_color("#3b82f6").unwrap();
1300        assert_eq!(theme.list_active, blue.alpha(0.2));
1301        assert_eq!(theme.tokens.list_active.background, blue.alpha(0.2).into());
1302
1303        // Gradient: the opaque `to` stop is clamped to 0.2, not left fully opaque.
1304        let faint = try_parse_color("#bfdbfe33").unwrap();
1305        assert_eq!(
1306            theme.tokens.table_active.background,
1307            linear_gradient(
1308                180.,
1309                linear_color_stop(faint.alpha(faint.a.min(0.2)), 0.),
1310                linear_color_stop(blue.alpha(0.2), 1.),
1311            )
1312        );
1313
1314        // Gradient: a transparent `from` stop stays transparent while the opaque
1315        // `to` stop is still clamped to 0.3 (selection cap).
1316        let clear = try_parse_color("#3b82f600").unwrap();
1317        assert_eq!(
1318            theme.tokens.selection.background,
1319            linear_gradient(
1320                180.,
1321                linear_color_stop(clear.alpha(clear.a.min(0.3)), 0.),
1322                linear_color_stop(blue.alpha(0.3), 1.),
1323            )
1324        );
1325    }
1326}