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