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

View file

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

View file

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

View file

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

View file

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

View file

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

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; use crate::scroll::ScrollableElement;
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::{
@ -1309,15 +1309,7 @@ impl Render for PopupMenu {
) )
.when(self.scrollable, |this| { .when(self.scrollable, |this| {
// TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed. // TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed.
this.child( this.vertical_scrollbar(&self.scroll_handle)
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(&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 super::{Scrollbar, ScrollbarAxis};
use gpui::{ use gpui::{
AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId, InspectorElementId, App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement, Pixels, Position, ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
ScrollHandle, SharedString, Stateful, StatefulInteractiveElement, Style, StyleRefinement, prelude::FluentBuilder,
Styled, Window, div, relative,
}; };
/// A scroll view is a container that allows the user to scroll through a large amount of content. /// A trait for elements that can be made scrollable with scrollbars.
pub struct Scrollable<E> { 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, id: ElementId,
element: Option<E>, element: E,
axis: ScrollbarAxis, axis: ScrollbarAxis,
/// This is a fake element to handle Styled, InteractiveElement, not used.
_element: Stateful<Div>,
} }
impl<E> Scrollable<E> impl<E> Scrollable<E>
where where
E: Element, E: InteractiveElement + Styled + ParentElement + Element,
{ {
pub(crate) fn new(axis: impl Into<ScrollbarAxis>, element: E) -> Self { #[track_caller]
let id = ElementId::Name(SharedString::from( fn new(element: E, axis: impl Into<ScrollbarAxis>) -> Self {
format!("scrollable-{:?}", element.id(),), let caller = Location::caller();
));
Self { Self {
element: Some(element), id: ElementId::CodeLocation(*caller),
_element: div().id("fake"), element,
id,
axis: axis.into(), 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> impl<E> Styled for Scrollable<E>
where where
E: Element + Styled, E: InteractiveElement + Styled + ParentElement + Element,
{ {
fn style(&mut self) -> &mut StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
if let Some(element) = &mut self.element { self.element.style()
element.style()
} else {
self._element.style()
}
} }
} }
impl<E> InteractiveElement for Scrollable<E> impl<E> ParentElement for Scrollable<E>
where where
E: Element + InteractiveElement, E: InteractiveElement + Styled + ParentElement + Element,
{ {
fn interactivity(&mut self) -> &mut Interactivity { fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
if let Some(element) = &mut self.element { self.element.extend(elements)
element.interactivity()
} else {
self._element.interactivity()
}
} }
} }
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 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 { let style = self.element.style().clone();
self *self.element.style() = StyleRefinement::default();
div()
.id(self.id)
.size_full()
.refine_style(&style)
.relative()
.child(
div()
.id("scroll-area")
.flex()
.size_full()
.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(render_scrollbar(
"scrollbar",
&scroll_handle,
self.axis,
window,
cx,
))
} }
} }
impl<E> Element for Scrollable<E> impl ScrollableElement for Div {}
impl<E> ScrollableElement for Stateful<E>
where where
E: Element, E: ParentElement + Styled + Element,
Self: InteractiveElement,
{ {
type RequestLayoutState = AnyElement; }
type PrepaintState = ScrollViewState;
fn id(&self) -> Option<ElementId> { #[derive(IntoElement)]
Some(self.id.clone()) struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
} id: ElementId,
axis: ScrollbarAxis,
scroll_handle: Rc<H>,
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { impl<H> RenderOnce for ScrollbarLayer<H>
None where
} H: ScrollbarHandle + Clone + 'static,
{
fn request_layout( fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
&mut self, render_scrollbar(self.id, self.scroll_handle.as_ref(), self.axis, window, cx)
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()
.size_full()
.overflow_hidden()
.child(
div()
.id(scroll_id)
.track_scroll(&element_state.handle)
.overflow_scroll()
.relative()
.size_full()
.child(div().children(content)),
)
.child(
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)
} }
} }
#[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(scroll_handle).id(id).axis(axis))
}

View file

@ -18,16 +18,16 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH) /// 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 MIN_THUMB_SIZE: f32 = 48.;
const THUMB_WIDTH: Pixels = px(6.); const THUMB_WIDTH: Pixels = px(6.);
const THUMB_RADIUS: Pixels = px(6. / 2.); 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_WIDTH: Pixels = px(8.);
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.); 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_DURATION: f32 = 3.0;
const FADE_OUT_DELAY: f32 = 2.0; const FADE_OUT_DELAY: f32 = 2.0;

View file

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

View file

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

View file

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

View file

@ -1,10 +1,7 @@
use crate::{ use crate::{ActiveTheme, PixelsExt as _};
ActiveTheme, PixelsExt as _,
scroll::{Scrollable, ScrollbarAxis},
};
use gpui::{ use gpui::{
App, BoxShadow, Corners, DefiniteLength, Div, Edges, Element, FocusHandle, Hsla, ParentElement, App, BoxShadow, Corners, DefiniteLength, Div, Edges, FocusHandle, Hsla, ParentElement, Pixels,
Pixels, Refineable, StyleRefinement, Styled, Window, div, point, px, Refineable, StyleRefinement, Styled, Window, div, point, px,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -165,17 +162,6 @@ pub trait StyledExt: Styled + Sized {
self.border_1().border_color(cx.theme().ring) 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_thin, THIN);
font_weight!(font_extralight, EXTRA_LIGHT); font_weight!(font_extralight, EXTRA_LIGHT);
font_weight!(font_light, LIGHT); font_weight!(font_light, LIGHT);

View file

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

View file

@ -11,7 +11,7 @@ use crate::{
StyledExt, StyledExt,
actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp}, actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp},
list::ListItem, list::ListItem,
scroll::Scrollbar, scroll::ScrollableElement,
}; };
const CONTEXT: &str = "Tree"; const CONTEXT: &str = "Tree";
@ -347,54 +347,41 @@ impl Render for TreeState {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let render_item = self.render_item.clone(); let render_item = self.render_item.clone();
div() div().id("tree-state").size_full().relative().child(
.id("tree-state") uniform_list("entries", self.entries.len(), {
.size_full() cx.processor(move |state, visible_range: Range<usize>, window, cx| {
.relative() let mut items = Vec::with_capacity(visible_range.len());
.child( for ix in visible_range {
uniform_list("entries", self.entries.len(), { let entry = &state.entries[ix];
cx.processor(move |state, visible_range: Range<usize>, window, cx| { let selected = Some(ix) == state.selected_ix;
let mut items = Vec::with_capacity(visible_range.len()); let item = (render_item)(ix, entry, selected, window, cx);
for ix in visible_range {
let entry = &state.entries[ix];
let selected = Some(ix) == state.selected_ix;
let item = (render_item)(ix, entry, selected, window, cx);
let el = div() let el = div()
.id(ix) .id(ix)
.child(item.disabled(entry.item().is_disabled()).selected(selected)) .child(item.disabled(entry.item().is_disabled()).selected(selected))
.when(!entry.item().is_disabled(), |this| { .when(!entry.item().is_disabled(), |this| {
this.on_mouse_down( this.on_mouse_down(
MouseButton::Left, MouseButton::Left,
cx.listener({ cx.listener({
move |this, _, window, cx| { move |this, _, window, cx| {
this.on_entry_click(ix, window, cx); this.on_entry_click(ix, window, cx);
} }
}), }),
) )
}); });
items.push(el) items.push(el)
} }
items items
})
}) })
.flex_grow() })
.size_full() .flex_grow()
.track_scroll(self.scroll_handle.clone()) .size_full()
.with_sizing_behavior(ListSizingBehavior::Auto) .track_scroll(self.scroll_handle.clone())
.into_any_element(), .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 { impl RenderOnce for Tree {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let focus_handle = self.state.read(cx).focus_handle.clone(); let focus_handle = self.state.read(cx).focus_handle.clone();
let scroll_handle = self.state.read(cx).scroll_handle.clone();
self.state self.state
.update(cx, |state, _| state.render_item = self.render_item); .update(cx, |state, _| state.render_item = self.render_item);
@ -448,6 +436,7 @@ impl RenderOnce for Tree {
.size_full() .size_full()
.child(self.state) .child(self.state)
.refine_style(&self.style) .refine_style(&self.style)
.vertical_scrollbar(&scroll_handle)
} }
} }

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, ScrollbarAxis, ScrollbarShow}, scroll::{ScrollableElement, ScrollbarAxis, ScrollbarShow},
StyledExt as _, StyledExt as _,
}; };
``` ```
@ -20,21 +20,31 @@ use gpui_component::{
### Basic Scrollable Container ### 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 ```rust
use gpui::{div, Axis}; use gpui::{div, Axis};
use gpui_component::ScrollableElement;
div() div()
.id("scrollable-container")
.size_full() .size_full()
.child("Your content here") .child("Your content here")
.scrollable(Axis::Vertical) .overflow_scrollbar()
``` ```
### Scrollable with Content ### Vertical Scrolling
```rust ```rust
v_flex() v_flex()
.id("scrollable-container")
.overflow_y_scrollbar()
.gap_2() .gap_2()
.p_4() .p_4()
.child("Scrollable Content") .child("Scrollable Content")
@ -45,13 +55,14 @@ v_flex()
.bg(cx.theme().secondary) .bg(cx.theme().secondary)
.child(format!("Item {}", i)) .child(format!("Item {}", i))
})) }))
.scrollable(Axis::Vertical)
``` ```
### Horizontal Scrolling ### Horizontal Scrolling
```rust ```rust
h_flex() h_flex()
.id("scrollable-container")
.overflow_x_scrollbar()
.gap_2() .gap_2()
.p_4() .p_4()
.children((0..50).map(|i| { .children((0..50).map(|i| {
@ -61,14 +72,15 @@ h_flex()
.bg(cx.theme().accent) .bg(cx.theme().accent)
.child(format!("Card {}", i)) .child(format!("Card {}", i))
})) }))
.scrollable(Axis::Horizontal)
``` ```
### Both Directions ### Both Directions
```rust ```rust
div() div()
.id("scrollable-container")
.size_full() .size_full()
.overflow_scrollbar()
.child( .child(
div() div()
.w(px(2000.)) // Wide content .w(px(2000.)) // Wide content
@ -76,7 +88,6 @@ div()
.bg(cx.theme().background) .bg(cx.theme().background)
.child("Large content area") .child("Large content area")
) )
.scrollable(ScrollbarAxis::Both)
``` ```
## Custom Scrollbars ## Custom Scrollbars
@ -86,7 +97,7 @@ div()
For more control, you can create scrollbars manually: For more control, you can create scrollbars manually:
```rust ```rust
use gpui_component::scroll::{Scrollbar}; use gpui_component::scroll::{ScrollableElement};
pub struct ScrollableView { pub struct ScrollableView {
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
@ -105,21 +116,11 @@ impl Render for ScrollableView {
.size_full() .size_full()
.child("Your scrollable content") .child("Your scrollable content")
) )
.child( .vertical_scrollbar(&self.scroll_handle)
Scrollbar::vertical(&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 ## Virtualization
### VirtualList for Large Datasets ### VirtualList for Large Datasets
@ -253,6 +254,7 @@ impl Render for FileBrowser {
v_flex() v_flex()
.gap_1() .gap_1()
.p_2() .p_2()
.overflow_y_scrollbar()
.children(self.files.iter().map(|file| { .children(self.files.iter().map(|file| {
div() div()
.h(px(32.)) .h(px(32.))
@ -263,7 +265,6 @@ impl Render for FileBrowser {
.hover(|style| style.bg(cx.theme().secondary_hover)) .hover(|style| style.bg(cx.theme().secondary_hover))
.child(file.clone()) .child(file.clone())
})) }))
.scrollable(Axis::Vertical)
) )
} }
} }