scrollbar: Manage ScrollbarState in Scrollbar internal. (#1690)

This PR to improve the Scrollbar API to manage the state in the
internal.

## Break Changes

Describe any breaking changes introduced by this pull request. If none,
remove this section.

- Removed `scrollbar_state` argument from `Scrollbar::new`,
`Scrollbar::both`, `Scrollbar::vertical` and `Scrollbar::horizontal`.

```diff
- Scrollbar::horizontal(&self.scrollbar_state, &self.scroll_handle)
+ Scrollbar::horizontal(&self.scroll_handle)

- Scrollbar::vertical(&self.scrollbar_state, &self.scroll_handle)
+ Scrollbar::vertical(&self.scroll_handle)
```

- Change `struct ScrollbarState` to private, we not need this not.

```diff
- pub struct ScrollbarState {
+ struct ScrollbarState {
```

- Renamed `trait ScrollHandleOffsetable` to `trait ScrollbarHanle`.

```diff
- pub trait ScrollHandleOffsetable {
+ pub trait ScrollbarHanle {
```

- Removed `Scrollbar::both`, now use `Scrollbar::new` instead.

```diff
- Scrollbar::both(&scroll_handle)
+ Scrollbar::new(&scroll_handle)
```
This commit is contained in:
Jason Lee 2025-11-27 11:39:58 +08:00 committed by GitHub
parent 7f956e0f55
commit 4a3f520c5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 129 additions and 341 deletions

View file

@ -1,19 +1,17 @@
use std::rc::Rc;
use gpui::{
div, px, size, App, AppContext, Axis, Context, Entity, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Pixels, Render, Size, Styled, Window,
App, AppContext, Axis, Context, Entity, FocusHandle, Focusable, InteractiveElement,
IntoElement, ParentElement, Pixels, Render, Size, Styled, Window, div, px, size,
};
use gpui_component::{
ActiveTheme as _, Selectable, StyledExt as _,
button::{Button, ButtonGroup},
h_flex,
scroll::ScrollbarState,
v_flex, ActiveTheme as _, Selectable, StyledExt as _,
h_flex, v_flex,
};
pub struct ScrollableStory {
focus_handle: FocusHandle,
scroll_state: ScrollbarState,
items: Vec<String>,
item_sizes: Rc<Vec<Size<Pixels>>>,
test_width: Pixels,
@ -33,7 +31,6 @@ impl ScrollableStory {
Self {
focus_handle: cx.focus_handle(),
scroll_state: ScrollbarState::default(),
items,
item_sizes: Rc::new(item_sizes),
test_width,
@ -69,7 +66,6 @@ impl ScrollableStory {
.map(|_| size(self.test_width, ITEM_HEIGHT))
.collect::<Vec<_>>()
.into();
self.scroll_state = ScrollbarState::default();
cx.notify();
}

View file

@ -9,14 +9,13 @@ use gpui_component::{
button::{Button, ButtonGroup},
divider::Divider,
h_flex,
scroll::{Scrollbar, ScrollbarAxis, ScrollbarState},
scroll::{Scrollbar, ScrollbarAxis},
v_flex, v_virtual_list,
};
pub struct VirtualListStory {
focus_handle: FocusHandle,
scroll_handle: VirtualListScrollHandle,
scroll_state: ScrollbarState,
items: Vec<String>,
item_sizes: Rc<Vec<Size<Pixels>>>,
columns_count: usize,
@ -35,7 +34,6 @@ impl VirtualListStory {
Self {
focus_handle: cx.focus_handle(),
scroll_handle: VirtualListScrollHandle::new(),
scroll_state: ScrollbarState::default(),
items,
item_sizes: Rc::new(item_sizes),
columns_count: 100,
@ -68,8 +66,6 @@ impl VirtualListStory {
}
self.item_sizes = Rc::new(self.items.iter().map(|_| ITEM_SIZE).collect());
self.scroll_state = ScrollbarState::default();
cx.notify();
}
@ -293,10 +289,7 @@ impl Render for VirtualListStory {
.left_0()
.right_0()
.bottom_0()
.child(
Scrollbar::both(&self.scroll_state, &self.scroll_handle)
.axis(self.axis),
)
.child(Scrollbar::new(&self.scroll_handle).axis(self.axis))
}),
),
),

View file

@ -7,7 +7,7 @@ use std::{
use crate::{
ActiveTheme, Icon, IconName, h_flex,
history::{History, HistoryItem},
scroll::{Scrollbar, ScrollbarShow, ScrollbarState},
scroll::{Scrollbar, ScrollbarShow},
v_flex,
};
@ -139,7 +139,6 @@ pub struct Tiles {
resizing_drag_data: Option<ResizeDrag>,
bounds: Bounds<Pixels>,
history: History<TileChange>,
scroll_state: ScrollbarState,
scroll_handle: ScrollHandle,
scrollbar_show: Option<ScrollbarShow>,
}
@ -195,7 +194,6 @@ impl Tiles {
resizing_drag_data: None,
bounds: Bounds::default(),
history: History::new().group_interval(std::time::Duration::from_millis(100)),
scroll_state: ScrollbarState::default(),
scroll_handle: ScrollHandle::default(),
}
}
@ -1220,7 +1218,7 @@ impl Render for Tiles {
.right_0()
.bottom_0()
.child(
Scrollbar::both(&self.scroll_state, &self.scroll_handle)
Scrollbar::new(&self.scroll_handle)
.scroll_size(scroll_size)
.when_some(self.scrollbar_show, |this, scrollbar_show| {
this.scrollbar_show(scrollbar_show)

View file

@ -1,8 +1,8 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, relative, AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity,
InteractiveElement as _, IntoElement, IsZero, MouseButton, ParentElement as _, Pixels, Rems,
RenderOnce, StyleRefinement, Styled, Window,
AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, InteractiveElement as _,
IntoElement, IsZero, MouseButton, ParentElement as _, Pixels, Rems, RenderOnce,
StyleRefinement, Styled, Window, div, px, relative,
};
use crate::button::{Button, ButtonVariants as _};
@ -10,9 +10,9 @@ use crate::input::clear_button;
use crate::input::element::{LINE_NUMBER_RIGHT_MARGIN, RIGHT_MARGIN};
use crate::scroll::Scrollbar;
use crate::spinner::Spinner;
use crate::{h_flex, Selectable, StyledExt};
use crate::{v_flex, ActiveTheme};
use crate::{ActiveTheme, v_flex};
use crate::{IconName, Size};
use crate::{Selectable, StyledExt, h_flex};
use crate::{Sizable, StyleSized};
use super::InputState;
@ -213,9 +213,9 @@ impl Input {
};
let scrollbar = if !state.soft_wrap {
Scrollbar::both(&state.scroll_state, &state.scroll_handle)
Scrollbar::new(&state.scroll_handle)
} else {
Scrollbar::vertical(&state.scroll_state, &state.scroll_handle)
Scrollbar::vertical(&state.scroll_handle)
};
this.relative().child(

View file

@ -32,7 +32,7 @@ use crate::input::{
text_wrapper::LineLayout,
};
use crate::input::{RopeExt as _, Selection};
use crate::{Root, history::History, scroll::ScrollbarState};
use crate::{Root, history::History};
use crate::{highlighter::DiagnosticSet, input::text_wrapper::LineItem};
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
@ -295,7 +295,6 @@ pub struct InputState {
pub(crate) scroll_handle: ScrollHandle,
/// The deferred scroll offset to apply on next layout.
pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
pub(super) scroll_state: ScrollbarState,
/// The size of the scrollable content.
pub(crate) scroll_size: gpui::Size<Pixels>,
@ -396,7 +395,6 @@ impl InputState {
last_selected_range: None,
last_cursor: None,
scroll_handle: ScrollHandle::new(),
scroll_state: ScrollbarState::default(),
scroll_size: gpui::size(px(0.), px(0.)),
deferred_scroll_offset: None,
preferred_column: None,

View file

@ -5,20 +5,21 @@ use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
use crate::input::InputState;
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
use crate::{
ActiveTheme, IconName, Size,
input::{Input, InputEvent},
scroll::{Scrollbar, ScrollbarState},
v_flex, ActiveTheme, IconName, Size,
scroll::Scrollbar,
v_flex,
};
use crate::{list::ListDelegate, v_virtual_list, VirtualListScrollHandle};
use crate::{Icon, IndexPath, Selectable, Sizable, StyledExt};
use crate::{VirtualListScrollHandle, list::ListDelegate, v_virtual_list};
use gpui::{
div, prelude::FluentBuilder, AppContext, Entity, FocusHandle, Focusable, InteractiveElement,
IntoElement, KeyBinding, Length, MouseButton, ParentElement, Render, Styled, Task, Window,
App, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, EventEmitter,
ListSizingBehavior, RenderOnce, ScrollStrategy, SharedString, StatefulInteractiveElement,
StyleRefinement, Subscription, px, size,
};
use gpui::{
px, size, App, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement,
EventEmitter, ListSizingBehavior, RenderOnce, ScrollStrategy, SharedString,
StatefulInteractiveElement, StyleRefinement, Subscription,
AppContext, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding,
Length, MouseButton, ParentElement, Render, Styled, Task, Window, div, prelude::FluentBuilder,
};
use rust_i18n::t;
use smol::Timer;
@ -72,7 +73,6 @@ pub struct ListState<D: ListDelegate> {
delegate: D,
last_query: Option<String>,
scroll_handle: VirtualListScrollHandle,
scroll_state: ScrollbarState,
rows_cache: RowsCache,
selected_index: Option<IndexPath>,
item_to_measure_index: IndexPath,
@ -111,7 +111,6 @@ where
deferred_scroll_to_index: None,
mouse_right_clicked_index: None,
scroll_handle: VirtualListScrollHandle::new(),
scroll_state: ScrollbarState::default(),
reset_on_cancel: true,
_search_task: Task::ready(()),
_load_more_task: Task::ready(()),
@ -484,7 +483,6 @@ where
let rows_cache = self.rows_cache.clone();
let scrollbar_visible = self.options.scrollbar_visible;
let scroll_handle = self.scroll_handle.clone();
let scroll_state = self.scroll_state.clone();
let measured_size = rows_cache.measured_size();
v_flex()
@ -550,7 +548,7 @@ where
}
})
.when(scrollbar_visible, |this| {
this.child(Scrollbar::vertical(&scroll_state, &scroll_handle))
this.child(Scrollbar::vertical(&scroll_handle))
})
}
}

View file

@ -1,7 +1,7 @@
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
use crate::actions::{SelectLeft, SelectRight};
use crate::menu::menu_item::MenuItemElement;
use crate::scroll::{Scrollbar, ScrollbarState};
use crate::scroll::Scrollbar;
use crate::{ActiveTheme, Icon, IconName, Sizable as _, h_flex, v_flex};
use crate::{Side, Size, StyledExt, kbd::Kbd};
use gpui::{
@ -287,7 +287,6 @@ pub struct PopupMenu {
scrollable: bool,
external_link_icon: bool,
scroll_handle: ScrollHandle,
scroll_state: ScrollbarState,
// This will update on render
submenu_anchor: (Corner, Pixels),
@ -309,7 +308,6 @@ impl PopupMenu {
bounds: Bounds::default(),
scrollable: false,
scroll_handle: ScrollHandle::default(),
scroll_state: ScrollbarState::default(),
external_link_icon: true,
size: Size::default(),
submenu_anchor: (Corner::TopLeft, Pixels::ZERO),
@ -1318,7 +1316,7 @@ impl Render for PopupMenu {
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(&self.scroll_state, &self.scroll_handle)),
.child(Scrollbar::vertical(&self.scroll_handle)),
)
})
}

View file

@ -1,9 +1,9 @@
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
use super::{Scrollbar, ScrollbarAxis};
use gpui::{
div, relative, AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId,
InspectorElementId, InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement,
Pixels, Position, ScrollHandle, SharedString, Stateful, StatefulInteractiveElement, Style,
StyleRefinement, Styled, Window,
AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId, InspectorElementId,
InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement, Pixels, Position,
ScrollHandle, SharedString, Stateful, StatefulInteractiveElement, Style, StyleRefinement,
Styled, Window, div, relative,
};
/// A scroll view is a container that allows the user to scroll through a large amount of content.
@ -69,7 +69,6 @@ where
#[doc(hidden)]
pub struct ScrollViewState {
state: ScrollbarState,
handle: ScrollHandle,
}
@ -77,7 +76,6 @@ impl Default for ScrollViewState {
fn default() -> Self {
Self {
handle: ScrollHandle::new(),
state: ScrollbarState::default(),
}
}
}
@ -184,9 +182,7 @@ where
.left_0()
.right_0()
.bottom_0()
.child(
Scrollbar::both(&element_state.state, &element_state.handle).axis(axis),
),
.child(Scrollbar::new(&element_state.handle).axis(axis)),
)
.into_any_element();

View file

@ -1,17 +1,18 @@
use std::{
cell::Cell,
ops::Deref,
panic::Location,
rc::Rc,
time::{Duration, Instant},
};
use crate::{ActiveTheme, AxisExt};
use gpui::{
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
IntoElement, IsZero, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
PaintQuad, Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer,
UniformListScrollHandle, Window,
App, Axis, BorderStyle, Bounds, ContentMask, Corner, CursorStyle, Edges, Element, ElementId,
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId, IntoElement, IsZero,
LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer, UniformListScrollHandle, Window,
fill, point, px, relative, size,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -54,7 +55,7 @@ impl ScrollbarShow {
}
/// A trait for scroll handles that can get and set offset.
pub trait ScrollHandleOffsetable {
pub trait ScrollbarHandle: 'static {
/// Get the current offset of the scroll handle.
fn offset(&self) -> Point<Pixels>;
/// Set the offset of the scroll handle.
@ -67,7 +68,7 @@ pub trait ScrollHandleOffsetable {
fn end_drag(&self) {}
}
impl ScrollHandleOffsetable for ScrollHandle {
impl ScrollbarHandle for ScrollHandle {
fn offset(&self) -> Point<Pixels> {
self.offset()
}
@ -81,7 +82,7 @@ impl ScrollHandleOffsetable for ScrollHandle {
}
}
impl ScrollHandleOffsetable for UniformListScrollHandle {
impl ScrollbarHandle for UniformListScrollHandle {
fn offset(&self) -> Point<Pixels> {
self.0.borrow().base_handle.offset()
}
@ -96,7 +97,7 @@ impl ScrollHandleOffsetable for UniformListScrollHandle {
}
}
impl ScrollHandleOffsetable for ListState {
impl ScrollbarHandle for ListState {
fn offset(&self) -> Point<Pixels> {
self.scroll_px_offset_for_scrollbar()
}
@ -120,11 +121,11 @@ impl ScrollHandleOffsetable for ListState {
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct ScrollbarStateInner {
struct ScrollbarStateInner {
hovered_axis: Option<Axis>,
hovered_on_thumb: Option<Axis>,
dragged_axis: Option<Axis>,
@ -307,10 +308,10 @@ impl ScrollbarAxis {
/// Scrollbar control for scroll-area or a uniform-list.
pub struct Scrollbar {
pub(crate) id: ElementId,
axis: ScrollbarAxis,
scrollbar_show: Option<ScrollbarShow>,
scroll_handle: Rc<dyn ScrollHandleOffsetable>,
state: ScrollbarState,
scroll_handle: Rc<dyn ScrollbarHandle>,
scroll_size: Option<Size<Pixels>>,
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
///
@ -320,14 +321,15 @@ pub struct Scrollbar {
}
impl Scrollbar {
fn new(
axis: impl Into<ScrollbarAxis>,
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
/// Create a new scrollbar.
///
/// This will have both vertical and horizontal scrollbars.
#[track_caller]
pub fn new<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
let caller = Location::caller();
Self {
state: state.clone(),
axis: axis.into(),
id: ElementId::CodeLocation(*caller),
axis: ScrollbarAxis::Both,
scrollbar_show: None,
scroll_handle: Rc::new(scroll_handle.clone()),
max_fps: 120,
@ -335,28 +337,24 @@ impl Scrollbar {
}
}
/// Create with vertical and horizontal scrollbar.
pub fn both(
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
Self::new(ScrollbarAxis::Both, state, scroll_handle)
}
/// Create with horizontal scrollbar.
pub fn horizontal(
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
Self::new(ScrollbarAxis::Horizontal, state, scroll_handle)
#[track_caller]
pub fn horizontal<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
Self::new(scroll_handle).axis(ScrollbarAxis::Horizontal)
}
/// Create with vertical scrollbar.
pub fn vertical(
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
#[track_caller]
pub fn vertical<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
Self::new(scroll_handle).axis(ScrollbarAxis::Vertical)
}
/// Set a specific element id, default is the [`Location::caller`].
///
/// NOTE: In most cases, you don't need to set a specific id for scrollbar.
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
self.id = id.into();
self
}
/// Set the scrollbar show mode [`ScrollbarShow`], if not set use the `cx.theme().scrollbar_show`.
@ -473,6 +471,7 @@ impl IntoElement for Scrollbar {
#[doc(hidden)]
pub struct PrepaintState {
hitbox: Hitbox,
scrollbar_state: ScrollbarState,
states: Vec<AxisPrepaintState>,
}
@ -499,7 +498,7 @@ impl Element for Scrollbar {
type PrepaintState = PrepaintState;
fn id(&self) -> Option<gpui::ElementId> {
None
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
@ -536,6 +535,11 @@ impl Element for Scrollbar {
window.insert_hitbox(bounds, HitboxBehavior::Normal)
});
let state = window
.use_state(cx, |_, _| ScrollbarState::default())
.read(cx)
.clone();
let mut states = vec![];
let mut has_both = self.axis.is_both();
let scroll_size = self
@ -601,7 +605,6 @@ impl Element for Scrollbar {
};
let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
let state = self.state.clone();
let is_always_to_show = scrollbar_show.is_always();
let is_hover_to_show = scrollbar_show.is_hover();
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
@ -716,7 +719,11 @@ impl Element for Scrollbar {
})
}
PrepaintState { hitbox, states }
PrepaintState {
hitbox,
states,
scrollbar_state: state,
}
}
fn paint(
@ -729,16 +736,17 @@ impl Element for Scrollbar {
window: &mut Window,
cx: &mut App,
) {
let scrollbar_state = &prepaint.scrollbar_state;
let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
let view_id = window.current_view();
let hitbox_bounds = prepaint.hitbox.bounds;
let is_visible = self.state.get().is_scrollbar_visible() || scrollbar_show.is_always();
let is_visible = scrollbar_state.get().is_scrollbar_visible() || scrollbar_show.is_always();
let is_hover_to_show = scrollbar_show.is_hover();
// Update last_scroll_time when offset is changed.
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
self.state.set(
self.state
if self.scroll_handle.offset() != scrollbar_state.get().last_scroll_offset {
scrollbar_state.set(
scrollbar_state
.get()
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
);
@ -798,7 +806,7 @@ impl Element for Scrollbar {
});
window.on_mouse_event({
let state = self.state.clone();
let state = scrollbar_state.clone();
let scroll_handle = self.scroll_handle.clone();
move |event: &ScrollWheelEvent, phase, _, cx| {
@ -818,7 +826,7 @@ impl Element for Scrollbar {
if is_hover_to_show || is_visible {
window.on_mouse_event({
let state = self.state.clone();
let state = scrollbar_state.clone();
let scroll_handle = self.scroll_handle.clone();
move |event: &MouseDownEvent, phase, _, cx| {
@ -867,7 +875,7 @@ impl Element for Scrollbar {
window.on_mouse_event({
let scroll_handle = self.scroll_handle.clone();
let state = self.state.clone();
let state = scrollbar_state.clone();
let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
move |event: &MouseMoveEvent, _, _, cx| {
@ -955,8 +963,8 @@ impl Element for Scrollbar {
});
window.on_mouse_event({
let state = scrollbar_state.clone();
let scroll_handle = self.scroll_handle.clone();
let state = self.state.clone();
move |_event: &MouseUpEvent, phase, _, cx| {
if phase.bubble() {

View file

@ -1,16 +1,17 @@
use gpui::{
div, list, prelude::FluentBuilder as _, px, App, Entity, InteractiveElement as _, IntoElement,
ListAlignment, ListState, ParentElement as _, SharedString, Styled, Window,
App, Entity, InteractiveElement as _, IntoElement, ListAlignment, ListState,
ParentElement as _, SharedString, Styled, Window, div, list, prelude::FluentBuilder as _, px,
};
use rust_i18n::t;
use crate::{
ActiveTheme, IconName, Sizable,
button::{Button, ButtonVariants},
h_flex,
label::Label,
scroll::{Scrollbar, ScrollbarState},
setting::{settings::SettingsState, RenderOptions, SettingGroup},
v_flex, ActiveTheme, IconName, Sizable,
scroll::Scrollbar,
setting::{RenderOptions, SettingGroup, settings::SettingsState},
v_flex,
};
/// A setting page that can contain multiple setting groups.
@ -100,16 +101,11 @@ impl SettingPage {
.collect::<Vec<_>>();
let groups_count = groups.len();
let (scroll_state, list_state) = window
let list_state = window
.use_keyed_state(
SharedString::from(format!("list-state:{}", ix)),
cx,
|_, _| {
(
ScrollbarState::default(),
ListState::new(groups_count, ListAlignment::Top, px(100.)),
)
},
|_, _| ListState::new(groups_count, ListAlignment::Top, px(100.)),
)
.read(cx)
.clone();
@ -188,7 +184,7 @@ impl SettingPage {
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(&scroll_state, &list_state)),
.child(Scrollbar::vertical(&list_state)),
),
)
}

View file

@ -5,7 +5,7 @@ use crate::{
actions::{Cancel, SelectDown, SelectUp},
h_flex,
menu::{ContextMenuExt, PopupMenu},
scroll::{ScrollableMask, Scrollbar, ScrollbarState},
scroll::{ScrollableMask, Scrollbar},
v_flex,
};
use gpui::{
@ -95,9 +95,7 @@ pub struct TableState<D: TableDelegate> {
pub col_fixed: bool,
pub vertical_scroll_handle: UniformListScrollHandle,
pub vertical_scroll_state: ScrollbarState,
pub horizontal_scroll_handle: VirtualListScrollHandle,
pub horizontal_scroll_state: ScrollbarState,
selected_row: Option<usize>,
selection_state: SelectionState,
@ -127,8 +125,6 @@ where
col_groups: Vec::new(),
horizontal_scroll_handle: VirtualListScrollHandle::new(),
vertical_scroll_handle: UniformListScrollHandle::new(),
vertical_scroll_state: ScrollbarState::default(),
horizontal_scroll_state: ScrollbarState::default(),
selection_state: SelectionState::Row,
selected_row: None,
right_clicked_row: None,
@ -1209,10 +1205,7 @@ where
.right_0()
.bottom_0()
.w(Scrollbar::width())
.child(
Scrollbar::vertical(&self.vertical_scroll_state, &self.vertical_scroll_handle)
.max_fps(60),
),
.child(Scrollbar::vertical(&self.vertical_scroll_handle).max_fps(60)),
)
}
@ -1228,10 +1221,7 @@ where
.right_0()
.bottom_0()
.h(Scrollbar::width())
.child(Scrollbar::horizontal(
&self.horizontal_scroll_state,
&self.horizontal_scroll_handle,
))
.child(Scrollbar::horizontal(&self.horizontal_scroll_handle))
}
}

View file

@ -6,25 +6,24 @@ use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId,
Entity, EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement,
IntoElement, KeyBinding, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
ParentElement, Pixels, Point, RenderOnce, SharedString, Size, StyleRefinement, Styled, Timer,
Window,
AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId, Entity,
EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement,
KeyBinding, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
Pixels, Point, RenderOnce, SharedString, Size, StyleRefinement, Styled, Timer, Window, div, px,
};
use smol::stream::StreamExt;
use crate::highlighter::HighlightTheme;
use crate::scroll::{Scrollbar, ScrollbarState};
use crate::scroll::Scrollbar;
use crate::{ActiveTheme, StyledExt, v_flex};
use crate::{
global_state::GlobalState,
input::{self},
text::{
node::{self, NodeContext},
TextViewStyle,
node::{self, NodeContext},
},
};
use crate::{v_flex, ActiveTheme, StyledExt};
const CONTEXT: &'static str = "TextView";
@ -218,7 +217,6 @@ pub(crate) struct TextViewState {
/// Is current in selection.
is_selecting: bool,
is_selectable: bool,
scrollbar_state: ScrollbarState,
list_state: ListState,
}
@ -234,7 +232,6 @@ impl TextViewState {
selection_positions: (None, None),
is_selecting: false,
is_selectable: false,
scrollbar_state: ScrollbarState::default(),
list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
}
}
@ -600,7 +597,6 @@ impl Element for TextView {
self.init_state = Some(InitState::Initialized { tx });
}
let scrollbar_state = &self.state.read(cx).scrollbar_state;
let list_state = &self.state.read(cx).list_state;
let focus_handle = self
@ -638,7 +634,7 @@ impl Element for TextView {
.top_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(scrollbar_state, list_state)),
.child(Scrollbar::vertical(list_state)),
)
})
.into_any_element();
@ -800,7 +796,7 @@ fn selection_bounds(
#[cfg(test)]
mod tests {
use super::*;
use gpui::{point, px, size, Bounds};
use gpui::{Bounds, point, px, size};
#[test]
fn test_text_view_state_selection_bounds() {

View file

@ -1,17 +1,17 @@
use std::{cell::RefCell, ops::Range, rc::Rc};
use gpui::{
div, prelude::FluentBuilder as _, uniform_list, App, Context, ElementId, Entity, FocusHandle,
InteractiveElement as _, IntoElement, KeyBinding, ListSizingBehavior, MouseButton,
ParentElement, Render, RenderOnce, SharedString, StyleRefinement, Styled,
UniformListScrollHandle, Window,
App, Context, ElementId, Entity, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding,
ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, SharedString,
StyleRefinement, Styled, UniformListScrollHandle, Window, div, prelude::FluentBuilder as _,
uniform_list,
};
use crate::{
StyledExt,
actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp},
list::ListItem,
scroll::{Scrollbar, ScrollbarState},
StyledExt,
scroll::Scrollbar,
};
const CONTEXT: &str = "Tree";
@ -179,7 +179,6 @@ impl TreeItem {
pub struct TreeState {
focus_handle: FocusHandle,
entries: Vec<TreeEntry>,
scrollbar_state: ScrollbarState,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
render_item: Rc<dyn Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem>,
@ -191,7 +190,6 @@ impl TreeState {
Self {
selected_ix: None,
focus_handle: cx.focus_handle(),
scrollbar_state: ScrollbarState::default(),
scroll_handle: UniformListScrollHandle::default(),
entries: Vec::new(),
render_item: Rc::new(|_, _, _, _, _| ListItem::new(0)),
@ -395,10 +393,7 @@ impl Render for TreeState {
.right_0()
.bottom_0()
.w(Scrollbar::width())
.child(Scrollbar::vertical(
&self.scrollbar_state,
&self.scroll_handle,
)),
.child(Scrollbar::vertical(&self.scroll_handle)),
)
}
}

View file

@ -1,4 +1,4 @@
//! Vistual List for render a large number of differently sized rows/columns.
//! Virtual List for render a large number of differently sized rows/columns.
//!
//! > NOTE: This must ensure each column width or row height.
//!
@ -18,15 +18,15 @@ use std::{
};
use gpui::{
div, point, px, size, Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask,
Context, DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox,
Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask, Context,
DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox,
InteractiveElement, IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render,
ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement,
Styled, Window,
Styled, Window, div, point, px, size,
};
use smallvec::SmallVec;
use crate::{scroll::ScrollHandleOffsetable, AxisExt, PixelsExt};
use crate::{AxisExt, PixelsExt, scroll::ScrollbarHandle};
struct VirtualListScrollHandleState {
axis: Axis,
@ -57,7 +57,7 @@ impl AsRef<ScrollHandle> for VirtualListScrollHandle {
}
}
impl ScrollHandleOffsetable for VirtualListScrollHandle {
impl ScrollbarHandle for VirtualListScrollHandle {
fn offset(&self) -> Point<Pixels> {
self.base_handle.offset()
}

View file

@ -11,7 +11,7 @@ A comprehensive scrollable container component that provides custom scrollbars,
```rust
use gpui_component::{
scroll::{Scrollable, ScrollbarState, ScrollbarAxis, ScrollbarShow},
scroll::{Scrollable, ScrollbarAxis, ScrollbarShow},
StyledExt as _,
};
```
@ -86,10 +86,9 @@ div()
For more control, you can create scrollbars manually:
```rust
use gpui_component::scroll::{Scrollbar, ScrollbarState};
use gpui_component::scroll::{Scrollbar};
pub struct ScrollableView {
scroll_state: ScrollbarState,
scroll_handle: ScrollHandle,
}
@ -107,7 +106,7 @@ impl Render for ScrollableView {
.child("Your scrollable content")
)
.child(
Scrollbar::vertical(&self.scroll_state, &self.scroll_handle)
Scrollbar::vertical(&self.scroll_handle)
)
}
}
@ -116,63 +115,11 @@ impl Render for ScrollableView {
### Customizing Scrollbar Behavior
```rust
Scrollbar::both(&scroll_state, &scroll_handle)
Scrollbar::both(&scroll_handle)
.axis(ScrollbarAxis::Vertical)
.scroll_size(size(px(1000.), px(2000.))) // Custom content size
```
## Scroll Tracking
### ScrollbarState Management
The `ScrollbarState` tracks scrollbar visibility, hover states, and drag interactions:
```rust
use gpui_component::scroll::ScrollbarState;
pub struct MyView {
scroll_state: ScrollbarState,
}
impl MyView {
fn new(cx: &mut Context<Self>) -> Self {
Self {
scroll_state: ScrollbarState::default(),
}
}
}
```
### Responding to Scroll Events
```rust
// In your render method
div()
.on_scroll_wheel(|view, event, _, cx| {
// Handle scroll wheel events
if event.delta.y != px(0.) {
println!("Scrolled vertically: {:?}", event.delta.y);
}
})
.scrollable(Axis::Vertical)
```
### Programmatic Scrolling
```rust
// Using ScrollHandle for programmatic control
impl MyView {
fn scroll_to_top(&mut self) {
self.scroll_handle.set_offset(point(px(0.), px(0.)));
}
fn scroll_to_bottom(&mut self) {
let max_offset = self.scroll_handle.max_offset();
self.scroll_handle.set_offset(point(px(0.), max_offset.y));
}
}
```
## Virtualization
### VirtualList for Large Datasets
@ -287,110 +234,6 @@ Sync scrollbar behavior with system preferences:
Theme::sync_scrollbar_appearance(cx);
```
## Advanced Usage
### ScrollableMask for Custom Scroll Areas
For advanced scroll control over specific areas:
```rust
use gpui_component::scroll::ScrollableMask;
ScrollableMask::new(Axis::Vertical, &scroll_handle)
.debug() // Show debug borders
```
### Performance Optimization
For high-performance scrolling with many elements:
```rust
// Limit scroll update frequency
Scrollbar::vertical(&state, &handle)
.max_fps(60) // Limit to 60 FPS during drag
```
### Nested Scrollable Areas
```rust
v_flex()
.size_full()
.child(
// Outer vertical scroll
v_flex()
.flex_1()
.scrollable(Axis::Vertical)
.child(
// Inner horizontal scroll
h_flex()
.w_full()
.scrollable(Axis::Horizontal)
.child("Nested scrollable content")
)
)
```
## API Reference
### Scrollable
| Method | Description |
| -------------------- | ----------------------------- |
| `new(axis, element)` | Create scrollable wrapper |
| `vertical()` | Set vertical scrolling only |
| `horizontal()` | Set horizontal scrolling only |
| `set_axis(axis)` | Change scroll axis |
### ScrollbarAxis
| Variant | Description |
| ------------ | ---------------------------- |
| `Vertical` | Vertical scrollbar only |
| `Horizontal` | Horizontal scrollbar only |
| `Both` | Both vertical and horizontal |
### ScrollbarState
| Method | Description |
| ----------- | -------------------------- |
| `default()` | Create new scrollbar state |
### Scrollbar
| Method | Description |
| --------------------------- | --------------------------- |
| `vertical(state, handle)` | Create vertical scrollbar |
| `horizontal(state, handle)` | Create horizontal scrollbar |
| `both(state, handle)` | Create both scrollbars |
| `axis(axis)` | Set scrollbar axis |
| `scroll_size(size)` | Set custom content size |
### VirtualListScrollHandle
| Method | Description |
| --------------------------------- | ------------------------- |
| `new()` | Create new handle |
| `scroll_to_item(index, strategy)` | Scroll to specific item |
| `offset()` | Get current scroll offset |
| `set_offset(point)` | Set scroll position |
| `content_size()` | Get total content size |
### ScrollStrategy
| Variant | Description |
| -------- | -------------------- |
| `Top` | Align item to top |
| `Center` | Center item in view |
| `Bottom` | Align item to bottom |
### ScrollbarShow
| Variant | Description |
| ----------- | ----------------------- |
| `Scrolling` | Show only during scroll |
| `Hover` | Show on hover |
| `Always` | Always visible |
## Examples
### File Browser with Scrolling
@ -398,7 +241,6 @@ v_flex()
```rust
pub struct FileBrowser {
files: Vec<String>,
scroll_state: ScrollbarState,
}
impl Render for FileBrowser {
@ -482,19 +324,3 @@ impl Render for DataTable {
}
}
```
## Performance Tips
1. **Use Virtualization**: For lists with >100 items, use `VirtualList`
2. **Limit Scroll Updates**: Use `max_fps()` for heavy content during drag
3. **Optimize Render**: Avoid complex rendering in scroll event handlers
4. **Batch Updates**: Group multiple scroll changes together
5. **Memory Management**: Virtual lists only render visible items
## Best Practices
1. **Consistent Behavior**: Keep scroll behavior consistent across your app
2. **Visual Feedback**: Provide clear scroll indicators for long content
3. **Responsive Design**: Ensure scrolling works well on different screen sizes
4. **Error Handling**: Handle edge cases like empty content gracefully
5. **Testing**: Test with various content sizes and screen readers