gpui_component/scroll/
scrollable.rs

1use std::{panic::Location, rc::Rc};
2
3use crate::{StyledExt, scroll::ScrollbarHandle};
4
5use super::{Scrollbar, ScrollbarAxis};
6use gpui::{
7    App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
8    ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
9    prelude::FluentBuilder,
10};
11
12/// A trait for elements that can be made scrollable with scrollbars.
13pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
14    /// Adds a scrollbar to the element.
15    #[track_caller]
16    fn scrollbar<H: ScrollbarHandle + Clone>(
17        self,
18        scroll_handle: &H,
19        axis: impl Into<ScrollbarAxis>,
20    ) -> Self {
21        self.child(ScrollbarLayer {
22            id: "scrollbar_layer".into(),
23            axis: axis.into(),
24            scroll_handle: Rc::new(scroll_handle.clone()),
25        })
26    }
27
28    /// Adds a vertical scrollbar to the element.
29    #[track_caller]
30    fn vertical_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
31        self.scrollbar(scroll_handle, ScrollbarAxis::Vertical)
32    }
33    /// Adds a horizontal scrollbar to the element.
34    #[track_caller]
35    fn horizontal_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
36        self.scrollbar(scroll_handle, ScrollbarAxis::Horizontal)
37    }
38
39    /// Almost equivalent to [`StatefulInteractiveElement::overflow_scroll`], but adds scrollbars.
40    #[track_caller]
41    fn overflow_scrollbar(self) -> Scrollable<Self> {
42        Scrollable::new(self, ScrollbarAxis::Both)
43    }
44
45    /// Almost equivalent to [`StatefulInteractiveElement::overflow_x_scroll`], but adds Horizontal scrollbar.
46    #[track_caller]
47    fn overflow_x_scrollbar(self) -> Scrollable<Self> {
48        Scrollable::new(self, ScrollbarAxis::Horizontal)
49    }
50
51    /// Almost equivalent to [`StatefulInteractiveElement::overflow_y_scroll`], but adds Vertical scrollbar.
52    #[track_caller]
53    fn overflow_y_scrollbar(self) -> Scrollable<Self> {
54        Scrollable::new(self, ScrollbarAxis::Vertical)
55    }
56}
57
58/// A scrollable element wrapper that adds scrollbars to an interactive element.
59#[derive(IntoElement)]
60pub struct Scrollable<E: InteractiveElement + Styled + ParentElement + Element> {
61    id: ElementId,
62    element: E,
63    axis: ScrollbarAxis,
64}
65
66impl<E> Scrollable<E>
67where
68    E: InteractiveElement + Styled + ParentElement + Element,
69{
70    #[track_caller]
71    fn new(element: E, axis: impl Into<ScrollbarAxis>) -> Self {
72        let caller = Location::caller();
73        Self {
74            id: ElementId::CodeLocation(*caller),
75            element,
76            axis: axis.into(),
77        }
78    }
79}
80
81impl<E> Styled for Scrollable<E>
82where
83    E: InteractiveElement + Styled + ParentElement + Element,
84{
85    fn style(&mut self) -> &mut StyleRefinement {
86        self.element.style()
87    }
88}
89
90impl<E> ParentElement for Scrollable<E>
91where
92    E: InteractiveElement + Styled + ParentElement + Element,
93{
94    fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
95        self.element.extend(elements)
96    }
97}
98
99impl InteractiveElement for Scrollable<Div> {
100    fn interactivity(&mut self) -> &mut gpui::Interactivity {
101        self.element.interactivity()
102    }
103}
104
105impl InteractiveElement for Scrollable<Stateful<Div>> {
106    fn interactivity(&mut self) -> &mut gpui::Interactivity {
107        self.element.interactivity()
108    }
109}
110
111impl<E> RenderOnce for Scrollable<E>
112where
113    E: InteractiveElement + Styled + ParentElement + Element + 'static,
114{
115    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
116        let scroll_handle = window
117            .use_keyed_state(self.id.clone(), cx, |_, _| ScrollHandle::default())
118            .read(cx)
119            .clone();
120
121        let style = self.element.style().clone();
122        *self.element.style() = StyleRefinement::default();
123
124        div()
125            .id(self.id)
126            .size_full()
127            .refine_style(&style)
128            .relative()
129            .child(
130                div()
131                    .id("scroll-area")
132                    .flex()
133                    .size_full()
134                    .track_scroll(&scroll_handle)
135                    .map(|this| match self.axis {
136                        ScrollbarAxis::Vertical => this.flex_col().overflow_y_scroll(),
137                        ScrollbarAxis::Horizontal => this.flex_row().overflow_x_scroll(),
138                        ScrollbarAxis::Both => this.overflow_scroll(),
139                    })
140                    .child(self.element.flex_1()),
141            )
142            .child(render_scrollbar(
143                "scrollbar",
144                &scroll_handle,
145                self.axis,
146                window,
147                cx,
148            ))
149    }
150}
151
152impl ScrollableElement for Div {}
153impl<E> ScrollableElement for Stateful<E>
154where
155    E: ParentElement + Styled + Element,
156    Self: InteractiveElement,
157{
158}
159
160#[derive(IntoElement)]
161struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
162    id: ElementId,
163    axis: ScrollbarAxis,
164    scroll_handle: Rc<H>,
165}
166
167impl<H> RenderOnce for ScrollbarLayer<H>
168where
169    H: ScrollbarHandle + Clone + 'static,
170{
171    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
172        render_scrollbar(self.id, self.scroll_handle.as_ref(), self.axis, window, cx)
173    }
174}
175
176#[inline]
177#[track_caller]
178fn render_scrollbar<H: ScrollbarHandle + Clone>(
179    id: impl Into<ElementId>,
180    scroll_handle: &H,
181    axis: ScrollbarAxis,
182    window: &mut Window,
183    cx: &mut App,
184) -> Div {
185    // Do not render scrollbar when inspector is picking elements,
186    // to allow us to pick the background elements.
187    let is_inspector_picking = window.is_inspector_picking(cx);
188    if is_inspector_picking {
189        return div();
190    }
191
192    div()
193        .absolute()
194        .top_0()
195        .left_0()
196        .right_0()
197        .bottom_0()
198        .child(Scrollbar::new(scroll_handle).id(id).axis(axis))
199}