1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! `ScrollArea` — a bounded, scrollable container.
//!
//! Desktop UIs scroll; most builders assume their content fits. Wrap an
//! overflowing column (or row) in a `ScrollArea` and give it a bound. There are
//! two, and which one is right is a layout question, not a preference:
//! `max_height` for a list that occupies a fixed slice of a larger layout, and
//! `fill` for a pane that should be as tall as whatever the window gives it.
//! Each instance needs a unique id so gpui can track its scroll offset.
//!
//! It draws a [`Scrollbar`] over its edge whenever the content overflows, so a
//! long list says how long it is; `.scrollbar(false)` leaves it bare.
use crate::devtools::Probed;
use crate::scrollbar::Scrollbar;
use gpui::prelude::*;
use gpui::{
div, px, AnyElement, App, ElementId, Entity, IntoElement, ScrollHandle, SharedString, Window,
};
/// A scrollable region. `ScrollArea::new("id").max_height(240.0)`, or
/// `ScrollArea::new("id").fill()` to take the space the parent has left.
#[derive(IntoElement)]
pub struct ScrollArea {
id: ElementId,
children: Vec<AnyElement>,
max_height: Option<f32>,
fill: bool,
horizontal: bool,
scrollbar: bool,
}
impl ScrollArea {
pub fn new(id: impl Into<ElementId>) -> Self {
ScrollArea {
id: id.into(),
children: Vec::new(),
max_height: None,
fill: false,
horizontal: false,
scrollbar: true,
}
}
/// Clip to this height (px) and scroll past it.
pub fn max_height(mut self, height: f32) -> Self {
self.max_height = Some(height);
self
}
/// Take the space the parent has left over, and scroll past it — the mode
/// for a full-height pane, where any fixed number is wrong at every window
/// size but one.
///
/// The parent still has to be bounded itself; filling an unbounded parent
/// sizes to the content and there is nothing to scroll.
pub fn fill(mut self) -> Self {
self.fill = true;
self
}
/// Draw a scrollbar while the content overflows (on by default). It floats
/// over the edge rather than taking layout space, so turning it off changes
/// nothing about where the content sits.
pub fn scrollbar(mut self, show: bool) -> Self {
self.scrollbar = show;
self
}
/// Scroll horizontally instead of vertically.
pub fn horizontal(mut self, horizontal: bool) -> Self {
self.horizontal = horizontal;
self
}
}
impl ParentElement for ScrollArea {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl RenderOnce for ScrollArea {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let bound: SharedString = match (self.fill, self.max_height) {
(true, _) => "fill".into(),
(false, Some(height)) => format!("{height}px").into(),
(false, None) => "none".into(),
};
// The scrollbar needs the offset, so the scroller tracks a handle we own.
// It lives in element state under this area's id, across frames.
let state: Entity<ScrollHandle> =
window.use_keyed_state((self.id.clone(), "handle"), cx, |_, _| ScrollHandle::new());
let handle = state.read(cx).clone();
// Two boxes: the outer one is what the parent lays out (so it takes the
// fill/cap rules the scroller used to), the inner one scrolls and the bar
// floats over the outer one — an absolute child of the scroller itself
// would scroll away with the content.
let mut outer = div().relative().flex();
let mut inner = div().id(self.id.clone()).flex().track_scroll(&handle);
if self.horizontal {
inner = inner.flex_row().overflow_x_scroll().min_w_0();
if self.fill {
// Three settings for three parents: `flex_1` claims the leftover
// main axis under a flex parent, the relative size does the same
// under a plain block one (where grow means nothing, and where a
// flex basis would win anyway if both applied), and the zero
// minimum is what lets the box shrink under its content instead
// of pushing the parent open.
outer = outer.flex_1().w_full().min_w_0();
}
inner = inner.w_full();
} else {
inner = inner.flex_col().overflow_y_scroll().min_h_0();
if self.fill {
outer = outer.flex_1().h_full().min_h_0();
}
inner = inner.h_full().w_full();
}
// A cap still applies while filling: grow into the window, but never
// past this. Both boxes carry it — the outer so it stops growing, the
// inner so it is the one that scrolls.
if let Some(height) = self.max_height {
outer = outer.max_h(px(height));
inner = inner.max_h(px(height));
}
let bar = self
.scrollbar
.then(|| Scrollbar::new((self.id.clone(), "bar"), &handle).horizontal(self.horizontal));
outer
.child(inner.children(self.children))
.children(bar)
.probe("ScrollArea")
.attr("axis", if self.horizontal { "x" } else { "y" })
.attr("bound", bound)
.attr("scrollbar", if self.scrollbar { "on" } else { "off" })
}
}