tui_scrollview/
scroll_view.rs

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