Refactor the scrollbar component (#36105)

Closes https://github.com/zed-industries/zed/issues/37621
Improves https://github.com/zed-industries/zed/issues/24623

Adding scrollbars withing Zed's UI currently is rather cumbersome, as it
requires the copying of a lot of code in order for these to work. Wiring
up settings for scrollbar visibilty always has to be done at the call
site and the state has to be saved and maintained by the caller as well.
Similarly, reserving space has to also be handled by the caller.

This PR changes the way scrollbars work in Zed fundamentally by making
use of the new `use_keyed_state` APIs: Instead of saving the state at
the call site, the window now keeps track of the state corresponding to
scrollbars. This enables us to add scrollbars with e.g. one simple call
on divs:
```rust
div()
    .vertical_scrollbar(window, cx)
```
will add a scrollbar to the corresponding container. There are some more
improvements regarding tracking of scrollbar visibility settings (which
is now handled by a trait for each setting that supports this) as well
as reserving space.
Additionally, all needed stuff for layouting, catching events and
reserving space is also now managed by the scrollbar component instead.
This drastically reduces the amount of event listeners and makes
layouting of two scrollbars easier.

Furthermore, this paves the way for more improvements to scrollbars,
such as graceful auto-hide. Only downsight here is that we lose some
customizability in a few areas. However, once this lands, we gain the
ability to quickly follow these up without breaking stuff elsewhere.

This also already fixes a few bugs:
- Scrollbars no longer flicker on first render. 
- Auto-hide now properly works for all scrollbars.
- If the content size changes, the scrollbar is updated on the same
frame. Both of these happened because we were computing the scrollbar
sizes too early, causing us to use the sizes from the previous frame or
unitialized sizes.
- The project panel no longer jumps if scrolled all the way to the
bottom and the scrollbar actually auto-hides.

Still TODO:
- [x] Fix scrolling in the debugger memory view
- [x] Clean up some more in the scrollbar component and reduce clones
there
- [x] Ensure we don't over-notify the entity the scrollbar is rendered
within
- [x] Make sure auto-hide properly works for all cases
- [x] Check whether we want to implement the scrollbar trait for
`UniformList`s as well
    - ~~ [ ] Use for uniformlist where possible~~ Postponed
- [x] Improve layout for cases where we render both scrollbars.

Release Notes:

- N/A
This commit is contained in:
Finn Evers
2025-09-13 00:43:16 +02:00
committed by GitHub
parent 53c5db4495
commit ded6467604
35 changed files with 1566 additions and 2386 deletions
+3 -3
View File
@@ -55,7 +55,7 @@ pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPla
pub use edit_prediction::Direction;
pub use editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar,
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap,
};
pub use editor_settings_controls::*;
pub use element::{
@@ -166,7 +166,7 @@ use project::{
};
use rand::seq::SliceRandom;
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
use selections_collection::{
MutableSelectionsCollection, SelectionsCollection, resolve_selections,
};
@@ -196,7 +196,7 @@ use theme::{
};
use ui::{
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*, scrollbars::ScrollbarAutoHide,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{
+7 -17
View File
@@ -7,6 +7,7 @@ use project::project_settings::DiagnosticSeverity;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings};
use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
use util::serde::default_true;
/// Imports from the VSCode settings at
@@ -205,23 +206,6 @@ pub struct Gutter {
pub folds: bool,
}
/// When to show the scrollbar in the editor.
///
/// Default: auto
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowScrollbar {
/// Show the scrollbar if there's important information or
/// follow the system's configured behavior.
Auto,
/// Match the system's configured behavior.
System,
/// Always show the scrollbar.
Always,
/// Never show the scrollbar.
Never,
}
/// When to show the minimap in the editor.
///
/// Default: never
@@ -778,6 +762,12 @@ impl EditorSettings {
}
}
impl ScrollbarVisibility for EditorSettings {
fn visibility(&self, _cx: &App) -> ShowScrollbar {
self.scrollbar.show
}
}
impl Settings for EditorSettings {
type FileContent = EditorSettingsContent;
+2 -2
View File
@@ -18,7 +18,7 @@ use crate::{
editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
MinimapThumb, MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes,
ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
ScrollbarDiagnostics, ShowMinimap,
},
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
hover_popover::{
@@ -85,7 +85,7 @@ use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
use ui::utils::ensure_minimum_contrast;
use ui::{
ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
right_click_menu,
right_click_menu, scrollbars::ShowScrollbar,
};
use unicode_segmentation::UnicodeSegmentation;
use util::post_inc;
+15 -77
View File
@@ -9,8 +9,8 @@ use anyhow::Context as _;
use gpui::{
AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla,
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
TextStyleRefinement, Window, div, px,
StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement,
Window, div, px,
};
use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry};
@@ -23,7 +23,7 @@ use std::{borrow::Cow, cell::RefCell};
use std::{ops::Range, sync::Arc, time::Duration};
use std::{path::PathBuf, rc::Rc};
use theme::ThemeSettings;
use ui::{Scrollbar, ScrollbarState, prelude::*, theme_is_transparent};
use ui::{Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
use url::Url;
use util::TryFutureExt;
use workspace::{OpenOptions, OpenVisible, Workspace};
@@ -184,7 +184,6 @@ pub fn hover_at_inlay(
let hover_popover = InfoPopover {
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(false)),
anchor: None,
@@ -387,7 +386,6 @@ fn show_hover(
local_diagnostic,
markdown,
border_color,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
background_color,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
@@ -457,7 +455,6 @@ fn show_hover(
info_popovers.push(InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
@@ -507,7 +504,6 @@ fn show_hover(
info_popovers.push(InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
@@ -846,7 +842,6 @@ pub struct InfoPopover {
pub symbol_range: RangeInEditor,
pub parsed_content: Option<Entity<Markdown>>,
pub scroll_handle: ScrollHandle,
pub scrollbar_state: ScrollbarState,
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Option<Anchor>,
_subscription: Option<Subscription>,
@@ -891,7 +886,12 @@ impl InfoPopover {
.on_url_click(open_markdown_url),
),
)
.child(self.render_vertical_scrollbar(cx))
.custom_scrollbars(
Scrollbars::for_settings::<EditorSettings>()
.tracked_scroll_handle(self.scroll_handle.clone()),
window,
cx,
)
})
.into_any_element()
}
@@ -905,39 +905,6 @@ impl InfoPopover {
cx.notify();
self.scroll_handle.set_offset(current);
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("info-popover-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
}
pub struct DiagnosticPopover {
@@ -949,7 +916,6 @@ pub struct DiagnosticPopover {
pub anchor: Anchor,
_subscription: Subscription,
pub scroll_handle: ScrollHandle,
pub scrollbar_state: ScrollbarState,
}
impl DiagnosticPopover {
@@ -1013,43 +979,15 @@ impl DiagnosticPopover {
),
),
)
.child(self.render_vertical_scrollbar(cx)),
.custom_scrollbars(
Scrollbars::for_settings::<EditorSettings>()
.tracked_scroll_handle(self.scroll_handle.clone()),
window,
cx,
),
)
.into_any_element()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("diagnostic-popover-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
}
#[cfg(test)]
+3 -7
View File
@@ -12,7 +12,7 @@ use crate::{
};
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use core::fmt::Debug;
use gpui::{Along, App, Axis, Context, Global, Pixels, Task, Window, point, px};
use gpui::{Along, App, Axis, Context, Pixels, Task, Window, point, px};
use language::language_settings::{AllLanguageSettings, SoftWrap};
use language::{Bias, Point};
pub use scroll_amount::ScrollAmount;
@@ -21,6 +21,7 @@ use std::{
cmp::Ordering,
time::{Duration, Instant},
};
use ui::scrollbars::ScrollbarAutoHide;
use util::ResultExt;
use workspace::{ItemId, WorkspaceId};
@@ -29,11 +30,6 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
pub struct WasScrolled(pub(crate) bool);
#[derive(Default)]
pub struct ScrollbarAutoHide(pub bool);
impl Global for ScrollbarAutoHide {}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollAnchor {
pub offset: gpui::Point<f32>,
@@ -327,7 +323,7 @@ impl ScrollManager {
cx.notify();
}
if cx.default_global::<ScrollbarAutoHide>().0 {
if cx.default_global::<ScrollbarAutoHide>().should_hide() {
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
+6 -29
View File
@@ -2,8 +2,8 @@ use crate::actions::ShowSignatureHelp;
use crate::hover_popover::open_markdown_url;
use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style};
use gpui::{
App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful,
StyledText, Task, TextStyle, Window, combine_highlights,
App, Context, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, StyledText, Task,
TextStyle, Window, combine_highlights,
};
use language::BufferSnapshot;
use markdown::{Markdown, MarkdownElement};
@@ -15,8 +15,8 @@ use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString,
StatefulInteractiveElement, Styled, StyledExt, div, px, relative,
LabelSize, ParentElement, Pixels, SharedString, StatefulInteractiveElement, Styled, StyledExt,
WithScrollbar, div, relative,
};
// Language-specific settings may define quotes as "brackets", so filter them out separately.
@@ -243,7 +243,6 @@ impl Editor {
.min(signatures.len().saturating_sub(1));
let signature_help_popover = SignatureHelpPopover {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
style,
signatures,
current_signature,
@@ -330,7 +329,6 @@ pub struct SignatureHelpPopover {
pub signatures: Vec<SignatureHelp>,
pub current_signature: usize,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
}
impl SignatureHelpPopover {
@@ -391,7 +389,8 @@ impl SignatureHelpPopover {
)
}),
)
.child(self.render_vertical_scrollbar(cx));
.vertical_scrollbar(window, cx);
let controls = if self.signatures.len() > 1 {
let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
.shape(IconButtonShape::Square)
@@ -460,26 +459,4 @@ impl SignatureHelpPopover {
.child(main_content)
.into_any_element()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("signature_help_scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| cx.stop_propagation())
.on_any_mouse_down(|_, _, cx| cx.stop_propagation())
.on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify()))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_1()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
}