#![deny(missing_docs)] //! # Theme Settings //! //! This crate provides theme settings integration for Zed, //! bridging the theme system with the settings infrastructure. mod schema; mod settings; use std::sync::Arc; use ::settings::{IntoGpui, Settings, SettingsStore}; use anyhow::{Context as _, Result}; use gpui::{App, Font, HighlightStyle, Pixels, Refineable, px}; use gpui_util::ResultExt; use theme::{ AccentColors, Appearance, AppearanceContent, DEFAULT_DARK_THEME, DEFAULT_ICON_THEME_NAME, GlobalTheme, LoadThemes, PlayerColor, PlayerColors, StatusColors, SyntaxTheme, SystemAppearance, SystemColors, Theme, ThemeColors, ThemeFamily, ThemeRegistry, ThemeSettingsProvider, ThemeStyles, default_color_scales, try_parse_color, }; pub use crate::schema::{ FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, ThemeContent, ThemeFamilyContent, ThemeStyleContent, WindowBackgroundContent, status_colors_refinement, syntax_overrides, theme_colors_refinement, }; use crate::settings::adjust_buffer_font_size; pub use crate::settings::{ AgentBufferFontSize, AgentUiFontSize, BufferLineHeight, FontFamilyName, GitCommitBufferFontSize, IconThemeName, IconThemeSelection, MarkdownPreviewFontSize, ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings, adjust_agent_buffer_font_size, adjust_agent_ui_font_size, adjust_git_commit_buffer_font_size, adjust_markdown_preview_font_size, adjust_ui_font_size, adjusted_font_size, appearance_to_mode, buffer_line_height_from_settings, clamp_font_size, default_theme, observe_buffer_font_size_adjustment, reset_agent_buffer_font_size, reset_agent_ui_font_size, reset_buffer_font_size, reset_git_commit_buffer_font_size, reset_markdown_preview_font_size, reset_ui_font_size, set_icon_theme, set_mode, set_theme, setup_ui_font, }; pub use theme::UiDensity; struct ThemeSettingsProviderImpl; impl ThemeSettingsProvider for ThemeSettingsProviderImpl { fn ui_font<'a>(&'a self, cx: &'a App) -> &'a Font { &ThemeSettings::get_global(cx).ui_font } fn buffer_font<'a>(&'a self, cx: &'a App) -> &'a Font { &ThemeSettings::get_global(cx).buffer_font } fn ui_font_size(&self, cx: &App) -> Pixels { ThemeSettings::get_global(cx).ui_font_size(cx) } fn buffer_font_size(&self, cx: &App) -> Pixels { ThemeSettings::get_global(cx).buffer_font_size(cx) } fn ui_density(&self, cx: &App) -> UiDensity { ThemeSettings::get_global(cx).ui_density } } /// Initialize the theme system with settings integration. /// /// This is the full initialization for the application. It calls [`theme::init`] /// and then wires up settings observation for theme/font changes. pub fn init(themes_to_load: LoadThemes, cx: &mut App) { let load_user_themes = matches!(&themes_to_load, LoadThemes::All(_)); theme::init(themes_to_load, cx); theme::set_theme_settings_provider(Box::new(ThemeSettingsProviderImpl), cx); if load_user_themes { let registry = ThemeRegistry::global(cx); load_bundled_themes(®istry); } let theme = configured_theme(cx); let icon_theme = configured_icon_theme(cx); GlobalTheme::update_theme(cx, theme); GlobalTheme::update_icon_theme(cx, icon_theme); let settings = ThemeSettings::get_global(cx); let mut prev_buffer_font_size_settings = settings.buffer_font_size_settings(); let mut prev_ui_font_size_settings = settings.ui_font_size_settings(); let mut prev_agent_ui_font_size_settings = settings.agent_ui_font_size_settings(); let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings(); let mut prev_git_commit_buffer_font_size_settings = settings.git_commit_buffer_font_size_settings(); let mut prev_markdown_preview_font_size_settings = settings.markdown_preview_font_size_settings(); let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0); let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0); let mut prev_theme_overrides = ( settings.experimental_theme_overrides.clone(), settings.theme_overrides.clone(), ); cx.observe_global::(move |cx| { let settings = ThemeSettings::get_global(cx); let buffer_font_size_settings = settings.buffer_font_size_settings(); let ui_font_size_settings = settings.ui_font_size_settings(); let agent_ui_font_size_settings = settings.agent_ui_font_size_settings(); let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings(); let git_commit_buffer_font_size_settings = settings.git_commit_buffer_font_size_settings(); let markdown_preview_font_size_settings = settings.markdown_preview_font_size_settings(); let theme_name = settings.theme.name(SystemAppearance::global(cx).0); let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0); let theme_overrides = ( settings.experimental_theme_overrides.clone(), settings.theme_overrides.clone(), ); if buffer_font_size_settings != prev_buffer_font_size_settings { prev_buffer_font_size_settings = buffer_font_size_settings; reset_buffer_font_size(cx); } if ui_font_size_settings != prev_ui_font_size_settings { prev_ui_font_size_settings = ui_font_size_settings; reset_ui_font_size(cx); } if agent_ui_font_size_settings != prev_agent_ui_font_size_settings { prev_agent_ui_font_size_settings = agent_ui_font_size_settings; reset_agent_ui_font_size(cx); } if agent_buffer_font_size_settings != prev_agent_buffer_font_size_settings { prev_agent_buffer_font_size_settings = agent_buffer_font_size_settings; reset_agent_buffer_font_size(cx); } if git_commit_buffer_font_size_settings != prev_git_commit_buffer_font_size_settings { prev_git_commit_buffer_font_size_settings = git_commit_buffer_font_size_settings; reset_git_commit_buffer_font_size(cx); } if markdown_preview_font_size_settings != prev_markdown_preview_font_size_settings { prev_markdown_preview_font_size_settings = markdown_preview_font_size_settings; reset_markdown_preview_font_size(cx); } if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides { prev_theme_name = theme_name; prev_theme_overrides = theme_overrides; reload_theme(cx); } if icon_theme_name != prev_icon_theme_name { prev_icon_theme_name = icon_theme_name; reload_icon_theme(cx); } }) .detach(); } fn configured_theme(cx: &mut App) -> Arc { let themes = ThemeRegistry::default_global(cx); let theme_settings = ThemeSettings::get_global(cx); let system_appearance = SystemAppearance::global(cx); let theme_name = theme_settings.theme.name(*system_appearance); let theme = match themes.get(&theme_name.0) { Ok(theme) => theme, Err(err) => { if themes.extensions_loaded() { log::error!("{err}"); } themes .get(default_theme(*system_appearance)) .unwrap_or_else(|_| themes.get(DEFAULT_DARK_THEME).unwrap()) } }; theme_settings.apply_theme_overrides(theme) } fn configured_icon_theme(cx: &mut App) -> Arc { let themes = ThemeRegistry::default_global(cx); let theme_settings = ThemeSettings::get_global(cx); let system_appearance = SystemAppearance::global(cx); let icon_theme_name = theme_settings.icon_theme.name(*system_appearance); match themes.get_icon_theme(&icon_theme_name.0) { Ok(theme) => theme, Err(err) => { if themes.extensions_loaded() { log::error!("{err}"); } themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap() } } } /// Reloads the current theme from settings. pub fn reload_theme(cx: &mut App) { let theme = configured_theme(cx); GlobalTheme::update_theme(cx, theme); cx.refresh_windows(); } /// Reloads the current icon theme from settings. pub fn reload_icon_theme(cx: &mut App) { let icon_theme = configured_icon_theme(cx); GlobalTheme::update_icon_theme(cx, icon_theme); cx.refresh_windows(); } /// Loads the themes bundled with the Zed binary into the registry. pub fn load_bundled_themes(registry: &ThemeRegistry) { let theme_paths = registry .assets() .list("themes/") .expect("failed to list theme assets") .into_iter() .filter(|path| path.ends_with(".json")); for path in theme_paths { let Some(theme) = registry.assets().load(&path).log_err().flatten() else { continue; }; let Some(theme_family) = serde_json::from_slice(&theme) .with_context(|| format!("failed to parse theme at path \"{path}\"")) .log_err() else { continue; }; let refined = refine_theme_family(theme_family); registry.insert_theme_families([refined]); } } /// Loads a user theme from the given bytes into the registry. pub fn load_user_theme(registry: &ThemeRegistry, bytes: &[u8]) -> Result<()> { let theme = deserialize_user_theme(bytes)?; let refined = refine_theme_family(theme); registry.insert_theme_families([refined]); Ok(()) } /// Deserializes a user theme from the given bytes. pub fn deserialize_user_theme(bytes: &[u8]) -> Result { let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(bytes)?; for theme in &theme_family.themes { if theme .style .colors .deprecated_scrollbar_thumb_background .is_some() { log::warn!( r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, theme_name = theme.name ) } } Ok(theme_family) } /// Refines a [`ThemeFamilyContent`] and its [`ThemeContent`]s into a [`ThemeFamily`]. pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFamily { let id = uuid::Uuid::new_v4().to_string(); let name = theme_family_content.name.clone(); let author = theme_family_content.author.clone(); let themes: Vec = theme_family_content .themes .iter() .map(|theme_content| refine_theme(theme_content)) .collect(); ThemeFamily { id, name: name.into(), author: author.into(), themes, scales: default_color_scales(), } } /// Refines a [`ThemeContent`] into a [`Theme`]. pub fn refine_theme(theme: &ThemeContent) -> Theme { let appearance = match theme.appearance { AppearanceContent::Light => Appearance::Light, AppearanceContent::Dark => Appearance::Dark, }; let mut refined_status_colors = match theme.appearance { AppearanceContent::Light => StatusColors::light(), AppearanceContent::Dark => StatusColors::dark(), }; let mut status_colors_refinement = status_colors_refinement(&theme.style.status); theme::apply_status_color_defaults(&mut status_colors_refinement); refined_status_colors.refine(&status_colors_refinement); let mut refined_player_colors = match theme.appearance { AppearanceContent::Light => PlayerColors::light(), AppearanceContent::Dark => PlayerColors::dark(), }; merge_player_colors(&mut refined_player_colors, &theme.style.players); let mut refined_theme_colors = match theme.appearance { AppearanceContent::Light => ThemeColors::light(), AppearanceContent::Dark => ThemeColors::dark(), }; let mut theme_colors_refinement = theme_colors_refinement( &theme.style.colors, &status_colors_refinement, theme.appearance == AppearanceContent::Light, ); theme::apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors); refined_theme_colors.refine(&theme_colors_refinement); let mut refined_accent_colors = match theme.appearance { AppearanceContent::Light => AccentColors::light(), AppearanceContent::Dark => AccentColors::dark(), }; merge_accent_colors(&mut refined_accent_colors, &theme.style.accents); let syntax_highlights = theme.style.syntax.iter().map(|(syntax_token, highlight)| { ( syntax_token.clone(), HighlightStyle { color: highlight .color .as_ref() .and_then(|color| try_parse_color(color).ok()), background_color: highlight .background_color .as_ref() .and_then(|color| try_parse_color(color).ok()), font_style: highlight.font_style.map(|s| s.into_gpui()), font_weight: highlight.font_weight.map(|w| w.into_gpui()), ..Default::default() }, ) }); let syntax_theme = Arc::new(SyntaxTheme::new(syntax_highlights)); let window_background_appearance = theme .style .window_background_appearance .map(|w| w.into_gpui()) .unwrap_or_default(); Theme { id: uuid::Uuid::new_v4().to_string(), name: theme.name.clone().into(), appearance, styles: ThemeStyles { system: SystemColors::default(), window_background_appearance, accents: refined_accent_colors, colors: refined_theme_colors, status: refined_status_colors, player: refined_player_colors, syntax: syntax_theme, }, } } /// Merges player color overrides into the given [`PlayerColors`]. pub fn merge_player_colors( player_colors: &mut PlayerColors, user_player_colors: &[::settings::PlayerColorContent], ) { if user_player_colors.is_empty() { return; } for (idx, player) in user_player_colors.iter().enumerate() { let cursor = player .cursor .as_ref() .and_then(|color| try_parse_color(color).ok()); let background = player .background .as_ref() .and_then(|color| try_parse_color(color).ok()); let selection = player .selection .as_ref() .and_then(|color| try_parse_color(color).ok()); if let Some(player_color) = player_colors.0.get_mut(idx) { *player_color = PlayerColor { cursor: cursor.unwrap_or(player_color.cursor), background: background.unwrap_or(player_color.background), selection: selection.unwrap_or(player_color.selection), }; } else { player_colors.0.push(PlayerColor { cursor: cursor.unwrap_or_default(), background: background.unwrap_or_default(), selection: selection.unwrap_or_default(), }); } } } /// Merges accent color overrides into the given [`AccentColors`]. pub fn merge_accent_colors( accent_colors: &mut AccentColors, user_accent_colors: &[::settings::AccentContent], ) { if user_accent_colors.is_empty() { return; } let colors = user_accent_colors .iter() .filter_map(|accent_color| { accent_color .0 .as_ref() .and_then(|color| try_parse_color(color).ok()) }) .collect::>(); if !colors.is_empty() { accent_colors.0 = Arc::from(colors); } } /// Increases the buffer font size by 1 pixel, without persisting the result in the settings. /// This will be effective until the app is restarted. pub fn increase_buffer_font_size(cx: &mut App) { adjust_buffer_font_size(cx, |size| size + px(1.0)); } /// Decreases the buffer font size by 1 pixel, without persisting the result in the settings. /// This will be effective until the app is restarted. pub fn decrease_buffer_font_size(cx: &mut App) { adjust_buffer_font_size(cx, |size| size - px(1.0)); }