scrollbar: Introduce overflow_scrollbar to adds Scrollbars to elements. (#1694)

## Description

1. Add `overflow_scrollbar`, `overflow_y_scrollbar`, `overflow_x_scroll`
to GPUI elements to let them has scrollbars. It is almost like the
`overscroll` but adds the Scrollbar.
2. And adjust the display scrollbar of margining 4px of the container.
3. Add `scrollbar`, `vertical_scrollbar`, `horizontal_scrollbar` with
your own scroll handle.

<img width="1181" height="911" alt="image"
src="https://github.com/user-attachments/assets/8b02cd54-d527-4f22-b9be-f762ede22122"
/>

## Break Changes

- There `Scrollable` trait and it `scrollable` method has been removed.

```diff
- div().id("contents").scrollable(Axis::Vertical)
+ div().id("contents").overflow_y_scrollbar()
```
This commit is contained in:
Jason Lee 2025-11-27 19:29:50 +08:00 committed by GitHub
parent 0b1fab5b08
commit 118a0f500e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 274 additions and 361 deletions

View file

@ -55,9 +55,8 @@ mod welcome_story;
use gpui::{
Action, AnyElement, AnyView, App, AppContext, Bounds, Context, Div, Entity, EventEmitter,
Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels,
Render, RenderOnce, SharedString, Size, StatefulInteractiveElement, StyleRefinement, Styled,
Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::FluentBuilder as _, px,
rems, size,
Render, RenderOnce, SharedString, Size, StyleRefinement, Styled, Window, WindowBounds,
WindowKind, WindowOptions, actions, div, prelude::FluentBuilder as _, px, rems, size,
};
pub use accordion_story::AccordionStory;
@ -121,7 +120,7 @@ use gpui_component::{
h_flex,
menu::PopupMenu,
notification::Notification,
scroll::ScrollbarShow,
scroll::{ScrollableElement as _, ScrollbarShow},
v_flex,
};
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
@ -773,22 +772,14 @@ impl Focusable for StoryContainer {
}
impl Render for StoryContainer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
div()
.id("story-container")
.size_full()
.overflow_y_scroll()
.overflow_y_scrollbar()
.p(self.paddings)
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_action_panel_info))
.on_action(cx.listener(Self::on_action_toggle_search))
.when_some(self.story.clone(), |this, story| {
this.child(
v_flex()
.id("story-children")
.w_full()
.flex_1()
.p(self.paddings)
.child(story),
)
})
.when_some(self.story.clone(), |this, story| this.child(story))
}
}

View file

