Skip to main content

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}