Skip to main content

gpui_component/theme/
schema.rs

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