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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,20 +5,21 @@ use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
use crate::input::InputState; use crate::input::InputState;
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache}; use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
use crate::{ use crate::{
ActiveTheme, IconName, Size,
input::{Input, InputEvent}, input::{Input, InputEvent},
scroll::{Scrollbar, ScrollbarState}, scroll::Scrollbar,
v_flex, ActiveTheme, IconName, Size, v_flex,
}; };
use crate::{list::ListDelegate, v_virtual_list, VirtualListScrollHandle};
use crate::{Icon, IndexPath, Selectable, Sizable, StyledExt}; use crate::{Icon, IndexPath, Selectable, Sizable, StyledExt};
use crate::{VirtualListScrollHandle, list::ListDelegate, v_virtual_list};
use gpui::{ use gpui::{
div, prelude::FluentBuilder, AppContext, Entity, FocusHandle, Focusable, InteractiveElement, App, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, EventEmitter,
IntoElement, KeyBinding, Length, MouseButton, ParentElement, Render, Styled, Task, Window, ListSizingBehavior, RenderOnce, ScrollStrategy, SharedString, StatefulInteractiveElement,
StyleRefinement, Subscription, px, size,
}; };
use gpui::{ use gpui::{
px, size, App, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, AppContext, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding,
EventEmitter, ListSizingBehavior, RenderOnce, ScrollStrategy, SharedString, Length, MouseButton, ParentElement, Render, Styled, Task, Window, div, prelude::FluentBuilder,
StatefulInteractiveElement, StyleRefinement, Subscription,
}; };
use rust_i18n::t; use rust_i18n::t;
use smol::Timer; use smol::Timer;
@ -72,7 +73,6 @@ pub struct ListState<D: ListDelegate> {
delegate: D, delegate: D,
last_query: Option<String>, last_query: Option<String>,
scroll_handle: VirtualListScrollHandle, scroll_handle: VirtualListScrollHandle,
scroll_state: ScrollbarState,
rows_cache: RowsCache, rows_cache: RowsCache,
selected_index: Option<IndexPath>, selected_index: Option<IndexPath>,
item_to_measure_index: IndexPath, item_to_measure_index: IndexPath,
@ -111,7 +111,6 @@ where
deferred_scroll_to_index: None, deferred_scroll_to_index: None,
mouse_right_clicked_index: None, mouse_right_clicked_index: None,
scroll_handle: VirtualListScrollHandle::new(), scroll_handle: VirtualListScrollHandle::new(),
scroll_state: ScrollbarState::default(),
reset_on_cancel: true, reset_on_cancel: true,
_search_task: Task::ready(()), _search_task: Task::ready(()),
_load_more_task: Task::ready(()), _load_more_task: Task::ready(()),
@ -484,7 +483,6 @@ where
let rows_cache = self.rows_cache.clone(); let rows_cache = self.rows_cache.clone();
let scrollbar_visible = self.options.scrollbar_visible; let scrollbar_visible = self.options.scrollbar_visible;
let scroll_handle = self.scroll_handle.clone(); let scroll_handle = self.scroll_handle.clone();
let scroll_state = self.scroll_state.clone();
let measured_size = rows_cache.measured_size(); let measured_size = rows_cache.measured_size();
v_flex() v_flex()
@ -550,7 +548,7 @@ where
} }
}) })
.when(scrollbar_visible, |this| { .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::{Cancel, Confirm, SelectDown, SelectUp};
use crate::actions::{SelectLeft, SelectRight}; use crate::actions::{SelectLeft, SelectRight};
use crate::menu::menu_item::MenuItemElement; 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::{ActiveTheme, Icon, IconName, Sizable as _, h_flex, v_flex};
use crate::{Side, Size, StyledExt, kbd::Kbd}; use crate::{Side, Size, StyledExt, kbd::Kbd};
use gpui::{ use gpui::{
@ -287,7 +287,6 @@ pub struct PopupMenu {
scrollable: bool, scrollable: bool,
external_link_icon: bool, external_link_icon: bool,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
scroll_state: ScrollbarState,
// This will update on render // This will update on render
submenu_anchor: (Corner, Pixels), submenu_anchor: (Corner, Pixels),
@ -309,7 +308,6 @@ impl PopupMenu {
bounds: Bounds::default(), bounds: Bounds::default(),
scrollable: false, scrollable: false,
scroll_handle: ScrollHandle::default(), scroll_handle: ScrollHandle::default(),
scroll_state: ScrollbarState::default(),
external_link_icon: true, external_link_icon: true,
size: Size::default(), size: Size::default(),
submenu_anchor: (Corner::TopLeft, Pixels::ZERO), submenu_anchor: (Corner::TopLeft, Pixels::ZERO),
@ -1318,7 +1316,7 @@ impl Render for PopupMenu {
.left_0() .left_0()
.right_0() .right_0()
.bottom_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::{ use gpui::{
div, relative, AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId, AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId, InspectorElementId,
InspectorElementId, InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement, InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement, Pixels, Position,
Pixels, Position, ScrollHandle, SharedString, Stateful, StatefulInteractiveElement, Style, ScrollHandle, SharedString, Stateful, StatefulInteractiveElement, Style, StyleRefinement,
StyleRefinement, Styled, Window, Styled, Window, div, relative,
}; };
/// A scroll view is a container that allows the user to scroll through a large amount of content. /// 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)] #[doc(hidden)]
pub struct ScrollViewState { pub struct ScrollViewState {
state: ScrollbarState,
handle: ScrollHandle, handle: ScrollHandle,
} }
@ -77,7 +76,6 @@ impl Default for ScrollViewState {
fn default() -> Self { fn default() -> Self {
Self { Self {
handle: ScrollHandle::new(), handle: ScrollHandle::new(),
state: ScrollbarState::default(),
} }
} }
} }
@ -184,9 +182,7 @@ where
.left_0() .left_0()
.right_0() .right_0()
.bottom_0() .bottom_0()
.child( .child(Scrollbar::new(&element_state.handle).axis(axis)),
Scrollbar::both(&element_state.state, &element_state.handle).axis(axis),
),
) )
.into_any_element(); .into_any_element();

View file

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

View file

@ -1,16 +1,17 @@
use gpui::{ use gpui::{
div, list, prelude::FluentBuilder as _, px, App, Entity, InteractiveElement as _, IntoElement, App, Entity, InteractiveElement as _, IntoElement, ListAlignment, ListState,
ListAlignment, ListState, ParentElement as _, SharedString, Styled, Window, ParentElement as _, SharedString, Styled, Window, div, list, prelude::FluentBuilder as _, px,
}; };
use rust_i18n::t; use rust_i18n::t;
use crate::{ use crate::{
ActiveTheme, IconName, Sizable,
button::{Button, ButtonVariants}, button::{Button, ButtonVariants},
h_flex, h_flex,
label::Label, label::Label,
scroll::{Scrollbar, ScrollbarState}, scroll::Scrollbar,
setting::{settings::SettingsState, RenderOptions, SettingGroup}, setting::{RenderOptions, SettingGroup, settings::SettingsState},
v_flex, ActiveTheme, IconName, Sizable, v_flex,
}; };
/// A setting page that can contain multiple setting groups. /// A setting page that can contain multiple setting groups.
@ -100,16 +101,11 @@ impl SettingPage {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let groups_count = groups.len(); let groups_count = groups.len();
let (scroll_state, list_state) = window let list_state = window
.use_keyed_state( .use_keyed_state(
SharedString::from(format!("list-state:{}", ix)), SharedString::from(format!("list-state:{}", ix)),
cx, cx,
|_, _| { |_, _| ListState::new(groups_count, ListAlignment::Top, px(100.)),
(
ScrollbarState::default(),
ListState::new(groups_count, ListAlignment::Top, px(100.)),
)
},
) )
.read(cx) .read(cx)
.clone(); .clone();
@ -188,7 +184,7 @@ impl SettingPage {
.left_0() .left_0()
.right_0() .right_0()
.bottom_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}, actions::{Cancel, SelectDown, SelectUp},
h_flex, h_flex,
menu::{ContextMenuExt, PopupMenu}, menu::{ContextMenuExt, PopupMenu},
scroll::{ScrollableMask, Scrollbar, ScrollbarState}, scroll::{ScrollableMask, Scrollbar},
v_flex, v_flex,
}; };
use gpui::{ use gpui::{
@ -95,9 +95,7 @@ pub struct TableState<D: TableDelegate> {
pub col_fixed: bool, pub col_fixed: bool,
pub vertical_scroll_handle: UniformListScrollHandle, pub vertical_scroll_handle: UniformListScrollHandle,
pub vertical_scroll_state: ScrollbarState,
pub horizontal_scroll_handle: VirtualListScrollHandle, pub horizontal_scroll_handle: VirtualListScrollHandle,
pub horizontal_scroll_state: ScrollbarState,
selected_row: Option<usize>, selected_row: Option<usize>,
selection_state: SelectionState, selection_state: SelectionState,
@ -127,8 +125,6 @@ where
col_groups: Vec::new(), col_groups: Vec::new(),
horizontal_scroll_handle: VirtualListScrollHandle::new(), horizontal_scroll_handle: VirtualListScrollHandle::new(),
vertical_scroll_handle: UniformListScrollHandle::new(), vertical_scroll_handle: UniformListScrollHandle::new(),
vertical_scroll_state: ScrollbarState::default(),
horizontal_scroll_state: ScrollbarState::default(),
selection_state: SelectionState::Row, selection_state: SelectionState::Row,
selected_row: None, selected_row: None,
right_clicked_row: None, right_clicked_row: None,
@ -1209,10 +1205,7 @@ where
.right_0() .right_0()
.bottom_0() .bottom_0()
.w(Scrollbar::width()) .w(Scrollbar::width())
.child( .child(Scrollbar::vertical(&self.vertical_scroll_handle).max_fps(60)),
Scrollbar::vertical(&self.vertical_scroll_state, &self.vertical_scroll_handle)
.max_fps(60),
),
) )
} }
@ -1228,10 +1221,7 @@ where
.right_0() .right_0()
.bottom_0() .bottom_0()
.h(Scrollbar::width()) .h(Scrollbar::width())
.child(Scrollbar::horizontal( .child(Scrollbar::horizontal(&self.horizontal_scroll_handle))
&self.horizontal_scroll_state,
&self.horizontal_scroll_handle,
))
} }
} }

