1#![warn(clippy::pedantic)]
3#![allow(
4 clippy::cast_possible_truncation,
5 clippy::cast_precision_loss,
6 clippy::cast_sign_loss,
7 clippy::module_name_repetitions
8)]
9
10use core::iter;
11
12use ratatui_core::buffer::Buffer;
13use ratatui_core::layout::Rect;
14use ratatui_core::style::Style;
15use ratatui_core::symbols::scrollbar::{DOUBLE_HORIZONTAL, DOUBLE_VERTICAL, Set};
16use ratatui_core::widgets::StatefulWidget;
17use strum::{Display, EnumString};
18use unicode_width::UnicodeWidthStr;
19
20#[cfg(not(feature = "std"))]
21use crate::polyfills::F64Polyfills;
22
23#[derive(Debug, Clone, Eq, PartialEq, Hash)]
87pub struct Scrollbar<'a> {
88 orientation: ScrollbarOrientation,
89 thumb_style: Style,
90 thumb_symbol: &'a str,
91 track_style: Style,
92 track_symbol: Option<&'a str>,
93 begin_symbol: Option<&'a str>,
94 begin_style: Style,
95 end_symbol: Option<&'a str>,
96 end_style: Style,
97}
98
99#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub enum ScrollbarOrientation {
111 #[default]
113 VerticalRight,
114 VerticalLeft,
116 HorizontalBottom,
118 HorizontalTop,
120}
121
122#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub struct ScrollbarState {
150 content_length: usize,
152 position: usize,
154 viewport_content_length: usize,
158}
159
160#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
166#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
167pub enum ScrollDirection {
168 #[default]
170 Forward,
171 Backward,
173}
174
175impl Default for Scrollbar<'_> {
176 fn default() -> Self {
177 Self::new(ScrollbarOrientation::default())
178 }
179}
180
181impl<'a> Scrollbar<'a> {
182 #[must_use = "creates the Scrollbar"]
187 pub const fn new(orientation: ScrollbarOrientation) -> Self {
188 let symbols = if orientation.is_vertical() {
189 DOUBLE_VERTICAL
190 } else {
191 DOUBLE_HORIZONTAL
192 };
193 Self::new_with_symbols(orientation, &symbols)
194 }
195
196 #[must_use = "creates the Scrollbar"]
198 const fn new_with_symbols(orientation: ScrollbarOrientation, symbols: &Set<'a>) -> Self {
199 Self {
200 orientation,
201 thumb_symbol: symbols.thumb,
202 thumb_style: Style::new(),
203 track_symbol: Some(symbols.track),
204 track_style: Style::new(),
205 begin_symbol: Some(symbols.begin),
206 begin_style: Style::new(),
207 end_symbol: Some(symbols.end),
208 end_style: Style::new(),
209 }
210 }
211
212 #[must_use = "method moves the value of self and returns the modified value"]
221 pub const fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
222 self.orientation = orientation;
223 let symbols = if self.orientation.is_vertical() {
224 DOUBLE_VERTICAL
225 } else {
226 DOUBLE_HORIZONTAL
227 };
228 self.symbols(symbols)
229 }
230
231 #[must_use = "method moves the value of self and returns the modified value"]
238 pub const fn orientation_and_symbol(
239 mut self,
240 orientation: ScrollbarOrientation,
241 symbols: Set<'a>,
242 ) -> Self {
243 self.orientation = orientation;
244 self.symbols(symbols)
245 }
246
247 #[must_use = "method moves the value of self and returns the modified value"]
254 pub const fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
255 self.thumb_symbol = thumb_symbol;
256 self
257 }
258
259 #[must_use = "method moves the value of self and returns the modified value"]
271 pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
272 self.thumb_style = thumb_style.into();
273 self
274 }
275
276 #[must_use = "method moves the value of self and returns the modified value"]
282 pub const fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
283 self.track_symbol = track_symbol;
284 self
285 }
286
287 #[must_use = "method moves the value of self and returns the modified value"]
298 pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
299 self.track_style = track_style.into();
300 self
301 }
302
303 #[must_use = "method moves the value of self and returns the modified value"]
309 pub const fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
310 self.begin_symbol = begin_symbol;
311 self
312 }
313
314 #[must_use = "method moves the value of self and returns the modified value"]
325 pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
326 self.begin_style = begin_style.into();
327 self
328 }
329
330 #[must_use = "method moves the value of self and returns the modified value"]
336 pub const fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
337 self.end_symbol = end_symbol;
338 self
339 }
340
341 #[must_use = "method moves the value of self and returns the modified value"]
352 pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
353 self.end_style = end_style.into();
354 self
355 }
356
357 #[expect(clippy::needless_pass_by_value)] #[must_use = "method moves the value of self and returns the modified value"]
375 pub const fn symbols(mut self, symbols: Set<'a>) -> Self {
376 self.thumb_symbol = symbols.thumb;
377 if self.track_symbol.is_some() {
378 self.track_symbol = Some(symbols.track);
379 }
380 if self.begin_symbol.is_some() {
381 self.begin_symbol = Some(symbols.begin);
382 }
383 if self.end_symbol.is_some() {
384 self.end_symbol = Some(symbols.end);
385 }
386 self
387 }
388
389 #[must_use = "method moves the value of self and returns the modified value"]
407 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
408 let style = style.into();
409 self.track_style = style;
410 self.thumb_style = style;
411 self.begin_style = style;
412 self.end_style = style;
413 self
414 }
415}
416
417impl ScrollbarState {
418 #[must_use = "creates the ScrollbarState"]
423 pub const fn new(content_length: usize) -> Self {
424 Self {
425 content_length,
426 position: 0,
427 viewport_content_length: 0,
428 }
429 }
430
431 #[must_use = "method moves the value of self and returns the modified value"]
437 pub const fn position(mut self, position: usize) -> Self {
438 self.position = position;
439 self
440 }
441
442 #[must_use = "method moves the value of self and returns the modified value"]
449 pub const fn content_length(mut self, content_length: usize) -> Self {
450 self.content_length = content_length;
451 self
452 }
453
454 #[must_use = "method moves the value of self and returns the modified value"]
458 pub const fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
459 self.viewport_content_length = viewport_content_length;
460 self
461 }
462
463 pub const fn prev(&mut self) {
465 self.position = self.position.saturating_sub(1);
466 }
467
468 pub fn next(&mut self) {
470 self.position = self
471 .position
472 .saturating_add(1)
473 .min(self.content_length.saturating_sub(1));
474 }
475
476 pub const fn first(&mut self) {
478 self.position = 0;
479 }
480
481 pub const fn last(&mut self) {
483 self.position = self.content_length.saturating_sub(1);
484 }
485
486 pub fn scroll(&mut self, direction: ScrollDirection) {
488 match direction {
489 ScrollDirection::Forward => {
490 self.next();
491 }
492 ScrollDirection::Backward => {
493 self.prev();
494 }
495 }
496 }
497
498 #[must_use = "returns the current position within the scrollable content"]
500 pub const fn get_position(&self) -> usize {
501 self.position
502 }
503}
504
505impl StatefulWidget for Scrollbar<'_> {
506 type State = ScrollbarState;
507
508 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
509 if state.content_length == 0 || self.track_length_excluding_arrow_heads(area) == 0 {
510 return;
511 }
512
513 if let Some(area) = self.scrollbar_area(area) {
514 let areas = area.columns().flat_map(Rect::rows);
515 let bar_symbols = self.bar_symbols(area, state);
516 for (area, bar) in areas.zip(bar_symbols) {
517 if let Some((symbol, style)) = bar {
518 buf.set_string(area.x, area.y, symbol, style);
519 }
520 }
521 }
522 }
523}
524
525impl Scrollbar<'_> {
526 fn bar_symbols(
528 &self,
529 area: Rect,
530 state: &ScrollbarState,
531 ) -> impl Iterator<Item = Option<(&str, Style)>> {
532 let (track_start_len, thumb_len, track_end_len) = self.part_lengths(area, state);
533
534 let begin = self.begin_symbol.map(|s| Some((s, self.begin_style)));
535 let track = Some(self.track_symbol.map(|s| (s, self.track_style)));
536 let thumb = Some(Some((self.thumb_symbol, self.thumb_style)));
537 let end = self.end_symbol.map(|s| Some((s, self.end_style)));
538
539 iter::once(begin)
541 .chain(iter::repeat_n(track, track_start_len))
543 .chain(iter::repeat_n(thumb, thumb_len))
545 .chain(iter::repeat_n(track, track_end_len))
547 .chain(iter::once(end))
549 .flatten()
550 }
551
552 fn part_lengths(&self, area: Rect, state: &ScrollbarState) -> (usize, usize, usize) {
562 let track_length = f64::from(self.track_length_excluding_arrow_heads(area));
563 let viewport_length = self.viewport_length(state, area) as f64;
564
565 let max_position = state.content_length.saturating_sub(1) as f64;
569 let start_position = (state.position as f64).clamp(0.0, max_position);
570 let max_viewport_position = max_position + viewport_length;
571 let end_position = start_position + viewport_length;
572
573 let thumb_start = start_position * track_length / max_viewport_position;
576 let thumb_end = end_position * track_length / max_viewport_position;
577
578 let thumb_start = thumb_start.round().clamp(0.0, track_length - 1.0) as usize;
582 let thumb_end = thumb_end.round().clamp(0.0, track_length) as usize;
583
584 let thumb_length = thumb_end.saturating_sub(thumb_start).max(1);
585 let track_end_length = (track_length as usize).saturating_sub(thumb_start + thumb_length);
586
587 (thumb_start, thumb_length, track_end_length)
588 }
589
590 fn scrollbar_area(&self, area: Rect) -> Option<Rect> {
591 match self.orientation {
592 ScrollbarOrientation::VerticalLeft => area.columns().next(),
593 ScrollbarOrientation::VerticalRight => area.columns().next_back(),
594 ScrollbarOrientation::HorizontalTop => area.rows().next(),
595 ScrollbarOrientation::HorizontalBottom => area.rows().next_back(),
596 }
597 }
598
599 fn track_length_excluding_arrow_heads(&self, area: Rect) -> u16 {
607 let start_len = self.begin_symbol.map_or(0, |s| s.width() as u16);
608 let end_len = self.end_symbol.map_or(0, |s| s.width() as u16);
609 let arrows_len = start_len.saturating_add(end_len);
610 if self.orientation.is_vertical() {
611 area.height.saturating_sub(arrows_len)
612 } else {
613 area.width.saturating_sub(arrows_len)
614 }
615 }
616
617 const fn viewport_length(&self, state: &ScrollbarState, area: Rect) -> usize {
618 if state.viewport_content_length != 0 {
619 state.viewport_content_length
620 } else if self.orientation.is_vertical() {
621 area.height as usize
622 } else {
623 area.width as usize
624 }
625 }
626}
627
628impl ScrollbarOrientation {
629 #[must_use = "returns the requested kind of the scrollbar"]
631 pub const fn is_vertical(&self) -> bool {
632 matches!(self, Self::VerticalRight | Self::VerticalLeft)
633 }
634
635 #[must_use = "returns the requested kind of the scrollbar"]
637 pub const fn is_horizontal(&self) -> bool {
638 matches!(self, Self::HorizontalBottom | Self::HorizontalTop)
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use alloc::format;
645 use alloc::string::ToString;
646 use core::str::FromStr;
647
648 use ratatui_core::text::Text;
649 use ratatui_core::widgets::Widget;
650 use rstest::{fixture, rstest};
651 use strum::ParseError;
652
653 use super::*;
654
655 #[test]
656 fn scroll_direction_to_string() {
657 assert_eq!(ScrollDirection::Forward.to_string(), "Forward");
658 assert_eq!(ScrollDirection::Backward.to_string(), "Backward");
659 }
660
661 #[test]
662 fn scroll_direction_from_str() {
663 assert_eq!("Forward".parse(), Ok(ScrollDirection::Forward));
664 assert_eq!("Backward".parse(), Ok(ScrollDirection::Backward));
665 assert_eq!(
666 ScrollDirection::from_str(""),
667 Err(ParseError::VariantNotFound)
668 );
669 }
670
671 #[test]
672 fn scrollbar_orientation_to_string() {
673 use ScrollbarOrientation::*;
674 assert_eq!(VerticalRight.to_string(), "VerticalRight");
675 assert_eq!(VerticalLeft.to_string(), "VerticalLeft");
676 assert_eq!(HorizontalBottom.to_string(), "HorizontalBottom");
677 assert_eq!(HorizontalTop.to_string(), "HorizontalTop");
678 }
679
680 #[test]
681 fn scrollbar_orientation_from_str() {
682 use ScrollbarOrientation::*;
683 assert_eq!("VerticalRight".parse(), Ok(VerticalRight));
684 assert_eq!("VerticalLeft".parse(), Ok(VerticalLeft));
685 assert_eq!("HorizontalBottom".parse(), Ok(HorizontalBottom));
686 assert_eq!("HorizontalTop".parse(), Ok(HorizontalTop));
687 assert_eq!(
688 ScrollbarOrientation::from_str(""),
689 Err(ParseError::VariantNotFound)
690 );
691 }
692
693 #[fixture]
694 fn scrollbar_no_arrows() -> Scrollbar<'static> {
695 Scrollbar::new(ScrollbarOrientation::HorizontalTop)
696 .begin_symbol(None)
697 .end_symbol(None)
698 .track_symbol(Some("-"))
699 .thumb_symbol("#")
700 }
701
702 #[rstest]
703 #[case::area_2_position_0("#-", 0, 2)]
704 #[case::area_2_position_1("-#", 1, 2)]
705 fn render_scrollbar_simplest(
706 #[case] expected: &str,
707 #[case] position: usize,
708 #[case] content_length: usize,
709 scrollbar_no_arrows: Scrollbar,
710 ) {
711 let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
712 let mut state = ScrollbarState::new(content_length).position(position);
713 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
714 assert_eq!(buffer, Buffer::with_lines([expected]));
715 }
716
717 #[rstest]
718 #[case::position_0("#####-----", 0, 10)]
719 #[case::position_1("-#####----", 1, 10)]
720 #[case::position_2("-#####----", 2, 10)]
721 #[case::position_3("--#####---", 3, 10)]
722 #[case::position_4("--#####---", 4, 10)]
723 #[case::position_5("---#####--", 5, 10)]
724 #[case::position_6("---#####--", 6, 10)]
725 #[case::position_7("----#####-", 7, 10)]
726 #[case::position_8("----#####-", 8, 10)]
727 #[case::position_9("-----#####", 9, 10)]
728 fn render_scrollbar_simple(
729 #[case] expected: &str,
730 #[case] position: usize,
731 #[case] content_length: usize,
732 scrollbar_no_arrows: Scrollbar,
733 ) {
734 let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
735 let mut state = ScrollbarState::new(content_length).position(position);
736 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
737 assert_eq!(buffer, Buffer::with_lines([expected]));
738 }
739
740 #[rstest]
741 #[case::position_0(" ", 0, 0)]
742 fn render_scrollbar_nobar(
743 #[case] expected: &str,
744 #[case] position: usize,
745 #[case] content_length: usize,
746 scrollbar_no_arrows: Scrollbar,
747 ) {
748 let size = expected.width();
749 let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
750 let mut state = ScrollbarState::new(content_length).position(position);
751 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
752 assert_eq!(buffer, Buffer::with_lines([expected]));
753 }
754
755 #[rstest]
756 #[case::fullbar_position_0("##########", 0, 1)]
757 #[case::almost_fullbar_position_0("#########-", 0, 2)]
758 #[case::almost_fullbar_position_1("-#########", 1, 2)]
759 fn render_scrollbar_fullbar(
760 #[case] expected: &str,
761 #[case] position: usize,
762 #[case] content_length: usize,
763 scrollbar_no_arrows: Scrollbar,
764 ) {
765 let size = expected.width();
766 let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
767 let mut state = ScrollbarState::new(content_length).position(position);
768 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
769 assert_eq!(buffer, Buffer::with_lines([expected]));
770 }
771
772 #[rstest]
773 #[case::position_0("#########-", 0, 2)]
774 #[case::position_1("-#########", 1, 2)]
775 fn render_scrollbar_almost_fullbar(
776 #[case] expected: &str,
777 #[case] position: usize,
778 #[case] content_length: usize,
779 scrollbar_no_arrows: Scrollbar,
780 ) {
781 let size = expected.width();
782 let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
783 let mut state = ScrollbarState::new(content_length).position(position);
784 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
785 assert_eq!(buffer, Buffer::with_lines([expected]));
786 }
787
788 #[rstest]
789 #[case::position_0("█████═════", 0, 10)]
790 #[case::position_1("═█████════", 1, 10)]
791 #[case::position_2("═█████════", 2, 10)]
792 #[case::position_3("══█████═══", 3, 10)]
793 #[case::position_4("══█████═══", 4, 10)]
794 #[case::position_5("═══█████══", 5, 10)]
795 #[case::position_6("═══█████══", 6, 10)]
796 #[case::position_7("════█████═", 7, 10)]
797 #[case::position_8("════█████═", 8, 10)]
798 #[case::position_9("═════█████", 9, 10)]
799 #[case::position_out_of_bounds("═════█████", 100, 10)]
800 fn render_scrollbar_without_symbols(
801 #[case] expected: &str,
802 #[case] position: usize,
803 #[case] content_length: usize,
804 ) {
805 let size = expected.width() as u16;
806 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
807 let mut state = ScrollbarState::new(content_length).position(position);
808 Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
809 .begin_symbol(None)
810 .end_symbol(None)
811 .render(buffer.area, &mut buffer, &mut state);
812 assert_eq!(buffer, Buffer::with_lines([expected]));
813 }
814
815 #[rstest]
816 #[case::position_0("█████ ", 0, 10)]
817 #[case::position_1(" █████ ", 1, 10)]
818 #[case::position_2(" █████ ", 2, 10)]
819 #[case::position_3(" █████ ", 3, 10)]
820 #[case::position_4(" █████ ", 4, 10)]
821 #[case::position_5(" █████ ", 5, 10)]
822 #[case::position_6(" █████ ", 6, 10)]
823 #[case::position_7(" █████ ", 7, 10)]
824 #[case::position_8(" █████ ", 8, 10)]
825 #[case::position_9(" █████", 9, 10)]
826 #[case::position_out_of_bounds(" █████", 100, 10)]
827 fn render_scrollbar_without_track_symbols(
828 #[case] expected: &str,
829 #[case] position: usize,
830 #[case] content_length: usize,
831 ) {
832 let size = expected.width() as u16;
833 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
834 let mut state = ScrollbarState::new(content_length).position(position);
835 Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
836 .track_symbol(None)
837 .begin_symbol(None)
838 .end_symbol(None)
839 .render(buffer.area, &mut buffer, &mut state);
840 assert_eq!(buffer, Buffer::with_lines([expected]));
841 }
842
843 #[rstest]
844 #[case::position_0("█████-----", 0, 10)]
845 #[case::position_1("-█████----", 1, 10)]
846 #[case::position_2("-█████----", 2, 10)]
847 #[case::position_3("--█████---", 3, 10)]
848 #[case::position_4("--█████---", 4, 10)]
849 #[case::position_5("---█████--", 5, 10)]
850 #[case::position_6("---█████--", 6, 10)]
851 #[case::position_7("----█████-", 7, 10)]
852 #[case::position_8("----█████-", 8, 10)]
853 #[case::position_9("-----█████", 9, 10)]
854 #[case::position_out_of_bounds("-----█████", 100, 10)]
855 fn render_scrollbar_without_track_symbols_over_content(
856 #[case] expected: &str,
857 #[case] position: usize,
858 #[case] content_length: usize,
859 ) {
860 let size = expected.width() as u16;
861 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
862 let width = buffer.area.width as usize;
863 let s = "";
864 Text::from(format!("{s:-^width$}")).render(buffer.area, &mut buffer);
865 let mut state = ScrollbarState::new(content_length).position(position);
866 Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
867 .track_symbol(None)
868 .begin_symbol(None)
869 .end_symbol(None)
870 .render(buffer.area, &mut buffer, &mut state);
871 assert_eq!(buffer, Buffer::with_lines([expected]));
872 }
873
874 #[rstest]
875 #[case::position_0("<####---->", 0, 10)]
876 #[case::position_1("<#####--->", 1, 10)]
877 #[case::position_2("<-####--->", 2, 10)]
878 #[case::position_3("<-####--->", 3, 10)]
879 #[case::position_4("<--####-->", 4, 10)]
880 #[case::position_5("<--####-->", 5, 10)]
881 #[case::position_6("<---####->", 6, 10)]
882 #[case::position_7("<---####->", 7, 10)]
883 #[case::position_8("<---#####>", 8, 10)]
884 #[case::position_9("<----####>", 9, 10)]
885 #[case::position_one_out_of_bounds("<----####>", 10, 10)]
886 #[case::position_few_out_of_bounds("<----####>", 15, 10)]
887 #[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
888 fn render_scrollbar_with_symbols(
889 #[case] expected: &str,
890 #[case] position: usize,
891 #[case] content_length: usize,
892 ) {
893 let size = expected.width() as u16;
894 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
895 let mut state = ScrollbarState::new(content_length).position(position);
896 Scrollbar::new(ScrollbarOrientation::HorizontalTop)
897 .begin_symbol(Some("<"))
898 .end_symbol(Some(">"))
899 .track_symbol(Some("-"))
900 .thumb_symbol("#")
901 .render(buffer.area, &mut buffer, &mut state);
902 assert_eq!(buffer, Buffer::with_lines([expected]));
903 }
904
905 #[rstest]
906 #[case::position_0("█████═════", 0, 10)]
907 #[case::position_1("═█████════", 1, 10)]
908 #[case::position_2("═█████════", 2, 10)]
909 #[case::position_3("══█████═══", 3, 10)]
910 #[case::position_4("══█████═══", 4, 10)]
911 #[case::position_5("═══█████══", 5, 10)]
912 #[case::position_6("═══█████══", 6, 10)]
913 #[case::position_7("════█████═", 7, 10)]
914 #[case::position_8("════█████═", 8, 10)]
915 #[case::position_9("═════█████", 9, 10)]
916 #[case::position_out_of_bounds("═════█████", 100, 10)]
917 fn render_scrollbar_horizontal_bottom(
918 #[case] expected: &str,
919 #[case] position: usize,
920 #[case] content_length: usize,
921 ) {
922 let size = expected.width() as u16;
923 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
924 let mut state = ScrollbarState::new(content_length).position(position);
925 Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
926 .begin_symbol(None)
927 .end_symbol(None)
928 .render(buffer.area, &mut buffer, &mut state);
929 let empty_string = " ".repeat(size as usize);
930 assert_eq!(buffer, Buffer::with_lines([&empty_string, expected]));
931 }
932
933 #[rstest]
934 #[case::position_0("█████═════", 0, 10)]
935 #[case::position_1("═█████════", 1, 10)]
936 #[case::position_2("═█████════", 2, 10)]
937 #[case::position_3("══█████═══", 3, 10)]
938 #[case::position_4("══█████═══", 4, 10)]
939 #[case::position_5("═══█████══", 5, 10)]
940 #[case::position_6("═══█████══", 6, 10)]
941 #[case::position_7("════█████═", 7, 10)]
942 #[case::position_8("════█████═", 8, 10)]
943 #[case::position_9("═════█████", 9, 10)]
944 #[case::position_out_of_bounds("═════█████", 100, 10)]
945 fn render_scrollbar_horizontal_top(
946 #[case] expected: &str,
947 #[case] position: usize,
948 #[case] content_length: usize,
949 ) {
950 let size = expected.width() as u16;
951 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
952 let mut state = ScrollbarState::new(content_length).position(position);
953 Scrollbar::new(ScrollbarOrientation::HorizontalTop)
954 .begin_symbol(None)
955 .end_symbol(None)
956 .render(buffer.area, &mut buffer, &mut state);
957 let empty_string = " ".repeat(size as usize);
958 assert_eq!(buffer, Buffer::with_lines([expected, &empty_string]));
959 }
960
961 #[rstest]
962 #[case::position_0("<####---->", 0, 10)]
963 #[case::position_1("<#####--->", 1, 10)]
964 #[case::position_2("<-####--->", 2, 10)]
965 #[case::position_3("<-####--->", 3, 10)]
966 #[case::position_4("<--####-->", 4, 10)]
967 #[case::position_5("<--####-->", 5, 10)]
968 #[case::position_6("<---####->", 6, 10)]
969 #[case::position_7("<---####->", 7, 10)]
970 #[case::position_8("<---#####>", 8, 10)]
971 #[case::position_9("<----####>", 9, 10)]
972 #[case::position_one_out_of_bounds("<----####>", 10, 10)]
973 #[case::position_few_out_of_bounds("<----####>", 15, 10)]
974 #[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
975 fn render_scrollbar_vertical_left(
976 #[case] expected: &str,
977 #[case] position: usize,
978 #[case] content_length: usize,
979 ) {
980 let size = expected.width() as u16;
981 let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
982 let mut state = ScrollbarState::new(content_length).position(position);
983 Scrollbar::new(ScrollbarOrientation::VerticalLeft)
984 .begin_symbol(Some("<"))
985 .end_symbol(Some(">"))
986 .track_symbol(Some("-"))
987 .thumb_symbol("#")
988 .render(buffer.area, &mut buffer, &mut state);
989 let bar = expected.chars().map(|c| format!("{c} "));
990 assert_eq!(buffer, Buffer::with_lines(bar));
991 }
992
993 #[rstest]
994 #[case::position_0("<####---->", 0, 10)]
995 #[case::position_1("<#####--->", 1, 10)]
996 #[case::position_2("<-####--->", 2, 10)]
997 #[case::position_3("<-####--->", 3, 10)]
998 #[case::position_4("<--####-->", 4, 10)]
999 #[case::position_5("<--####-->", 5, 10)]
1000 #[case::position_6("<---####->", 6, 10)]
1001 #[case::position_7("<---####->", 7, 10)]
1002 #[case::position_8("<---#####>", 8, 10)]
1003 #[case::position_9("<----####>", 9, 10)]
1004 #[case::position_one_out_of_bounds("<----####>", 10, 10)]
1005 #[case::position_few_out_of_bounds("<----####>", 15, 10)]
1006 #[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
1007 fn render_scrollbar_vertical_right(
1008 #[case] expected: &str,
1009 #[case] position: usize,
1010 #[case] content_length: usize,
1011 ) {
1012 let size = expected.width() as u16;
1013 let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
1014 let mut state = ScrollbarState::new(content_length).position(position);
1015 Scrollbar::new(ScrollbarOrientation::VerticalRight)
1016 .begin_symbol(Some("<"))
1017 .end_symbol(Some(">"))
1018 .track_symbol(Some("-"))
1019 .thumb_symbol("#")
1020 .render(buffer.area, &mut buffer, &mut state);
1021 let bar = expected.chars().map(|c| format!(" {c}"));
1022 assert_eq!(buffer, Buffer::with_lines(bar));
1023 }
1024
1025 #[rstest]
1026 #[case::position_0("##--------", 0, 10)]
1027 #[case::position_1("-##-------", 1, 10)]
1028 #[case::position_2("--##------", 2, 10)]
1029 #[case::position_3("---##-----", 3, 10)]
1030 #[case::position_4("----#-----", 4, 10)]
1031 #[case::position_5("-----#----", 5, 10)]
1032 #[case::position_6("-----##---", 6, 10)]
1033 #[case::position_7("------##--", 7, 10)]
1034 #[case::position_8("-------##-", 8, 10)]
1035 #[case::position_9("--------##", 9, 10)]
1036 #[case::position_one_out_of_bounds("--------##", 10, 10)]
1037 fn custom_viewport_length(
1038 #[case] expected: &str,
1039 #[case] position: usize,
1040 #[case] content_length: usize,
1041 scrollbar_no_arrows: Scrollbar,
1042 ) {
1043 let size = expected.width() as u16;
1044 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
1045 let mut state = ScrollbarState::new(content_length)
1046 .position(position)
1047 .viewport_content_length(2);
1048 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
1049 assert_eq!(buffer, Buffer::with_lines([expected]));
1050 }
1051
1052 #[rstest]
1055 #[case::position_0("#----", 0, 100)]
1056 #[case::position_10("#----", 10, 100)]
1057 #[case::position_20("-#---", 20, 100)]
1058 #[case::position_30("-#---", 30, 100)]
1059 #[case::position_40("--#--", 40, 100)]
1060 #[case::position_50("--#--", 50, 100)]
1061 #[case::position_60("---#-", 60, 100)]
1062 #[case::position_70("---#-", 70, 100)]
1063 #[case::position_80("----#", 80, 100)]
1064 #[case::position_90("----#", 90, 100)]
1065 #[case::position_one_out_of_bounds("----#", 100, 100)]
1066 fn thumb_visible_on_very_small_track(
1067 #[case] expected: &str,
1068 #[case] position: usize,
1069 #[case] content_length: usize,
1070 scrollbar_no_arrows: Scrollbar,
1071 ) {
1072 let size = expected.width() as u16;
1073 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
1074 let mut state = ScrollbarState::new(content_length)
1075 .position(position)
1076 .viewport_content_length(2);
1077 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
1078 assert_eq!(buffer, Buffer::with_lines([expected]));
1079 }
1080
1081 #[rstest]
1082 #[case::scrollbar_height_0(10, 0)]
1083 #[case::scrollbar_width_0(0, 10)]
1084 fn do_not_render_with_empty_area(#[case] width: u16, #[case] height: u16) {
1085 let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
1086 .begin_symbol(Some("<"))
1087 .end_symbol(Some(">"))
1088 .track_symbol(Some("-"))
1089 .thumb_symbol("#");
1090 let zero_width_area = Rect::new(0, 0, width, height);
1091 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
1092
1093 let mut state = ScrollbarState::new(10);
1094 scrollbar.render(zero_width_area, &mut buffer, &mut state);
1095 }
1096
1097 #[rstest]
1098 #[case::vertical_left(ScrollbarOrientation::VerticalLeft)]
1099 #[case::vertical_right(ScrollbarOrientation::VerticalRight)]
1100 #[case::horizontal_top(ScrollbarOrientation::HorizontalTop)]
1101 #[case::horizontal_bottom(ScrollbarOrientation::HorizontalBottom)]
1102 fn render_in_minimal_buffer(#[case] orientation: ScrollbarOrientation) {
1103 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
1104 let scrollbar = Scrollbar::new(orientation);
1105 let mut state = ScrollbarState::new(10).position(5);
1106 scrollbar.render(buffer.area, &mut buffer, &mut state);
1108 assert_eq!(buffer, Buffer::with_lines([" "]));
1109 }
1110
1111 #[rstest]
1112 #[case::vertical_left(ScrollbarOrientation::VerticalLeft)]
1113 #[case::vertical_right(ScrollbarOrientation::VerticalRight)]
1114 #[case::horizontal_top(ScrollbarOrientation::HorizontalTop)]
1115 #[case::horizontal_bottom(ScrollbarOrientation::HorizontalBottom)]
1116 fn render_in_zero_size_buffer(#[case] orientation: ScrollbarOrientation) {
1117 let mut buffer = Buffer::empty(Rect::ZERO);
1118 let scrollbar = Scrollbar::new(orientation);
1119 let mut state = ScrollbarState::new(10).position(5);
1120 scrollbar.render(buffer.area, &mut buffer, &mut state);
1122 }
1123}