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
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}