tui_scrollview/
scroll_view.rs

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