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