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)]
51pub struct ScrollView {
52 buf: Buffer,
53 size: Size,
54 vertical_scrollbar_visibility: ScrollbarVisibility,
55 horizontal_scrollbar_visibility: ScrollbarVisibility,
56}
57
58#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
60pub enum ScrollbarVisibility {
61 #[default]
63 Automatic,
64 Always,
66 Never,
68}
69
70impl ScrollView {
71 pub fn new(size: Size) -> Self {
75 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 pub const fn size(&self) -> Size {
87 self.size
88 }
89
90 pub const fn area(&self) -> Rect {
92 self.buf.area
93 }
94
95 pub const fn buf(&self) -> &Buffer {
97 &self.buf
98 }
99
100 pub const fn buf_mut(&mut self) -> &mut Buffer {
114 &mut self.buf
115 }
116
117 pub const fn vertical_scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
133 self.vertical_scrollbar_visibility = visibility;
134 self
135 }
136
137 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 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 pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
190 widget.render(area, &mut self.buf);
191 }
192
193 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 max_x_offset = self
218 .buf
219 .area
220 .width
221 .saturating_sub(area.width.saturating_sub(1));
222 let max_y_offset = self
223 .buf
224 .area
225 .height
226 .saturating_sub(area.height.saturating_sub(1));
227
228 x = x.min(max_x_offset);
229 y = y.min(max_y_offset);
230 state.offset = (x, y).into();
231 state.size = Some(self.size);
232 state.page_size = Some(area.into());
233 let visible_area = self
234 .render_scrollbars(area, buf, state)
235 .intersection(self.buf.area);
236 self.render_visible_area(area, buf, visible_area);
237 }
238}
239
240impl ScrollView {
241 fn render_scrollbars(&self, area: Rect, buf: &mut Buffer, state: &mut ScrollViewState) -> Rect {
244 let horizontal_space = area.width as i32 - self.size.width as i32;
249 let vertical_space = area.height as i32 - self.size.height as i32;
250
251 if horizontal_space > 0 {
253 state.offset.x = 0;
254 }
255 if vertical_space > 0 {
256 state.offset.y = 0;
257 }
258
259 let (show_horizontal, show_vertical) =
260 self.visible_scrollbars(horizontal_space, vertical_space);
261
262 let new_height = if show_horizontal {
263 let width = area.width.saturating_sub(show_vertical as u16);
265 let render_area = Rect { width, ..area };
266 self.render_horizontal_scrollbar(render_area, buf, state);
268 area.height.saturating_sub(1)
269 } else {
270 area.height
271 };
272
273 let new_width = if show_vertical {
274 let height = area.height.saturating_sub(show_horizontal as u16);
276 let render_area = Rect { height, ..area };
277 self.render_vertical_scrollbar(render_area, buf, state);
279 area.width.saturating_sub(1)
280 } else {
281 area.width
282 };
283
284 Rect::new(state.offset.x, state.offset.y, new_width, new_height)
285 }
286
287 const fn visible_scrollbars(&self, horizontal_space: i32, vertical_space: i32) -> (bool, bool) {
296 type V = crate::scroll_view::ScrollbarVisibility;
297
298 match (
299 self.horizontal_scrollbar_visibility,
300 self.vertical_scrollbar_visibility,
301 ) {
302 (V::Always, V::Always) => (true, true),
304 (V::Never, V::Never) => (false, false),
305 (V::Always, V::Never) => (true, false),
306 (V::Never, V::Always) => (false, true),
307
308 (V::Automatic, V::Never) => (horizontal_space < 0, false),
310 (V::Never, V::Automatic) => (false, vertical_space < 0),
311
312 (V::Always, V::Automatic) => (true, vertical_space <= 0),
316 (V::Automatic, V::Always) => (horizontal_space <= 0, true),
317
318 (V::Automatic, V::Automatic) => {
320 if horizontal_space >= 0 && vertical_space >= 0 {
321 (false, false)
323 } else if horizontal_space < 0 && vertical_space < 0 {
324 (true, true)
326 } else if horizontal_space > 0 && vertical_space < 0 {
327 (false, true)
329 } else if horizontal_space < 0 && vertical_space > 0 {
330 (true, false)
332 } else {
333 (true, true)
336 }
337 }
338 }
339 }
340
341 fn render_vertical_scrollbar(&self, area: Rect, buf: &mut Buffer, state: &ScrollViewState) {
342 let scrollbar_height = self.size.height.saturating_sub(area.height);
343 let mut scrollbar_state =
344 ScrollbarState::new(scrollbar_height as usize).position(state.offset.y as usize);
345 let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
346 scrollbar.render(area, buf, &mut scrollbar_state);
347 }
348
349 fn render_horizontal_scrollbar(&self, area: Rect, buf: &mut Buffer, state: &ScrollViewState) {
350 let scrollbar_width = self.size.width.saturating_sub(area.width);
351 let mut scrollbar_state =
352 ScrollbarState::new(scrollbar_width as usize).position(state.offset.x as usize);
353 let scrollbar = Scrollbar::new(ScrollbarOrientation::HorizontalBottom);
354 scrollbar.render(area, buf, &mut scrollbar_state);
355 }
356
357 fn render_visible_area(&self, area: Rect, buf: &mut Buffer, visible_area: Rect) {
358 for (src_row, dst_row) in visible_area.rows().zip(area.rows()) {
360 for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
361 buf[dst_col] = self.buf[src_col].clone();
362 }
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use ratatui_core::text::Span;
370 use rstest::{fixture, rstest};
371
372 use super::*;
373
374 #[fixture]
391 fn scroll_view() -> ScrollView {
392 let mut scroll_view = ScrollView::new(Size::new(10, 10));
393 for y in 0..10 {
394 for x in 0..10 {
395 let c = char::from_u32((x + y * 10) % 26 + 65).unwrap();
396 let widget = Span::raw(format!("{c}"));
397 let area = Rect::new(x as u16, y as u16, 1, 1);
398 scroll_view.render_widget(widget, area);
399 }
400 }
401 scroll_view
402 }
403
404 #[rstest]
405 fn zero_offset(scroll_view: ScrollView) {
406 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
407 let mut state = ScrollViewState::default();
408 scroll_view.render(buf.area, &mut buf, &mut state);
409 assert_eq!(
410 buf,
411 Buffer::with_lines(vec![
412 "ABCDE▲",
413 "KLMNO█",
414 "UVWXY█",
415 "EFGHI║",
416 "OPQRS▼",
417 "◄██═► ",
418 ])
419 )
420 }
421
422 #[rstest]
423 fn move_right(scroll_view: ScrollView) {
424 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
425 let mut state = ScrollViewState::with_offset((3, 0).into());
426 scroll_view.render(buf.area, &mut buf, &mut state);
427 assert_eq!(
428 buf,
429 Buffer::with_lines(vec![
430 "DEFGH▲",
431 "NOPQR█",
432 "XYZAB█",
433 "HIJKL║",
434 "RSTUV▼",
435 "◄═██► ",
436 ])
437 )
438 }
439
440 #[rstest]
441 fn move_down(scroll_view: ScrollView) {
442 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
443 let mut state = ScrollViewState::with_offset((0, 3).into());
444 scroll_view.render(buf.area, &mut buf, &mut state);
445 assert_eq!(
446 buf,
447 Buffer::with_lines(vec![
448 "EFGHI▲",
449 "OPQRS║",
450 "YZABC█",
451 "IJKLM█",
452 "STUVW▼",
453 "◄██═► ",
454 ])
455 )
456 }
457
458 #[rstest]
459 fn hides_both_scrollbars(scroll_view: ScrollView) {
460 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
461 let mut state = ScrollViewState::new();
462 scroll_view.render(buf.area, &mut buf, &mut state);
463 assert_eq!(
464 buf,
465 Buffer::with_lines(vec![
466 "ABCDEFGHIJ",
467 "KLMNOPQRST",
468 "UVWXYZABCD",
469 "EFGHIJKLMN",
470 "OPQRSTUVWX",
471 "YZABCDEFGH",
472 "IJKLMNOPQR",
473 "STUVWXYZAB",
474 "CDEFGHIJKL",
475 "MNOPQRSTUV",
476 ])
477 )
478 }
479
480 #[rstest]
481 fn hides_horizontal_scrollbar(scroll_view: ScrollView) {
482 let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
483 let mut state = ScrollViewState::new();
484 scroll_view.render(buf.area, &mut buf, &mut state);
485 assert_eq!(
486 buf,
487 Buffer::with_lines(vec![
488 "ABCDEFGHIJ▲",
489 "KLMNOPQRST█",
490 "UVWXYZABCD█",
491 "EFGHIJKLMN█",
492 "OPQRSTUVWX█",
493 "YZABCDEFGH█",
494 "IJKLMNOPQR█",
495 "STUVWXYZAB█",
496 "CDEFGHIJKL▼",
497 ])
498 )
499 }
500
501 #[rstest]
502 fn hides_vertical_scrollbar(scroll_view: ScrollView) {
503 let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
504 let mut state = ScrollViewState::new();
505 scroll_view.render(buf.area, &mut buf, &mut state);
506 assert_eq!(
507 buf,
508 Buffer::with_lines(vec![
509 "ABCDEFGHI",
510 "KLMNOPQRS",
511 "UVWXYZABC",
512 "EFGHIJKLM",
513 "OPQRSTUVW",
514 "YZABCDEFG",
515 "IJKLMNOPQ",
516 "STUVWXYZA",
517 "CDEFGHIJK",
518 "MNOPQRSTU",
519 "◄███████►",
520 ])
521 )
522 }
523
524 #[rstest]
527 fn does_not_hide_horizontal_scrollbar(scroll_view: ScrollView) {
528 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
529 let mut state = ScrollViewState::new();
530 scroll_view.render(buf.area, &mut buf, &mut state);
531 assert_eq!(
532 buf,
533 Buffer::with_lines(vec![
534 "ABCDEFGHI▲",
535 "KLMNOPQRS█",
536 "UVWXYZABC█",
537 "EFGHIJKLM█",
538 "OPQRSTUVW█",
539 "YZABCDEFG█",
540 "IJKLMNOPQ║",
541 "STUVWXYZA▼",
542 "◄███████► ",
543 ])
544 )
545 }
546
547 #[rstest]
550 fn does_not_hide_vertical_scrollbar(scroll_view: ScrollView) {
551 let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
552 let mut state = ScrollViewState::new();
553 scroll_view.render(buf.area, &mut buf, &mut state);
554 assert_eq!(
555 buf,
556 Buffer::with_lines(vec![
557 "ABCDEFGH▲",
558 "KLMNOPQR█",
559 "UVWXYZAB█",
560 "EFGHIJKL█",
561 "OPQRSTUV█",
562 "YZABCDEF█",
563 "IJKLMNOP█",
564 "STUVWXYZ█",
565 "CDEFGHIJ▼",
566 "◄█████═► ",
567 ])
568 )
569 }
570
571 #[rstest]
574 fn ensure_buffer_offset_is_correct(scroll_view: ScrollView) {
575 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
576 let mut state = ScrollViewState::with_offset((2, 3).into());
577 scroll_view.render(Rect::new(5, 6, 7, 8), &mut buf, &mut state);
578 assert_eq!(
579 buf,
580 Buffer::with_lines(vec![
581 " ",
582 " ",
583 " ",
584 " ",
585 " ",
586 " ",
587 " GHIJKL▲ ",
588 " QRSTUV║ ",
589 " ABCDEF█ ",
590 " KLMNOP█ ",
591 " UVWXYZ█ ",
592 " EFGHIJ█ ",
593 " OPQRST▼ ",
594 " ◄═███► ",
595 " ",
596 " ",
597 " ",
598 " ",
599 " ",
600 " ",
601 ])
602 )
603 }
604 #[rstest]
606 fn ensure_buffer_last_elements(scroll_view: ScrollView) {
607 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
608 let mut state = ScrollViewState::with_offset((5, 5).into());
609 scroll_view.render(buf.area, &mut buf, &mut state);
610 assert_eq!(
611 buf,
612 Buffer::with_lines(vec![
613 "DEFGH▲",
614 "NOPQR║",
615 "XYZAB█",
616 "HIJKL█",
617 "RSTUV▼",
618 "◄═██► ",
619 ])
620 )
621 }
622 #[rstest]
623 fn zero_width(scroll_view: ScrollView) {
624 let mut buf = Buffer::empty(Rect::new(0, 0, 0, 10));
625 let mut state = ScrollViewState::new();
626 scroll_view.render(buf.area, &mut buf, &mut state);
627 assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 0, 10)));
628 }
629
630 #[rstest]
631 fn zero_height(scroll_view: ScrollView) {
632 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 0));
633 let mut state = ScrollViewState::new();
634 scroll_view.render(buf.area, &mut buf, &mut state);
635 assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 10, 0)));
636 }
637
638 #[rstest]
639 fn never_vertical_scrollbar(mut scroll_view: ScrollView) {
640 scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
641 let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
642 let mut state = ScrollViewState::new();
643 scroll_view.render(buf.area, &mut buf, &mut state);
644 assert_eq!(
645 buf,
646 Buffer::with_lines(vec![
647 "ABCDEFGHIJ ",
648 "KLMNOPQRST ",
649 "UVWXYZABCD ",
650 "EFGHIJKLMN ",
651 "OPQRSTUVWX ",
652 "YZABCDEFGH ",
653 "IJKLMNOPQR ",
654 "STUVWXYZAB ",
655 "CDEFGHIJKL ",
656 ])
657 )
658 }
659
660 #[rstest]
661 fn never_horizontal_scrollbar(mut scroll_view: ScrollView) {
662 scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
663 let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
664 let mut state = ScrollViewState::new();
665 scroll_view.render(buf.area, &mut buf, &mut state);
666 assert_eq!(
667 buf,
668 Buffer::with_lines(vec![
669 "ABCDEFGHI",
670 "KLMNOPQRS",
671 "UVWXYZABC",
672 "EFGHIJKLM",
673 "OPQRSTUVW",
674 "YZABCDEFG",
675 "IJKLMNOPQ",
676 "STUVWXYZA",
677 "CDEFGHIJK",
678 "MNOPQRSTU",
679 " ",
680 ])
681 )
682 }
683
684 #[rstest]
685 fn does_not_trigger_horizontal_scrollbar(mut scroll_view: ScrollView) {
686 scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
687 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
688 let mut state = ScrollViewState::new();
689 scroll_view.render(buf.area, &mut buf, &mut state);
690 assert_eq!(
691 buf,
692 Buffer::with_lines(vec![
693 "ABCDEFGHIJ",
694 "KLMNOPQRST",
695 "UVWXYZABCD",
696 "EFGHIJKLMN",
697 "OPQRSTUVWX",
698 "YZABCDEFGH",
699 "IJKLMNOPQR",
700 "STUVWXYZAB",
701 "CDEFGHIJKL",
702 ])
703 )
704 }
705
706 #[rstest]
707 fn does_not_trigger_vertical_scrollbar(mut scroll_view: ScrollView) {
708 scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
709 let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
710 let mut state = ScrollViewState::new();
711 scroll_view.render(buf.area, &mut buf, &mut state);
712 assert_eq!(
713 buf,
714 Buffer::with_lines(vec![
715 "ABCDEFGHI",
716 "KLMNOPQRS",
717 "UVWXYZABC",
718 "EFGHIJKLM",
719 "OPQRSTUVW",
720 "YZABCDEFG",
721 "IJKLMNOPQ",
722 "STUVWXYZA",
723 "CDEFGHIJK",
724 "MNOPQRSTU",
725 ])
726 )
727 }
728
729 #[rstest]
730 fn does_not_render_vertical_scrollbar(mut scroll_view: ScrollView) {
731 scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
732 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
733 let mut state = ScrollViewState::default();
734 scroll_view.render(buf.area, &mut buf, &mut state);
735 assert_eq!(
736 buf,
737 Buffer::with_lines(vec![
738 "ABCDEF",
739 "KLMNOP",
740 "UVWXYZ",
741 "EFGHIJ",
742 "OPQRST",
743 "◄███═►",
744 ])
745 )
746 }
747
748 #[rstest]
749 fn does_not_render_horizontal_scrollbar(mut scroll_view: ScrollView) {
750 scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
751 let mut buf = Buffer::empty(Rect::new(0, 0, 7, 6));
752 let mut state = ScrollViewState::default();
753 scroll_view.render(buf.area, &mut buf, &mut state);
754 assert_eq!(
755 buf,
756 Buffer::with_lines(vec![
757 "ABCDEF▲",
758 "KLMNOP█",
759 "UVWXYZ█",
760 "EFGHIJ█",
761 "OPQRST║",
762 "YZABCD▼",
763 ])
764 )
765 }
766
767 #[rstest]
768 #[rustfmt::skip]
769 fn does_not_render_both_scrollbars(mut scroll_view: ScrollView) {
770 scroll_view = scroll_view.scrollbars_visibility(ScrollbarVisibility::Never);
771 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
772 let mut state = ScrollViewState::default();
773 scroll_view.render(buf.area, &mut buf, &mut state);
774 assert_eq!(
775 buf,
776 Buffer::with_lines(vec![
777 "ABCDEF",
778 "KLMNOP",
779 "UVWXYZ",
780 "EFGHIJ",
781 "OPQRST",
782 "YZABCD",
783 ])
784 )
785 }
786
787 #[rstest]
788 #[rustfmt::skip]
789 fn render_stateful_widget(mut scroll_view: ScrollView) {
790 use ratatui_widgets::list::{List, ListState};
791 scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
792 let mut buf = Buffer::empty(Rect::new(0, 0, 7, 5));
793 let mut state = ScrollViewState::default();
794 let mut list_state = ListState::default();
795 let items: Vec<String> = (1..=10).map(|i| format!("Item {i}")).collect();
796 let list = List::new(items);
797 scroll_view.render_stateful_widget(list, scroll_view.area(), &mut list_state);
798 scroll_view.render(buf.area, &mut buf, &mut state);
799 assert_eq!(
800 buf,
801 Buffer::with_lines(vec![
802 "Item 1▲",
803 "Item 2█",
804 "Item 3█",
805 "Item 4║",
806 "Item 5▼",
807 ])
808 )
809 }
810}