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