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    #[rstest]
436    fn zero_offset(scroll_view: ScrollView) {
437        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
438        let mut state = ScrollViewState::default();
439        scroll_view.render(buf.area, &mut buf, &mut state);
440        assert_eq!(
441            buf,
442            Buffer::with_lines(vec![
443                "ABCDE▲",
444                "KLMNO█",
445                "UVWXY█",
446                "EFGHI║",
447                "OPQRS▼",
448                "◄██═► ",
449            ])
450        )
451    }
452
453    #[rstest]
454    fn render_by_reference_matches_owned_render(scroll_view: ScrollView) {
455        let mut owned_state = ScrollViewState::default();
456        let mut borrowed_state = ScrollViewState::default();
457        let mut owned_buf = Buffer::empty(Rect::new(0, 0, 6, 6));
458        let mut borrowed_buf = Buffer::empty(Rect::new(0, 0, 6, 6));
459
460        scroll_view
461            .clone()
462            .render(owned_buf.area, &mut owned_buf, &mut owned_state);
463        (&scroll_view).render(borrowed_buf.area, &mut borrowed_buf, &mut borrowed_state);
464
465        assert_eq!(borrowed_buf, owned_buf);
466        assert_eq!(borrowed_state, owned_state);
467    }
468
469    #[rstest]
470    fn move_right(scroll_view: ScrollView) {
471        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
472        let mut state = ScrollViewState::with_offset((3, 0).into());
473        scroll_view.render(buf.area, &mut buf, &mut state);
474        assert_eq!(
475            buf,
476            Buffer::with_lines(vec![
477                "DEFGH▲",
478                "NOPQR█",
479                "XYZAB█",
480                "HIJKL║",
481                "RSTUV▼",
482                "◄═██► ",
483            ])
484        )
485    }
486
487    #[rstest]
488    fn move_down(scroll_view: ScrollView) {
489        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
490        let mut state = ScrollViewState::with_offset((0, 3).into());
491        scroll_view.render(buf.area, &mut buf, &mut state);
492        assert_eq!(
493            buf,
494            Buffer::with_lines(vec![
495                "EFGHI▲",
496                "OPQRS║",
497                "YZABC█",
498                "IJKLM█",
499                "STUVW▼",
500                "◄██═► ",
501            ])
502        )
503    }
504
505    #[rstest]
506    fn is_not_at_bottom_until_the_last_row_is_visible(scroll_view: ScrollView) {
507        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
508        let mut state = ScrollViewState::with_offset((0, 4).into());
509
510        scroll_view.render(buf.area, &mut buf, &mut state);
511
512        assert_eq!(
513            buf,
514            Buffer::with_lines(vec![
515                "OPQRS▲",
516                "YZABC║",
517                "IJKLM█",
518                "STUVW█",
519                "CDEFG▼",
520                "◄██═► ",
521            ])
522        );
523        assert!(!state.is_at_bottom());
524    }
525
526    #[rstest]
527    fn move_to_bottom(scroll_view: ScrollView) {
528        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
529        let mut state = ScrollViewState::default();
530
531        // Prior to rendering, page and buffer size are unknown. We default to `true`.
532        assert!(state.is_at_bottom());
533
534        scroll_view.clone().render(buf.area, &mut buf, &mut state);
535
536        // The vertical view size is five which means the page size is five.
537        // We have not scrolled yet, so the view is at the top and not at the bottom.
538        // => We see the top five rows
539        assert!(!state.is_at_bottom());
540
541        // Since the content height is ten,
542        assert_eq!(state.size.unwrap().height, 10);
543        // if we scroll down one page (five rows),
544        state.scroll_down();
545        state.scroll_down();
546        state.scroll_down();
547        state.scroll_down();
548        state.scroll_down();
549
550        // we reach the bottom,
551        assert!(state.is_at_bottom());
552        assert_eq!(state.offset.y, 5);
553
554        // and we see the last five rows of the content.
555        scroll_view.render(buf.area, &mut buf, &mut state);
556        assert_eq!(
557            buf,
558            Buffer::with_lines(vec![
559                "YZABC▲",
560                "IJKLM║",
561                "STUVW█",
562                "CDEFG█",
563                "MNOPQ▼",
564                "◄██═► ",
565            ])
566        );
567
568        // We could also jump directly to the bottom...
569        state.scroll_to_bottom();
570        assert!(state.is_at_bottom());
571
572        // ...which sets the offset to the last row of content,
573        // ensuring to be at the bottom regardless of the page size.
574        assert_eq!(state.offset.y, state.size.unwrap().height - 1);
575    }
576
577    #[rstest]
578    fn rendering_at_bottom_uses_the_last_full_page(scroll_view: ScrollView) {
579        let mut buf = Buffer::empty(Rect::new(0, 0, 11, 6));
580        let mut state = ScrollViewState::default();
581
582        state.scroll_to_bottom();
583        scroll_view.render(buf.area, &mut buf, &mut state);
584
585        assert_eq!(
586            buf,
587            Buffer::with_lines(vec![
588                "OPQRSTUVWX▲",
589                "YZABCDEFGH║",
590                "IJKLMNOPQR█",
591                "STUVWXYZAB█",
592                "CDEFGHIJKL█",
593                "MNOPQRSTUV▼",
594            ])
595        );
596        assert_eq!(state.offset.y, 4);
597        assert_eq!(state.page_size.unwrap().height, 6);
598    }
599
600    #[rstest]
601    #[case::always_always(
602        ScrollbarVisibility::Always,
603        ScrollbarVisibility::Always,
604        1,
605        1,
606        (true, true)
607    )]
608    #[case::never_never(
609        ScrollbarVisibility::Never,
610        ScrollbarVisibility::Never,
611        -1,
612        -1,
613        (false, false)
614    )]
615    #[case::always_never(
616        ScrollbarVisibility::Always,
617        ScrollbarVisibility::Never,
618        1,
619        1,
620        (true, false)
621    )]
622    #[case::never_always(
623        ScrollbarVisibility::Never,
624        ScrollbarVisibility::Always,
625        1,
626        1,
627        (false, true)
628    )]
629    #[case::automatic_never_needs_horizontal(
630        ScrollbarVisibility::Automatic,
631        ScrollbarVisibility::Never,
632        -1,
633        1,
634        (true, false)
635    )]
636    #[case::automatic_never_fits_horizontal(
637        ScrollbarVisibility::Automatic,
638        ScrollbarVisibility::Never,
639        1,
640        -1,
641        (false, false)
642    )]
643    #[case::never_automatic_needs_vertical(
644        ScrollbarVisibility::Never,
645        ScrollbarVisibility::Automatic,
646        1,
647        -1,
648        (false, true)
649    )]
650    #[case::never_automatic_fits_vertical(
651        ScrollbarVisibility::Never,
652        ScrollbarVisibility::Automatic,
653        -1,
654        1,
655        (false, false)
656    )]
657    #[case::always_automatic_exact_fit(
658        ScrollbarVisibility::Always,
659        ScrollbarVisibility::Automatic,
660        1,
661        0,
662        (true, true)
663    )]
664    #[case::always_automatic_vertical_fits(
665        ScrollbarVisibility::Always,
666        ScrollbarVisibility::Automatic,
667        1,
668        1,
669        (true, false)
670    )]
671    #[case::automatic_always_exact_fit(
672        ScrollbarVisibility::Automatic,
673        ScrollbarVisibility::Always,
674        0,
675        1,
676        (true, true)
677    )]
678    #[case::automatic_always_horizontal_fits(
679        ScrollbarVisibility::Automatic,
680        ScrollbarVisibility::Always,
681        1,
682        1,
683        (false, true)
684    )]
685    #[case::automatic_automatic_both_fit(
686        ScrollbarVisibility::Automatic,
687        ScrollbarVisibility::Automatic,
688        1,
689        1,
690        (false, false)
691    )]
692    #[case::automatic_automatic_both_overflow(
693        ScrollbarVisibility::Automatic,
694        ScrollbarVisibility::Automatic,
695        -1,
696        -1,
697        (true, true)
698    )]
699    #[case::automatic_automatic_only_vertical_overflows(
700        ScrollbarVisibility::Automatic,
701        ScrollbarVisibility::Automatic,
702        1,
703        -1,
704        (false, true)
705    )]
706    #[case::automatic_automatic_only_horizontal_overflows(
707        ScrollbarVisibility::Automatic,
708        ScrollbarVisibility::Automatic,
709        -1,
710        1,
711        (true, false)
712    )]
713    #[case::automatic_automatic_exact_fit_with_other_overflow(
714        ScrollbarVisibility::Automatic,
715        ScrollbarVisibility::Automatic,
716        0,
717        -1,
718        (true, true)
719    )]
720    fn visible_scrollbars_honors_visibility_policy(
721        #[case] horizontal_visibility: ScrollbarVisibility,
722        #[case] vertical_visibility: ScrollbarVisibility,
723        #[case] horizontal_space: i32,
724        #[case] vertical_space: i32,
725        #[case] expected: (bool, bool),
726    ) {
727        let scroll_view = ScrollView::new(Size::new(1, 1))
728            .horizontal_scrollbar_visibility(horizontal_visibility)
729            .vertical_scrollbar_visibility(vertical_visibility);
730
731        assert_eq!(
732            scroll_view.visible_scrollbars(horizontal_space, vertical_space),
733            expected
734        );
735    }
736
737    #[rstest]
738    fn hides_both_scrollbars(scroll_view: ScrollView) {
739        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
740        let mut state = ScrollViewState::new();
741        scroll_view.render(buf.area, &mut buf, &mut state);
742        assert_eq!(
743            buf,
744            Buffer::with_lines(vec![
745                "ABCDEFGHIJ",
746                "KLMNOPQRST",
747                "UVWXYZABCD",
748                "EFGHIJKLMN",
749                "OPQRSTUVWX",
750                "YZABCDEFGH",
751                "IJKLMNOPQR",
752                "STUVWXYZAB",
753                "CDEFGHIJKL",
754                "MNOPQRSTUV",
755            ])
756        )
757    }
758
759    #[rstest]
760    fn hides_horizontal_scrollbar(scroll_view: ScrollView) {
761        let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
762        let mut state = ScrollViewState::new();
763        scroll_view.render(buf.area, &mut buf, &mut state);
764        assert_eq!(
765            buf,
766            Buffer::with_lines(vec![
767                "ABCDEFGHIJ▲",
768                "KLMNOPQRST█",
769                "UVWXYZABCD█",
770                "EFGHIJKLMN█",
771                "OPQRSTUVWX█",
772                "YZABCDEFGH█",
773                "IJKLMNOPQR█",
774                "STUVWXYZAB█",
775                "CDEFGHIJKL▼",
776            ])
777        )
778    }
779
780    #[rstest]
781    fn hides_vertical_scrollbar(scroll_view: ScrollView) {
782        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
783        let mut state = ScrollViewState::new();
784        scroll_view.render(buf.area, &mut buf, &mut state);
785        assert_eq!(
786            buf,
787            Buffer::with_lines(vec![
788                "ABCDEFGHI",
789                "KLMNOPQRS",
790                "UVWXYZABC",
791                "EFGHIJKLM",
792                "OPQRSTUVW",
793                "YZABCDEFG",
794                "IJKLMNOPQ",
795                "STUVWXYZA",
796                "CDEFGHIJK",
797                "MNOPQRSTU",
798                "◄███████►",
799            ])
800        )
801    }
802
803    /// Tests the scenario where the vertical scrollbar steals a column from the right side of the
804    /// buffer which causes the horizontal scrollbar to be shown.
805    #[rstest]
806    fn does_not_hide_horizontal_scrollbar(scroll_view: ScrollView) {
807        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
808        let mut state = ScrollViewState::new();
809        scroll_view.render(buf.area, &mut buf, &mut state);
810        assert_eq!(
811            buf,
812            Buffer::with_lines(vec![
813                "ABCDEFGHI▲",
814                "KLMNOPQRS█",
815                "UVWXYZABC█",
816                "EFGHIJKLM█",
817                "OPQRSTUVW█",
818                "YZABCDEFG█",
819                "IJKLMNOPQ║",
820                "STUVWXYZA▼",
821                "◄███████► ",
822            ])
823        )
824    }
825
826    /// Tests the scenario where the horizontal scrollbar steals a row from the bottom side of the
827    /// buffer which causes the vertical scrollbar to be shown.
828    #[rstest]
829    fn does_not_hide_vertical_scrollbar(scroll_view: ScrollView) {
830        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
831        let mut state = ScrollViewState::new();
832        scroll_view.render(buf.area, &mut buf, &mut state);
833        assert_eq!(
834            buf,
835            Buffer::with_lines(vec![
836                "ABCDEFGH▲",
837                "KLMNOPQR█",
838                "UVWXYZAB█",
839                "EFGHIJKL█",
840                "OPQRSTUV█",
841                "YZABCDEF█",
842                "IJKLMNOP█",
843                "STUVWXYZ█",
844                "CDEFGHIJ▼",
845                "◄█████═► ",
846            ])
847        )
848    }
849
850    /// The purpose of this test is to ensure that the buffer offset is correctly calculated when
851    /// rendering a scroll view into a buffer (i.e. the buffer offset is not always (0, 0)).
852    #[rstest]
853    fn ensure_buffer_offset_is_correct(scroll_view: ScrollView) {
854        let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
855        let mut state = ScrollViewState::with_offset((2, 3).into());
856        scroll_view.render(Rect::new(5, 6, 7, 8), &mut buf, &mut state);
857        assert_eq!(
858            buf,
859            Buffer::with_lines(vec![
860                "                    ",
861                "                    ",
862                "                    ",
863                "                    ",
864                "                    ",
865                "                    ",
866                "     GHIJKL▲        ",
867                "     QRSTUV║        ",
868                "     ABCDEF█        ",
869                "     KLMNOP█        ",
870                "     UVWXYZ█        ",
871                "     EFGHIJ█        ",
872                "     OPQRST▼        ",
873                "     ◄═███►         ",
874                "                    ",
875                "                    ",
876                "                    ",
877                "                    ",
878                "                    ",
879                "                    ",
880            ])
881        )
882    }
883    /// The purpose of this test is to ensure that the last elements are rendered.
884    #[rstest]
885    fn ensure_buffer_last_elements(scroll_view: ScrollView) {
886        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
887        let mut state = ScrollViewState::with_offset((5, 5).into());
888        scroll_view.render(buf.area, &mut buf, &mut state);
889        assert_eq!(
890            buf,
891            Buffer::with_lines(vec![
892                "DEFGH▲",
893                "NOPQR║",
894                "XYZAB█",
895                "HIJKL█",
896                "RSTUV▼",
897                "◄═██► ",
898            ])
899        )
900    }
901    #[rstest]
902    fn zero_width(scroll_view: ScrollView) {
903        let mut buf = Buffer::empty(Rect::new(0, 0, 0, 10));
904        let mut state = ScrollViewState::new();
905        scroll_view.render(buf.area, &mut buf, &mut state);
906        assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 0, 10)));
907    }
908
909    #[rstest]
910    fn zero_height(scroll_view: ScrollView) {
911        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 0));
912        let mut state = ScrollViewState::new();
913        scroll_view.render(buf.area, &mut buf, &mut state);
914        assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 10, 0)));
915    }
916
917    #[rstest]
918    fn never_vertical_scrollbar(mut scroll_view: ScrollView) {
919        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
920        let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
921        let mut state = ScrollViewState::new();
922        scroll_view.render(buf.area, &mut buf, &mut state);
923        assert_eq!(
924            buf,
925            Buffer::with_lines(vec![
926                "ABCDEFGHIJ ",
927                "KLMNOPQRST ",
928                "UVWXYZABCD ",
929                "EFGHIJKLMN ",
930                "OPQRSTUVWX ",
931                "YZABCDEFGH ",
932                "IJKLMNOPQR ",
933                "STUVWXYZAB ",
934                "CDEFGHIJKL ",
935            ])
936        )
937    }
938
939    #[rstest]
940    fn never_horizontal_scrollbar(mut scroll_view: ScrollView) {
941        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
942        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
943        let mut state = ScrollViewState::new();
944        scroll_view.render(buf.area, &mut buf, &mut state);
945        assert_eq!(
946            buf,
947            Buffer::with_lines(vec![
948                "ABCDEFGHI",
949                "KLMNOPQRS",
950                "UVWXYZABC",
951                "EFGHIJKLM",
952                "OPQRSTUVW",
953                "YZABCDEFG",
954                "IJKLMNOPQ",
955                "STUVWXYZA",
956                "CDEFGHIJK",
957                "MNOPQRSTU",
958                "         ",
959            ])
960        )
961    }
962
963    #[rstest]
964    fn does_not_trigger_horizontal_scrollbar(mut scroll_view: ScrollView) {
965        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
966        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
967        let mut state = ScrollViewState::new();
968        scroll_view.render(buf.area, &mut buf, &mut state);
969        assert_eq!(
970            buf,
971            Buffer::with_lines(vec![
972                "ABCDEFGHIJ",
973                "KLMNOPQRST",
974                "UVWXYZABCD",
975                "EFGHIJKLMN",
976                "OPQRSTUVWX",
977                "YZABCDEFGH",
978                "IJKLMNOPQR",
979                "STUVWXYZAB",
980                "CDEFGHIJKL",
981            ])
982        )
983    }
984
985    #[rstest]
986    fn does_not_trigger_vertical_scrollbar(mut scroll_view: ScrollView) {
987        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
988        let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
989        let mut state = ScrollViewState::new();
990        scroll_view.render(buf.area, &mut buf, &mut state);
991        assert_eq!(
992            buf,
993            Buffer::with_lines(vec![
994                "ABCDEFGHI",
995                "KLMNOPQRS",
996                "UVWXYZABC",
997                "EFGHIJKLM",
998                "OPQRSTUVW",
999                "YZABCDEFG",
1000                "IJKLMNOPQ",
1001                "STUVWXYZA",
1002                "CDEFGHIJK",
1003                "MNOPQRSTU",
1004            ])
1005        )
1006    }
1007
1008    #[rstest]
1009    fn does_not_render_vertical_scrollbar(mut scroll_view: ScrollView) {
1010        scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
1011        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
1012        let mut state = ScrollViewState::default();
1013        scroll_view.render(buf.area, &mut buf, &mut state);
1014        assert_eq!(
1015            buf,
1016            Buffer::with_lines(vec![
1017                "ABCDEF",
1018                "KLMNOP",
1019                "UVWXYZ",
1020                "EFGHIJ",
1021                "OPQRST",
1022                "◄███═►",
1023            ])
1024        )
1025    }
1026
1027    #[rstest]
1028    fn does_not_render_horizontal_scrollbar(mut scroll_view: ScrollView) {
1029        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
1030        let mut buf = Buffer::empty(Rect::new(0, 0, 7, 6));
1031        let mut state = ScrollViewState::default();
1032        scroll_view.render(buf.area, &mut buf, &mut state);
1033        assert_eq!(
1034            buf,
1035            Buffer::with_lines(vec![
1036                "ABCDEF▲",
1037                "KLMNOP█",
1038                "UVWXYZ█",
1039                "EFGHIJ█",
1040                "OPQRST║",
1041                "YZABCD▼",
1042            ])
1043        )
1044    }
1045
1046    #[rstest]
1047    #[rustfmt::skip]
1048    fn does_not_render_both_scrollbars(mut scroll_view: ScrollView) {
1049        scroll_view = scroll_view.scrollbars_visibility(ScrollbarVisibility::Never);
1050        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 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 render_stateful_widget(mut scroll_view: ScrollView) {
1069        use ratatui_widgets::list::{List, ListState};
1070        scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
1071        let mut buf = Buffer::empty(Rect::new(0, 0, 7, 5));
1072        let mut state = ScrollViewState::default();
1073        let mut list_state = ListState::default();
1074        let items: Vec<String> = (1..=10).map(|i| format!("Item {i}")).collect();
1075        let list = List::new(items);
1076        scroll_view.render_stateful_widget(list, scroll_view.area(), &mut list_state);
1077        scroll_view.render(buf.area, &mut buf, &mut state);
1078        assert_eq!(
1079            buf,
1080            Buffer::with_lines(vec![
1081                "Item 1▲",
1082                "Item 2█",
1083                "Item 3█",
1084                "Item 4║",
1085                "Item 5▼",
1086            ])
1087        )
1088    }
1089}