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