From 1ec466b72851c95ac20e868daa60a19e0464154e Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 8 May 2025 09:06:10 +0200 Subject: [PATCH] editor: Ensure scrollbar thumb is not layouted when content size is smaller than viewport (#30189) As discussed and explained in https://github.com/zed-industries/zed/pull/26893#discussion_r2074102719 This PR fixes an issue where we would have zero-divisions during scrollbar layouting for small files. This happened due to the fact that for small files, https://github.com/zed-industries/zed/blob/9c1b2afa492755a1e8cb0a77f929845941c97cdc/crates/editor/src/element.rs#L8562-L8563 would be `NaN`, since `(total_text_units - text_units_per_page).max(0.)` would return `0.`, which we would divide by. However, this was neccessary to be in place, as this prevented the scroll thumb from being rendered for small files: Due to this being `NaN`, the thumb origin would be `Pixels(NaN)`, which prevented the rendering of the scrollbar thumb. This PR fixes this behavior by accounting for this scenario and changing the thumb bounds to be an `Option>` instead. This furthermore has the advantage that we have to compute the thumb only once and storing it in the layout, which was previously not possible. Most notably, this enables scrollbar markers to show for smaller files: https://github.com/user-attachments/assets/9fa5d240-8795-4fae-9933-aed144df4f5e Currently, no markers are shown due to the fact that `Pixels(NaN)` is set as the origin point. Also, I changed that the cursor style will only be changed on the scrollbar hitbox when we will actually show a thumb. This way, for small files (where viewport > content size) the cursor will not change when a user hovers with their mouse over the scrollbars hitbox. Theoretically, I could also include the change mentioned in https://github.com/zed-industries/zed/pull/26893#discussion_r2076316956 here. Given the introduction of the minimap as well as #29316 and the cursor style taken care of here, removing the guard would not change anything and creates the possibility to soon introduce scrollbars for auto height editors. Please let me know whether we want to have this in this PR or whether I shall create a seperate one. Release Notes: - Enabled scrollbar marker rendering for small files. --- crates/editor/src/element.rs | 127 ++++++++++++++++++++++------------- 1 file changed, 80 insertions(+), 47 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0f20ac626e..e44f3b1893 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1461,7 +1461,7 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> Option { - if !snapshot.mode.is_full() || !self.editor.read(cx).show_scrollbars { + if !self.editor.read(cx).show_scrollbars || self.style.scrollbar_width.is_zero() { return None; } @@ -1604,6 +1604,7 @@ impl EditorElement { minimap_line_height, scroll_position, minimap_scroll_top, + show_thumb, ); minimap_editor.update(cx, |editor, cx| { @@ -1633,7 +1634,6 @@ impl EditorElement { Some(MinimapLayout { minimap, thumb_layout: layout, - show_thumb, thumb_border_style: minimap_settings.thumb_border, minimap_line_height, minimap_scroll_top, @@ -5236,8 +5236,6 @@ impl EditorElement { for (scrollbar_layout, axis) in scrollbars_layout.iter_scrollbars() { let hitbox = &scrollbar_layout.hitbox; - let thumb_bounds = scrollbar_layout.thumb_bounds(); - if scrollbars_layout.visible { let scrollbar_edges = match axis { ScrollbarAxis::Horizontal => Edges { @@ -5279,26 +5277,31 @@ impl EditorElement { } } - let scrollbar_thumb_color = match scrollbar_layout.thumb_state { - ScrollbarThumbState::Dragging => { - cx.theme().colors().scrollbar_thumb_active_background - } - ScrollbarThumbState::Hovered => { - cx.theme().colors().scrollbar_thumb_hover_background - } - ScrollbarThumbState::Idle => cx.theme().colors().scrollbar_thumb_background, - }; - window.paint_quad(quad( - thumb_bounds, - Corners::default(), - scrollbar_thumb_color, - scrollbar_edges, - cx.theme().colors().scrollbar_thumb_border, - BorderStyle::Solid, - )); + if let Some(thumb_bounds) = scrollbar_layout.thumb_bounds { + let scrollbar_thumb_color = match scrollbar_layout.thumb_state { + ScrollbarThumbState::Dragging => { + cx.theme().colors().scrollbar_thumb_active_background + } + ScrollbarThumbState::Hovered => { + cx.theme().colors().scrollbar_thumb_hover_background + } + ScrollbarThumbState::Idle => { + cx.theme().colors().scrollbar_thumb_background + } + }; + window.paint_quad(quad( + thumb_bounds, + Corners::default(), + scrollbar_thumb_color, + scrollbar_edges, + cx.theme().colors().scrollbar_thumb_border, + BorderStyle::Solid, + )); + + window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox)); + } }) } - window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox)); } window.on_mouse_event({ @@ -5343,7 +5346,10 @@ impl EditorElement { cx.stop_propagation(); } else if let Some((layout, axis)) = scrollbars_layout.get_hovered_axis(window) { - if layout.thumb_bounds().contains(&event.position) { + if layout + .thumb_bounds + .is_some_and(|bounds| bounds.contains(&event.position)) + { editor .scroll_manager .set_hovered_scroll_thumb_axis(axis, cx); @@ -5398,10 +5404,13 @@ impl EditorElement { hitbox, visible_range, text_unit_size, + thumb_bounds, .. } = scrollbar_layout; - let thumb_bounds = scrollbar_layout.thumb_bounds(); + let Some(thumb_bounds) = thumb_bounds else { + return; + }; editor.update(cx, |editor, cx| { editor @@ -5731,12 +5740,11 @@ impl EditorElement { fn paint_minimap(&self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { if let Some(mut layout) = layout.minimap.take() { let minimap_hitbox = layout.thumb_layout.hitbox.clone(); - let thumb_bounds = layout.thumb_layout.thumb_bounds(); window.paint_layer(layout.thumb_layout.hitbox.bounds, |window| { window.with_element_namespace("minimap", |window| { layout.minimap.paint(window, cx); - if layout.show_thumb { + if let Some(thumb_bounds) = layout.thumb_layout.thumb_bounds { let minimap_thumb_border = match layout.thumb_border_style { MinimapThumbBorder::Full => Edges::all(ScrollbarLayout::BORDER_WIDTH), MinimapThumbBorder::LeftOnly => Edges { @@ -5853,6 +5861,10 @@ impl EditorElement { let event_position = event.position; + let Some(thumb_bounds) = layout.thumb_layout.thumb_bounds else { + return; + }; + editor.update(cx, |editor, cx| { if !thumb_bounds.contains(&event_position) { let click_position = @@ -8426,6 +8438,7 @@ impl EditorScrollbars { glyph_grid_cell.along(axis), content_offset.along(axis), scroll_position.along(axis), + show_scrollbars, thumb_state, axis, ) @@ -8460,10 +8473,8 @@ struct ScrollbarLayout { hitbox: Hitbox, visible_range: Range, text_unit_size: Pixels, - content_offset: Pixels, - thumb_size: Pixels, + thumb_bounds: Option>, thumb_state: ScrollbarThumbState, - axis: ScrollbarAxis, } impl ScrollbarLayout { @@ -8479,6 +8490,7 @@ impl ScrollbarLayout { glyph_space: Pixels, content_offset: Pixels, scroll_position: f32, + show_thumb: bool, thumb_state: ScrollbarThumbState, axis: ScrollbarAxis, ) -> Self { @@ -8495,6 +8507,7 @@ impl ScrollbarLayout { glyph_space, content_offset, scroll_position, + show_thumb, thumb_state, axis, ) @@ -8507,6 +8520,7 @@ impl ScrollbarLayout { minimap_line_height: Pixels, scroll_position: f32, minimap_scroll_top: f32, + show_thumb: bool, ) -> Self { // The scrollbar thumb size is calculated as // (visible_content/total_content) × scrollbar_track_length. @@ -8535,6 +8549,7 @@ impl ScrollbarLayout { minimap_line_height, track_top_offset, scroll_position, + show_thumb, ScrollbarThumbState::Idle, ScrollbarAxis::Vertical, ) @@ -8548,6 +8563,7 @@ impl ScrollbarLayout { glyph_space: Pixels, content_offset: Pixels, scroll_position: f32, + show_thumb: bool, thumb_state: ScrollbarThumbState, axis: ScrollbarAxis, ) -> Self { @@ -8559,36 +8575,54 @@ impl ScrollbarLayout { let thumb_size = (track_length * thumb_percentage) .max(ScrollbarLayout::MIN_THUMB_SIZE) .min(track_length); - let text_unit_size = - (track_length - thumb_size) / (total_text_units - text_units_per_page).max(0.); + + let text_unit_divisor = (total_text_units - text_units_per_page).max(0.); + + let content_larger_than_viewport = text_unit_divisor > 0.; + + let text_unit_size = if content_larger_than_viewport { + (track_length - thumb_size) / text_unit_divisor + } else { + glyph_space + }; + + let thumb_bounds = (show_thumb && content_larger_than_viewport).then(|| { + Self::thumb_bounds( + &scrollbar_track_hitbox, + content_offset, + visible_range.start, + text_unit_size, + thumb_size, + axis, + ) + }); ScrollbarLayout { hitbox: scrollbar_track_hitbox, visible_range, text_unit_size, - content_offset, - thumb_size, + thumb_bounds, thumb_state, - axis, } } - fn thumb_bounds(&self) -> Bounds { - let scrollbar_track = &self.hitbox.bounds; + fn thumb_bounds( + scrollbar_track: &Hitbox, + content_offset: Pixels, + visible_range_start: f32, + text_unit_size: Pixels, + thumb_size: Pixels, + axis: ScrollbarAxis, + ) -> Bounds { + let thumb_origin = scrollbar_track.origin.apply_along(axis, |origin| { + origin + content_offset + visible_range_start * text_unit_size + }); Bounds::new( - scrollbar_track - .origin - .apply_along(self.axis, |origin| self.thumb_origin(origin)), - scrollbar_track - .size - .apply_along(self.axis, |_| self.thumb_size), + thumb_origin, + scrollbar_track.size.apply_along(axis, |_| thumb_size), ) } - fn thumb_origin(&self, origin: Pixels) -> Pixels { - origin + self.content_offset + self.visible_range.start * self.text_unit_size - } - fn marker_quads_for_ranges( &self, row_ranges: impl IntoIterator>, @@ -8677,7 +8711,6 @@ struct MinimapLayout { pub minimap_scroll_top: f32, pub minimap_line_height: Pixels, pub thumb_border_style: MinimapThumbBorder, - pub show_thumb: bool, pub max_scroll_top: f32, }