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