View file

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

View file

@ -1,17 +1,17 @@
use std::{cell::RefCell, ops::Range, rc::Rc}; use std::{cell::RefCell, ops::Range, rc::Rc};
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, uniform_list, App, Context, ElementId, Entity, FocusHandle, App, Context, ElementId, Entity, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding,
InteractiveElement as _, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, SharedString,
ParentElement, Render, RenderOnce, SharedString, StyleRefinement, Styled, StyleRefinement, Styled, UniformListScrollHandle, Window, div, prelude::FluentBuilder as _,
UniformListScrollHandle, Window, uniform_list,
}; };
use crate::{ use crate::{
StyledExt,
actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp}, actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp},
list::ListItem, list::ListItem,
scroll::{Scrollbar, ScrollbarState}, scroll::Scrollbar,
StyledExt,
}; };
const CONTEXT: &str = "Tree"; const CONTEXT: &str = "Tree";
@ -179,7 +179,6 @@ impl TreeItem {
pub struct TreeState { pub struct TreeState {
focus_handle: FocusHandle, focus_handle: FocusHandle,
entries: Vec<TreeEntry>, entries: Vec<TreeEntry>,
scrollbar_state: ScrollbarState,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>, selected_ix: Option<usize>,
render_item: Rc<dyn Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem>, render_item: Rc<dyn Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem>,
@ -191,7 +190,6 @@ impl TreeState {
Self { Self {
selected_ix: None, selected_ix: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
scrollbar_state: ScrollbarState::default(),
scroll_handle: UniformListScrollHandle::default(), scroll_handle: UniformListScrollHandle::default(),
entries: Vec::new(), entries: Vec::new(),
render_item: Rc::new(|_, _, _, _, _| ListItem::new(0)), render_item: Rc::new(|_, _, _, _, _| ListItem::new(0)),
@ -395,10 +393,7 @@ impl Render for TreeState {
.right_0() .right_0()
.bottom_0() .bottom_0()
.w(Scrollbar::width()) .w(Scrollbar::width())
.child(Scrollbar::vertical( .child(Scrollbar::vertical(&self.scroll_handle)),
&self.scrollbar_state,
&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. //! > NOTE: This must ensure each column width or row height.
//! //!
@ -18,15 +18,15 @@ use std::{
}; };
use gpui::{ use gpui::{
div, point, px, size, Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask, Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask, Context,
Context, DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox, DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox,
InteractiveElement, IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render, InteractiveElement, IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render,
ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement, ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement,
Styled, Window, Styled, Window, div, point, px, size,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{scroll::ScrollHandleOffsetable, AxisExt, PixelsExt}; use crate::{AxisExt, PixelsExt, scroll::ScrollbarHandle};
struct VirtualListScrollHandleState { struct VirtualListScrollHandleState {
axis: Axis, axis: Axis,
@ -57,7 +57,7 @@ impl AsRef<ScrollHandle> for VirtualListScrollHandle {
} }
} }
impl ScrollHandleOffsetable for VirtualListScrollHandle { impl ScrollbarHandle for VirtualListScrollHandle {
fn offset(&self) -> Point<Pixels> { fn offset(&self) -> Point<Pixels> {
self.base_handle.offset() self.base_handle.offset()
} }

View file

@ -11,7 +11,7 @@ A comprehensive scrollable container component that provides custom scrollbars,
```rust ```rust
use gpui_component::{ use gpui_component::{
scroll::{Scrollable, ScrollbarState, ScrollbarAxis, ScrollbarShow}, scroll::{Scrollable, ScrollbarAxis, ScrollbarShow},
StyledExt as _, StyledExt as _,
}; };
``` ```
@ -86,10 +86,9 @@ div()
For more control, you can create scrollbars manually: For more control, you can create scrollbars manually:
```rust ```rust
use gpui_component::scroll::{Scrollbar, ScrollbarState}; use gpui_component::scroll::{Scrollbar};
pub struct ScrollableView { pub struct ScrollableView {
scroll_state: ScrollbarState,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
} }
@ -107,7 +106,7 @@ impl Render for ScrollableView {
.child("Your scrollable content") .child("Your scrollable content")
) )
.child( .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 ### Customizing Scrollbar Behavior
```rust ```rust
Scrollbar::both(&scroll_state, &scroll_handle) Scrollbar::both(&scroll_handle)
.axis(ScrollbarAxis::Vertical) .axis(ScrollbarAxis::Vertical)
.scroll_size(size(px(1000.), px(2000.))) // Custom content size .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 ## Virtualization
### VirtualList for Large Datasets ### VirtualList for Large Datasets
@ -287,110 +234,6 @@ Sync scrollbar behavior with system preferences:
Theme::sync_scrollbar_appearance(cx); 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 ## Examples
### File Browser with Scrolling ### File Browser with Scrolling
@ -398,7 +241,6 @@ v_flex()
```rust ```rust
pub struct FileBrowser { pub struct FileBrowser {
files: Vec<String>, files: Vec<String>,
scroll_state: ScrollbarState,
} }
impl Render for FileBrowser { 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