guise/scrollarea.rs
1//! `ScrollArea` — a bounded, scrollable container.
2//!
3//! Desktop UIs scroll; most builders assume their content fits. Wrap an
4//! overflowing column (or row) in a `ScrollArea` and give it a bound. There are
5//! two, and which one is right is a layout question, not a preference:
6//! `max_height` for a list that occupies a fixed slice of a larger layout, and
7//! `fill` for a pane that should be as tall as whatever the window gives it.
8//! Each instance needs a unique id so gpui can track its scroll offset.
9
10use crate::devtools::Probed;
11use gpui::prelude::*;
12use gpui::{div, px, AnyElement, App, ElementId, IntoElement, SharedString, Window};
13
14/// A scrollable region. `ScrollArea::new("id").max_height(240.0)`, or
15/// `ScrollArea::new("id").fill()` to take the space the parent has left.
16#[derive(IntoElement)]
17pub struct ScrollArea {
18 id: ElementId,
19 children: Vec<AnyElement>,
20 max_height: Option<f32>,
21 fill: bool,
22 horizontal: bool,
23}
24
25impl ScrollArea {
26 pub fn new(id: impl Into<ElementId>) -> Self {
27 ScrollArea {
28 id: id.into(),
29 children: Vec::new(),
30 max_height: None,
31 fill: false,
32 horizontal: false,
33 }
34 }
35
36 /// Clip to this height (px) and scroll past it.
37 pub fn max_height(mut self, height: f32) -> Self {
38 self.max_height = Some(height);
39 self
40 }
41
42 /// Take the space the parent has left over, and scroll past it — the mode
43 /// for a full-height pane, where any fixed number is wrong at every window
44 /// size but one.
45 ///
46 /// The parent still has to be bounded itself; filling an unbounded parent
47 /// sizes to the content and there is nothing to scroll.
48 pub fn fill(mut self) -> Self {
49 self.fill = true;
50 self
51 }
52
53 /// Scroll horizontally instead of vertically.
54 pub fn horizontal(mut self, horizontal: bool) -> Self {
55 self.horizontal = horizontal;
56 self
57 }
58}
59
60impl ParentElement for ScrollArea {
61 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
62 self.children.extend(elements);
63 }
64}
65
66impl RenderOnce for ScrollArea {
67 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
68 let bound: SharedString = match (self.fill, self.max_height) {
69 (true, _) => "fill".into(),
70 (false, Some(height)) => format!("{height}px").into(),
71 (false, None) => "none".into(),
72 };
73 let mut el = div().id(self.id).flex();
74 el = if self.horizontal {
75 let el = el.flex_row().overflow_x_scroll();
76 if self.fill {
77 // Three settings for three parents: `flex_1` claims the leftover
78 // main axis under a flex parent, the relative size does the same
79 // under a plain block one (where grow means nothing, and where a
80 // flex basis would win anyway if both applied), and the zero
81 // minimum is what lets the box shrink under its content instead
82 // of pushing the parent open.
83 el.flex_1().w_full().min_w_0()
84 } else {
85 el
86 }
87 } else {
88 let el = el.flex_col().overflow_y_scroll();
89 if self.fill {
90 el.flex_1().h_full().min_h_0()
91 } else {
92 el
93 }
94 };
95 // A cap still applies while filling: grow into the window, but never
96 // past this.
97 if let Some(height) = self.max_height {
98 el = el.max_h(px(height));
99 }
100 el.children(self.children)
101 .probe("ScrollArea")
102 .attr("axis", if self.horizontal { "x" } else { "y" })
103 .attr("bound", bound)
104 }
105}