## Objective Right-clicking a buffer header in a multibuffer showed a context menu whose item spacing did not match other context menus. The menu is drawn via a deferred draw, which inherits the editor's text style stack, so its line height followed `buffer_line_height` instead of the default UI line height. This is the same root cause as #24504, which #25172 fixed for the editor's mouse context menu and completion popovers. The buffer header menu was a remaining call site. ## Solution `ContextMenu` now applies the default (`comfortable`) line height itself in `render`, next to its existing rem size and font family normalization, so menus render the same regardless of where they are opened from. This fixes the all workarounds and the need for them too: the workaround in `layout_mouse_context_menu` is removed, and the buffer header ends up needing no changes at all. The completions and code actions popovers keep their override in `element.rs`: they are not `ui::ContextMenu`s and compute their sizes with eager `window.line_height()` reads while being built, so styling on the elements they return cannot cover them. Migrating them could be a follow-up. ## Testing - `cargo check --workspace` passes, `./script/clippy` on the touched crates passes. - Manual: set `"buffer_line_height": "standard"` (or any extreme custom value to see it better), open a multibuffer (e.g. project search), right-click a buffer header, and compare the menu with another context menu (e.g. a tab's) — spacing matches. The editor's mouse context menu, which lost its own override, renders as before, and with default settings there is no visual change anywhere. - Tested on macOS, change is platform-independent styling. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [ ] Tests cover the new/changed behavior (visual styling fix; menu text styles have no existing test coverage) - [x] Performance impact has been considered and is acceptable ## Showcase Regular context menu: <img width="425" height="164" alt="A context menu elsewhere in the app, showing normal item spacing" src="https://github.com/user-attachments/assets/1fc13981-b394-444f-883a-ce3ef462c386" /> Before: <img width="425" height="164" alt="Buffer header context menu before the fix, with tighter item spacing following the custom buffer_line_height" src="https://github.com/user-attachments/assets/9774076c-7e0c-41dd-b54f-53de58b24336" /> After: <img width="425" height="164" alt="Buffer header context menu after the fix, with item spacing matching other context menus" src="https://github.com/user-attachments/assets/ae76e37c-8e39-474c-8a5e-e229e69173fb" /> --- Release Notes: - Fixed the file header context menu in multibuffers not matching other context menus' spacing when a custom `buffer_line_height` is set. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
448 lines
16 KiB
Rust
448 lines
16 KiB
Rust
#![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::<SettingsStore>(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<Theme> {
|
|
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<theme::IconTheme> {
|
|
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<ThemeFamilyContent> {
|
|
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> = 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::<Vec<_>>();
|
|
|
|
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));
|
|
}
|