From bc6095f2c0addabc37e2e8cb761adbe240d031ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D0=B8=D0=BD=D0=B0?= Date: Sun, 16 Aug 2026 11:41:17 +0000 Subject: [PATCH] editor: Fix buffer header context menu line height in multibuffers (#61923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: A context menu elsewhere in the app,
showing normal item spacing Before: Buffer header context menu before the
fix, with tighter item spacing following the custom buffer_line_height After: Buffer header context menu after the
fix, with item spacing matching other context menus --- 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 Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- crates/editor/src/element/mouse.rs | 49 +++++++++------------ crates/settings_ui/src/page_data.rs | 6 ++- crates/theme/src/buffer_line_height.rs | 22 +++++++++ crates/theme/src/theme.rs | 2 + crates/theme_settings/src/settings.rs | 41 ++++------------- crates/theme_settings/src/theme_settings.rs | 8 ++-- crates/ui/src/components/context_menu.rs | 9 +++- 7 files changed, 69 insertions(+), 68 deletions(-) create mode 100644 crates/theme/src/buffer_line_height.rs diff --git a/crates/editor/src/element/mouse.rs b/crates/editor/src/element/mouse.rs index a5fc7b969ab..7db3f77f5bf 100644 --- a/crates/editor/src/element/mouse.rs +++ b/crates/editor/src/element/mouse.rs @@ -4,17 +4,16 @@ use std::time::{Duration, Instant}; use collections::HashMap; use feature_flags::{DiffReviewFeatureFlag, FeatureFlagAppExt as _}; use gpui::{ - AnyElement, App, AvailableSpace, ClickEvent, Context, DefiniteLength, DispatchPhase, Element, - MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, - ParentElement, Pixels, PressureStage, ScrollDelta, ScrollWheelEvent, TextStyleRefinement, - Window, anchored, deferred, point, px, + AnyElement, App, AvailableSpace, ClickEvent, Context, DispatchPhase, Element, MouseButton, + MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, + ParentElement, Pixels, PressureStage, ScrollDelta, ScrollWheelEvent, Window, anchored, + deferred, point, px, }; use multi_buffer::MultiBufferRow; use project::DisableAiSettings; use settings::Settings; use sum_tree::Bias; use text::SelectionGoal; -use theme_settings::BufferLineHeight; use util::{RangeExt, debug_panic, post_inc}; use super::{EditorElement, EditorLayout, LineNumberLayout, PositionMap, SplitSide}; @@ -322,33 +321,25 @@ impl EditorElement { } })?; - let text_style = TextStyleRefinement { - line_height: Some(DefiniteLength::Fraction( - BufferLineHeight::Comfortable.value(), - )), - ..Default::default() - }; - window.with_text_style(Some(text_style), |window| { - let mut element = self.editor.read_with(cx, |editor, _| { - let mouse_context_menu = editor.mouse_context_menu.as_ref()?; - let context_menu = mouse_context_menu.context_menu.clone(); + let mut element = self.editor.read_with(cx, |editor, _| { + let mouse_context_menu = editor.mouse_context_menu.as_ref()?; + let context_menu = mouse_context_menu.context_menu.clone(); - Some( - deferred( - anchored() - .position(position) - .child(context_menu) - .anchor(gpui::Anchor::TopLeft) - .snap_to_window_with_margin(px(8.)), - ) - .with_priority(1) - .into_any(), + Some( + deferred( + anchored() + .position(position) + .child(context_menu) + .anchor(gpui::Anchor::TopLeft) + .snap_to_window_with_margin(px(8.)), ) - })?; + .with_priority(1) + .into_any(), + ) + })?; - element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx); - Some(element) - }) + element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx); + Some(element) } pub(super) fn paint_mouse_listeners( diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 3253cc71f14..9b24982dfbc 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -952,8 +952,10 @@ fn appearance_page() -> SettingsPage { } settings::BufferLineHeightDiscriminants::Custom => { let custom_value = - theme_settings::BufferLineHeight::from(*settings_value) - .value(); + theme_settings::buffer_line_height_from_settings( + *settings_value, + ) + .value(); settings::BufferLineHeight::Custom(custom_value) } }; diff --git a/crates/theme/src/buffer_line_height.rs b/crates/theme/src/buffer_line_height.rs new file mode 100644 index 00000000000..3a1c466307a --- /dev/null +++ b/crates/theme/src/buffer_line_height.rs @@ -0,0 +1,22 @@ +/// The buffer's line height. +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub enum BufferLineHeight { + /// A less dense line height. + #[default] + Comfortable, + /// The default line height. + Standard, + /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. + Custom(f32), +} + +impl BufferLineHeight { + /// Returns the value of the line height. + pub fn value(&self) -> f32 { + match self { + BufferLineHeight::Comfortable => 1.618, + BufferLineHeight::Standard => 1.3, + BufferLineHeight::Custom(line_height) => *line_height, + } + } +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b973d8f25c1..a23bedd504f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -8,6 +8,7 @@ //! //! A theme is a collection of colors used to build a consistent appearance for UI components across the application. +mod buffer_line_height; mod color_space; mod default_colors; mod fallback_themes; @@ -31,6 +32,7 @@ use gpui::{ }; use serde::Deserialize; +pub use crate::buffer_line_height::*; pub use crate::color_space::*; pub use crate::default_colors::*; pub use crate::fallback_themes::{apply_status_color_defaults, apply_theme_color_defaults}; diff --git a/crates/theme_settings/src/settings.rs b/crates/theme_settings/src/settings.rs index 68812368d9b..d9a3270ef32 100644 --- a/crates/theme_settings/src/settings.rs +++ b/crates/theme_settings/src/settings.rs @@ -359,38 +359,13 @@ pub fn set_mode(content: &mut SettingsContent, mode: ThemeAppearanceMode) { } } -/// The buffer's line height. -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum BufferLineHeight { - /// A less dense line height. - #[default] - Comfortable, - /// The default line height. - Standard, - /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. - Custom(f32), -} +pub use theme::BufferLineHeight; -impl From for BufferLineHeight { - fn from(value: settings::BufferLineHeight) -> Self { - match value { - settings::BufferLineHeight::Comfortable => BufferLineHeight::Comfortable, - settings::BufferLineHeight::Standard => BufferLineHeight::Standard, - settings::BufferLineHeight::Custom(line_height) => { - BufferLineHeight::Custom(line_height) - } - } - } -} - -impl BufferLineHeight { - /// Returns the value of the line height. - pub fn value(&self) -> f32 { - match self { - BufferLineHeight::Comfortable => 1.618, - BufferLineHeight::Standard => 1.3, - BufferLineHeight::Custom(line_height) => *line_height, - } +pub fn buffer_line_height_from_settings(value: settings::BufferLineHeight) -> BufferLineHeight { + match value { + settings::BufferLineHeight::Comfortable => BufferLineHeight::Comfortable, + settings::BufferLineHeight::Standard => BufferLineHeight::Standard, + settings::BufferLineHeight::Custom(line_height) => BufferLineHeight::Custom(line_height), } } @@ -754,7 +729,9 @@ impl settings::Settings for ThemeSettings { style: FontStyle::default(), }, buffer_font_size: clamp_font_size(content.buffer_font_size.unwrap().into_gpui()), - buffer_line_height: content.buffer_line_height.unwrap().into(), + buffer_line_height: buffer_line_height_from_settings( + content.buffer_line_height.unwrap(), + ), agent_ui_font_family: content .agent_ui_font_family .as_ref() diff --git a/crates/theme_settings/src/theme_settings.rs b/crates/theme_settings/src/theme_settings.rs index ff47093c0cb..f56a46b4050 100644 --- a/crates/theme_settings/src/theme_settings.rs +++ b/crates/theme_settings/src/theme_settings.rs @@ -33,10 +33,10 @@ pub use crate::settings::{ 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, - 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, + 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; diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 7b8f734ce4a..cea25ab5e73 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -5,7 +5,7 @@ use crate::{ use gpui::{ Action, Anchor, AnyElement, App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Role, - Size, Subscription, TaskExt, anchored, canvas, prelude::*, px, + Size, Subscription, TaskExt, anchored, canvas, prelude::*, px, relative, }; use menu::{SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious}; use std::{ @@ -14,6 +14,7 @@ use std::{ rc::Rc, time::{Duration, Instant}, }; +use theme::BufferLineHeight; #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum SubmenuOpenTrigger { @@ -2194,6 +2195,10 @@ impl Render for ContextMenu { let theme_settings = theme::theme_settings(cx); let ui_font_size = theme_settings.ui_font_size(cx); let ui_font_family = theme_settings.ui_font(cx).family.clone(); + // Menus can be deferred from inside elements that override the text + // style (e.g. the editor with a custom `buffer_line_height`), so always + // apply the default line height to render the same everywhere. + let line_height = relative(BufferLineHeight::Comfortable.value()); let window_size = window.viewport_size(); let rem_size = window.rem_size(); let is_wide_window = window_size.width / rem_size > rems_from_px(800_f32).0; @@ -2244,6 +2249,7 @@ impl Render for ContextMenu { WithRemSize::new(ui_font_size) .occlude() .font_family(ui_font_family.clone()) + .line_height(line_height) .elevation_2(cx) .w_full() .p_2() @@ -2271,6 +2277,7 @@ impl Render for ContextMenu { WithRemSize::new(ui_font_size) .occlude() .font_family(ui_font_family.clone()) + .line_height(line_height) .elevation_2(cx) .flex() .flex_row()