From 4a3f520c5f67eb0882e8294d422d5da24bf43145 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 27 Nov 2025 11:39:58 +0800 Subject: [PATCH] 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) ``` --- crates/story/src/scrollable_story.rs | 12 +- crates/story/src/virtual_list_story.rs | 11 +- crates/ui/src/dock/tiles.rs | 6 +- crates/ui/src/input/input.rs | 14 +- crates/ui/src/input/state.rs | 4 +- crates/ui/src/list/list.rs | 22 ++- crates/ui/src/menu/popup_menu.rs | 6 +- crates/ui/src/scroll/scrollable.rs | 16 +-- crates/ui/src/scroll/scrollbar.rs | 106 +++++++------- crates/ui/src/setting/page.rs | 22 ++- crates/ui/src/table/state.rs | 16 +-- crates/ui/src/text/text_view.rs | 22 ++- crates/ui/src/tree.rs | 19 +-- crates/ui/src/virtual_list.rs | 12 +- docs/docs/components/scrollable.md | 182 +------------------------ 15 files changed, 129 insertions(+), 341 deletions(-) diff --git a/crates/story/src/scrollable_story.rs b/crates/story/src/scrollable_story.rs index 19d140cf..719a2547 100644 --- a/crates/story/src/scrollable_story.rs +++ b/crates/story/src/scrollable_story.rs @@ -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, item_sizes: Rc>>, 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::>() .into(); - self.scroll_state = ScrollbarState::default(); cx.notify(); } diff --git a/crates/story/src/virtual_list_story.rs b/crates/story/src/virtual_list_story.rs index ddf97b11..497cc35e 100644 --- a/crates/story/src/virtual_list_story.rs +++ b/crates/story/src/virtual_list_story.rs @@ -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, item_sizes: Rc>>, 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)) }), ), ), diff --git a/crates/ui/src/dock/tiles.rs b/crates/ui/src/dock/tiles.rs index ced5b6bf..eaf2ed83 100644 --- a/crates/ui/src/dock/tiles.rs +++ b/crates/ui/src/dock/tiles.rs @@ -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, bounds: Bounds, history: History, - scroll_state: ScrollbarState, scroll_handle: ScrollHandle, scrollbar_show: Option, } @@ -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) diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 0d78a20a..df3d3f54 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -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( diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 8254f164..b8a5a67d 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -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>, - pub(super) scroll_state: ScrollbarState, /// The size of the scrollable content. pub(crate) scroll_size: gpui::Size, @@ -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, diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 3f390e3b..e062e212 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -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 { delegate: D, last_query: Option, scroll_handle: VirtualListScrollHandle, - scroll_state: ScrollbarState, rows_cache: RowsCache, selected_index: Option, 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)) }) } } diff --git a/crates/ui/src/menu/popup_menu.rs b/crates/ui/src/menu/popup_menu.rs index c74512f2..fb2bdb51 100644 --- a/crates/ui/src/menu/popup_menu.rs +++ b/crates/ui/src/menu/popup_menu.rs @@ -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)), ) }) } diff --git a/crates/ui/src/scroll/scrollable.rs b/crates/ui/src/scroll/scrollable.rs index 9c4f53fe..03f72f5f 100644 --- a/crates/ui/src/scroll/scrollable.rs +++ b/crates/ui/src/scroll/scrollable.rs @@ -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(); diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index de427b1b..21c3451d 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -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; /// 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 { self.offset() } @@ -81,7 +82,7 @@ impl ScrollHandleOffsetable for ScrollHandle { } } -impl ScrollHandleOffsetable for UniformListScrollHandle { +impl ScrollbarHandle for UniformListScrollHandle { fn offset(&self) -> Point { 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 { self.scroll_px_offset_for_scrollbar() } @@ -120,11 +121,11 @@ impl ScrollHandleOffsetable for ListState { #[doc(hidden)] #[derive(Debug, Clone)] -pub struct ScrollbarState(Rc>); +struct ScrollbarState(Rc>); #[doc(hidden)] #[derive(Debug, Clone, Copy)] -pub struct ScrollbarStateInner { +struct ScrollbarStateInner { hovered_axis: Option, hovered_on_thumb: Option, dragged_axis: Option, @@ -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, - scroll_handle: Rc, - state: ScrollbarState, + scroll_handle: Rc, scroll_size: Option>, /// 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, - 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(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(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(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) -> 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, } @@ -499,7 +498,7 @@ impl Element for Scrollbar { type PrepaintState = PrepaintState; fn id(&self) -> Option { - 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() { diff --git a/crates/ui/src/setting/page.rs b/crates/ui/src/setting/page.rs index ffbb45e4..593dee3e 100644 --- a/crates/ui/src/setting/page.rs +++ b/crates/ui/src/setting/page.rs @@ -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::>(); 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)), ), ) } diff --git a/crates/ui/src/table/state.rs b/crates/ui/src/table/state.rs index 716520c8..eb4343f5 100644 --- a/crates/ui/src/table/state.rs +++ b/crates/ui/src/table/state.rs @@ -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 { 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, 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)) } } diff --git a/crates/ui/src/text/text_view.rs b/crates/ui/src/text/text_view.rs index d0e4ad17..b82ff30b 100644 --- a/crates/ui/src/text/text_view.rs +++ b/crates/ui/src/text/text_view.rs @@ -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() { diff --git a/crates/ui/src/tree.rs b/crates/ui/src/tree.rs index d1411813..9c9003c0 100644 --- a/crates/ui/src/tree.rs +++ b/crates/ui/src/tree.rs @@ -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, - scrollbar_state: ScrollbarState, scroll_handle: UniformListScrollHandle, selected_ix: Option, render_item: Rc 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)), ) } } diff --git a/crates/ui/src/virtual_list.rs b/crates/ui/src/virtual_list.rs index 68dbbdf5..5a43157d 100644 --- a/crates/ui/src/virtual_list.rs +++ b/crates/ui/src/virtual_list.rs @@ -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 for VirtualListScrollHandle { } } -impl ScrollHandleOffsetable for VirtualListScrollHandle { +impl ScrollbarHandle for VirtualListScrollHandle { fn offset(&self) -> Point { self.base_handle.offset() } diff --git a/docs/docs/components/scrollable.md b/docs/docs/components/scrollable.md index 7f4b579b..30e07739 100644 --- a/docs/docs/components/scrollable.md +++ b/docs/docs/components/scrollable.md @@ -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 { - 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, - 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