Skip to main content

tui_scrollbar/scrollbar/
render.rs

1//! Rendering helpers and `Widget` implementation for [`ScrollBar`].
2//!
3//! The core widget delegates rendering to these helpers so the draw logic is grouped separately
4//! from configuration and input handling. Keep rendering changes localized here.
5
6use 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    /// Renders the scrollbar into the provided buffer.
23    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    /// Renders arrow endcaps into the buffer before the thumb/track.
45    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    /// Renders the vertical track and thumb into the provided area.
68    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    /// Renders the horizontal track and thumb into the provided area.
87    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    /// Chooses the vertical glyph + style for a track cell fill.
106    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    /// Chooses the horizontal glyph + style for a track cell fill.
123    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}