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