tui_scrollview/
scroll_view.rs

1use ratatui::buffer::Buffer;
2use ratatui::layout::{Rect, Size};
3use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget};
4
5use crate::ScrollViewState;
6
7/// A widget that can scroll its contents
8///
9/// Allows you to render a widget into a buffer larger than the area it is rendered into, and then
10/// scroll the contents of that buffer around.
11///
12/// Note that the origin of the buffer is always at (0, 0), and the buffer is always the size of the
13/// size passed to `new`. The `ScrollView` widget itself is responsible for rendering the visible
14/// area of the buffer into the main buffer.
15///
16/// # Examples
17///
18/// ```rust
19/// use ratatui::{prelude::*, layout::Size, widgets::*};
20/// use tui_scrollview::{ScrollView, ScrollViewState};
21///
22/// # fn render(buf: &mut Buffer) {
23/// let mut scroll_view = ScrollView::new(Size::new(20, 20));
24///
25/// // render a few widgets into the buffer at various positions
26/// scroll_view.render_widget(Paragraph::new("Hello, world!"), Rect::new(0, 0, 20, 1));
27/// scroll_view.render_widget(Paragraph::new("Hello, world!"), Rect::new(10, 10, 20, 1));
28/// scroll_view.render_widget(Paragraph::new("Hello, world!"), Rect::new(15, 15, 20, 1));
29///
30/// // You can also render widgets into the buffer programmatically
31/// Line::raw("Hello, world!").render(Rect::new(0, 0, 20, 1), scroll_view.buf_mut());
32///
33/// // usually you would store the state of the scroll view in a struct that implements
34/// // StatefulWidget (or in your app state if you're using an `App` struct)
35/// let mut state = ScrollViewState::default();
36///
37/// // you can also scroll the view programmatically
38/// state.scroll_down();
39///
40/// // render the scroll view into the main buffer at the given position within a widget
41/// let scroll_view_area = Rect::new(0, 0, 10, 10);
42/// scroll_view.render(scroll_view_area, buf, &mut state);
43/// # }
44/// // or if you're rendering in a terminal draw closure instead of from within another widget:
45/// # fn terminal_draw(frame: &mut Frame, scroll_view: ScrollView, state: &mut ScrollViewState) {
46/// frame.render_stateful_widget(scroll_view, frame.size(), state);
47/// # }
48/// ```
49#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
50pub struct ScrollView {
51    buf: Buffer,
52    size: Size,
53    vertical_scrollbar_visibility: ScrollbarVisibility,
54    horizontal_scrollbar_visibility: ScrollbarVisibility,
55}
56
57/// The visbility of the vertical and horizontal scrollbars.
58#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
59pub enum ScrollbarVisibility {
60    /// Render the scrollbar only whenever needed.
61    #[default]
62    Automatic,
63    /// Always render the scrollbar.
64    Always,
65    /// Never render the scrollbar (hide it).
66    Never,
67}
68
69impl ScrollView {
70    /// Create a new scroll view with a buffer of the given size
71    ///
72    /// The buffer will be empty, with coordinates ranging from (0, 0) to (size.width, size.height).
73    pub fn new(size: Size) -> Self {
74        // TODO: this is replaced with Rect::from(size) in the next version of ratatui
75        let area = Rect::new(0, 0, size.width, size.height);
76        Self {
77            buf: Buffer::empty(area),
78            size,
79            horizontal_scrollbar_visibility: ScrollbarVisibility::default(),
80            vertical_scrollbar_visibility: ScrollbarVisibility::default(),
81        }
82    }
83
84    /// The content size of the scroll view
85    pub const fn size(&self) -> Size {
86        self.size
87    }
88
89    /// The area of the buffer that is available to be scrolled
90    pub const fn area(&self) -> Rect {
91        self.buf.area
92    }
93
94    /// The buffer containing the contents of the scroll view
95    pub const fn buf(&self) -> &Buffer {
96        &self.buf
97    }
98
99    /// The mutable buffer containing the contents of the scroll view
100    ///
101    /// This can be used to render widgets into the buffer programmatically
102    ///
103    /// # Examples
104    ///
105    /// ```rust
106    /// # use ratatui::{prelude::*, layout::Size, widgets::*};
107    /// # use tui_scrollview::ScrollView;
108    ///
109    /// let mut scroll_view = ScrollView::new(Size::new(20, 20));
110    /// Line::raw("Hello, world!").render(Rect::new(0, 0, 20, 1), scroll_view.buf_mut());
111    /// ```
112    pub const fn buf_mut(&mut self) -> &mut Buffer {
113        &mut self.buf
114    }
115
116    /// Set the visibility of the vertical scrollbar
117    ///
118    /// See [`ScrollbarVisibility`] for all the options.
119    ///
120    /// This is a fluent setter method which must be chained or used as it consumes self
121    ///
122    /// # Examples
123    ///
124    /// ```rust
125    /// # use ratatui::{prelude::*, layout::Size, widgets::*};
126    /// # use tui_scrollview::{ScrollView, ScrollbarVisibility};
127    ///
128    /// let mut scroll_view = ScrollView::new(Size::new(20, 20))
129    ///     .vertical_scrollbar_visibility(ScrollbarVisibility::Always);
130    /// ```
131    pub const fn vertical_scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
132        self.vertical_scrollbar_visibility = visibility;
133        self
134    }
135
136    /// Set the visibility of the horizontal scrollbar
137    ///
138    /// See [`ScrollbarVisibility`] for all the options.
139    ///
140    /// This is a fluent setter method which must be chained or used as it consumes self
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// # use ratatui::{prelude::*, layout::Size, widgets::*};
146    /// # use tui_scrollview::{ScrollView, ScrollbarVisibility};
147    ///
148    /// let mut scroll_view = ScrollView::new(Size::new(20, 20))
149    ///     .horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
150    /// ```
151    pub const fn horizontal_scrollbar_visibility(
152        mut self,
153        visibility: ScrollbarVisibility,
154    ) -> Self {
155        self.horizontal_scrollbar_visibility = visibility;
156        self
157    }
158
159    /// Set the visibility of both vertical and horizontal scrollbars
160    ///
161    /// See [`ScrollbarVisibility`] for all the options.
162    ///
163    /// This is a fluent setter method which must be chained or used as it consumes self
164    ///
165    /// # Examples
166    ///
167    /// ```rust
168    /// # use ratatui::{prelude::*, layout::Size, widgets::*};
169    /// # use tui_scrollview::{ScrollView, ScrollbarVisibility};
170    ///
171    /// let mut scroll_view =
172    ///     ScrollView::new(Size::new(20, 20)).scrollbars_visibility(ScrollbarVisibility::Automatic);
173    /// ```
174    pub const fn scrollbars_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
175        self.vertical_scrollbar_visibility = visibility;
176        self.horizontal_scrollbar_visibility = visibility;
177        self
178    }
179
180    /// Render a widget into the scroll buffer
181    ///
182    /// This is the equivalent of `Frame::render_widget`, but renders the widget into the scroll
183    /// buffer rather than the main buffer. The widget will be rendered into the area of the buffer
184    /// specified by the `area` parameter.
185    ///
186    /// This should not be confused with the `render` method, which renders the visible area of the
187    /// ScrollView into the main buffer.
188    pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
189        widget.render(area, &mut self.buf);
190    }
191
192    /// Render a stateful widget into the scroll buffer
193    ///
194    /// This is the equivalent of `Frame::render_stateful_widget`, but renders the stateful widget
195    /// into the scroll buffer rather than the main buffer. The stateful widget will be rendered
196    /// into the area of the buffer specified by the `area` parameter.
197    ///
198    /// This should not be confused with the `render` method, which renders the visible area of the
199    /// ScrollView into the main buffer.
200    pub fn render_stateful_widget<W: StatefulWidget>(
201        &mut self,
202        widget: W,
203        area: Rect,
204        state: &mut W::State,
205    ) {
206        widget.render(area, &mut self.buf, state);
207    }
208}
209
210impl StatefulWidget for ScrollView {
211    type State = ScrollViewState;
212
213    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
214        let (mut x, mut y) = state.offset.into();
215        // ensure that we don't scroll past the end of the buffer in either direction
216        let max_x_offset = self
217            .buf
218            .area
219            .width
220            .saturating_sub(area.width.saturating_sub(1));
221        let max_y_offset = self
222            .buf
223            .area
224            .height
225            .saturating_sub(area.height.saturating_sub(1));
226
227        x = x.min(max_x_offset);
228        y = y.min(max_y_offset);
229        state.offset = (x, y).into();
230        state.size = Some(self.size);
231        state.page_size = Some(area.into());
232        let visible_area = self
233            .render_scrollbars(area, buf, state)
234            .intersection(self.buf.area);
235        self.render_visible_area(area, buf, visible_area);
236    }
237}
238
239impl ScrollView {
240    /// Render needed scrollbars and return remaining area relative to
241    /// scrollview's buffer area.
242    fn render_scrollbars(&self, area: Rect, buf: &mut Buffer, state: &mut ScrollViewState) -> Rect {
243        // fit value per direction
244        //   > 0 => fits
245        //  == 0 => exact fit
246        //   < 0 => does not fit
247        let horizontal_space = area.width as i32 - self.size.width as i32;
248        let vertical_space = area.height as i32 - self.size.height as i32;
249
250        // if it fits in that direction, reset state to reflect it
251        if horizontal_space > 0 {
252            state.offset.x = 0;
253        }
254        if vertical_space > 0 {
255            state.offset.y = 0;
256        }
257
258        let (show_horizontal, show_vertical) =
259            self.visible_scrollbars(horizontal_space, vertical_space);
260
261        let new_height = if show_horizontal {
262            // if both bars are rendered, avoid the corner
263            let width = area.width.saturating_sub(show_vertical as u16);
264            let render_area = Rect { width, ..area };
265            // render scrollbar, update available space
266            self.render_horizontal_scrollbar(render_area, buf, state);
267            area.height.saturating_sub(1)
268        } else {
269            area.height
270        };
271
272        let new_width = if show_vertical {
273            // if both bars are rendered, avoid the corner
274            let height = area.height.saturating_sub(show_horizontal as u16);
275            let render_area = Rect { height, ..area };
276            // render scrollbar, update available space
277            self.render_vertical_scrollbar(render_area, buf, state);
278            area.width.saturating_sub(1)
279        } else {
280            area.width
281        };
282
283        Rect::new(state.offset.x, state.offset.y, new_width, new_height)
284    }
285
286    /// Resolve whether to render each scrollbar.
287    ///
288    /// Considers the visibility options set by the user and whether the scrollview size fits into
289    /// the the available area on each direction.
290    ///
291    /// The space arguments are the difference between the scrollview size and the available area.
292    ///
293    /// Returns a bool tuple with (horizontal, vertical) resolutions.
294    const fn visible_scrollbars(&self, horizontal_space: i32, vertical_space: i32) -> (bool, bool) {
295        type V = crate::scroll_view::ScrollbarVisibility;
296
297        match (
298            self.horizontal_scrollbar_visibility,
299            self.vertical_scrollbar_visibility,
300        ) {
301            // straightfoward, no need to check fit values
302            (V::Always, V::Always) => (true, true),
303            (V::Never, V::Never) => (false, false),
304            (V::Always, V::Never) => (true, false),
305            (V::Never, V::Always) => (false, true),
306
307            // Auto => render scrollbar only if it doesn't fit
308            (V::Automatic, V::Never) => (horizontal_space < 0, false),
309            (V::Never, V::Automatic) => (false, vertical_space < 0),
310
311            // Auto => render scrollbar if:
312            //   it doesn't fit; or
313            //   exact fit (other scrollbar steals a line and triggers it)
314            (V::Always, V::Automatic) => (true, vertical_space <= 0),
315            (V::Automatic, V::Always) => (horizontal_space <= 0, true),
316
317            // depends solely on fit values
318            (V::Automatic, V::Automatic) => {
319                if horizontal_space >= 0 && vertical_space >= 0 {
320                    // there is enough space for both dimensions
321                    (false, false)
322                } else if horizontal_space < 0 && vertical_space < 0 {
323                    // there is not enough space for either dimension
324                    (true, true)
325                } else if horizontal_space > 0 && vertical_space < 0 {
326                    // horizontal fits, vertical does not
327                    (false, true)
328                } else if horizontal_space < 0 && vertical_space > 0 {
329                    // vertical fits, horizontal does not
330                    (true, false)
331                } else {
332                    // one is an exact fit and other does not fit which triggers both scrollbars to
333                    // be visible because the other scrollbar will steal a line from the buffer
334                    (true, true)
335                }
336            }
337        }
338    }
339
340    fn render_vertical_scrollbar(&self, area: Rect, buf: &mut Buffer, state: &ScrollViewState) {
341        let scrollbar_height = self.size.height.saturating_sub(area.height);
342        let mut scrollbar_state =
343            ScrollbarState::new(scrollbar_height as usize).position(state.offset.y as usize);
344        let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
345        scrollbar.render(area, buf, &mut scrollbar_state);
346    }
347
348    fn render_horizontal_scrollbar(&self, area: Rect, buf: &mut Buffer, state: &ScrollViewState) {
349        let scrollbar_width = self.size.width.saturating_sub(area.width);
350        let mut scrollbar_state =
351            ScrollbarState::new(scrollbar_width as usize).position(state.offset.x as usize);
352        let scrollbar = Scrollbar::new(ScrollbarOrientation::HorizontalBottom);
353        scrollbar.render(area, buf, &mut scrollbar_state);
354    }
355
356    fn render_visible_area(&self, area: Rect, buf: &mut Buffer, visible_area: Rect) {
357        // TODO: there's probably a more efficient way to do this
358        for (src_row, dst_row) in visible_area.rows().zip(area.rows()) {
359            for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
360                buf[dst_col] = self.buf[src_col].clone();
361            }
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use ratatui::text::Span;
369    use rstest::{fixture, rstest};
370
371    use super::*;
372
373    /// Initialize a buffer and a scroll view with a buffer size of 10x10
374    ///
375    /// The buffer will be filled with characters from A to Z in a 10x10 grid
376    ///
377    /// ```plain
378    /// ABCDEFGHIJ
379    /// KLMNOPQRST
380    /// UVWXYZABCD
381    /// EFGHIJKLMN
382    /// OPQRSTUVWX
383    /// YZABCDEFGH
384    /// IJKLMNOPQR
385    /// STUVWXYZAB
386    /// CDEFGHIJKL
387    /// MNOPQRSTUV
388    /// ```
389    #[fixture]
390    fn scroll_view() -> ScrollView {
391        let mut scroll_view = ScrollView::new(Size::new(10, 10));
392        for y in 0..10 {
393            for x in 0..10 {
394                let c = char::from_u32((x + y * 10) % 26 + 65).unwrap();
395                let widget = Span::raw(format!("{c}"));
396                let area = Rect::new(x as u16, y as u16, 1, 1);
397                scroll_view.render_widget(widget, area);
398            }
399        }
400        scroll_view
401    }
402
403    #[rstest]
404    fn zero_offset(scroll_view: ScrollView) {
405        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
406        let mut state = ScrollViewState::default();
407        scroll_view.render(buf.area, &mut buf, &mut state);
408        assert_eq!(
409            buf,
410            Buffer::with_lines(vec![
411                "ABCDE▲",
412                "KLMNO█",
413                "UVWXY█",
414                "EFGHI║",
415                "OPQRS▼",
416                "◄██═► ",
417            ])
418        )
419    }
420
421    #[rstest]
422    fn move_right(scroll_view: ScrollView) {
423        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
424        let mut state = ScrollViewState::with_offset((3, 0).into());
425        scroll_view.render(buf.area, &mut buf, &mut state);
426        assert_eq!(
427            buf,
428            Buffer::with_lines(vec![
429                "DEFGH▲",
430                "NOPQR█",
431                "XYZAB█",
432                "HIJKL║",
433                "RSTUV▼",
434                "◄═██► ",
435            ])
436        )
437    }
438
439    #[rstest]
440    fn move_down(scroll_view: ScrollView) {
441        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
442        let mut state = ScrollViewState::with_offset((0, 3).into());
443        scroll_view.render(buf.area, &mut buf, &mut state);
444        assert_eq!(
445            buf,
446            Buffer::with_lines(vec![
447                "EFGHI▲",
448                "OPQRS║",
449                "YZABC█",
450                "IJKLM█",
451                "STUVW▼",
452                "◄██═► ",
453            ])
454        )
455    }
456
457    #[rstest]
458    fn hides_both_scrollbars(scroll_view: ScrollView) {
459        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
460        let mut state = ScrollViewState::new();
461        scroll_view.render(buf.area, &mut buf, &mut state);
462        assert_eq!(
463            buf,
464            Buffer::with_lines(vec![
465                "ABCDEFGHIJ",
466                "KLMNOPQRST",
467                "UVWXYZABCD",
468                "EFGHIJKLMN",
469                "OPQRSTUVWX",
470                "YZABCDEFGH",
471                "IJKLMNOPQR",
472                "STUVWXYZAB",
473                "CDEFGHIJKL",
474                "MNOPQRSTUV",
475            ])
476        )
477    }
478
479    #[rstest]
480    fn hides_horizontal_scrollbar(scroll_view: ScrollView) {
481        let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
482        let mut state = ScrollViewState::new();
483        scroll_view.render(buf.area, &mut buf, &mut state);
484        assert_eq!(
485            buf,
486            Buffer::with_lines(vec![
487                "ABCDEFGHIJ▲",
488                "KLMNOPQRST█",
489                "UVWXYZABCD█",
490                "EFGHIJKLMN█",
491                "OPQRSTUVWX█",
492                "YZABCDEFGH█",
493                "IJKLMNOPQR█",
494                "STUVWXYZAB█",
495                "CDEFGHIJKL▼",
496            ])
497        )
498    }
499
500    #[rstest]
501    fn hides_vertical_scrollbar(scroll_view: ScrollView) {
502        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
503        let mut state = ScrollViewState::new();
504        scroll_view.render(buf.area, &mut buf, &mut state);
505        assert_eq!(
506            buf,
507            Buffer::with_lines(vec![
508                "ABCDEFGHI",
509                "KLMNOPQRS",
510                "UVWXYZABC",
511                "EFGHIJKLM",
512                "OPQRSTUVW",
513                "YZABCDEFG",
514                "IJKLMNOPQ",
515                "STUVWXYZA",
516                "CDEFGHIJK",
517                "MNOPQRSTU",
518                "◄███████►",
519            ])
520        )
521    }
522
523    /// Tests the scenario where the vertical scollbar steals a column from the right side of the
524    /// buffer which causes the horizontal scrollbar to be shown.
525    #[rstest]
526    fn does_not_hide_horizontal_scrollbar(scroll_view: ScrollView) {
527        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
528        let mut state = ScrollViewState::new();
529        scroll_view.render(buf.area, &mut buf, &mut state);
530        assert_eq!(
531            buf,
532            Buffer::with_lines(vec![
533                "ABCDEFGHI▲",
534                "KLMNOPQRS█",
535                "UVWXYZABC█",
536                "EFGHIJKLM█",
537                "OPQRSTUVW█",
538                "YZABCDEFG█",
539                "IJKLMNOPQ║",
540                "STUVWXYZA▼",
541                "◄███████► ",
542            ])
543        )
544    }
545
546    /// Tests the scenario where the horizontal scollbar steals a row from the bottom side of the
547    /// buffer which causes the vertical scrollbar to be shown.
548    #[rstest]
549    fn does_not_hide_vertical_scrollbar(scroll_view: ScrollView) {
550        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
551        let mut state = ScrollViewState::new();
552        scroll_view.render(buf.area, &mut buf, &mut state);
553        assert_eq!(
554            buf,
555            Buffer::with_lines(vec![
556                "ABCDEFGH▲",
557                "KLMNOPQR█",
558                "UVWXYZAB█",
559                "EFGHIJKL█",
560                "OPQRSTUV█",
561                "YZABCDEF█",
562                "IJKLMNOP█",
563                "STUVWXYZ█",
564                "CDEFGHIJ▼",
565                "◄█████═► ",
566            ])
567        )
568    }
569
570    /// The purpose of this test is to ensure that the buffer offset is correctly calculated when
571    /// rendering a scroll view into a buffer (i.e. the buffer offset is not always (0, 0)).
572    #[rstest]
573    fn ensure_buffer_offset_is_correct(scroll_view: ScrollView) {
574        let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
575        let mut state = ScrollViewState::with_offset((2, 3).into());
576        scroll_view.render(Rect::new(5, 6, 7, 8), &mut buf, &mut state);
577        assert_eq!(
578            buf,
579            Buffer::with_lines(vec![
580                "                    ",
581                "                    ",
582                "                    ",
583                "                    ",
584                "                    ",
585                "                    ",
586                "     GHIJKL▲        ",
587                "     QRSTUV║        ",
588                "     ABCDEF█        ",
589                "     KLMNOP█        ",
590                "     UVWXYZ█        ",
591                "     EFGHIJ█        ",
592                "     OPQRST▼        ",
593                "     ◄═███►         ",
594                "                    ",
595                "                    ",
596                "                    ",
597                "                    ",
598                "                    ",
599                "                    ",
600            ])
601        )
602    }
603    /// The purpose of this test is to ensure that the last elements are rendered.
604    #[rstest]
605    fn ensure_buffer_last_elements(scroll_view: ScrollView) {
606        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
607        let mut state = ScrollViewState::with_offset((5, 5).into());
608        scroll_view.render(buf.area, &mut buf, &mut state);
609        assert_eq!(
610            buf,
611            Buffer::with_lines(vec![
612                "DEFGH▲",
613                "NOPQR║",
614                "XYZAB█",
615                "HIJKL█",
616                "RSTUV▼",
617                "◄═██► ",
618            ])
619        )
620    }
621    #[rstest]
622    #[should_panic(expected = "Scrollbar area is empty")]
623    fn zero_width(scroll_view: ScrollView) {
624        let mut buf = Buffer::empty(Rect::new(0, 0, 0, 10));
625        let mut state = ScrollViewState::new();
626        scroll_view.render(buf.area, &mut buf, &mut state);
627    }
628    #[rstest]
629    #[should_panic(expected = "Scrollbar area is empty")]
630    fn zero_height(scroll_view: ScrollView) {
631        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 0));
632        let mut state = ScrollViewState::new();
633        scroll_view.render(buf.area, &mut buf, &mut state);
634    }
635
636    #[rstest]
637    fn never_vertical_scrollbar(mut scroll_view: ScrollView) {
638        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
639        let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
640        let mut state = ScrollViewState::new();
641        scroll_view.render(buf.area, &mut buf, &mut state);
642        assert_eq!(
643            buf,
644            Buffer::with_lines(vec![
645                "ABCDEFGHIJ ",
646                "KLMNOPQRST ",
647                "UVWXYZABCD ",
648                "EFGHIJKLMN ",
649                "OPQRSTUVWX ",
650                "YZABCDEFGH ",
651                "IJKLMNOPQR ",
652                "STUVWXYZAB ",
653                "CDEFGHIJKL ",
654            ])
655        )
656    }
657
658    #[rstest]
659    fn never_horizontal_scrollbar(mut scroll_view: ScrollView) {
660        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
661        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
662        let mut state = ScrollViewState::new();
663        scroll_view.render(buf.area, &mut buf, &mut state);
664        assert_eq!(
665            buf,
666            Buffer::with_lines(vec![
667                "ABCDEFGHI",
668                "KLMNOPQRS",
669                "UVWXYZABC",
670                "EFGHIJKLM",
671                "OPQRSTUVW",
672                "YZABCDEFG",
673                "IJKLMNOPQ",
674                "STUVWXYZA",
675                "CDEFGHIJK",
676                "MNOPQRSTU",
677                "         ",
678            ])
679        )
680    }
681
682    #[rstest]
683    fn does_not_trigger_horizontal_scrollbar(mut scroll_view: ScrollView) {
684        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
685        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
686        let mut state = ScrollViewState::new();
687        scroll_view.render(buf.area, &mut buf, &mut state);
688        assert_eq!(
689            buf,
690            Buffer::with_lines(vec![
691                "ABCDEFGHIJ",
692                "KLMNOPQRST",
693                "UVWXYZABCD",
694                "EFGHIJKLMN",
695                "OPQRSTUVWX",
696                "YZABCDEFGH",
697                "IJKLMNOPQR",
698                "STUVWXYZAB",
699                "CDEFGHIJKL",
700            ])
701        )
702    }
703
704    #[rstest]
705    fn does_not_trigger_vertical_scrollbar(mut scroll_view: ScrollView) {
706        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
707        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
708        let mut state = ScrollViewState::new();
709        scroll_view.render(buf.area, &mut buf, &mut state);
710        assert_eq!(
711            buf,
712            Buffer::with_lines(vec![
713                "ABCDEFGHI",
714                "KLMNOPQRS",
715                "UVWXYZABC",
716                "EFGHIJKLM",
717                "OPQRSTUVW",
718                "YZABCDEFG",
719                "IJKLMNOPQ",
720                "STUVWXYZA",
721                "CDEFGHIJK",
722                "MNOPQRSTU",
723            ])
724        )
725    }
726
727    #[rstest]
728    fn does_not_render_vertical_scrollbar(mut scroll_view: ScrollView) {
729        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
730        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
731        let mut state = ScrollViewState::default();
732        scroll_view.render(buf.area, &mut buf, &mut state);
733        assert_eq!(
734            buf,
735            Buffer::with_lines(vec![
736                "ABCDEF",
737                "KLMNOP",
738                "UVWXYZ",
739                "EFGHIJ",
740                "OPQRST",
741                "◄███═►",
742            ])
743        )
744    }
745
746    #[rstest]
747    fn does_not_render_horizontal_scrollbar(mut scroll_view: ScrollView) {
748        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
749        let mut buf = Buffer::empty(Rect::new(0, 0, 7, 6));
750        let mut state = ScrollViewState::default();
751        scroll_view.render(buf.area, &mut buf, &mut state);
752        assert_eq!(
753            buf,
754            Buffer::with_lines(vec![
755                "ABCDEF▲",
756                "KLMNOP█",
757                "UVWXYZ█",
758                "EFGHIJ█",
759                "OPQRST║",
760                "YZABCD▼",
761            ])
762        )
763    }
764
765    #[rstest]
766    #[rustfmt::skip]
767    fn does_not_render_both_scrollbars(mut scroll_view: ScrollView) {
768        scroll_view = scroll_view.scrollbars_visibility(ScrollbarVisibility::Never);
769        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
770        let mut state = ScrollViewState::default();
771        scroll_view.render(buf.area, &mut buf, &mut state);
772        assert_eq!(
773            buf,
774            Buffer::with_lines(vec![
775                "ABCDEF",
776                "KLMNOP",
777                "UVWXYZ",
778                "EFGHIJ",
779                "OPQRST",
780                "YZABCD",
781            ])
782        )
783    }
784
785    #[rstest]
786    #[rustfmt::skip]
787    fn render_stateful_widget(mut scroll_view: ScrollView) {
788        use ratatui::widgets::{List, ListState};
789        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
790        let mut buf = Buffer::empty(Rect::new(0, 0, 7, 5));
791        let mut state = ScrollViewState::default();
792        let mut list_state = ListState::default();
793        let items: Vec<String> = (1..=10).map(|i| format!("Item {i}")).collect();
794        let list = List::new(items);
795        scroll_view.render_stateful_widget(list, scroll_view.area(), &mut list_state);
796        scroll_view.render(buf.area, &mut buf, &mut state);
797        assert_eq!(
798            buf,
799            Buffer::with_lines(vec![
800                "Item 1▲",
801                "Item 2█",
802                "Item 3█",
803                "Item 4║",
804                "Item 5▼",
805            ])
806        )
807    }
808}