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}