Skip to main content

egui_components/
scroll_area.rs

1//! `ScrollArea` — a theme-styled wrapper around [`egui::ScrollArea`].
2//!
3//! Plain [`egui::ScrollArea`] works fine; this wrapper exists so scrollable
4//! regions in a component layout get the same slim, modern scroll bars as the
5//! rest of the kit (the shadcn `ScrollArea` look) without each call site having
6//! to tweak `Style::spacing.scroll`. It mirrors upstream gpui-component's
7//! `scroll` module, whose `Scrollbar` likewise renders a thin overlay handle.
8//!
9//! It forwards the common [`egui::ScrollArea`] knobs and returns egui's own
10//! [`ScrollAreaOutput`], so it is a drop-in replacement:
11//!
12//! ```ignore
13//! sc::ScrollArea::vertical()
14//!     .max_height(200.0)
15//!     .show(ui, |ui| {
16//!         for i in 0..100 {
17//!             ui.add(sc::Label::new(format!("Row {i}")));
18//!         }
19//!     });
20//! ```
21
22use egui::scroll_area::ScrollAreaOutput;
23use egui::{Ui, Vec2b};
24use egui_components_theme::Theme;
25
26/// A scrollable region with themed scroll bars.
27pub struct ScrollArea {
28    inner: egui::ScrollArea,
29    floating: bool,
30}
31
32impl ScrollArea {
33    fn wrap(inner: egui::ScrollArea) -> Self {
34        Self {
35            inner,
36            floating: true,
37        }
38    }
39
40    /// Scrolls vertically only.
41    pub fn vertical() -> Self {
42        Self::wrap(egui::ScrollArea::vertical())
43    }
44
45    /// Scrolls horizontally only.
46    pub fn horizontal() -> Self {
47        Self::wrap(egui::ScrollArea::horizontal())
48    }
49
50    /// Scrolls on both axes.
51    pub fn both() -> Self {
52        Self::wrap(egui::ScrollArea::both())
53    }
54
55    /// Stable id source so scroll offset survives across frames / siblings.
56    pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
57        self.inner = self.inner.id_salt(id_salt);
58        self
59    }
60
61    /// Clamp the visible height; content beyond it scrolls.
62    pub fn max_height(mut self, height: f32) -> Self {
63        self.inner = self.inner.max_height(height);
64        self
65    }
66
67    /// Clamp the visible width; content beyond it scrolls.
68    pub fn max_width(mut self, width: f32) -> Self {
69        self.inner = self.inner.max_width(width);
70        self
71    }
72
73    /// Whether each axis shrinks to fit its content (defaults to egui's `true`).
74    pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
75        self.inner = self.inner.auto_shrink(auto_shrink);
76        self
77    }
78
79    /// If `true` (the default) the scroll bars float over the content and
80    /// expand on hover; if `false` they are solid and always reserve space.
81    pub fn floating(mut self, floating: bool) -> Self {
82        self.floating = floating;
83        self
84    }
85
86    /// Render the scrollable contents. Returns egui's [`ScrollAreaOutput`].
87    pub fn show<R>(
88        self,
89        ui: &mut Ui,
90        add_contents: impl FnOnce(&mut Ui) -> R,
91    ) -> ScrollAreaOutput<R> {
92        let _ = Theme::get(ui.ctx()); // ensure the theme is resolvable for callers
93
94        // Apply a slim scroll-bar geometry just for this region, then restore
95        // the previous style so surrounding widgets are unaffected. Only the
96        // scroll geometry is touched — handle colours come from the theme's
97        // installed widget visuals.
98        let prev = ui.spacing().scroll;
99        let mut scroll = if self.floating {
100            egui::style::ScrollStyle::thin()
101        } else {
102            egui::style::ScrollStyle::solid()
103        };
104        scroll.bar_width = if self.floating { 8.0 } else { 10.0 };
105        scroll.bar_inner_margin = 2.0;
106        scroll.handle_min_length = 24.0;
107        ui.style_mut().spacing.scroll = scroll;
108
109        let out = self.inner.show(ui, add_contents);
110
111        ui.style_mut().spacing.scroll = prev;
112        out
113    }
114}