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#[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#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
72pub enum ScrollbarVisibility {
73 #[default]
75 Automatic,
76 Always,
78 Never,
80}
81
82impl ScrollView {
83 pub fn new(size: Size) -> Self {
87 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 pub const fn size(&self) -> Size {
99 self.size
100 }
101
102 pub const fn area(&self) -> Rect {
104 self.buf.area
105 }
106
107 pub const fn buf(&self) -> &Buffer {
109 &self.buf
110 }
111
112 pub const fn buf_mut(&mut self) -> &mut Buffer {
126 &mut self.buf
127 }
128
129 pub const fn vertical_scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
145 self.vertical_scrollbar_visibility = visibility;
146 self
147 }
148
149 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 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 pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
202 widget.render(area, &mut self.buf);
203 }
204
205 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 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 horizontal_space > 0 {
248 x = 0;
249 }
250 if vertical_space > 0 {
251 y = 0;
252 }
253
254 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 fn render_scrollbars(&self, area: Rect, buf: &mut Buffer, state: &mut ScrollViewState) -> Rect {
275 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 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 let width = area.width.saturating_sub(show_vertical as u16);
296 let render_area = Rect { width, ..area };
297 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 let height = area.height.saturating_sub(show_horizontal as u16);
307 let render_area = Rect { height, ..area };
308 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 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 (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 (V::Automatic, V::Never) => (horizontal_space < 0, false),
341 (V::Never, V::Automatic) => (false, vertical_space < 0),
342
343 (V::Always, V::Automatic) => (true, vertical_space <= 0),
347 (V::Automatic, V::Always) => (horizontal_space <= 0, true),
348
349 (V::Automatic, V::Automatic) => {
351 if horizontal_space >= 0 && vertical_space >= 0 {
352 (false, false)
354 } else if horizontal_space < 0 && vertical_space < 0 {
355 (true, true)
357 } else if horizontal_space > 0 && vertical_space < 0 {
358 (false, true)
360 } else if horizontal_space < 0 && vertical_space > 0 {
361 (true, false)
363 } else {
364 (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 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 #[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 assert!(state.is_at_bottom());
553
554 scroll_view.clone().render(buf.area, &mut buf, &mut state);
555
556 assert!(!state.is_at_bottom());
560
561 assert_eq!(state.size.unwrap().height, 10);
563 state.scroll_down();
565 state.scroll_down();
566 state.scroll_down();
567 state.scroll_down();
568 state.scroll_down();
569
570 assert!(state.is_at_bottom());
572 assert_eq!(state.offset.y, 5);
573
574 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 state.scroll_to_bottom();
590 assert!(state.is_at_bottom());
591
592 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 #[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 #[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 #[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 #[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}