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 use ratatui_core::style::{Color, Style};
145
146 use super::*;
147 use crate::{GlyphSet, ScrollBarArrows, ScrollLengths};
148
149 fn assert_horizontal_thumb_walk(
150 glyph_set: GlyphSet,
151 track_char: char,
152 expected_lines: [&str; 9],
153 ) {
154 let lengths = ScrollLengths {
155 content_len: 8 * crate::SUBCELL,
156 viewport_len: 2 * crate::SUBCELL,
157 };
158
159 for (offset, expected_line) in expected_lines.into_iter().enumerate() {
160 let scrollbar = ScrollBar::horizontal(lengths)
161 .arrows(ScrollBarArrows::None)
162 .glyph_set(glyph_set.clone())
163 .offset(offset);
164 let mut buf = Buffer::empty(Rect::new(0, 0, 8, 1));
165 (&scrollbar).render(buf.area, &mut buf);
166
167 let mut expected = Buffer::with_lines(vec![expected_line]);
168 expected.set_style(expected.area, scrollbar.track_style);
169 for (x, symbol) in expected_line.chars().enumerate() {
170 if symbol != track_char {
171 expected[(x as u16, 0)].set_style(scrollbar.thumb_style);
172 }
173 }
174 assert_eq!(buf, expected);
175 }
176 }
177
178 #[test]
179 fn render_vertical_fractional_thumb() {
180 let scrollbar = ScrollBar::vertical(ScrollLengths {
181 content_len: 10,
182 viewport_len: 3,
183 })
184 .arrows(ScrollBarArrows::None)
185 .offset(1);
186 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 4));
187 (&scrollbar).render(buf.area, &mut buf);
188 let mut expected = Buffer::with_lines(vec!["▅", "▀", " ", " "]);
189 expected.set_style(expected.area, scrollbar.track_style);
190 expected[(0, 0)].set_style(scrollbar.thumb_style);
191 expected[(0, 1)].set_style(scrollbar.thumb_style);
192 assert_eq!(buf, expected);
193 }
194
195 #[test]
196 fn render_horizontal_fractional_thumb() {
197 let scrollbar = ScrollBar::horizontal(ScrollLengths {
198 content_len: 10,
199 viewport_len: 3,
200 })
201 .arrows(ScrollBarArrows::None)
202 .offset(1);
203 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
204 (&scrollbar).render(buf.area, &mut buf);
205 let mut expected = Buffer::with_lines(vec!["🮉▌ "]);
206 expected.set_style(expected.area, scrollbar.track_style);
207 expected[(0, 0)].set_style(scrollbar.thumb_style);
208 expected[(1, 0)].set_style(scrollbar.thumb_style);
209 assert_eq!(buf, expected);
210 }
211
212 #[test]
213 fn render_uses_custom_thumb_style_for_full_and_partial_cells() {
214 let track_style = Style::new().bg(Color::Rgb(10, 20, 30));
215 let thumb_style = Style::new()
216 .fg(Color::Rgb(255, 158, 100))
217 .bg(Color::Rgb(10, 20, 30));
218 let arrow_style = Style::new()
219 .fg(Color::Rgb(158, 206, 106))
220 .bg(Color::Rgb(10, 20, 30));
221 let scrollbar = ScrollBar::horizontal(ScrollLengths {
222 content_len: 10,
223 viewport_len: 3,
224 })
225 .arrows(ScrollBarArrows::Both)
226 .offset(1)
227 .track_style(track_style)
228 .thumb_style(thumb_style)
229 .arrow_style(arrow_style);
230
231 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
232 (&scrollbar).render(buf.area, &mut buf);
233
234 let mut expected = Buffer::with_lines(vec!["◀🮉▌ ▶"]);
235 expected.set_style(expected.area, track_style);
236 expected[(0, 0)].set_style(arrow_style);
237 expected[(1, 0)].set_style(thumb_style);
238 expected[(2, 0)].set_style(thumb_style);
239 expected[(5, 0)].set_style(arrow_style);
240 assert_eq!(buf, expected);
241 }
242
243 #[test]
244 fn render_horizontal_fractional_thumb_box_drawing_track() {
245 let scrollbar = ScrollBar::horizontal(ScrollLengths {
246 content_len: 10,
247 viewport_len: 3,
248 })
249 .arrows(ScrollBarArrows::None)
250 .offset(1)
251 .glyph_set(GlyphSet::box_drawing());
252 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
253 (&scrollbar).render(buf.area, &mut buf);
254 let mut expected = Buffer::with_lines(vec!["🮉▌──"]);
255 expected.set_style(expected.area, scrollbar.track_style);
256 expected[(0, 0)].set_style(scrollbar.thumb_style);
257 expected[(1, 0)].set_style(scrollbar.thumb_style);
258 assert_eq!(buf, expected);
259 }
260
261 #[test]
262 fn render_horizontal_fractional_thumb_unicode_glyphs() {
263 let scrollbar = ScrollBar::horizontal(ScrollLengths {
264 content_len: 10,
265 viewport_len: 3,
266 })
267 .arrows(ScrollBarArrows::None)
268 .offset(1)
269 .glyph_set(GlyphSet::unicode());
270 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
271 (&scrollbar).render(buf.area, &mut buf);
272 let mut expected = Buffer::with_lines(vec!["▐▌──"]);
273 expected.set_style(expected.area, scrollbar.track_style);
274 expected[(0, 0)].set_style(scrollbar.thumb_style);
275 expected[(1, 0)].set_style(scrollbar.thumb_style);
276 assert_eq!(buf, expected);
277 }
278
279 #[test]
280 fn render_horizontal_thumb_walk_minimal_glyphs() {
281 assert_horizontal_thumb_walk(
282 GlyphSet::minimal(),
283 ' ',
284 [
285 "██ ",
286 "🮋█▏ ",
287 "🮊█▎ ",
288 "🮉█▍ ",
289 "▐█▌ ",
290 "🮈█▋ ",
291 "🮇█▊ ",
292 "▕█▉ ",
293 " ██ ",
294 ],
295 );
296 }
297
298 #[test]
299 fn render_horizontal_thumb_walk_legacy_glyphs() {
300 assert_horizontal_thumb_walk(
301 GlyphSet::symbols_for_legacy_computing(),
302 '─',
303 [
304 "██──────",
305 "🮋█▏─────",
306 "🮊█▎─────",
307 "🮉█▍─────",
308 "▐█▌─────",
309 "🮈█▋─────",
310 "🮇█▊─────",
311 "▕█▉─────",
312 "─██─────",
313 ],
314 );
315 }
316
317 #[test]
318 fn render_horizontal_thumb_walk_unicode_glyphs() {
319 assert_horizontal_thumb_walk(
320 GlyphSet::unicode(),
321 '─',
322 [
323 "██──────",
324 "██▏─────",
325 "▐█▎─────",
326 "▐█▍─────",
327 "▐█▌─────",
328 "▐█▋─────",
329 "▕█▊─────",
330 "▕█▉─────",
331 "─██─────",
332 ],
333 );
334 }
335
336 #[test]
337 fn render_full_thumb_when_no_scroll() {
338 let scrollbar = ScrollBar::vertical(ScrollLengths {
339 content_len: 5,
340 viewport_len: 10,
341 })
342 .arrows(ScrollBarArrows::None);
343 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
344 (&scrollbar).render(buf.area, &mut buf);
345 let mut expected = Buffer::with_lines(vec!["█", "█", "█"]);
346 expected.set_style(expected.area, scrollbar.thumb_style);
347 assert_eq!(buf, expected);
348 }
349
350 #[test]
351 fn render_vertical_arrows() {
352 let scrollbar = ScrollBar::vertical(ScrollLengths {
353 content_len: 5,
354 viewport_len: 2,
355 })
356 .arrows(ScrollBarArrows::Both);
357 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
358 (&scrollbar).render(buf.area, &mut buf);
359 let mut expected = Buffer::with_lines(vec!["▲", "█", "▼"]);
360 expected[(0, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
361 expected[(0, 1)].set_style(scrollbar.thumb_style);
362 expected[(0, 2)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
363 assert_eq!(buf, expected);
364 }
365
366 #[test]
367 fn render_horizontal_arrows() {
368 let scrollbar = ScrollBar::horizontal(ScrollLengths {
369 content_len: 5,
370 viewport_len: 2,
371 })
372 .arrows(ScrollBarArrows::Both);
373 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
374 (&scrollbar).render(buf.area, &mut buf);
375 let mut expected = Buffer::with_lines(vec!["◀█▶"]);
376 expected[(0, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
377 expected[(1, 0)].set_style(scrollbar.thumb_style);
378 expected[(2, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
379 assert_eq!(buf, expected);
380 }
381}