retroglyph_widgets/widget/
scrollbar.rs1use retroglyph_core::{Backend, Color, Rect, Style, Terminal};
3
4use super::Widget;
5use crate::Theme;
6use crate::draw::thumb_geometry;
7
8#[derive(Clone, Copy, Debug)]
25pub struct Scrollbar {
26 total_len: usize,
27 visible_len: usize,
28 offset: usize,
29 track_style: Style,
30 thumb_style: Style,
31}
32
33impl Scrollbar {
34 #[must_use]
37 pub fn new(total_len: usize, visible_len: usize) -> Self {
38 Self {
39 total_len,
40 visible_len,
41 offset: 0,
42 track_style: Style::new(),
43 thumb_style: Style::new(),
44 }
45 }
46
47 #[must_use]
49 pub const fn offset(mut self, offset: usize) -> Self {
50 self.offset = offset;
51 self
52 }
53
54 #[must_use]
56 pub const fn track_style(mut self, style: Style) -> Self {
57 self.track_style = style;
58 self
59 }
60
61 #[must_use]
63 pub const fn thumb_style(mut self, style: Style) -> Self {
64 self.thumb_style = style;
65 self
66 }
67
68 #[must_use]
76 pub fn theme(self, theme: Theme) -> Self {
77 self.theme_on(theme, theme.panel_bg)
78 }
79
80 #[must_use]
85 pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
86 self.track_style = Style::new().bg(bg);
87 self.thumb_style = Style::new().bg(theme.border);
88 self
89 }
90}
91
92impl<B: Backend> Widget<B> for Scrollbar {
93 fn render(self, area: Rect, term: &mut Terminal<B>) {
94 if area.width() == 0 || area.height() == 0 {
95 return;
96 }
97
98 for y in area.top()..area.bottom() {
99 for x in area.left()..area.right() {
100 term.put_styled(x, y, ' ', self.track_style);
101 }
102 }
103
104 let Some((start, len)) =
105 thumb_geometry(area, self.total_len, self.visible_len, self.offset)
106 else {
107 return;
108 };
109 for y in (area.top() + start)..(area.top() + start + len) {
110 for x in area.left()..area.right() {
111 term.put_styled(x, y, ' ', self.thumb_style);
112 }
113 }
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use retroglyph_core::{Color, Headless};
120
121 use super::*;
122
123 #[test]
124 fn draws_a_plain_track_with_no_thumb_when_nothing_to_scroll() {
125 let area = Rect::new(0, 0, 1, 5);
126 let mut term = Terminal::new(Headless::new(1, 5));
127 let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
128 let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
129 Scrollbar::new(3, 5)
130 .track_style(track)
131 .thumb_style(thumb)
132 .render(area, &mut term);
133 for y in 0..5 {
134 assert_eq!(
135 term.grid().get(0, y).style().background(),
136 track.background()
137 );
138 }
139 }
140
141 #[test]
142 fn draws_the_thumb_over_the_track() {
143 let area = Rect::new(0, 0, 1, 10);
144 let mut term = Terminal::new(Headless::new(1, 10));
145 let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
146 let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
147 Scrollbar::new(20, 5)
148 .offset(0)
149 .track_style(track)
150 .thumb_style(thumb)
151 .render(area, &mut term);
152
153 let (start, len) = thumb_geometry(area, 20, 5, 0).unwrap();
154 for y in 0..10 {
155 let bg = term.grid().get(0, y).style().background();
156 if y >= start && y < start + len {
157 assert_eq!(bg, thumb.background());
158 } else {
159 assert_eq!(bg, track.background());
160 }
161 }
162 }
163
164 #[test]
165 fn theme_maps_named_roles_onto_track_and_thumb() {
166 let area = Rect::new(0, 0, 1, 10);
167 let mut term = Terminal::new(Headless::new(1, 10));
168 Scrollbar::new(20, 5)
169 .theme(Theme::DARK)
170 .render(area, &mut term);
171
172 let (start, len) = thumb_geometry(area, 20, 5, 0).unwrap();
173 for y in 0..10 {
174 let bg = term.grid().get(0, y).style().background();
175 if y >= start && y < start + len {
176 assert_eq!(bg, Theme::DARK.border);
177 } else {
178 assert_eq!(bg, Theme::DARK.panel_bg);
179 }
180 }
181 }
182
183 #[test]
184 fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
185 let area = Rect::new(0, 0, 1, 10);
186 let mut term = Terminal::new(Headless::new(1, 10));
187 Scrollbar::new(20, 5)
188 .theme_on(Theme::DARK, Color::Default)
189 .render(area, &mut term);
190
191 let (start, len) = thumb_geometry(area, 20, 5, 0).unwrap();
192 for y in 0..10 {
193 let bg = term.grid().get(0, y).style().background();
194 if y >= start && y < start + len {
195 assert_eq!(bg, Theme::DARK.border);
196 } else {
197 assert_eq!(bg, Color::Default);
198 }
199 }
200 }
201
202 #[test]
203 fn offset_defaults_to_zero() {
204 let area = Rect::new(0, 0, 1, 10);
205 let mut term = Terminal::new(Headless::new(1, 10));
206 let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
207 let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
208 Scrollbar::new(20, 5)
209 .track_style(track)
210 .thumb_style(thumb)
211 .render(area, &mut term);
212
213 let (start, _) = thumb_geometry(area, 20, 5, 0).unwrap();
214 assert_eq!(start, 0);
215 }
216}