1use ratatui::{layout::Size, prelude::*, widgets::*};
2
3use crate::ScrollViewState;
4
5#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
48pub struct ScrollView {
49 buf: Buffer,
50 size: Size,
51 vertical_scrollbar_visibility: ScrollbarVisibility,
52 horizontal_scrollbar_visibility: ScrollbarVisibility,
53}
54
55#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
57pub enum ScrollbarVisibility {
58 #[default]
60 Automatic,
61 Always,
63 Never,
65}
66
67impl ScrollView {
68 pub fn new(size: Size) -> Self {
72 let area = Rect::new(0, 0, size.width, size.height);
74 Self {
75 buf: Buffer::empty(area),
76 size,
77 horizontal_scrollbar_visibility: ScrollbarVisibility::default(),
78 vertical_scrollbar_visibility: ScrollbarVisibility::default(),
79 }
80 }
81
82 pub fn size(&self) -> Size {
84 self.size
85 }
86
87 pub fn area(&self) -> Rect {
89 self.buf.area
90 }
91
92 pub fn buf(&self) -> &Buffer {
94 &self.buf
95 }
96
97 pub fn buf_mut(&mut self) -> &mut Buffer {
111 &mut self.buf
112 }
113
114 pub fn vertical_scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
129 self.vertical_scrollbar_visibility = visibility;
130 self
131 }
132
133 pub fn horizontal_scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
148 self.horizontal_scrollbar_visibility = visibility;
149 self
150 }
151
152 pub fn scrollbars_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
167 self.vertical_scrollbar_visibility = visibility;
168 self.horizontal_scrollbar_visibility = visibility;
169 self
170 }
171
172 pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
181 widget.render(area, &mut self.buf);
182 }
183}
184
185impl StatefulWidget for ScrollView {
186 type State = ScrollViewState;
187
188 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
189 let (mut x, mut y) = state.offset.into();
190 let max_x_offset = self
192 .buf
193 .area
194 .width
195 .saturating_sub(area.width.saturating_sub(1));
196 let max_y_offset = self
197 .buf
198 .area
199 .height
200 .saturating_sub(area.height.saturating_sub(1));
201
202 x = x.min(max_x_offset);
203 y = y.min(max_y_offset);
204 state.offset = (x, y).into();
205 state.size = Some(self.size);
206 state.page_size = Some(area.into());
207 let visible_area = self
208 .render_scrollbars(area, buf, state)
209 .intersection(self.buf.area);
210 self.render_visible_area(area, buf, visible_area);
211 }
212}
213
214impl ScrollView {
215 fn render_scrollbars(&self, area: Rect, buf: &mut Buffer, state: &mut ScrollViewState) -> Rect {
218 let horizontal_space = area.width as i32 - self.size.width as i32;
223 let vertical_space = area.height as i32 - self.size.height as i32;
224
225 if horizontal_space > 0 {
227 state.offset.x = 0;
228 }
229 if vertical_space > 0 {
230 state.offset.y = 0;
231 }
232
233 let (show_horizontal, show_vertical) =
234 self.visible_scrollbars(horizontal_space, vertical_space);
235
236 let mut new_width = area.width;
237 let mut new_height = area.height;
238
239 if show_horizontal {
240 let width = area.width.saturating_sub(show_vertical as u16);
242 let render_area = Rect { width, ..area };
243 self.render_horizontal_scrollbar(render_area, buf, state);
245 new_height = area.height.saturating_sub(1);
246 }
247
248 if show_vertical {
249 let height = area.height.saturating_sub(show_horizontal as u16);
251 let render_area = Rect { height, ..area };
252 self.render_vertical_scrollbar(render_area, buf, state);
254 new_width = area.width.saturating_sub(1);
255 }
256
257 Rect::new(state.offset.x, state.offset.y, new_width, new_height)
258 }
259
260 fn visible_scrollbars(&self, horizontal_space: i32, vertical_space: i32) -> (bool, bool) {
269 type V = crate::scroll_view::ScrollbarVisibility;
270
271 match (
272 self.horizontal_scrollbar_visibility,
273 self.vertical_scrollbar_visibility,
274 ) {
275 (V::Always, V::Always) => (true, true),
277 (V::Never, V::Never) => (false, false),
278 (V::Always, V::Never) => (true, false),
279 (V::Never, V::Always) => (false, true),
280
281 (V::Automatic, V::Never) => (horizontal_space < 0, false),
283 (V::Never, V::Automatic) => (false, vertical_space < 0),
284
285 (V::Always, V::Automatic) => (true, vertical_space <= 0),
289 (V::Automatic, V::Always) => (horizontal_space <= 0, true),
290
291 (V::Automatic, V::Automatic) => {
293 if horizontal_space >= 0 && vertical_space >= 0 {
294 (false, false)
296 } else if horizontal_space < 0 && vertical_space < 0 {
297 (true, true)
299 } else if horizontal_space > 0 && vertical_space < 0 {
300 (false, true)
302 } else if horizontal_space < 0 && vertical_space > 0 {
303 (true, false)
305 } else {
306 (true, true)
309 }
310 }
311 }
312 }
313
314 fn render_vertical_scrollbar(&self, area: Rect, buf: &mut Buffer, state: &ScrollViewState) {
315 let scrollbar_height = self.size.height.saturating_sub(area.height);
316 let mut scrollbar_state =
317 ScrollbarState::new(scrollbar_height as usize).position(state.offset.y as usize);
318 let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
319 scrollbar.render(area, buf, &mut scrollbar_state);
320 }
321
322 fn render_horizontal_scrollbar(&self, area: Rect, buf: &mut Buffer, state: &ScrollViewState) {
323 let scrollbar_width = self.size.width.saturating_sub(area.width);
324 let mut scrollbar_state =
325 ScrollbarState::new(scrollbar_width as usize).position(state.offset.x as usize);
326 let scrollbar = Scrollbar::new(ScrollbarOrientation::HorizontalBottom);
327 scrollbar.render(area, buf, &mut scrollbar_state);
328 }
329
330 fn render_visible_area(&self, area: Rect, buf: &mut Buffer, visible_area: Rect) {
331 for (src_row, dst_row) in visible_area.rows().zip(area.rows()) {
333 for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
334 buf[dst_col] = self.buf[src_col].clone();
335 }
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use rstest::{fixture, rstest};
344
345 #[fixture]
362 fn scroll_view() -> ScrollView {
363 let mut scroll_view = ScrollView::new(Size::new(10, 10));
364 for y in 0..10 {
365 for x in 0..10 {
366 let c = char::from_u32((x + y * 10) % 26 + 65).unwrap();
367 let widget = Span::raw(format!("{c}"));
368 let area = Rect::new(x as u16, y as u16, 1, 1);
369 scroll_view.render_widget(widget, area);
370 }
371 }
372 scroll_view
373 }
374
375 #[rstest]
376 fn zero_offset(scroll_view: ScrollView) {
377 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
378 let mut state = ScrollViewState::default();
379 scroll_view.render(buf.area, &mut buf, &mut state);
380 assert_eq!(
381 buf,
382 Buffer::with_lines(vec![
383 "ABCDE▲",
384 "KLMNO█",
385 "UVWXY█",
386 "EFGHI║",
387 "OPQRS▼",
388 "◄██═► ",
389 ])
390 )
391 }
392
393 #[rstest]
394 fn move_right(scroll_view: ScrollView) {
395 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
396 let mut state = ScrollViewState::with_offset((3, 0).into());
397 scroll_view.render(buf.area, &mut buf, &mut state);
398 assert_eq!(
399 buf,
400 Buffer::with_lines(vec![
401 "DEFGH▲",
402 "NOPQR█",
403 "XYZAB█",
404 "HIJKL║",
405 "RSTUV▼",
406 "◄═██► ",
407 ])
408 )
409 }
410
411 #[rstest]
412 fn move_down(scroll_view: ScrollView) {
413 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
414 let mut state = ScrollViewState::with_offset((0, 3).into());
415 scroll_view.render(buf.area, &mut buf, &mut state);
416 assert_eq!(
417 buf,
418 Buffer::with_lines(vec![
419 "EFGHI▲",
420 "OPQRS║",
421 "YZABC█",
422 "IJKLM█",
423 "STUVW▼",
424 "◄██═► ",
425 ])
426 )
427 }
428
429 #[rstest]
430 fn hides_both_scrollbars(scroll_view: ScrollView) {
431 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
432 let mut state = ScrollViewState::new();
433 scroll_view.render(buf.area, &mut buf, &mut state);
434 assert_eq!(
435 buf,
436 Buffer::with_lines(vec![
437 "ABCDEFGHIJ",
438 "KLMNOPQRST",
439 "UVWXYZABCD",
440 "EFGHIJKLMN",
441 "OPQRSTUVWX",
442 "YZABCDEFGH",
443 "IJKLMNOPQR",
444 "STUVWXYZAB",
445 "CDEFGHIJKL",
446 "MNOPQRSTUV",
447 ])
448 )
449 }
450
451 #[rstest]
452 fn hides_horizontal_scrollbar(scroll_view: ScrollView) {
453 let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
454 let mut state = ScrollViewState::new();
455 scroll_view.render(buf.area, &mut buf, &mut state);
456 assert_eq!(
457 buf,
458 Buffer::with_lines(vec![
459 "ABCDEFGHIJ▲",
460 "KLMNOPQRST█",
461 "UVWXYZABCD█",
462 "EFGHIJKLMN█",
463 "OPQRSTUVWX█",
464 "YZABCDEFGH█",
465 "IJKLMNOPQR█",
466 "STUVWXYZAB█",
467 "CDEFGHIJKL▼",
468 ])
469 )
470 }
471
472 #[rstest]
473 fn hides_vertical_scrollbar(scroll_view: ScrollView) {
474 let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
475 let mut state = ScrollViewState::new();
476 scroll_view.render(buf.area, &mut buf, &mut state);
477 assert_eq!(
478 buf,
479 Buffer::with_lines(vec![
480 "ABCDEFGHI",
481 "KLMNOPQRS",
482 "UVWXYZABC",
483 "EFGHIJKLM",
484 "OPQRSTUVW",
485 "YZABCDEFG",
486 "IJKLMNOPQ",
487 "STUVWXYZA",
488 "CDEFGHIJK",
489 "MNOPQRSTU",
490 "◄███████►",
491 ])
492 )
493 }
494
495 #[rstest]
498 fn does_not_hide_horizontal_scrollbar(scroll_view: ScrollView) {
499 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
500 let mut state = ScrollViewState::new();
501 scroll_view.render(buf.area, &mut buf, &mut state);
502 assert_eq!(
503 buf,
504 Buffer::with_lines(vec![
505 "ABCDEFGHI▲",
506 "KLMNOPQRS█",
507 "UVWXYZABC█",
508 "EFGHIJKLM█",
509 "OPQRSTUVW█",
510 "YZABCDEFG█",
511 "IJKLMNOPQ║",
512 "STUVWXYZA▼",
513 "◄███████► ",
514 ])
515 )
516 }
517
518 #[rstest]
521 fn does_not_hide_vertical_scrollbar(scroll_view: ScrollView) {
522 let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
523 let mut state = ScrollViewState::new();
524 scroll_view.render(buf.area, &mut buf, &mut state);
525 assert_eq!(
526 buf,
527 Buffer::with_lines(vec![
528 "ABCDEFGH▲",
529 "KLMNOPQR█",
530 "UVWXYZAB█",
531 "EFGHIJKL█",
532 "OPQRSTUV█",
533 "YZABCDEF█",
534 "IJKLMNOP█",
535 "STUVWXYZ█",
536 "CDEFGHIJ▼",
537 "◄█████═► ",
538 ])
539 )
540 }
541
542 #[rstest]
545 fn ensure_buffer_offset_is_correct(scroll_view: ScrollView) {
546 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
547 let mut state = ScrollViewState::with_offset((2, 3).into());
548 scroll_view.render(Rect::new(5, 6, 7, 8), &mut buf, &mut state);
549 assert_eq!(
550 buf,
551 Buffer::with_lines(vec![
552 " ",
553 " ",
554 " ",
555 " ",
556 " ",
557 " ",
558 " GHIJKL▲ ",
559 " QRSTUV║ ",
560 " ABCDEF█ ",
561 " KLMNOP█ ",
562 " UVWXYZ█ ",
563 " EFGHIJ█ ",
564 " OPQRST▼ ",
565 " ◄═███► ",
566 " ",
567 " ",
568 " ",
569 " ",
570 " ",
571 " ",
572 ])
573 )
574 }
575 #[rstest]
577 fn ensure_buffer_last_elements(scroll_view: ScrollView) {
578 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
579 let mut state = ScrollViewState::with_offset((5, 5).into());
580 scroll_view.render(buf.area, &mut buf, &mut state);
581 assert_eq!(
582 buf,
583 Buffer::with_lines(vec![
584 "DEFGH▲",
585 "NOPQR║",
586 "XYZAB█",
587 "HIJKL█",
588 "RSTUV▼",
589 "◄═██► ",
590 ])
591 )
592 }
593 #[rstest]
594 #[should_panic(expected = "Scrollbar area is empty")]
595 fn zero_width(scroll_view: ScrollView) {
596 let mut buf = Buffer::empty(Rect::new(0, 0, 0, 10));
597 let mut state = ScrollViewState::new();
598 scroll_view.render(buf.area, &mut buf, &mut state);
599 }
600 #[rstest]
601 #[should_panic(expected = "Scrollbar area is empty")]
602 fn zero_height(scroll_view: ScrollView) {
603 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 0));
604 let mut state = ScrollViewState::new();
605 scroll_view.render(buf.area, &mut buf, &mut state);
606 }
607
608 #[rstest]
609 fn never_vertical_scrollbar(mut scroll_view: ScrollView) {
610 scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
611 let mut buf = Buffer::empty(Rect::new(0, 0, 11, 9));
612 let mut state = ScrollViewState::new();
613 scroll_view.render(buf.area, &mut buf, &mut state);
614 assert_eq!(
615 buf,
616 Buffer::with_lines(vec![
617 "ABCDEFGHIJ ",
618 "KLMNOPQRST ",
619 "UVWXYZABCD ",
620 "EFGHIJKLMN ",
621 "OPQRSTUVWX ",
622 "YZABCDEFGH ",
623 "IJKLMNOPQR ",
624 "STUVWXYZAB ",
625 "CDEFGHIJKL ",
626 ])
627 )
628 }
629
630 #[rstest]
631 fn never_horizontal_scrollbar(mut scroll_view: ScrollView) {
632 scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
633 let mut buf = Buffer::empty(Rect::new(0, 0, 9, 11));
634 let mut state = ScrollViewState::new();
635 scroll_view.render(buf.area, &mut buf, &mut state);
636 assert_eq!(
637 buf,
638 Buffer::with_lines(vec![
639 "ABCDEFGHI",
640 "KLMNOPQRS",
641 "UVWXYZABC",
642 "EFGHIJKLM",
643 "OPQRSTUVW",
644 "YZABCDEFG",
645 "IJKLMNOPQ",
646 "STUVWXYZA",
647 "CDEFGHIJK",
648 "MNOPQRSTU",
649 " ",
650 ])
651 )
652 }
653
654 #[rstest]
655 fn does_not_trigger_horizontal_scrollbar(mut scroll_view: ScrollView) {
656 scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
657 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 9));
658 let mut state = ScrollViewState::new();
659 scroll_view.render(buf.area, &mut buf, &mut state);
660 assert_eq!(
661 buf,
662 Buffer::with_lines(vec![
663 "ABCDEFGHIJ",
664 "KLMNOPQRST",
665 "UVWXYZABCD",
666 "EFGHIJKLMN",
667 "OPQRSTUVWX",
668 "YZABCDEFGH",
669 "IJKLMNOPQR",
670 "STUVWXYZAB",
671 "CDEFGHIJKL",
672 ])
673 )
674 }
675
676 #[rstest]
677 fn does_not_trigger_vertical_scrollbar(mut scroll_view: ScrollView) {
678 scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
679 let mut buf = Buffer::empty(Rect::new(0, 0, 9, 10));
680 let mut state = ScrollViewState::new();
681 scroll_view.render(buf.area, &mut buf, &mut state);
682 assert_eq!(
683 buf,
684 Buffer::with_lines(vec![
685 "ABCDEFGHI",
686 "KLMNOPQRS",
687 "UVWXYZABC",
688 "EFGHIJKLM",
689 "OPQRSTUVW",
690 "YZABCDEFG",
691 "IJKLMNOPQ",
692 "STUVWXYZA",
693 "CDEFGHIJK",
694 "MNOPQRSTU",
695 ])
696 )
697 }
698
699 #[rstest]
700 fn does_not_render_vertical_scrollbar(mut scroll_view: ScrollView) {
701 scroll_view = scroll_view.vertical_scrollbar_visibility(ScrollbarVisibility::Never);
702 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
703 let mut state = ScrollViewState::default();
704 scroll_view.render(buf.area, &mut buf, &mut state);
705 assert_eq!(
706 buf,
707 Buffer::with_lines(vec![
708 "ABCDEF",
709 "KLMNOP",
710 "UVWXYZ",
711 "EFGHIJ",
712 "OPQRST",
713 "◄███═►",
714 ])
715 )
716 }
717
718 #[rstest]
719 fn does_not_render_horizontal_scrollbar(mut scroll_view: ScrollView) {
720 scroll_view = scroll_view.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
721 let mut buf = Buffer::empty(Rect::new(0, 0, 7, 6));
722 let mut state = ScrollViewState::default();
723 scroll_view.render(buf.area, &mut buf, &mut state);
724 assert_eq!(
725 buf,
726 Buffer::with_lines(vec![
727 "ABCDEF▲",
728 "KLMNOP█",
729 "UVWXYZ█",
730 "EFGHIJ█",
731 "OPQRST║",
732 "YZABCD▼",
733 ])
734 )
735 }
736
737 #[rstest]
738 #[rustfmt::skip]
739 fn does_not_render_both_scrollbars(mut scroll_view: ScrollView) {
740 scroll_view = scroll_view.scrollbars_visibility(ScrollbarVisibility::Never);
741 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6));
742 let mut state = ScrollViewState::default();
743 scroll_view.render(buf.area, &mut buf, &mut state);
744 assert_eq!(
745 buf,
746 Buffer::with_lines(vec![
747 "ABCDEF",
748 "KLMNOP",
749 "UVWXYZ",
750 "EFGHIJ",
751 "OPQRST",
752 "YZABCD",
753 ])
754 )
755 }
756}