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, 


9c1b2afa49/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<Bounds<Pixels>>` 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.
This commit is contained in:
Finn Evers 2025-05-08 09:06:10 +02:00 committed by GitHub
parent a127ff4a4f
commit 1ec466b728
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1461,7 +1461,7 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Option<EditorScrollbars> {
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<f32>,
text_unit_size: Pixels,
content_offset: Pixels,
thumb_size: Pixels,
thumb_bounds: Option<Bounds<Pixels>>,
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<Pixels> {
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<Pixels> {
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<Item = ColoredRange<DisplayRow>>,
@ -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,
}