retroglyph_widgets/widget/
scrollbar.rs1use retroglyph_core::{Backend, 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(mut self, theme: Theme) -> Self {
77 self.track_style = Style::new().bg(theme.panel_bg);
78 self.thumb_style = Style::new().bg(theme.border);
79 self
80 }
81}
82
83impl<B: Backend> Widget<B> for Scrollbar {
84 fn render(self, area: Rect, term: &mut Terminal<B>) {
85 if area.width() == 0 || area.height() == 0 {
86 return;
87 }
88
89 for y in area.top()..area.bottom() {
90 for x in area.left()..area.right() {
91 term.put_styled(x, y, ' ', self.track_style);
92 }
93 }
94
95 let Some((start, len)) =
96 thumb_geometry(area, self.total_len, self.visible_len, self.offset)
97 else {
98 return;
99 };
100 for y in (area.top() + start)..(area.top() + start + len) {
101 for x in area.left()..area.right() {
102 term.put_styled(x, y, ' ', self.thumb_style);
103 }
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use retroglyph_core::{Color, Headless};
111
112 use super::*;
113
114 #[test]
115 fn draws_a_plain_track_with_no_thumb_when_nothing_to_scroll() {
116 let area = Rect::new(0, 0, 1, 5);
117 let mut term = Terminal::new(Headless::new(1, 5));
118 let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
119 let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
120 Scrollbar::new(3, 5)
121 .track_style(track)
122 .thumb_style(thumb)
123 .render(area, &mut term);
124 for y in 0..5 {
125 assert_eq!(
126 term.grid().get(0, y).style().background(),
127 track.background()
128 );
129 }
130 }
131
132 #[test]
133 fn draws_the_thumb_over_the_track() {
134 let area = Rect::new(0, 0, 1, 10);
135 let mut term = Terminal::new(Headless::new(1, 10));
136 let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
137 let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
138 Scrollbar::new(20, 5)
139 .offset(0)
140 .track_style(track)
141 .thumb_style(thumb)
142 .render(area, &mut term);
143
144 let (start, len) = thumb_geometry(area, 20, 5, 0).unwrap();
145 for y in 0..10 {
146 let bg = term.grid().get(0, y).style().background();
147 if y >= start && y < start + len {
148 assert_eq!(bg, thumb.background());
149 } else {
150 assert_eq!(bg, track.background());
151 }
152 }
153 }
154
155 #[test]
156 fn theme_maps_named_roles_onto_track_and_thumb() {
157 let area = Rect::new(0, 0, 1, 10);
158 let mut term = Terminal::new(Headless::new(1, 10));
159 Scrollbar::new(20, 5)
160 .theme(Theme::DARK)
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, Theme::DARK.border);
168 } else {
169 assert_eq!(bg, Theme::DARK.panel_bg);
170 }
171 }
172 }
173
174 #[test]
175 fn offset_defaults_to_zero() {
176 let area = Rect::new(0, 0, 1, 10);
177 let mut term = Terminal::new(Headless::new(1, 10));
178 let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
179 let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
180 Scrollbar::new(20, 5)
181 .track_style(track)
182 .thumb_style(thumb)
183 .render(area, &mut term);
184
185 let (start, _) = thumb_geometry(area, 20, 5, 0).unwrap();
186 assert_eq!(start, 0);
187 }
188}