1use ratatui_core::buffer::Buffer;
7use ratatui_core::layout::Rect;
8use ratatui_core::style::Style;
9use ratatui_core::widgets::Widget;
10
11use super::{ArrowLayout, ScrollBar, ScrollBarOrientation};
12use crate::metrics::{CellFill, ScrollMetrics};
13use crate::ScrollLengths;
14
15impl Widget for &ScrollBar {
16 fn render(self, area: Rect, buf: &mut Buffer) {
17 self.render_inner(area, buf);
18 }
19}
20
21impl ScrollBar {
22 fn render_inner(&self, area: Rect, buf: &mut Buffer) {
24 if area.width == 0 || area.height == 0 {
25 return;
26 }
27
28 let layout = self.arrow_layout(area);
29 self.render_arrows(&layout, buf);
30 if layout.track_area.width == 0 || layout.track_area.height == 0 {
31 return;
32 }
33
34 match self.orientation {
35 ScrollBarOrientation::Vertical => {
36 self.render_vertical_track(layout.track_area, buf);
37 }
38 ScrollBarOrientation::Horizontal => {
39 self.render_horizontal_track(layout.track_area, buf);
40 }
41 }
42 }
43
44 fn render_arrows(&self, layout: &ArrowLayout, buf: &mut Buffer) {
46 let arrow_style = self.arrow_style.unwrap_or(self.track_style);
47 if let Some((x, y)) = layout.start {
48 let glyph = match self.orientation {
49 ScrollBarOrientation::Vertical => self.glyph_set.arrow_vertical_start,
50 ScrollBarOrientation::Horizontal => self.glyph_set.arrow_horizontal_start,
51 };
52 let cell = &mut buf[(x, y)];
53 cell.set_char(glyph);
54 cell.set_style(arrow_style);
55 }
56 if let Some((x, y)) = layout.end {
57 let glyph = match self.orientation {
58 ScrollBarOrientation::Vertical => self.glyph_set.arrow_vertical_end,
59 ScrollBarOrientation::Horizontal => self.glyph_set.arrow_horizontal_end,
60 };
61 let cell = &mut buf[(x, y)];
62 cell.set_char(glyph);
63 cell.set_style(arrow_style);
64 }
65 }
66
67 fn render_vertical_track(&self, area: Rect, buf: &mut Buffer) {
69 let metrics = ScrollMetrics::new(
70 ScrollLengths {
71 content_len: self.content_len,
72 viewport_len: self.viewport_len,
73 },
74 self.offset,
75 area.height,
76 );
77 let x = area.x;
78 for (idx, y) in (area.y..area.y.saturating_add(area.height)).enumerate() {
79 let (glyph, style) = self.glyph_for_vertical(metrics.cell_fill(idx));
80 let cell = &mut buf[(x, y)];
81 cell.set_char(glyph);
82 cell.set_style(style);
83 }
84 }
85
86 fn render_horizontal_track(&self, area: Rect, buf: &mut Buffer) {
88 let metrics = ScrollMetrics::new(
89 ScrollLengths {
90 content_len: self.content_len,
91 viewport_len: self.viewport_len,
92 },
93 self.offset,
94 area.width,
95 );
96 let y = area.y;
97 for (idx, x) in (area.x..area.x.saturating_add(area.width)).enumerate() {
98 let (glyph, style) = self.glyph_for_horizontal(metrics.cell_fill(idx));
99 let cell = &mut buf[(x, y)];
100 cell.set_char(glyph);
101 cell.set_style(style);
102 }
103 }
104
105 fn glyph_for_vertical(&self, fill: CellFill) -> (char, Style) {
107 match fill {
108 CellFill::Empty => (self.glyph_set.track_vertical, self.track_style),
109 CellFill::Full => (self.glyph_set.thumb_vertical_lower[7], self.thumb_style),
110 CellFill::Partial { start, len } => {
111 let index = len.saturating_sub(1) as usize;
112 let glyph = if start == 0 {
113 self.glyph_set.thumb_vertical_upper[index]
114 } else {
115 self.glyph_set.thumb_vertical_lower[index]
116 };
117 (glyph, self.thumb_style)
118 }
119 }
120 }
121
122 fn glyph_for_horizontal(&self, fill: CellFill) -> (char, Style) {
124 match fill {
125 CellFill::Empty => (self.glyph_set.track_horizontal, self.track_style),
126 CellFill::Full => (self.glyph_set.thumb_horizontal_left[7], self.thumb_style),
127 CellFill::Partial { start, len } => {
128 let index = len.saturating_sub(1) as usize;
129 let glyph = if start == 0 {
130 self.glyph_set.thumb_horizontal_left[index]
131 } else {
132 self.glyph_set.thumb_horizontal_right[index]
133 };
134 (glyph, self.thumb_style)
135 }
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use ratatui_core::buffer::Buffer;
143 use ratatui_core::layout::Rect;
144
145 use super::*;
146 use crate::{GlyphSet, ScrollBarArrows, ScrollLengths};
147
148 fn assert_horizontal_thumb_walk(
149 glyph_set: GlyphSet,
150 track_char: char,
151 expected_lines: [&str; 9],
152 ) {
153 let lengths = ScrollLengths {
154 content_len: 8 * crate::SUBCELL,
155 viewport_len: 2 * crate::SUBCELL,
156 };
157
158 for (offset, expected_line) in expected_lines.into_iter().enumerate() {
159 let scrollbar = ScrollBar::horizontal(lengths)
160 .arrows(ScrollBarArrows::None)
161 .glyph_set(glyph_set.clone())
162 .offset(offset);
163 let mut buf = Buffer::empty(Rect::new(0, 0, 8, 1));
164 (&scrollbar).render(buf.area, &mut buf);
165
166 let mut expected = Buffer::with_lines(vec![expected_line]);
167 expected.set_style(expected.area, scrollbar.track_style);
168 for (x, symbol) in expected_line.chars().enumerate() {
169 if symbol != track_char {
170 expected[(x as u16, 0)].set_style(scrollbar.thumb_style);
171 }
172 }
173 assert_eq!(buf, expected);
174 }
175 }
176
177 #[test]
178 fn render_vertical_fractional_thumb() {
179 let scrollbar = ScrollBar::vertical(ScrollLengths {
180 content_len: 10,
181 viewport_len: 3,
182 })
183 .arrows(ScrollBarArrows::None)
184 .offset(1);
185 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 4));
186 (&scrollbar).render(buf.area, &mut buf);
187 let mut expected = Buffer::with_lines(vec!["▅", "▀", " ", " "]);
188 expected.set_style(expected.area, scrollbar.track_style);
189 expected[(0, 0)].set_style(scrollbar.thumb_style);
190 expected[(0, 1)].set_style(scrollbar.thumb_style);
191 assert_eq!(buf, expected);
192 }
193
194 #[test]
195 fn render_horizontal_fractional_thumb() {
196 let scrollbar = ScrollBar::horizontal(ScrollLengths {
197 content_len: 10,
198 viewport_len: 3,
199 })
200 .arrows(ScrollBarArrows::None)
201 .offset(1);
202 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
203 (&scrollbar).render(buf.area, &mut buf);
204 let mut expected = Buffer::with_lines(vec!["🮉▌ "]);
205 expected.set_style(expected.area, scrollbar.track_style);
206 expected[(0, 0)].set_style(scrollbar.thumb_style);
207 expected[(1, 0)].set_style(scrollbar.thumb_style);
208 assert_eq!(buf, expected);
209 }
210
211 #[test]
212 fn render_horizontal_fractional_thumb_box_drawing_track() {
213 let scrollbar = ScrollBar::horizontal(ScrollLengths {
214 content_len: 10,
215 viewport_len: 3,
216 })
217 .arrows(ScrollBarArrows::None)
218 .offset(1)
219 .glyph_set(GlyphSet::box_drawing());
220 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
221 (&scrollbar).render(buf.area, &mut buf);
222 let mut expected = Buffer::with_lines(vec!["🮉▌──"]);
223 expected.set_style(expected.area, scrollbar.track_style);
224 expected[(0, 0)].set_style(scrollbar.thumb_style);
225 expected[(1, 0)].set_style(scrollbar.thumb_style);
226 assert_eq!(buf, expected);
227 }
228
229 #[test]
230 fn render_horizontal_fractional_thumb_unicode_glyphs() {
231 let scrollbar = ScrollBar::horizontal(ScrollLengths {
232 content_len: 10,
233 viewport_len: 3,
234 })
235 .arrows(ScrollBarArrows::None)
236 .offset(1)
237 .glyph_set(GlyphSet::unicode());
238 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
239 (&scrollbar).render(buf.area, &mut buf);
240 let mut expected = Buffer::with_lines(vec!["▐▌──"]);
241 expected.set_style(expected.area, scrollbar.track_style);
242 expected[(0, 0)].set_style(scrollbar.thumb_style);
243 expected[(1, 0)].set_style(scrollbar.thumb_style);
244 assert_eq!(buf, expected);
245 }
246
247 #[test]
248 fn render_horizontal_thumb_walk_minimal_glyphs() {
249 assert_horizontal_thumb_walk(
250 GlyphSet::minimal(),
251 ' ',
252 [
253 "██ ",
254 "🮋█▏ ",
255 "🮊█▎ ",
256 "🮉█▍ ",
257 "▐█▌ ",
258 "🮈█▋ ",
259 "🮇█▊ ",
260 "▕█▉ ",
261 " ██ ",
262 ],
263 );
264 }
265
266 #[test]
267 fn render_horizontal_thumb_walk_legacy_glyphs() {
268 assert_horizontal_thumb_walk(
269 GlyphSet::symbols_for_legacy_computing(),
270 '─',
271 [
272 "██──────",
273 "🮋█▏─────",
274 "🮊█▎─────",
275 "🮉█▍─────",
276 "▐█▌─────",
277 "🮈█▋─────",
278 "🮇█▊─────",
279 "▕█▉─────",
280 "─██─────",
281 ],
282 );
283 }
284
285 #[test]
286 fn render_horizontal_thumb_walk_unicode_glyphs() {
287 assert_horizontal_thumb_walk(
288 GlyphSet::unicode(),
289 '─',
290 [
291 "██──────",
292 "██▏─────",
293 "▐█▎─────",
294 "▐█▍─────",
295 "▐█▌─────",
296 "▐█▋─────",
297 "▕█▊─────",
298 "▕█▉─────",
299 "─██─────",
300 ],
301 );
302 }
303
304 #[test]
305 fn render_full_thumb_when_no_scroll() {
306 let scrollbar = ScrollBar::vertical(ScrollLengths {
307 content_len: 5,
308 viewport_len: 10,
309 })
310 .arrows(ScrollBarArrows::None);
311 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
312 (&scrollbar).render(buf.area, &mut buf);
313 let mut expected = Buffer::with_lines(vec!["█", "█", "█"]);
314 expected.set_style(expected.area, scrollbar.thumb_style);
315 assert_eq!(buf, expected);
316 }
317
318 #[test]
319 fn render_vertical_arrows() {
320 let scrollbar = ScrollBar::vertical(ScrollLengths {
321 content_len: 5,
322 viewport_len: 2,
323 })
324 .arrows(ScrollBarArrows::Both);
325 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
326 (&scrollbar).render(buf.area, &mut buf);
327 let mut expected = Buffer::with_lines(vec!["▲", "█", "▼"]);
328 expected[(0, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
329 expected[(0, 1)].set_style(scrollbar.thumb_style);
330 expected[(0, 2)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
331 assert_eq!(buf, expected);
332 }
333
334 #[test]
335 fn render_horizontal_arrows() {
336 let scrollbar = ScrollBar::horizontal(ScrollLengths {
337 content_len: 5,
338 viewport_len: 2,
339 })
340 .arrows(ScrollBarArrows::Both);
341 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
342 (&scrollbar).render(buf.area, &mut buf);
343 let mut expected = Buffer::with_lines(vec!["◀█▶"]);
344 expected[(0, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
345 expected[(1, 0)].set_style(scrollbar.thumb_style);
346 expected[(2, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
347 assert_eq!(buf, expected);
348 }
349}