Skip to main content

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///
51/// If you store the `ScrollView`, render it by reference so the same prepared buffer can be reused
52/// across frames.
53///
54/// ```rust
55/// use ratatui::prelude::*;
56/// use tui_scrollview::{ScrollView, ScrollViewState};
57///
58/// # fn terminal_draw(frame: &mut Frame, scroll_view: &ScrollView, state: &mut ScrollViewState) {
59/// frame.render_stateful_widget(scroll_view, frame.area(), state);
60/// # }
61/// ```
62#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
63pub struct ScrollView {
64    buf: Buffer,
65    size: Size,
66    vertical_scrollbar_visibility: ScrollbarVisibility,
67    horizontal_scrollbar_visibility: ScrollbarVisibility,
68}
69
70/// The visibility of the vertical and horizontal scrollbars.
71#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
72pub enum ScrollbarVisibility {
73    /// Render the scrollbar only whenever needed.
74    #[default]
75    Automatic,
76    /// Always render the scrollbar.
77    Always,
78    /// Never render the scrollbar (hide it).
79    Never,
80}
81
82impl ScrollView {
83    /// Create a new scroll view with a buffer of the given size
84    ///
85    /// The buffer will be empty, with coordinates ranging from (0, 0) to (size.width, size.height).
86    pub fn new(size: Size) -> Self {
87        // TODO: this is replaced with Rect::from(size) in the next version of ratatui
88        let area = Rect::new(0, 0, size.width, size.height);
89        Self {
90            buf: Buffer::empty(area),
91            size,
92            horizontal_scrollbar_visibility: ScrollbarVisibility::default(),
93            vertical_scrollbar_visibility: ScrollbarVisibility::default(),
94        }
95    }
96
97    /// The content size of the scroll view
98    pub const fn size(&self) -> Size {
99        self.size
100    }
101
102    /// The area of the buffer that is available to be scrolled
103    pub const fn area(&self) -> Rect {
104        self.buf.area
105    }
106
107    /// The buffer containing the contents of the scroll view
108    pub const fn buf(&self) -> &Buffer {
109        &self.buf
110    }
111
112    /// The mutable buffer containing the contents of the scroll view
113    ///
114    /// This can be used to render widgets into the buffer programmatically
115    ///
116    /// # Examples
117    ///
118    /// ```rust
119    /// # use ratatui::{prelude::*, layout::Size, widgets::*};
120    /// # use tui_scrollview::ScrollView;
121    ///
122    /// let mut scroll_view = ScrollView::new(Size::new(20, 20));
123    /// Line::raw("Hello, world!").render(Rect::new(0, 0, 20, 1), scroll_view.buf_mut());
124    /// ```
125    pub const fn buf_mut(&mut self) -> &mut Buffer {
126        &mut self.buf
127    }
128
129    /// Set the visibility of the vertical scrollbar
130    ///
131    /// See [`ScrollbarVisibility`] for all the options.
132    ///
133    /// This is a fluent setter method which must be chained or used as it consumes self
134    ///
135    /// # Examples
136    ///
137    /// ```rust
138    /// # use ratatui::{prelude::*, layout::Size, widgets::*};
139    /// # use tui_scrollview::{ScrollView, ScrollbarVisibility};
140    ///
141    /// let mut scroll_view = ScrollView::new(Size::new(20, 20))
142    ///     .vertical_scrollbar_visibility(ScrollbarVisibility::Always);
143    /// ```
144    pub const fn vertical_scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
145        self.vertical_scrollbar_visibility = visibility;
146        self
147    }
148
149    /// Set the visibility of the horizontal scrollbar
150    ///
151    /// See [`ScrollbarVisibility`] for all the options.
152    ///
153    /// This is a fluent setter method which must be chained or used as it consumes self
154    ///
155    /// # Examples
156    ///
157    /// ```rust
158    /// # use ratatui::{prelude::*, layout::Size, widgets::*};
159    /// # use tui_scrollview::{ScrollView, ScrollbarVisibility};
160    ///
161    /// let mut scroll_view = ScrollView::new(Size::new(20, 20))
162    ///     .horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
163    /// ```
164    pub const fn horizontal_scrollbar_visibility(
165        mut self,
166        visibility: ScrollbarVisibility,
167    ) -> Self {
168        self.horizontal_scrollbar_visibility = visibility;
169        self
170    }
171
172    /// Set the visibility of both vertical and horizontal scrollbars
173    ///
174    /// See [`ScrollbarVisibility`] for all the options.
175    ///
176    /// This is a fluent setter method which must be chained or used as it consumes self
177    ///
178    /// # Examples
179    ///
180    /// ```rust
181    /// # use ratatui::{prelude::*, layout::Size, widgets::*};
182    /// # use tui_scrollview::{ScrollView, ScrollbarVisibility};
183    ///
184    /// let mut scroll_view =
185    ///     ScrollView::new(Size::new(20, 20)).scrollbars_visibility(ScrollbarVisibility::Automatic);
186    /// ```
187    pub const fn scrollbars_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
188        self.vertical_scrollbar_visibility = visibility;
189        self.horizontal_scrollbar_visibility = visibility;
190        self
191    }
192
193    /// Render a widget into the scroll buffer
194    ///
195    /// This is the equivalent of `Frame::render_widget`, but renders the widget into the scroll
196    /// buffer rather than the main buffer. The widget will be rendered into the area of the buffer
197    /// 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_widget<W: Widget>(&mut self, widget: W, area: Rect) {
202        widget.render(area, &mut self.buf);
203    }
204
205    /// Render a stateful widget into the scroll buffer
206    ///
207    /// This is the equivalent of `Frame::render_stateful_widget`, but renders the stateful widget
208    /// into the scroll buffer rather than the main buffer. The stateful widget will be rendered
209    /// into the area of the buffer specified by the `area` parameter.
210    ///
211    /// This should not be confused with the `render` method, which renders the visible area of the
212    /// ScrollView into the main buffer.
213    pub fn render_stateful_widget<W: StatefulWidget>(
214        &mut self,
215        widget: W,
216        area: Rect,
217        state: &mut W::State,
218    ) {
219        widget.render(area, &mut self.buf, state);
220    }
221}
222
223impl StatefulWidget for ScrollView {
224    type State = ScrollViewState;
225
226    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
227        (&self).render(area, buf, state);
228    }
229}
230
231impl StatefulWidget for &ScrollView {
232    type State = ScrollViewState;
233
234    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
235        let (mut x, mut y) = state.offset.into();
236        let horizontal_space = area.width as i32 - self.size.width as i32;
237        let vertical_space = area.height as i32 - self.size.height as i32;
238        let (show_horizontal, show_vertical) =
239            self.visible_scrollbars(horizontal_space, vertical_space);
240
241        // Scrollbars steal space from the viewport. Clamp offsets against that final viewport,
242        // not the raw render area, so the bottom position is the last full page of content.
243        let viewport_width = area.width.saturating_sub(show_vertical as u16);
244        let viewport_height = area.height.saturating_sub(show_horizontal as u16);
245
246        // If the content fits in a direction, discard any stale offset for that direction.
247        if horizontal_space > 0 {
248            x = 0;
249        }
250        if vertical_space > 0 {
251            y = 0;
252        }
253
254        // `saturating_sub` covers both "content smaller than viewport" and zero-sized viewport
255        // cases. Zero-sized areas later panic while rendering scrollbars, matching existing
256        // behavior, but this arithmetic still must not wrap before that boundary.
257        let max_x_offset = self.buf.area.width.saturating_sub(viewport_width);
258        let max_y_offset = self.buf.area.height.saturating_sub(viewport_height);
259
260        x = x.min(max_x_offset);
261        y = y.min(max_y_offset);
262        state.offset = (x, y).into();
263        state.size = Some(self.size);
264        let viewport_area = self.render_scrollbars(area, buf, state);
265        state.page_size = Some(viewport_area.as_size());
266        let visible_area = viewport_area.intersection(self.buf.area);
267        self.render_visible_area(area, buf, visible_area);
268    }
269}
270
271impl ScrollView {
272    /// Render needed scrollbars and return remaining area relative to
273    /// scrollview's buffer area.
274    fn render_scrollbars(&self, area: Rect, buf: &mut Buffer, state: &mut ScrollViewState) -> Rect {
275        // fit value per direction
276        //   > 0 => fits
277        //  == 0 => exact fit
278        //   < 0 => does not fit
279        let horizontal_space = area.width as i32 - self.size.width as i32;
280        let vertical_space = area.height as i32 - self.size.height as i32;
281
282        // If the content fits in a direction, reset state to reflect it.
283        if horizontal_space > 0 {
284            state.offset.x = 0;
285        }
286        if vertical_space > 0 {
287            state.offset.y = 0;
288        }
289
290        let (show_horizontal, show_vertical) =
291            self.visible_scrollbars(horizontal_space, vertical_space);
292
293        let new_height = if show_horizontal {
294            // if both bars are rendered, avoid the corner
295            let width = area.width.saturating_sub(show_vertical as u16);
296            let render_area = Rect { width, ..area };
297            // render scrollbar, update available space
298            self.render_horizontal_scrollbar(render_area, buf, state);
299            area.height.saturating_sub(1)
300        } else {
301            area.height
302        };
303
304        let new_width = if show_vertical {
305            // if both bars are rendered, avoid the corner
306            let height = area.height.saturating_sub(show_horizontal as u16);
307            let render_area = Rect { height, ..area };
308            // render scrollbar, update available space
309            self.render_vertical_scrollbar(render_area, buf, state);
310            area.width.saturating_sub(1)
311        } else {
312            area.width
313        };
314
315        Rect::new(state.offset.x, state.offset.y, new_width, new_height)
316    }
317
318    /// Resolve whether to render each scrollbar.
319    ///
320    /// Considers the visibility options set by the user and whether the scrollview size fits into
321    /// the the available area on each direction.
322    ///
323    /// The space arguments are the difference between the scrollview size and the available area.
324    ///
325    /// Returns a bool tuple with (horizontal, vertical) resolutions.
326    const fn visible_scrollbars(&self, horizontal_space: i32, vertical_space: i32) -> (bool, bool) {
327        type V = crate::scroll_view::ScrollbarVisibility;
328
329        match (
330            self.horizontal_scrollbar_visibility,
331            self.vertical_scrollbar_visibility,
332        ) {
333            // straightforward, no need to check fit values
334            (V::Always, V::Always) => (true, true),
335            (V::Never, V::Never) => (false, false),
336            (V::Always, V::Never) => (true, false),
337            (V::Never, V::Always) => (false, true),
338
339            // Auto => render scrollbar only if it doesn't fit
340            (V::Automatic, V::Never) => (horizontal_space < 0, false),
341            (V::Never, V::Automatic) => (false, vertical_space < 0),
342
343            // Auto => render scrollbar if:
344            //   it doesn't fit; or
345            //   exact fit (other scrollbar steals a line and triggers it)
346            (V::Always, V::Automatic) => (true, vertical_space <= 0),
347            (V::Automatic, V::Always) => (horizontal_space <= 0, true),
348
349            // depends solely on fit values
350            (V::Automatic, V::Automatic) => {
351                if horizontal_space >= 0 && vertical_space >= 0 {
352                    // there is enough space for both dimensions
353                    (false, false)
354                } else if horizontal_space < 0 && vertical_space < 0 {
355                    // there is not enough space for either dimension
356                    (true, true)
357                } else if horizontal_space > 0 && vertical_space < 0 {
358                    // horizontal fits, vertical does not
359                    (false, true)
360                } else if horizontal_space < 0 && vertical_space > 0 {
361                    // vertical fits, horizontal does not
362                    (true, false)
363                } else {
364                    // one is an exact fit and other does not fit which triggers both scrollbars to
365                    // be visible because the other scrollbar will steal a line from the buffer
366                    (true, true)
367                }
368            }
369        }
370    }
371
372    fn render_vertical_scrollbar(&self, area: Rect, buf: &mut Buffer, state: &ScrollViewState) {
373        let scrollbar_height = self.size.height.saturating_sub(area.height);
374        let mut scrollbar_state =
375            ScrollbarState::new(scrollbar_height as usize).position(state.offset.y as usize);
376        let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
377        scrollbar.render(area, buf, &mut scrollbar_state);
378    }
379
380    fn render_horizontal_scrollbar(&self, area: Rect, buf: &mut Buffer, state: &ScrollViewState) {
381        let scrollbar_width = self.size.width.saturating_sub(area.width);
382        let mut scrollbar_state =
383            ScrollbarState::new(scrollbar_width as usize).position(state.offset.x as usize);
384        let scrollbar = Scrollbar::new(ScrollbarOrientation::HorizontalBottom);
385        scrollbar.render(area, buf, &mut scrollbar_state);
386    }
387
388    fn render_visible_area(&self, area: Rect, buf: &mut Buffer, visible_area: Rect) {
389        // TODO: there's probably a more efficient way to do this
390        for (src_row, dst_row) in visible_area.rows().zip(area.rows()) {
391            for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
392                buf[dst_col] = self.buf[src_col].clone();
393            }
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use ratatui_core::text::Span;
401    use rstest::{fixture, rstest};
402
403    use super::*;
404
405    /// Initialize a buffer and a scroll view with a buffer size of 10x10
406    ///
407    /// The buffer will be filled with characters from A to Z in a 10x10 grid
408    ///
409    /// ```plain
410    /// ABCDEFGHIJ
411    /// KLMNOPQRST
412    /// UVWXYZABCD
413    /// EFGHIJKLMN
414    /// OPQRSTUVWX
415    /// YZABCDEFGH
416    /// IJKLMNOPQR
417    /// STUVWXYZAB
418    /// CDEFGHIJKL
419    /// MNOPQRSTUV
420    /// ```
421    #[fixture]
422    fn scroll_view() -> ScrollView {
423        let mut scroll_view = ScrollView::new(Size::new(10, 10));
424        for y in 0..10 {
425            for x in 0..10 {
426                let c = char::from_u32((x + y * 10) % 26 + 65).unwrap();
427                let widget = Span::raw(format!("{c}"));
428                let area = Rect::new(x as u16, y as u16, 1, 1);
429                scroll_view.render_widget(widget, area);
430            }
431        }
432        scroll_view
433    }
434
435    #[test]
436    fn dimensions_follow_content_and_viewport_changes() {
437        let mut state = ScrollViewState::default();
438        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
439        ScrollView::new(Size::new(10, 10)).render(buf.area, &mut buf, &mut state);
440        assert_eq!(state.size(), Some(Size::new(10, 10)));
441        assert_eq!(state.page_size(), Some(Size::new(5, 5)));
442
443        let mut buf = Buffer::empty(Rect::new(0, 0, 8, 7));
444        ScrollView::new(Size::new(10, 10))
445            .scrollbars_visibility(ScrollbarVisibility::Never)
446            .render(buf.area, &mut buf, &mut state);
447        assert_eq!(state.size(), Some(Size::new(10, 10)));
448        assert_eq!(state.page_size(), Some(Size::new(8, 7)));
449
450        ScrollView::new(Size::new(2, 3)).render(buf.area, &mut buf, &mut state);
451        assert_eq!(state.size(), Some(Size::new(2, 3)));
452        assert_eq!(state.page_size(), Some(Size::new(8, 7)));
453    }
454
455    #[rstest]
456    fn zero_offset(scroll_view: ScrollView) {
457        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
458        let mut state = ScrollViewState::default();
459        scroll_view.render(buf.area, &mut buf, &mut state);
460        assert_eq!(
461            buf,
462            Buffer::with_lines(vec![
463                "ABCDE▲",
464                "KLMNO█",
465                "UVWXY█",
466                "EFGHI║",
467                "OPQRS▼",
468                "◄██═► ",
469            ])
470        )
471    }
472
473    #[rstest]
474    fn render_by_reference_matches_owned_render(scroll_view: ScrollView) {
475        let mut owned_state = ScrollViewState::default();
476        let mut borrowed_state = ScrollViewState::default();
477        let mut owned_buf = Buffer::empty(Rect::new(0, 0, 6, 6));
478        let mut borrowed_buf = Buffer::empty(Rect::new(0, 0, 6, 6));
479
480        scroll_view
481            .clone()
482            .render(owned_buf.area, &mut owned_buf, &mut owned_state);
483        (&scroll_view).render(borrowed_buf.area, &mut borrowed_buf, &mut borrowed_state);
484
485        assert_eq!(borrowed_buf, owned_buf);
486        assert_eq!(borrowed_state, owned_state);
487    }
488
489    #[rstest]
490    fn move_right(scroll_view: ScrollView) {
491        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
492        let mut state = ScrollViewState::with_offset((3, 0).into());
493        scroll_view.render(buf.area, &mut buf, &mut state);
494        assert_eq!(
495            buf,
496            Buffer::with_lines(vec![
497                "DEFGH▲",
498                "NOPQR█",
499                "XYZAB█",
500                "HIJKL║",
501                "RSTUV▼",
502                "◄═██► ",
503            ])
504        )
505    }
506
507    #[rstest]
508    fn move_down(scroll_view: ScrollView) {
509        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
510        let mut state = ScrollViewState::with_offset((0, 3).into());
511        scroll_view.render(buf.area, &mut buf, &mut state);
512        assert_eq!(
513            buf,
514            Buffer::with_lines(vec![
515                "EFGHI▲",
516                "OPQRS║",
517                "YZABC█",
518                "IJKLM█",
519                "STUVW▼",
520                "◄██═► ",
521            ])
522        )
523    }
524
525    #[rstest]
526    fn is_not_at_bottom_until_the_last_row_is_visible(scroll_view: ScrollView) {
527        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
528        let mut state = ScrollViewState::with_offset((0, 4).into());
529
530        scroll_view.render(buf.area, &mut buf, &mut state);
531
532        assert_eq!(
533            buf,
534            Buffer::with_lines(vec![
535                "OPQRS▲",
536                "YZABC║",
537                "IJKLM█",
538                "STUVW█",
539                "CDEFG▼",
540                "◄██═► ",
541            ])
542        );
543        assert!(!state.is_at_bottom());
544    }
545
546    #[rstest]
547    fn move_to_bottom(scroll_view: ScrollView) {
548        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
549        let mut state = ScrollViewState::default();
550
551        // Prior to rendering, page and buffer size are unknown. We default to `true`.
552        assert!(state.is_at_bottom());
553
554        scroll_view.clone().render(buf.area, &mut buf, &mut state);
555
556        // The vertical view size is five which means the page size is five.
557        // We have not scrolled yet, so the view is at the top and not at the bottom.
558        // => We see the top five rows
559        assert!(!state.is_at_bottom());
560
561        // Since the content height is ten,
562        assert_eq!(state.size.unwrap().height, 10);
563        // if we scroll down one page (five rows),
564        state.scroll_down();
565        state.scroll_down();
566        state.scroll_down();
567        state.scroll_down();
568        state.scroll_down();
569
570        // we reach the bottom,
571        assert!(state.is_at_bottom());
572        assert_eq!(state.offset.y, 5);
573
574        // and we see the last five rows of the content.
575        scroll_view.render(buf.area, &mut buf, &mut state);
576        assert_eq!(
577            buf,
578            Buffer::with_lines(vec![
579                "YZABC▲",
580                "IJKLM║",
581                "STUVW█",
582                "CDEFG█",
583                "MNOPQ▼",
584                "◄██═► ",
585            ])
586        );
587
588        // We could also jump directly to the bottom...
589        state.scroll_to_bottom();
590        assert!(state.is_at_bottom());
591
592        // ...which sets the offset to the last row of content,
593        // ensuring to be at the bottom regardless of the page size.
594        assert_eq!(state.offset.y, state.size.unwrap().height - 1);
595    }
596
597    #[rstest]
598    fn rendering_at_bottom_uses_the_last_full_page(scroll_view: ScrollView) {
599        let mut buf = Buffer::empty(Rect::new(0, 0, 11, 6));
600        let mut state = ScrollViewState::default();
601
602        state.scroll_to_bottom();
603        scroll_view.render(buf.area, &mut buf, &mut state);
604
605        assert_eq!(
606            buf,
607            Buffer::with_lines(vec![
608                "OPQRSTUVWX▲",
609                "YZABCDEFGH║",
610                "IJKLMNOPQR█",
611                "STUVWXYZAB█",
612                "CDEFGHIJKL█",
613                "MNOPQRSTUV▼",
614            ])
615        );
616        assert_eq!(state.offset.y, 4);
617        assert_eq!(state.page_size.unwrap().height, 6);
618    }
619
620    #[rstest]
621    #[case::always_always(
622        ScrollbarVisibility::Always,
623        ScrollbarVisibility::Always,
624        1,
625        1,
626        (true, true)
627    )]
628    #[case::never_never(
629        ScrollbarVisibility::Never,
630        ScrollbarVisibility::Never,
631        -1,
632        -1,
633        (false, false)
634    )]
635    #[case::always_never(
636        ScrollbarVisibility::Always,
637        ScrollbarVisibility::Never,
638        1,
639        1,
640        (true, false)
641    )]
642    #[case::never_always(
643        ScrollbarVisibility::Never,
644        ScrollbarVisibility::Always,
645        1,
646        1,
647        (false, true)
648    )]
649    #[case::automatic_never_needs_horizontal(
650        ScrollbarVisibility::Automatic,
651        ScrollbarVisibility::Never,
652        -1,
653        1,
654        (true, false)
655    )]
656    #[case::automatic_never_fits_horizontal(
657        ScrollbarVisibility::Automatic,
658        ScrollbarVisibility::Never,
659        1,
660        -1,
661        (false, false)
662    )]
663    #[case::never_automatic_needs_vertical(
664        ScrollbarVisibility::Never,
665        ScrollbarVisibility::Automatic,
666        1,
667        -1,
668        (false, true)
669    )]
670    #[case::never_automatic_fits_vertical(
671        ScrollbarVisibility::Never,
672        ScrollbarVisibility::Automatic,
673        -1,
674        1,
675        (false, false)
676    )]
677    #[case::always_automatic_exact_fit(
678        ScrollbarVisibility::Always,
679        ScrollbarVisibility::Automatic,
680        1,
681        0,
682        (true, true)
683    )]
684    #[case::always_automatic_vertical_fits(
685        ScrollbarVisibility::Always,
686        ScrollbarVisibility::Automatic,
687        1,
688        1,
689        (true, false)
690    )]
691    #[case::automatic_always_exact_fit(
692        ScrollbarVisibility::Automatic,
693        ScrollbarVisibility::Always,
694        0,
695        1,
696        (true, true)
697    )]
698    #[case::automatic_always_horizontal_fits(
699        ScrollbarVisibility::Automatic,
700        ScrollbarVisibility::Always,
701        1,
702        1,
703        (false, true)
704    )]
705    #[case::automatic_automatic_both_fit(
706        ScrollbarVisibility::Automatic,
707        ScrollbarVisibility::Automatic,
708        1,
709        1,
710        (false, false)
711    )]
712    #[case::automatic_automatic_both_overflow(
713        ScrollbarVisibility::Automatic,
714        ScrollbarVisibility::Automatic,
715        -1,
716        -1,
717        (true, true)
718    )]
719    #[case::automatic_automatic_only_vertical_overflows(
720        ScrollbarVisibility::Automatic,
721        ScrollbarVisibility::Automatic,
722        1,
723        -1,
724        (false, true)
725    )]
726    #[case::automatic_automatic_only_horizontal_overflows(
727        ScrollbarVisibility::Automatic,
728        ScrollbarVisibility::Automatic,
729        -1,
730        1,
731        (true, false)
732    )]
733    #[case::automatic_automatic_exact_fit_with_other_overflow(
734        ScrollbarVisibility::Automatic,
735        ScrollbarVisibility::Automatic,
736        0,
737        -1,
738        (true, true)
739    )]
740    fn visible_scrollbars_honors_visibility_policy(
741        #[case] horizontal_visibility: ScrollbarVisibility,
742        #[case] vertical_visibility: ScrollbarVisibility,
743        #[case] horizontal_space: i32,
744        #[case] vertical_space: i32,
745        #[case] expected: (bool, bool),
746    ) {
747        let scroll_view = ScrollView::new(Size::new(1, 1))
748            .horizontal_scrollbar_visibility(horizontal_visibility)
749            .vertical_scrollbar_visibility(vertical_visibility);
750
751        assert_eq!(
752            scroll_view.visible_scrollbars(horizontal_space, vertical_space),
753            expected
754        );
755    }
756
757    #[rstest]
758    fn hides_both_scrollbars(scroll_view: ScrollView) {
759        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
760        let mut state = ScrollViewState::new();
761        scroll_view.render(buf.area, &mut buf, &mut state);
762        assert_eq!(
763            buf,
764            Buffer::with_lines(vec![
765                "ABCDEFGHIJ",
766                "KLMNOPQRST",
767                "UVWXYZABCD",
768                "EFGHIJKLMN",
769                "OPQRSTUVWX",
770                "YZABCDEFGH",
771                "IJKLMNOPQR",
772                "STUVWXYZAB",
773                "CDEFGHIJKL",
774                "MNOPQRSTUV",
775            ])
776        )
777    }
778
779    #[rstest]
780    fn hides_horizontal_scrollbar(scroll_view: ScrollView) {
781        let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
782        let mut state = ScrollViewState::new();
783        scroll_view.render(buf.area, &mut buf, &mut state);
784        assert_eq!(
785            buf,
786            Buffer::with_lines(vec![
787                "ABCDEFGHIJ▲",
788                "KLMNOPQRST█",
789                "UVWXYZABCD█",
790                "EFGHIJKLMN█",
791                "OPQRSTUVWX█",
792                "YZABCDEFGH█",
793                "IJKLMNOPQR█",
794                "STUVWXYZAB█",
795                "CDEFGHIJKL▼",
796            ])
797        )
798    }
799
800    #[rstest]
801    fn hides_vertical_scrollbar(scroll_view: ScrollView) {
802        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
803        let mut state = ScrollViewState::new();
804        scroll_view.render(buf.area, &mut buf, &mut state);
805        assert_eq!(
806            buf,
807            Buffer::with_lines(vec![
808                "ABCDEFGHI",
809                "KLMNOPQRS",
810                "UVWXYZABC",
811                "EFGHIJKLM",
812                "OPQRSTUVW",
813                "YZABCDEFG",
814                "IJKLMNOPQ",
815                "STUVWXYZA",
816                "CDEFGHIJK",
817                "MNOPQRSTU",
818                "◄███████►",
819            ])
820        )
821    }
822
823    /// Tests the scenario where the vertical scrollbar steals a column from the right side of the
824    /// buffer which causes the horizontal scrollbar to be shown.
825    #[rstest]
826    fn does_not_hide_horizontal_scrollbar(scroll_view: ScrollView) {
827        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
828        let mut state = ScrollViewState::new();
829        scroll_view.render(buf.area, &mut buf, &mut state);
830        assert_eq!(
831            buf,
832            Buffer::with_lines(vec![
833                "ABCDEFGHI▲",
834                "KLMNOPQRS█",
835                "UVWXYZABC█",
836                "EFGHIJKLM█",
837                "OPQRSTUVW█",
838                "YZABCDEFG█",
839                "IJKLMNOPQ║",
840                "STUVWXYZA▼",
841                "◄███████► ",
842            ])
843        )
844    }
845
846    /// Tests the scenario where the horizontal scrollbar steals a row from the bottom side of the
847    /// buffer which causes the vertical scrollbar to be shown.
848    #[rstest]
849    fn does_not_hide_vertical_scrollbar(scroll_view: ScrollView) {
850        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
851        let mut state = ScrollViewState::new();
852        scroll_view.render(buf.area, &mut buf, &mut state);
853        assert_eq!(
854            buf,
855            Buffer::with_lines(vec![
856                "ABCDEFGH▲",
857                "KLMNOPQR█",
858                "UVWXYZAB█",
859                "EFGHIJKL█",
860                "OPQRSTUV█",
861                "YZABCDEF█",
862                "IJKLMNOP█",
863                "STUVWXYZ█",
864                "CDEFGHIJ▼",
865                "◄█████═► ",
866            ])
867        )
868    }
869
870    /// The purpose of this test is to ensure that the buffer offset is correctly calculated when
871    /// rendering a scroll view into a buffer (i.e. the buffer offset is not always (0, 0)).
872    #[rstest]
873    fn ensure_buffer_offset_is_correct(scroll_view: ScrollView) {
874        let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
875        let mut state = ScrollViewState::with_offset((2, 3).into());
876        scroll_view.render(Rect::new(5, 6, 7, 8), &mut buf, &mut state);
877        assert_eq!(
878            buf,
879            Buffer::with_lines(vec![
880                "                    ",
881                "                    ",
882                "                    ",
883                "                    ",
884                "                    ",
885                "                    ",
886                "     GHIJKL▲        ",
887                "     QRSTUV║        ",
888                "     ABCDEF█        ",
889                "     KLMNOP█        ",
890                "     UVWXYZ█        ",
891                "     EFGHIJ█        ",
892                "     OPQRST▼        ",
893                "     ◄═███►         ",
894                "                    ",
895                "                    ",
896                "                    ",
897                "                    ",
898                "                    ",
899                "                    ",
900            ])
901        )
902    }
903    /// The purpose of this test is to ensure that the last elements are rendered.
904    #[rstest]
905    fn ensure_buffer_last_elements(scroll_view: ScrollView) {
906        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
907        let mut state = ScrollViewState::with_offset((5, 5).into());
908        scroll_view.render(buf.area, &mut buf, &mut state);
909        assert_eq!(
910            buf,
911            Buffer::with_lines(vec![
912                "DEFGH▲",
913                "NOPQR║",
914                "XYZAB█",
915                "HIJKL█",
916                "RSTUV▼",
917                "◄═██► ",
918            ])
919        )
920    }
921    #[rstest]
922    fn zero_width(scroll_view: ScrollView) {
923        let mut buf = Buffer::empty(Rect::new(0, 0, 0, 10));
924        let mut state = ScrollViewState::new();
925        scroll_view.render(buf.area, &mut buf, &mut state);
926        assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 0, 10)));
927    }
928
929    #[rstest]
930    fn zero_height(scroll_view: ScrollView) {
931        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 0));
932        let mut state = ScrollViewState::new();
933        scroll_view.render(buf.area, &mut buf, &mut state);
934        assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 10, 0)));
935    }
936
937    #[rstest]
938    fn never_vertical_scrollbar(mut scroll_view: ScrollView) {
939        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
940        let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
941        let mut state = ScrollViewState::new();
942        scroll_view.render(buf.area, &mut buf, &mut state);
943        assert_eq!(
944            buf,
945            Buffer::with_lines(vec![
946                "ABCDEFGHIJ ",
947                "KLMNOPQRST ",
948                "UVWXYZABCD ",
949                "EFGHIJKLMN ",
950                "OPQRSTUVWX ",
951                "YZABCDEFGH ",
952                "IJKLMNOPQR ",
953                "STUVWXYZAB ",
954                "CDEFGHIJKL ",
955            ])
956        )
957    }
958
959    #[rstest]
960    fn never_horizontal_scrollbar(mut scroll_view: ScrollView) {
961        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
962        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
963        let mut state = ScrollViewState::new();
964        scroll_view.render(buf.area, &mut buf, &mut state);
965        assert_eq!(
966            buf,
967            Buffer::with_lines(vec![
968                "ABCDEFGHI",
969                "KLMNOPQRS",
970                "UVWXYZABC",
971                "EFGHIJKLM",
972                "OPQRSTUVW",
973                "YZABCDEFG",
974                "IJKLMNOPQ",
975                "STUVWXYZA",
976                "CDEFGHIJK",
977                "MNOPQRSTU",
978                "         ",
979            ])
980        )
981    }
982
983    #[rstest]
984    fn does_not_trigger_horizontal_scrollbar(mut scroll_view: ScrollView) {
985        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
986        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
987        let mut state = ScrollViewState::new();
988        scroll_view.render(buf.area, &mut buf, &mut state);
989        assert_eq!(
990            buf,
991            Buffer::with_lines(vec![
992                "ABCDEFGHIJ",
993                "KLMNOPQRST",
994                "UVWXYZABCD",
995                "EFGHIJKLMN",
996                "OPQRSTUVWX",
997                "YZABCDEFGH",
998                "IJKLMNOPQR",
999                "STUVWXYZAB",
1000                "CDEFGHIJKL",
1001            ])
1002        )
1003    }
1004
1005    #[rstest]
1006    fn does_not_trigger_vertical_scrollbar(mut scroll_view: ScrollView) {
1007        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
1008        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
1009        let mut state = ScrollViewState::new();
1010        scroll_view.render(buf.area, &mut buf, &mut state);
1011        assert_eq!(
1012            buf,
1013            Buffer::with_lines(vec![
1014                "ABCDEFGHI",
1015                "KLMNOPQRS",
1016                "UVWXYZABC",
1017                "EFGHIJKLM",
1018                "OPQRSTUVW",
1019                "YZABCDEFG",
1020                "IJKLMNOPQ",
1021                "STUVWXYZA",
1022                "CDEFGHIJK",
1023                "MNOPQRSTU",
1024            ])
1025        )
1026    }
1027
1028    #[rstest]
1029    fn does_not_render_vertical_scrollbar(mut scroll_view: ScrollView) {
1030        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
1031        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
1032        let mut state = ScrollViewState::default();
1033        scroll_view.render(buf.area, &mut buf, &mut state);
1034        assert_eq!(
1035            buf,
1036            Buffer::with_lines(vec![
1037                "ABCDEF",
1038                "KLMNOP",
1039                "UVWXYZ",
1040                "EFGHIJ",
1041                "OPQRST",
1042                "◄███═►",
1043            ])
1044        )
1045    }
1046
1047    #[rstest]
1048    fn does_not_render_horizontal_scrollbar(mut scroll_view: ScrollView) {
1049        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
1050        let mut buf = Buffer::empty(Rect::new(0, 0, 7, 6));
1051        let mut state = ScrollViewState::default();
1052        scroll_view.render(buf.area, &mut buf, &mut state);
1053        assert_eq!(
1054            buf,
1055            Buffer::with_lines(vec![
1056                "ABCDEF▲",
1057                "KLMNOP█",
1058                "UVWXYZ█",
1059                "EFGHIJ█",
1060                "OPQRST║",
1061                "YZABCD▼",
1062            ])
1063        )
1064    }
1065
1066    #[rstest]
1067    #[rustfmt::skip]
1068    fn does_not_render_both_scrollbars(mut scroll_view: ScrollView) {
1069        scroll_view = scroll_view.scrollbars_visibility(ScrollbarVisibility::Never);
1070        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
1071        let mut state = ScrollViewState::default();
1072        scroll_view.render(buf.area, &mut buf, &mut state);
1073        assert_eq!(
1074            buf,
1075            Buffer::with_lines(vec![
1076                "ABCDEF",
1077                "KLMNOP",
1078                "UVWXYZ",
1079                "EFGHIJ",
1080                "OPQRST",
1081                "YZABCD",
1082            ])
1083        )
1084    }
1085
1086    #[rstest]
1087    #[rustfmt::skip]
1088    fn render_stateful_widget(mut scroll_view: ScrollView) {
1089        use ratatui_widgets::list::{List, ListState};
1090        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
1091        let mut buf = Buffer::empty(Rect::new(0, 0, 7, 5));
1092        let mut state = ScrollViewState::default();
1093        let mut list_state = ListState::default();
1094        let items: Vec<String> = (1..=10).map(|i| format!("Item {i}")).collect();
1095        let list = List::new(items);
1096        scroll_view.render_stateful_widget(list, scroll_view.area(), &mut list_state);
1097        scroll_view.render(buf.area, &mut buf, &mut state);
1098        assert_eq!(
1099            buf,
1100            Buffer::with_lines(vec![
1101                "Item 1▲",
1102                "Item 2█",
1103                "Item 3█",
1104                "Item 4║",
1105                "Item 5▼",
1106            ])
1107        )
1108    }
1109}