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::{ScrollBarArrows, ScrollLengths};
147
148    #[test]
149    fn render_vertical_fractional_thumb() {
150        let scrollbar = ScrollBar::vertical(ScrollLengths {
151            content_len: 10,
152            viewport_len: 3,
153        })
154        .arrows(ScrollBarArrows::None)
155        .offset(1);
156        let mut buf = Buffer::empty(Rect::new(0, 0, 1, 4));
157        (&scrollbar).render(buf.area, &mut buf);
158        let mut expected = Buffer::with_lines(vec!["▅", "▀", "│", "│"]);
159        expected.set_style(expected.area, scrollbar.track_style);
160        expected[(0, 0)].set_style(scrollbar.thumb_style);
161        expected[(0, 1)].set_style(scrollbar.thumb_style);
162        assert_eq!(buf, expected);
163    }
164
165    #[test]
166    fn render_horizontal_fractional_thumb() {
167        let scrollbar = ScrollBar::horizontal(ScrollLengths {
168            content_len: 10,
169            viewport_len: 3,
170        })
171        .arrows(ScrollBarArrows::None)
172        .offset(1);
173        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
174        (&scrollbar).render(buf.area, &mut buf);
175        let mut expected = Buffer::with_lines(vec!["🮉▌──"]);
176        expected.set_style(expected.area, scrollbar.track_style);
177        expected[(0, 0)].set_style(scrollbar.thumb_style);
178        expected[(1, 0)].set_style(scrollbar.thumb_style);
179        assert_eq!(buf, expected);
180    }
181
182    #[test]
183    fn render_full_thumb_when_no_scroll() {
184        let scrollbar = ScrollBar::vertical(ScrollLengths {
185            content_len: 5,
186            viewport_len: 10,
187        })
188        .arrows(ScrollBarArrows::None);
189        let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
190        (&scrollbar).render(buf.area, &mut buf);
191        let mut expected = Buffer::with_lines(vec!["█", "█", "█"]);
192        expected.set_style(expected.area, scrollbar.thumb_style);
193        assert_eq!(buf, expected);
194    }
195
196    #[test]
197    fn render_vertical_arrows() {
198        let scrollbar = ScrollBar::vertical(ScrollLengths {
199            content_len: 5,
200            viewport_len: 2,
201        });
202        let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
203        (&scrollbar).render(buf.area, &mut buf);
204        let mut expected = Buffer::with_lines(vec!["▲", "█", "▼"]);
205        expected[(0, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
206        expected[(0, 1)].set_style(scrollbar.thumb_style);
207        expected[(0, 2)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
208        assert_eq!(buf, expected);
209    }
210
211    #[test]
212    fn render_horizontal_arrows() {
213        let scrollbar = ScrollBar::horizontal(ScrollLengths {
214            content_len: 5,
215            viewport_len: 2,
216        });
217        let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
218        (&scrollbar).render(buf.area, &mut buf);
219        let mut expected = Buffer::with_lines(vec!["◀█▶"]);
220        expected[(0, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
221        expected[(1, 0)].set_style(scrollbar.thumb_style);
222        expected[(2, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
223        assert_eq!(buf, expected);
224    }
225}