theme_settings.rs

  1#![deny(missing_docs)]
  2
  3//! # Theme Settings
  4//!
  5//! This crate provides theme settings integration for Zed,
  6//! bridging the theme system with the settings infrastructure.
  7
  8mod schema;
  9mod settings;
 10
 11use std::sync::Arc;
 12
 13use ::settings::{IntoGpui, Settings, SettingsStore};
 14use anyhow::{Context as _, Result};
 15use gpui::{App, HighlightStyle, Refineable};
 16use gpui_util::ResultExt;
 17use theme::{
 18    AccentColors, Appearance, AppearanceContent, DEFAULT_DARK_THEME, DEFAULT_ICON_THEME_NAME,
 19    GlobalTheme, LoadThemes, PlayerColor, PlayerColors, StatusColors, SyntaxTheme,
 20    SystemAppearance, SystemColors, Theme, ThemeColors, ThemeFamily, ThemeRegistry, ThemeStyles,
 21    default_color_scales, try_parse_color,
 22};
 23
 24pub use crate::schema::{
 25    FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent,
 26    ThemeColorsContent, ThemeContent, ThemeFamilyContent, ThemeStyleContent,
 27    WindowBackgroundContent, status_colors_refinement, syntax_overrides, theme_colors_refinement,
 28};
 29pub use crate::settings::{
 30    AgentFontSize, BufferLineHeight, FontFamilyName, IconThemeName, IconThemeSelection,
 31    ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings, UiDensity,
 32    adjust_agent_buffer_font_size, adjust_agent_ui_font_size, adjust_buffer_font_size,
 33    adjust_ui_font_size, adjusted_font_size, appearance_to_mode, clamp_font_size, default_theme,
 34    observe_buffer_font_size_adjustment, reset_agent_buffer_font_size, reset_agent_ui_font_size,
 35    reset_buffer_font_size, reset_ui_font_size, set_icon_theme, set_mode, set_theme, setup_ui_font,
 36};
 37
 38/// Initialize the theme system with settings integration.
 39///
 40/// This is the full initialization for the application. It calls [`theme::init`]
 41/// and then wires up settings observation for theme/font changes.
 42pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
 43    let load_user_themes = matches!(&themes_to_load, LoadThemes::All(_));
 44
 45    theme::init(themes_to_load, cx);
 46
 47    if load_user_themes {
 48        let registry = ThemeRegistry::global(cx);
 49        load_bundled_themes(&registry);
 50    }
 51
 52    let theme = configured_theme(cx);
 53    let icon_theme = configured_icon_theme(cx);
 54    GlobalTheme::update_theme(cx, theme);
 55    GlobalTheme::update_icon_theme(cx, icon_theme);
 56
 57    let settings = ThemeSettings::get_global(cx);
 58
 59    let mut prev_buffer_font_size_settings = settings.buffer_font_size_settings();
 60    let mut prev_ui_font_size_settings = settings.ui_font_size_settings();
 61    let mut prev_agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
 62    let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
 63    let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0);
 64    let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
 65    let mut prev_theme_overrides = (
 66        settings.experimental_theme_overrides.clone(),
 67        settings.theme_overrides.clone(),
 68    );
 69
 70    cx.observe_global::<SettingsStore>(move |cx| {
 71        let settings = ThemeSettings::get_global(cx);
 72
 73        let buffer_font_size_settings = settings.buffer_font_size_settings();
 74        let ui_font_size_settings = settings.ui_font_size_settings();
 75        let agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
 76        let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
 77        let theme_name = settings.theme.name(SystemAppearance::global(cx).0);
 78        let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
 79        let theme_overrides = (
 80            settings.experimental_theme_overrides.clone(),
 81            settings.theme_overrides.clone(),
 82        );
 83
 84        if buffer_font_size_settings != prev_buffer_font_size_settings {
 85            prev_buffer_font_size_settings = buffer_font_size_settings;
 86            reset_buffer_font_size(cx);
 87        }
 88
 89        if ui_font_size_settings != prev_ui_font_size_settings {
 90            prev_ui_font_size_settings = ui_font_size_settings;
 91            reset_ui_font_size(cx);
 92        }
 93
 94        if agent_ui_font_size_settings != prev_agent_ui_font_size_settings {
 95            prev_agent_ui_font_size_settings = agent_ui_font_size_settings;
 96            reset_agent_ui_font_size(cx);
 97        }
 98
 99        if agent_buffer_font_size_settings != prev_agent_buffer_font_size_settings {
100            prev_agent_buffer_font_size_settings = agent_buffer_font_size_settings;
101            reset_agent_buffer_font_size(cx);
102        }
103
104        if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides {
105            prev_theme_name = theme_name;
106            prev_theme_overrides = theme_overrides;
107            reload_theme(cx);
108        }
109
110        if icon_theme_name != prev_icon_theme_name {
111            prev_icon_theme_name = icon_theme_name;
112            reload_icon_theme(cx);
113        }
114    })
115    .detach();
116}
117
118fn configured_theme(cx: &mut App) -> Arc<Theme> {
119    let themes = ThemeRegistry::default_global(cx);
120    let theme_settings = ThemeSettings::get_global(cx);
121    let system_appearance = SystemAppearance::global(cx);
122
123    let theme_name = theme_settings.theme.name(*system_appearance);
124
125    let theme = match themes.get(&theme_name.0) {
126        Ok(theme) => theme,
127        Err(err) => {
128            if themes.extensions_loaded() {
129                log::error!("{err}");
130            }
131            themes
132                .get(default_theme(*system_appearance))
133                .unwrap_or_else(|_| themes.get(DEFAULT_DARK_THEME).unwrap())
134        }
135    };
136    theme_settings.apply_theme_overrides(theme)
137}
138
139fn configured_icon_theme(cx: &mut App) -> Arc<theme::IconTheme> {
140    let themes = ThemeRegistry::default_global(cx);
141    let theme_settings = ThemeSettings::get_global(cx);
142    let system_appearance = SystemAppearance::global(cx);
143
144    let icon_theme_name = theme_settings.icon_theme.name(*system_appearance);
145
146    match themes.get_icon_theme(&icon_theme_name.0) {
147        Ok(theme) => theme,
148        Err(err) => {
149            if themes.extensions_loaded() {
150                log::error!("{err}");
151            }
152            themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()
153        }
154    }
155}
156
157/// Reloads the current theme from settings.
158pub fn reload_theme(cx: &mut App) {
159    let theme = configured_theme(cx);
160    GlobalTheme::update_theme(cx, theme);
161    cx.refresh_windows();
162}
163
164/// Reloads the current icon theme from settings.
165pub fn reload_icon_theme(cx: &mut App) {
166    let icon_theme = configured_icon_theme(cx);
167    GlobalTheme::update_icon_theme(cx, icon_theme);
168    cx.refresh_windows();
169}
170
171/// Loads the themes bundled with the Zed binary into the registry.
172pub fn load_bundled_themes(registry: &ThemeRegistry) {
173    let theme_paths = registry
174        .assets()
175        .list("themes/")
176        .expect("failed to list theme assets")
177        .into_iter()
178        .filter(|path| path.ends_with(".json"));
179
180    for path in theme_paths {
181        let Some(theme) = registry.assets().load(&path).log_err().flatten() else {
182            continue;
183        };
184
185        let Some(theme_family) = serde_json::from_slice(&theme)
186            .with_context(|| format!("failed to parse theme at path \"{path}\""))
187            .log_err()
188        else {
189            continue;
190        };
191
192        let refined = refine_theme_family(theme_family);
193        registry.insert_theme_families([refined]);
194    }
195}
196
197/// Loads a user theme from the given bytes into the registry.
198pub fn load_user_theme(registry: &ThemeRegistry, bytes: &[u8]) -> Result<()> {
199    let theme = deserialize_user_theme(bytes)?;
200    let refined = refine_theme_family(theme);
201    registry.insert_theme_families([refined]);
202    Ok(())
203}
204
205/// Deserializes a user theme from the given bytes.
206pub fn deserialize_user_theme(bytes: &[u8]) -> Result<ThemeFamilyContent> {
207    let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(bytes)?;
208
209    for theme in &theme_family.themes {
210        if theme
211            .style
212            .colors
213            .deprecated_scrollbar_thumb_background
214            .is_some()
215        {
216            log::warn!(
217                r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
218                theme_name = theme.name
219            )
220        }
221    }
222
223    Ok(theme_family)
224}
225
226/// Refines a [`ThemeFamilyContent`] and its [`ThemeContent`]s into a [`ThemeFamily`].
227pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFamily {
228    let id = uuid::Uuid::new_v4().to_string();
229    let name = theme_family_content.name.clone();
230    let author = theme_family_content.author.clone();
231
232    let themes: Vec<Theme> = theme_family_content
233        .themes
234        .iter()
235        .map(|theme_content| refine_theme(theme_content))
236        .collect();
237
238    ThemeFamily {
239        id,
240        name: name.into(),
241        author: author.into(),
242        themes,
243        scales: default_color_scales(),
244    }
245}
246
247/// Refines a [`ThemeContent`] into a [`Theme`].
248pub fn refine_theme(theme: &ThemeContent) -> Theme {
249    let appearance = match theme.appearance {
250        AppearanceContent::Light => Appearance::Light,
251        AppearanceContent::Dark => Appearance::Dark,
252    };
253
254    let mut refined_status_colors = match theme.appearance {
255        AppearanceContent::Light => StatusColors::light(),
256        AppearanceContent::Dark => StatusColors::dark(),
257    };
258    let mut status_colors_refinement = status_colors_refinement(&theme.style.status);
259    theme::apply_status_color_defaults(&mut status_colors_refinement);
260    refined_status_colors.refine(&status_colors_refinement);
261
262    let mut refined_player_colors = match theme.appearance {
263        AppearanceContent::Light => PlayerColors::light(),
264        AppearanceContent::Dark => PlayerColors::dark(),
265    };
266    merge_player_colors(&mut refined_player_colors, &theme.style.players);
267
268    let mut refined_theme_colors = match theme.appearance {
269        AppearanceContent::Light => ThemeColors::light(),
270        AppearanceContent::Dark => ThemeColors::dark(),
271    };
272    let mut theme_colors_refinement =
273        theme_colors_refinement(&theme.style.colors, &status_colors_refinement);
274    theme::apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors);
275    refined_theme_colors.refine(&theme_colors_refinement);
276
277    let mut refined_accent_colors = match theme.appearance {
278        AppearanceContent::Light => AccentColors::light(),
279        AppearanceContent::Dark => AccentColors::dark(),
280    };
281    merge_accent_colors(&mut refined_accent_colors, &theme.style.accents);
282
283    let syntax_highlights = theme.style.syntax.iter().map(|(syntax_token, highlight)| {
284        (
285            syntax_token.clone(),
286            HighlightStyle {
287                color: highlight
288                    .color
289                    .as_ref()
290                    .and_then(|color| try_parse_color(color).ok()),
291                background_color: highlight
292                    .background_color
293                    .as_ref()
294                    .and_then(|color| try_parse_color(color).ok()),
295                font_style: highlight.font_style.map(|s| s.into_gpui()),
296                font_weight: highlight.font_weight.map(|w| w.into_gpui()),
297                ..Default::default()
298            },
299        )
300    });
301    let syntax_theme = Arc::new(SyntaxTheme::new(syntax_highlights));
302
303    let window_background_appearance = theme
304        .style
305        .window_background_appearance
306        .map(|w| w.into_gpui())
307        .unwrap_or_default();
308
309    Theme {
310        id: uuid::Uuid::new_v4().to_string(),
311        name: theme.name.clone().into(),
312        appearance,
313        styles: ThemeStyles {
314            system: SystemColors::default(),
315            window_background_appearance,
316            accents: refined_accent_colors,
317            colors: refined_theme_colors,
318            status: refined_status_colors,
319            player: refined_player_colors,
320            syntax: syntax_theme,
321        },
322    }
323}
324
325/// Merges player color overrides into the given [`PlayerColors`].
326pub fn merge_player_colors(
327    player_colors: &mut PlayerColors,
328    user_player_colors: &[::settings::PlayerColorContent],
329) {
330    if user_player_colors.is_empty() {
331        return;
332    }
333
334    for (idx, player) in user_player_colors.iter().enumerate() {
335        let cursor = player
336            .cursor
337            .as_ref()
338            .and_then(|color| try_parse_color(color).ok());
339        let background = player
340            .background
341            .as_ref()
342            .and_then(|color| try_parse_color(color).ok());
343        let selection = player
344            .selection
345            .as_ref()
346            .and_then(|color| try_parse_color(color).ok());
347
348        if let Some(player_color) = player_colors.0.get_mut(idx) {
349            *player_color = PlayerColor {
350                cursor: cursor.unwrap_or(player_color.cursor),
351                background: background.unwrap_or(player_color.background),
352                selection: selection.unwrap_or(player_color.selection),
353            };
354        } else {
355            player_colors.0.push(PlayerColor {
356                cursor: cursor.unwrap_or_default(),
357                background: background.unwrap_or_default(),
358                selection: selection.unwrap_or_default(),
359            });
360        }
361    }
362}
363
364/// Merges accent color overrides into the given [`AccentColors`].
365pub fn merge_accent_colors(
366    accent_colors: &mut AccentColors,
367    user_accent_colors: &[::settings::AccentContent],
368) {
369    if user_accent_colors.is_empty() {
370        return;
371    }
372
373    let colors = user_accent_colors
374        .iter()
375        .filter_map(|accent_color| {
376            accent_color
377                .0
378                .as_ref()
379                .and_then(|color| try_parse_color(color).ok())
380        })
381        .collect::<Vec<_>>();
382
383    if !colors.is_empty() {
384        accent_colors.0 = Arc::from(colors);
385    }
386}