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