From 70e690effd612ca9e17af24f3c8cf6bb9950cb8d Mon Sep 17 00:00:00 2001 From: Xiaobo Liu Date: Fri, 12 Jun 2026 23:37:55 +0800 Subject: [PATCH] editor: Align split buffer headers with scrollbar layout (#53782) Clip split buffer headers to the editor's content bounds so they no longer paint into the horizontal scrollbar area. Reuse the editor's computed right margin and horizontal scrollbar visibility instead of reimplementing scrollbar detection in split_editor_view. This keeps split headers aligned when minimap, inline blame, or scrollbar visibility change the available content area. Release Notes: - Fixed horizontal scrollbars not being rendered behind buffer headers in split editors --------- Signed-off-by: Xiaobo Liu Co-authored-by: dino --- crates/editor/src/editor.rs | 20 +++++ crates/editor/src/element.rs | 9 ++- crates/editor/src/split_editor_view.rs | 108 +++++++++++++++++-------- 3 files changed, 103 insertions(+), 34 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 862b7aaf6e..c91f92f563 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1096,6 +1096,16 @@ pub struct Editor { >, last_bounds: Option>, last_position_map: Option>, + /// The right margin (vertical scrollbar + minimap width) the editor was + /// last laid out with, updated on every prepaint. + /// Used later in the frame by `SplitBufferHeadersElement` to shrink the + /// width available to buffer headers. + last_right_margin: Pixels, + /// Whether the horizontal scrollbar was laid out as visible during the last + /// prepaint. + /// Used by `SplitBufferHeadersElement` to clip buffer headers so they don't + /// paint over the scrollbar. + last_horizontal_scrollbar_visible: bool, expect_bounds_change: Option>, runnables: RunnableData, bookmark_store: Option>, @@ -2264,6 +2274,8 @@ impl Editor { pixel_position_of_newest_cursor: None, last_bounds: None, last_position_map: None, + last_right_margin: Pixels::ZERO, + last_horizontal_scrollbar_visible: false, expect_bounds_change: None, gutter_dimensions: GutterDimensions::default(), style: None, @@ -2674,6 +2686,14 @@ impl Editor { self.last_bounds.as_ref() } + pub(crate) fn last_right_margin(&self) -> Pixels { + self.last_right_margin + } + + pub(crate) fn last_horizontal_scrollbar_visible(&self) -> bool { + self.last_horizontal_scrollbar_visible + } + pub fn working_directory(&self, cx: &App) -> Option { if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ec693e96f0..565852215a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -9276,8 +9276,15 @@ impl Element for EditorElement { diff_hunk_control_bounds, }); + let visible_horizontal_scrollbar = + scrollbars_layout.as_ref().is_some_and(|scrollbars_layout| { + scrollbars_layout.visible && scrollbars_layout.horizontal.is_some() + }); + self.editor.update(cx, |editor, _| { - editor.last_position_map = Some(position_map.clone()) + editor.last_position_map = Some(position_map.clone()); + editor.last_right_margin = right_margin; + editor.last_horizontal_scrollbar_visible = visible_horizontal_scrollbar; }); EditorLayout { diff --git a/crates/editor/src/split_editor_view.rs b/crates/editor/src/split_editor_view.rs index 468a2bf055..2dee964f35 100644 --- a/crates/editor/src/split_editor_view.rs +++ b/crates/editor/src/split_editor_view.rs @@ -8,17 +8,15 @@ use gpui::{ linear_color_stop, linear_gradient, point, px, size, }; use multi_buffer::{Anchor, ExcerptBoundaryInfo}; -use settings::Settings; use smallvec::smallvec; use text::BufferId; use theme::ActiveTheme; -use ui::scrollbars::ShowScrollbar; use ui::{h_flex, prelude::*, v_flex}; use gpui::ContentMask; use crate::{ - DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, + DisplayRow, Editor, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, RowExt, StickyHeaderExcerpt, display_map::Block, element::{EditorElement, SplitSide, header_jump_data, render_buffer_header}, @@ -172,7 +170,11 @@ impl RenderOnce for SplitEditorView { let state_for_drag = self.split_state.downgrade(); let state_for_drop = self.split_state.downgrade(); - let buffer_headers = SplitBufferHeadersElement::new(rhs_editor.clone(), self.style.clone()); + let buffer_headers = SplitBufferHeadersElement::new( + lhs_editor.clone(), + rhs_editor.clone(), + self.style.clone(), + ); let lhs_editor_for_order = lhs_editor; let rhs_editor_for_order = rhs_editor; @@ -234,13 +236,18 @@ impl RenderOnce for SplitEditorView { } struct SplitBufferHeadersElement { - editor: Entity, + lhs_editor: Entity, + rhs_editor: Entity, style: EditorStyle, } impl SplitBufferHeadersElement { - fn new(editor: Entity, style: EditorStyle) -> Self { - Self { editor, style } + fn new(lhs_editor: Entity, rhs_editor: Entity, style: EditorStyle) -> Self { + Self { + lhs_editor, + rhs_editor, + style, + } } } @@ -249,6 +256,7 @@ struct BufferHeaderLayout { } struct SplitBufferHeadersPrepaintState { + content_bounds: Bounds, sticky_header: Option, non_sticky_headers: Vec, } @@ -301,6 +309,7 @@ impl Element for SplitBufferHeadersElement { ) -> Self::PrepaintState { if bounds.size.width <= px(0.) || bounds.size.height <= px(0.) { return SplitBufferHeadersPrepaintState { + content_bounds: bounds, sticky_header: None, non_sticky_headers: Vec::new(), }; @@ -324,7 +333,7 @@ impl Element for SplitBufferHeadersElement { &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, + _bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, @@ -339,15 +348,20 @@ impl Element for SplitBufferHeadersElement { window.with_rem_size(rem_size, |window| { window.with_text_style(Some(text_style), |window| { - window.with_content_mask(Some(ContentMask { bounds }), |window| { - for header_layout in &mut prepaint.non_sticky_headers { - header_layout.element.paint(window, cx); - } + window.with_content_mask( + Some(ContentMask { + bounds: prepaint.content_bounds, + }), + |window| { + for header_layout in &mut prepaint.non_sticky_headers { + header_layout.element.paint(window, cx); + } - if let Some(mut sticky_header) = prepaint.sticky_header.take() { - sticky_header.paint(window, cx); - } - }); + if let Some(mut sticky_header) = prepaint.sticky_header.take() { + sticky_header.paint(window, cx); + } + }, + ); }); }); } @@ -378,21 +392,17 @@ impl SplitBufferHeadersElement { let line_height = window.line_height(); let snapshot = self - .editor + .rhs_editor .update(cx, |editor, cx| editor.snapshot(window, cx)); let scroll_position = snapshot.scroll_position(); // Compute right margin to avoid overlapping the scrollbar - let settings = EditorSettings::get_global(cx); - let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never; - let vertical_scrollbar_width = (scrollbars_shown - && settings.scrollbar.axes.vertical - && self.editor.read(cx).show_scrollbars.vertical) - .then_some(EditorElement::SCROLLBAR_WIDTH) - .unwrap_or_default(); - let available_width = bounds.size.width - vertical_scrollbar_width; + let content_bounds = self.content_bounds(bounds, cx); + let available_width = (content_bounds.size.width + - self.rhs_editor.read(cx).last_right_margin()) + .max(Pixels::ZERO); - let visible_height_in_lines = bounds.size.height / line_height; + let visible_height_in_lines = content_bounds.size.height / line_height; let max_row = snapshot.max_point().row(); let start_row = cmp::min(DisplayRow(scroll_position.y.floor() as u32), max_row); let end_row = cmp::min( @@ -412,7 +422,7 @@ impl SplitBufferHeadersElement { sticky_excerpt, &snapshot, scroll_position, - bounds, + content_bounds, available_width, line_height, &selected_buffer_ids, @@ -434,7 +444,7 @@ impl SplitBufferHeadersElement { let non_sticky_headers = self.build_non_sticky_headers( &snapshot, scroll_position, - bounds, + content_bounds, available_width, line_height, start_row, @@ -447,17 +457,36 @@ impl SplitBufferHeadersElement { ); SplitBufferHeadersPrepaintState { + content_bounds, sticky_header, non_sticky_headers, } } + fn content_bounds(&self, bounds: Bounds, cx: &App) -> Bounds { + // Left hand side and right hand side horizontal scrollbars are + // independent, so we clip the bottom if either is visible. + let horizontal_scrollbar_height = + (self.lhs_editor.read(cx).last_horizontal_scrollbar_visible() + || self.rhs_editor.read(cx).last_horizontal_scrollbar_visible()) + .then_some(self.style.scrollbar_width) + .unwrap_or(Pixels::ZERO); + + Bounds::new( + bounds.origin, + size( + bounds.size.width, + (bounds.size.height - horizontal_scrollbar_height).max(Pixels::ZERO), + ), + ) + } + fn compute_selection_info( &self, snapshot: &EditorSnapshot, cx: &App, ) -> (HashSet, HashMap) { - let editor = self.editor.read(cx); + let editor = self.rhs_editor.read(cx); let all_selections = editor .selections .all::(&snapshot.display_snapshot); @@ -541,7 +570,7 @@ impl SplitBufferHeadersElement { ) .child( render_buffer_header( - &self.editor, + &self.rhs_editor, excerpt, false, selected, @@ -579,7 +608,7 @@ impl SplitBufferHeadersElement { AvailableSpace::MinContent, ); - header.prepaint_as_root(origin, available_size, window, cx); + Self::prepaint_header(&mut header, origin, available_size, bounds, window, cx); header } @@ -624,7 +653,7 @@ impl SplitBufferHeadersElement { ); let mut header = render_buffer_header( - &self.editor, + &self.rhs_editor, excerpt, is_folded, selected, @@ -643,11 +672,24 @@ impl SplitBufferHeadersElement { AvailableSpace::MinContent, ); - header.prepaint_as_root(origin, available_size, window, cx); + Self::prepaint_header(&mut header, origin, available_size, bounds, window, cx); headers.push(BufferHeaderLayout { element: header }); } headers } + + fn prepaint_header( + header: &mut AnyElement, + origin: gpui::Point, + available_size: gpui::Size, + bounds: Bounds, + window: &mut Window, + cx: &mut App, + ) { + window.with_content_mask(Some(ContentMask { bounds }), |window| { + header.prepaint_as_root(origin, available_size, window, cx); + }); + } }