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
//! `ScrollArea` — a theme-styled wrapper around [`egui::ScrollArea`].
//!
//! Plain [`egui::ScrollArea`] works fine; this wrapper exists so scrollable
//! regions in a component layout get the same slim, modern scroll bars as the
//! rest of the kit (the shadcn `ScrollArea` look) without each call site having
//! to tweak `Style::spacing.scroll`. It mirrors upstream gpui-component's
//! `scroll` module, whose `Scrollbar` likewise renders a thin overlay handle.
//!
//! It forwards the common [`egui::ScrollArea`] knobs and returns egui's own
//! [`ScrollAreaOutput`], so it is a drop-in replacement:
//!
//! ```ignore
//! sc::ScrollArea::vertical()
//! .max_height(200.0)
//! .show(ui, |ui| {
//! for i in 0..100 {
//! ui.add(sc::Label::new(format!("Row {i}")));
//! }
//! });
//! ```
use egui::scroll_area::ScrollAreaOutput;
use egui::{Ui, Vec2b};
use egui_components_theme::Theme;
/// A scrollable region with themed scroll bars.
pub struct ScrollArea {
inner: egui::ScrollArea,
floating: bool,
}
impl ScrollArea {
fn wrap(inner: egui::ScrollArea) -> Self {
Self {
inner,
floating: true,
}
}
/// Scrolls vertically only.
pub fn vertical() -> Self {
Self::wrap(egui::ScrollArea::vertical())
}
/// Scrolls horizontally only.
pub fn horizontal() -> Self {
Self::wrap(egui::ScrollArea::horizontal())
}
/// Scrolls on both axes.
pub fn both() -> Self {
Self::wrap(egui::ScrollArea::both())
}
/// Stable id source so scroll offset survives across frames / siblings.
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
self.inner = self.inner.id_salt(id_salt);
self
}
/// Clamp the visible height; content beyond it scrolls.
pub fn max_height(mut self, height: f32) -> Self {
self.inner = self.inner.max_height(height);
self
}
/// Clamp the visible width; content beyond it scrolls.
pub fn max_width(mut self, width: f32) -> Self {
self.inner = self.inner.max_width(width);
self
}
/// Whether each axis shrinks to fit its content (defaults to egui's `true`).
pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
self.inner = self.inner.auto_shrink(auto_shrink);
self
}
/// If `true` (the default) the scroll bars float over the content and
/// expand on hover; if `false` they are solid and always reserve space.
pub fn floating(mut self, floating: bool) -> Self {
self.floating = floating;
self
}
/// Render the scrollable contents. Returns egui's [`ScrollAreaOutput`].
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> ScrollAreaOutput<R> {
let _ = Theme::get(ui.ctx()); // ensure the theme is resolvable for callers
// Apply a slim scroll-bar geometry just for this region, then restore
// the previous style so surrounding widgets are unaffected. Only the
// scroll geometry is touched — handle colours come from the theme's
// installed widget visuals.
let prev = ui.spacing().scroll;
let mut scroll = if self.floating {
egui::style::ScrollStyle::thin()
} else {
egui::style::ScrollStyle::solid()
};
scroll.bar_width = if self.floating { 8.0 } else { 10.0 };
scroll.bar_inner_margin = 2.0;
scroll.handle_min_length = 24.0;
ui.style_mut().spacing.scroll = scroll;
let out = self.inner.show(ui, add_contents);
ui.style_mut().spacing.scroll = prev;
out
}
}