@ -458,7 +458,6 @@ impl Render for ListStory {
.child(
h_flex()
.gap_2()
.flex_wrap()
.child(
Button::new("scroll-top")
.outline()

View file

@ -1,13 +1,15 @@
use std::rc::Rc;
use gpui::{
App, AppContext, Axis, Context, Entity, FocusHandle, Focusable, InteractiveElement,
IntoElement, ParentElement, Pixels, Render, Size, Styled, Window, div, px, size,
App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Pixels,
Render, Size, Styled, Window, div, px, size,
};
use gpui_component::{
ActiveTheme as _, Selectable, StyledExt as _,
ActiveTheme as _, Selectable,
button::{Button, ButtonGroup},
h_flex, v_flex,
h_flex,
scroll::ScrollableElement,
v_flex,
};
pub struct ScrollableStory {
@ -152,13 +154,12 @@ impl Render for ScrollableStory {
.min_h(px(200.))
.child(
v_flex()
.p_3()
.w(test_width)
.id("test-1")
.scrollable(Axis::Vertical)
.p_3()
.gap_1()
.overflow_y_scrollbar()
.child("Scrollable Example")
.children(self.items.iter().take(500).map(|item| {
.children(self.items.iter().map(|item| {
div()
.h(ITEM_HEIGHT)
.bg(cx.theme().background)

View file

@ -9,7 +9,7 @@ use gpui_component::{
button::{Button, ButtonGroup},
divider::Divider,
h_flex,
scroll::{Scrollbar, ScrollbarAxis},
scroll::{ScrollableElement, ScrollbarAxis},
v_flex, v_virtual_list,
};
@ -282,15 +282,7 @@ impl Render for VirtualListStory {
.border_color(cx.theme().border)
.gap_1(),
)
.child({
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::new(&self.scroll_handle).axis(self.axis))
}),
.scrollbar(&self.scroll_handle, self.axis),
),
),
)

View file

@ -1,18 +1,21 @@
use std::{rc::Rc, time::Duration};
use gpui::{
anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation, AnimationExt as _,
AnyElement, App, Axis, Bounds, BoxShadow, ClickEvent, Div, Edges, FocusHandle, Hsla,
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
RenderOnce, SharedString, StyleRefinement, Styled, Window,
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, ClickEvent, Div, Edges,
FocusHandle, Hsla, InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement,
Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla,
point, prelude::FluentBuilder, px, relative,
};
use rust_i18n::t;
use crate::{
ActiveTheme as _, IconName, Root, Sizable as _, StyledExt, WindowExt as _,
actions::{Cancel, Confirm},
animation::cubic_bezier,
button::{Button, ButtonVariant, ButtonVariants as _},
h_flex, v_flex, ActiveTheme as _, IconName, Root, Sizable as _, StyledExt, WindowExt as _,
h_flex,
scroll::ScrollableElement as _,
v_flex,
};
const CONTEXT: &str = "Dialog";
@ -496,9 +499,10 @@ impl RenderOnce for Dialog {
.child(
div().w_full().flex_1().overflow_hidden().child(
v_flex()
.id("contents")
.pl(paddings.left)
.pr(paddings.right)
.scrollable(Axis::Vertical)
.overflow_scrollbar()
.child(self.content),
),
)

View file

@ -1,8 +1,8 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, InteractiveElement as _,
IntoElement, IsZero, MouseButton, ParentElement as _, Pixels, Rems, RenderOnce,
StyleRefinement, Styled, Window, div, px, relative,
IntoElement, IsZero, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement,
Styled, Window, div, px, relative,
};
use crate::button::{Button, ButtonVariants as _};
@ -193,8 +193,6 @@ impl Input {
.unwrap_or(px(0.)),
};
const MIN_SCROLL_PADDING: Pixels = px(2.0);
v_flex()
.size_full()
.children(state.search_panel.clone())
@ -221,10 +219,10 @@ impl Input {
this.relative().child(
div()
.absolute()
.top(-paddings.top + MIN_SCROLL_PADDING)
.top(-paddings.top)
.left(left)
.right(-paddings.right + MIN_SCROLL_PADDING)
.bottom(-paddings.bottom + MIN_SCROLL_PADDING)
.right(-paddings.right)
.bottom(-paddings.bottom)
.child(scrollbar.scroll_size(scroll_size)),
)
} else {

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;
use crate::scroll::ScrollableElement;
use crate::{ActiveTheme, Icon, IconName, Sizable as _, h_flex, v_flex};
use crate::{Side, Size, StyledExt, kbd::Kbd};
use gpui::{
@ -1309,15 +1309,7 @@ impl Render for PopupMenu {
)
.when(self.scrollable, |this| {
// TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed.
this.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(&self.scroll_handle)),
)
this.vertical_scrollbar(&self.scroll_handle)
})
}
}

View file

@ -1,222 +1,199 @@
use std::{panic::Location, rc::Rc};
use crate::{StyledExt, scroll::ScrollbarHandle};
use super::{Scrollbar, ScrollbarAxis};
use gpui::{
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,
App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
prelude::FluentBuilder,
};
/// A scroll view is a container that allows the user to scroll through a large amount of content.
pub struct Scrollable<E> {
/// A trait for elements that can be made scrollable with scrollbars.
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
/// Adds a scrollbar to the element.
#[track_caller]
fn scrollbar<H: ScrollbarHandle + Clone>(
self,
scroll_handle: &H,
axis: impl Into<ScrollbarAxis>,
) -> Self {
self.child(ScrollbarLayer {
id: "scrollbar_layer".into(),
axis: axis.into(),
scroll_handle: Rc::new(scroll_handle.clone()),
})
}
/// Adds a vertical scrollbar to the element.
#[track_caller]
fn vertical_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
self.scrollbar(scroll_handle, ScrollbarAxis::Vertical)
}
/// Adds a horizontal scrollbar to the element.
#[track_caller]
fn horizontal_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
self.scrollbar(scroll_handle, ScrollbarAxis::Horizontal)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_scroll`], but adds scrollbars.
#[track_caller]
fn overflow_scrollbar(self) -> Scrollable<Self> {
Scrollable::new(self, ScrollbarAxis::Both)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_x_scroll`], but adds Horizontal scrollbar.
#[track_caller]
fn overflow_x_scrollbar(self) -> Scrollable<Self> {
Scrollable::new(self, ScrollbarAxis::Horizontal)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_y_scroll`], but adds Vertical scrollbar.
#[track_caller]
fn overflow_y_scrollbar(self) -> Scrollable<Self> {
Scrollable::new(self, ScrollbarAxis::Vertical)
}
}
/// A scrollable element wrapper that adds scrollbars to an interactive element.
#[derive(IntoElement)]
pub struct Scrollable<E: InteractiveElement + Styled + ParentElement + Element> {
id: ElementId,
element: Option<E>,
element: E,
axis: ScrollbarAxis,
/// This is a fake element to handle Styled, InteractiveElement, not used.
_element: Stateful<Div>,
}
impl<E> Scrollable<E>
where
E: Element,
E: InteractiveElement + Styled + ParentElement + Element,
{
pub(crate) fn new(axis: impl Into<ScrollbarAxis>, element: E) -> Self {
let id = ElementId::Name(SharedString::from(
format!("scrollable-{:?}", element.id(),),
));
#[track_caller]
fn new(element: E, axis: impl Into<ScrollbarAxis>) -> Self {
let caller = Location::caller();
Self {
element: Some(element),
_element: div().id("fake"),
id,
id: ElementId::CodeLocation(*caller),
element,
axis: axis.into(),
}
}
/// Set only a vertical scrollbar.
pub fn vertical(self) -> Self {
self.axis(ScrollbarAxis::Vertical)
}
/// Set only a horizontal scrollbar.
/// In current implementation, this is not supported yet.
pub fn horizontal(self) -> Self {
self.axis(ScrollbarAxis::Horizontal)
}
/// Set the axis of the scroll view.
pub fn axis(mut self, axis: impl Into<ScrollbarAxis>) -> Self {
self.axis = axis.into();
self
}
fn with_element_state<R>(
&mut self,
id: &GlobalElementId,
window: &mut Window,
cx: &mut App,
f: impl FnOnce(&mut Self, &mut ScrollViewState, &mut Window, &mut App) -> R,
) -> R {
window.with_optional_element_state::<ScrollViewState, _>(
Some(id),
|element_state, window| {
let mut element_state = element_state.unwrap().unwrap_or_default();
let result = f(self, &mut element_state, window, cx);
(result, Some(element_state))
},
)
}
}
#[doc(hidden)]
pub struct ScrollViewState {
handle: ScrollHandle,
}
impl Default for ScrollViewState {
fn default() -> Self {
Self {
handle: ScrollHandle::new(),
}
}
}
impl<E> ParentElement for Scrollable<E>
where
E: Element + ParentElement,
{
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
if let Some(element) = &mut self.element {
element.extend(elements);
}
}
}
impl<E> Styled for Scrollable<E>
where
E: Element + Styled,
E: InteractiveElement + Styled + ParentElement + Element,
{
fn style(&mut self) -> &mut StyleRefinement {
if let Some(element) = &mut self.element {
element.style()
} else {
self._element.style()
}
self.element.style()
}
}
impl<E> InteractiveElement for Scrollable<E>
impl<E> ParentElement for Scrollable<E>
where
E: Element + InteractiveElement,
E: InteractiveElement + Styled + ParentElement + Element,
{
fn interactivity(&mut self) -> &mut Interactivity {
if let Some(element) = &mut self.element {
element.interactivity()
} else {
self._element.interactivity()
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
self.element.extend(elements)
}
}
}
impl<E> StatefulInteractiveElement for Scrollable<E> where E: Element + StatefulInteractiveElement {}
impl<E> IntoElement for Scrollable<E>
impl InteractiveElement for Scrollable<Div> {
fn interactivity(&mut self) -> &mut gpui::Interactivity {
self.element.interactivity()
}
}
impl InteractiveElement for Scrollable<Stateful<Div>> {
fn interactivity(&mut self) -> &mut gpui::Interactivity {
self.element.interactivity()
}
}
impl<E> RenderOnce for Scrollable<E>
where
E: Element,
E: InteractiveElement + Styled + ParentElement + Element + 'static,
{
type Element = Self;
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let scroll_handle = window
.use_keyed_state(self.id.clone(), cx, |_, _| ScrollHandle::default())
.read(cx)
.clone();
fn into_element(self) -> Self::Element {
self
}
}
let style = self.element.style().clone();
*self.element.style() = StyleRefinement::default();
impl<E> Element for Scrollable<E>
where
E: Element,
{
type RequestLayoutState = AnyElement;
type PrepaintState = ScrollViewState;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.flex_grow = 1.0;
style.position = Position::Relative;
style.size.width = relative(1.0).into();
style.size.height = relative(1.0).into();
let axis = self.axis;
let scroll_id = self.id.clone();
let content = self.element.take().map(|c| c.into_any_element());
self.with_element_state(id.unwrap(), window, cx, |_, element_state, window, cx| {
let mut element = div()
.relative()
div()
.id(self.id)
.size_full()
.overflow_hidden()
.refine_style(&style)
.relative()
.child(
div()
.id(scroll_id)
.track_scroll(&element_state.handle)
.overflow_scroll()
.relative()
.id("scroll-area")
.flex()
.size_full()
.child(div().children(content)),
.track_scroll(&scroll_handle)
.map(|this| match self.axis {
ScrollbarAxis::Vertical => this.flex_col().overflow_y_scroll(),
ScrollbarAxis::Horizontal => this.flex_row().overflow_x_scroll(),
ScrollbarAxis::Both => this.overflow_scroll(),
})
.child(self.element.flex_1()),
)
.child(
.child(render_scrollbar(
"scrollbar",
&scroll_handle,
self.axis,
window,
cx,
))
}
}
impl ScrollableElement for Div {}
impl<E> ScrollableElement for Stateful<E>
where
E: ParentElement + Styled + Element,
Self: InteractiveElement,
{
}
#[derive(IntoElement)]
struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
id: ElementId,
axis: ScrollbarAxis,
scroll_handle: Rc<H>,
}
impl<H> RenderOnce for ScrollbarLayer<H>
where
H: ScrollbarHandle + Clone + 'static,
{
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
render_scrollbar(self.id, self.scroll_handle.as_ref(), self.axis, window, cx)
}
}
#[inline]
#[track_caller]
fn render_scrollbar<H: ScrollbarHandle + Clone>(
id: impl Into<ElementId>,
scroll_handle: &H,
axis: ScrollbarAxis,
window: &mut Window,
cx: &mut App,
) -> Div {
// Do not render scrollbar when inspector is picking elements,
// to allow us to pick the background elements.
let is_inspector_picking = window.is_inspector_picking(cx);
if is_inspector_picking {
return div();
}
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::new(&element_state.handle).axis(axis)),
)
.into_any_element();
let element_id = element.request_layout(window, cx);
let layout_id = window.request_layout(style, vec![element_id], cx);
(layout_id, element)
})
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
element.prepaint(window, cx);
// do nothing
ScrollViewState::default()
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx)
}
.child(Scrollbar::new(scroll_handle).id(id).axis(axis))
}

View file

@ -18,16 +18,16 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
const WIDTH: Pixels = px(2. * 2. + 8.);
const WIDTH: Pixels = px(4. * 2. + 8.);
const MIN_THUMB_SIZE: f32 = 48.;
const THUMB_WIDTH: Pixels = px(6.);
const THUMB_RADIUS: Pixels = px(6. / 2.);
const THUMB_INSET: Pixels = px(2.);
const THUMB_INSET: Pixels = px(4.);
const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
const THUMB_ACTIVE_INSET: Pixels = px(2.);
const THUMB_ACTIVE_INSET: Pixels = px(4.);
const FADE_OUT_DURATION: f32 = 3.0;
const FADE_OUT_DELAY: f32 = 2.0;

View file

@ -9,7 +9,7 @@ use crate::{
button::{Button, ButtonVariants},
h_flex,
label::Label,
scroll::Scrollbar,
scroll::ScrollableElement,
setting::{RenderOptions, SettingGroup, settings::SettingsState},
v_flex,
};
@ -177,15 +177,7 @@ impl SettingPage {
})
.size_full(),
)
.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(&list_state)),
),
.vertical_scrollbar(&list_state),
)
}
}

View file

@ -1,19 +1,21 @@
use std::{rc::Rc, time::Duration};
use gpui::{
anchored, div, point, prelude::FluentBuilder as _, px, Animation, AnimationExt as _,
AnyElement, App, Axis, ClickEvent, DefiniteLength, DismissEvent, Div, EventEmitter,
FocusHandle, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement,
Pixels, RenderOnce, Styled, Window,
Animation, AnimationExt as _, AnyElement, App,ClickEvent, DefiniteLength, DismissEvent, Div,
EventEmitter, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
ParentElement, Pixels, RenderOnce, Styled, Window, anchored, div, point,
prelude::FluentBuilder as _, px,
};
use crate::{
ActiveTheme, IconName, Placement, Sizable, StyledExt as _, WindowExt as _,
actions::Cancel,
button::{Button, ButtonVariants as _},
dialog::overlay_color,
h_flex,
scroll::ScrollableElement as _,
title_bar::TITLE_BAR_HEIGHT,
v_flex, ActiveTheme, IconName, Placement, Sizable, StyledExt as _, WindowExt as _,
v_flex,
};
const CONTEXT: &str = "Sheet";
@ -217,10 +219,7 @@ impl RenderOnce for Sheet {
)
.child(
// Body
div()
.flex_1()
.overflow_hidden()
.child(v_flex().scrollable(Axis::Vertical).child(self.content)),
div().flex_1().overflow_scrollbar().child(self.content),
)
.when_some(self.footer, |this, footer| {
// Footer

View file

@ -1,13 +1,14 @@
use crate::{
ActiveTheme, Collapsible, Icon, IconName, Side, Sizable, StyledExt,
button::{Button, ButtonVariants},
h_flex,
scroll::ScrollbarAxis,
v_flex, ActiveTheme, Collapsible, Icon, IconName, Side, Sizable, StyledExt,
scroll::ScrollableElement,
v_flex,
};
use gpui::{
div, prelude::FluentBuilder, px, AnyElement, App, ClickEvent, EdgesRefinement,
InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce, StyleRefinement,
Styled, Window,
AnyElement, App, ClickEvent, EdgesRefinement, InteractiveElement as _, IntoElement,
ParentElement, Pixels, RenderOnce, StyleRefinement, Styled, Window, div,
prelude::FluentBuilder, px,
};
use std::rc::Rc;
@ -219,6 +220,7 @@ impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> {
.child(
v_flex().id("content").flex_1().min_h_0().child(
v_flex()
.id("inner")
.gap_3()
.p_3()
.when(self.collapsed, |this| this.p_2())
@ -228,7 +230,7 @@ impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> {
.enumerate()
.map(|(ix, c)| div().id(ix).child(c.collapsed(self.collapsed))),
)
.scrollable(ScrollbarAxis::Vertical),
.overflow_y_scrollbar(),
),
)
.when_some(self.footer.take(), |this, footer| {

View file

@ -1,10 +1,7 @@
use crate::{
ActiveTheme, PixelsExt as _,
scroll::{Scrollable, ScrollbarAxis},
};
use crate::{ActiveTheme, PixelsExt as _};
use gpui::{
App, BoxShadow, Corners, DefiniteLength, Div, Edges, Element, FocusHandle, Hsla, ParentElement,
Pixels, Refineable, StyleRefinement, Styled, Window, div, point, px,
App, BoxShadow, Corners, DefiniteLength, Div, Edges, FocusHandle, Hsla, ParentElement, Pixels,
Refineable, StyleRefinement, Styled, Window, div, point, px,
};
use serde::{Deserialize, Serialize};
@ -165,17 +162,6 @@ pub trait StyledExt: Styled + Sized {
self.border_1().border_color(cx.theme().ring)
}
/// Wraps the element in a ScrollView.
///
/// Current this is only have a vertical scrollbar.
#[inline]
fn scrollable(self, axis: impl Into<ScrollbarAxis>) -> Scrollable<Self>
where
Self: Element,
{
Scrollable::new(axis, self)
}
font_weight!(font_thin, THIN);
font_weight!(font_extralight, EXTRA_LIGHT);
font_weight!(font_light, LIGHT);

View file

@ -14,7 +14,7 @@ use gpui::{
use smol::stream::StreamExt;
use crate::highlighter::HighlightTheme;
use crate::scroll::Scrollbar;
use crate::scroll::ScrollableElement;
use crate::{ActiveTheme, StyledExt, v_flex};
use crate::{
global_state::GlobalState,
@ -626,17 +626,7 @@ impl Element for TextView {
state: self.state.clone(),
})
.refine_style(&self.style)
.when(self.scrollable, |this| {
this.child(
div()
.absolute()
.w(Scrollbar::width())
.top_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(list_state)),
)
})
.vertical_scrollbar(list_state)
.into_any_element();
let layout_id = el.request_layout(window, cx);
(layout_id, el)

View file

@ -11,7 +11,7 @@ use crate::{
StyledExt,
actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp},
list::ListItem,
scroll::Scrollbar,
scroll::ScrollableElement,
};
const CONTEXT: &str = "Tree";
@ -347,11 +347,7 @@ impl Render for TreeState {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let render_item = self.render_item.clone();
div()
.id("tree-state")
.size_full()
.relative()
.child(
div().id("tree-state").size_full().relative().child(
uniform_list("entries", self.entries.len(), {
cx.processor(move |state, visible_range: Range<usize>, window, cx| {
let mut items = Vec::with_capacity(visible_range.len());
@ -386,15 +382,6 @@ impl Render for TreeState {
.with_sizing_behavior(ListSizingBehavior::Auto)
.into_any_element(),
)
.child(
div()
.absolute()
.top_0()
.right_0()
.bottom_0()
.w(Scrollbar::width())
.child(Scrollbar::vertical(&self.scroll_handle)),
)
}
}
@ -432,6 +419,7 @@ impl Styled for Tree {
impl RenderOnce for Tree {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let focus_handle = self.state.read(cx).focus_handle.clone();
let scroll_handle = self.state.read(cx).scroll_handle.clone();
self.state
.update(cx, |state, _| state.render_item = self.render_item);
@ -448,6 +436,7 @@ impl RenderOnce for Tree {
.size_full()
.child(self.state)
.refine_style(&self.style)
.vertical_scrollbar(&scroll_handle)
}
}

View file

@ -11,7 +11,7 @@ A comprehensive scrollable container component that provides custom scrollbars,
```rust
use gpui_component::{
scroll::{Scrollable, ScrollbarAxis, ScrollbarShow},
scroll::{ScrollableElement, ScrollbarAxis, ScrollbarShow},
StyledExt as _,
};
```
@ -20,21 +20,31 @@ use gpui_component::{
### Basic Scrollable Container
The simplest way to make any element scrollable is using the `scrollable()` method from `StyledExt`:
The simplest way to make any element scrollable is using the `overflow_scrollbar()` method from `ScrollableElement` trait.
This method is almost like the `overflow_scroll()` method, but it adds scrollbars.
- `overflow_scrollbar()` - Adds scrollbars for both axes as needed.
- `overflow_x_scrollbar()` - Adds horizontal scrollbar as needed.
- `overflow_y_scrollbar()` - Adds vertical scrollbar as needed.
```rust
use gpui::{div, Axis};
use gpui_component::ScrollableElement;
div()
.id("scrollable-container")
.size_full()
.child("Your content here")
.scrollable(Axis::Vertical)
.overflow_scrollbar()
```
### Scrollable with Content
### Vertical Scrolling
```rust
v_flex()
.id("scrollable-container")
.overflow_y_scrollbar()
.gap_2()
.p_4()
.child("Scrollable Content")
@ -45,13 +55,14 @@ v_flex()
.bg(cx.theme().secondary)
.child(format!("Item {}", i))
}))
.scrollable(Axis::Vertical)
```
### Horizontal Scrolling
```rust
h_flex()
.id("scrollable-container")
.overflow_x_scrollbar()
.gap_2()
.p_4()
.children((0..50).map(|i| {
@ -61,14 +72,15 @@ h_flex()
.bg(cx.theme().accent)
.child(format!("Card {}", i))
}))
.scrollable(Axis::Horizontal)
```
### Both Directions
```rust
div()
.id("scrollable-container")
.size_full()
.overflow_scrollbar()
.child(
div()
.w(px(2000.)) // Wide content
@ -76,7 +88,6 @@ div()
.bg(cx.theme().background)
.child("Large content area")
)
.scrollable(ScrollbarAxis::Both)
```
## Custom Scrollbars
@ -86,7 +97,7 @@ div()
For more control, you can create scrollbars manually:
```rust
use gpui_component::scroll::{Scrollbar};
use gpui_component::scroll::{ScrollableElement};
pub struct ScrollableView {
scroll_handle: ScrollHandle,
@ -105,21 +116,11 @@ impl Render for ScrollableView {
.size_full()
.child("Your scrollable content")
)
.child(
Scrollbar::vertical(&self.scroll_handle)
)
.vertical_scrollbar(&self.scroll_handle)
}
}
```
### Customizing Scrollbar Behavior
```rust
Scrollbar::both(&scroll_handle)
.axis(ScrollbarAxis::Vertical)
.scroll_size(size(px(1000.), px(2000.))) // Custom content size
```
## Virtualization
### VirtualList for Large Datasets
@ -253,6 +254,7 @@ impl Render for FileBrowser {
v_flex()
.gap_1()
.p_2()
.overflow_y_scrollbar()
.children(self.files.iter().map(|file| {
div()
.h(px(32.))
@ -263,7 +265,6 @@ impl Render for FileBrowser {
.hover(|style| style.bg(cx.theme().secondary_hover))
.child(file.clone())
}))
.scrollable(Axis::Vertical)
)
}
}