1#![warn(clippy::pedantic)]
2#![allow(
3 clippy::cast_possible_truncation,
4 clippy::cast_precision_loss,
5 clippy::cast_sign_loss,
6 clippy::module_name_repetitions
7)]
8
9use std::iter;
10
11use strum::{Display, EnumString};
12use unicode_width::UnicodeWidthStr;
13
14use crate::{
15 buffer::Buffer,
16 layout::Rect,
17 style::Style,
18 symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
19 widgets::StatefulWidget,
20};
21
22#[derive(Debug, Clone, Eq, PartialEq, Hash)]
89pub struct Scrollbar<'a> {
90 orientation: ScrollbarOrientation,
91 thumb_style: Style,
92 thumb_symbol: &'a str,
93 track_style: Style,
94 track_symbol: Option<&'a str>,
95 begin_symbol: Option<&'a str>,
96 begin_style: Style,
97 end_symbol: Option<&'a str>,
98 end_style: Style,
99}
100
101#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
111pub enum ScrollbarOrientation {
112 #[default]
114 VerticalRight,
115 VerticalLeft,
117 HorizontalBottom,
119 HorizontalTop,
121}
122
123#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
149#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
150pub struct ScrollbarState {
151 content_length: usize,
153 position: usize,
155 viewport_content_length: usize,
159}
160
161#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
167pub enum ScrollDirection {
168 #[default]
170 Forward,
171 Backward,
173}
174
175impl<'a> Default for Scrollbar<'a> {
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) -> 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,
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 #[allow(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) -> 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 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 fn first(&mut self) {
478 self.position = 0;
479 }
480
481 pub 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
499impl<'a> StatefulWidget for Scrollbar<'a> {
500 type State = ScrollbarState;
501
502 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
503 if state.content_length == 0 || self.track_length_excluding_arrow_heads(area) == 0 {
504 return;
505 }
506
507 let mut bar = self.bar_symbols(area, state);
508 let area = self.scollbar_area(area);
509 for x in area.left()..area.right() {
510 for y in area.top()..area.bottom() {
511 if let Some(Some((symbol, style))) = bar.next() {
512 buf.set_string(x, y, symbol, style);
513 }
514 }
515 }
516 }
517}
518
519impl Scrollbar<'_> {
520 fn bar_symbols(
522 &self,
523 area: Rect,
524 state: &ScrollbarState,
525 ) -> impl Iterator<Item = Option<(&str, Style)>> {
526 let (track_start_len, thumb_len, track_end_len) = self.part_lengths(area, state);
527
528 let begin = self.begin_symbol.map(|s| Some((s, self.begin_style)));
529 let track = Some(self.track_symbol.map(|s| (s, self.track_style)));
530 let thumb = Some(Some((self.thumb_symbol, self.thumb_style)));
531 let end = self.end_symbol.map(|s| Some((s, self.end_style)));
532
533 iter::once(begin)
535 .chain(iter::repeat(track).take(track_start_len))
537 .chain(iter::repeat(thumb).take(thumb_len))
539 .chain(iter::repeat(track).take(track_end_len))
541 .chain(iter::once(end))
543 .flatten()
544 }
545
546 fn part_lengths(&self, area: Rect, state: &ScrollbarState) -> (usize, usize, usize) {
556 let track_length = f64::from(self.track_length_excluding_arrow_heads(area));
557 let viewport_length = self.viewport_length(state, area) as f64;
558
559 let max_position = state.content_length.saturating_sub(1) as f64;
563 let start_position = (state.position as f64).clamp(0.0, max_position);
564 let max_viewport_position = max_position + viewport_length;
565 let end_position = start_position + viewport_length;
566
567 let thumb_start = start_position * track_length / max_viewport_position;
570 let thumb_end = end_position * track_length / max_viewport_position;
571
572 let thumb_start = thumb_start.round().clamp(0.0, track_length - 1.0) as usize;
576 let thumb_end = thumb_end.round().clamp(0.0, track_length) as usize;
577
578 let thumb_length = thumb_end.saturating_sub(thumb_start).max(1);
579 let track_end_length = (track_length as usize).saturating_sub(thumb_start + thumb_length);
580
581 (thumb_start, thumb_length, track_end_length)
582 }
583
584 fn scollbar_area(&self, area: Rect) -> Rect {
585 match self.orientation {
586 ScrollbarOrientation::VerticalLeft => area.columns().next(),
587 ScrollbarOrientation::VerticalRight => area.columns().last(),
588 ScrollbarOrientation::HorizontalTop => area.rows().next(),
589 ScrollbarOrientation::HorizontalBottom => area.rows().last(),
590 }
591 .expect("Scrollbar area is empty") }
593
594 fn track_length_excluding_arrow_heads(&self, area: Rect) -> u16 {
602 let start_len = self.begin_symbol.map_or(0, |s| s.width() as u16);
603 let end_len = self.end_symbol.map_or(0, |s| s.width() as u16);
604 let arrows_len = start_len.saturating_add(end_len);
605 if self.orientation.is_vertical() {
606 area.height.saturating_sub(arrows_len)
607 } else {
608 area.width.saturating_sub(arrows_len)
609 }
610 }
611
612 const fn viewport_length(&self, state: &ScrollbarState, area: Rect) -> usize {
613 if state.viewport_content_length != 0 {
614 state.viewport_content_length
615 } else if self.orientation.is_vertical() {
616 area.height as usize
617 } else {
618 area.width as usize
619 }
620 }
621}
622
623impl ScrollbarOrientation {
624 #[must_use = "returns the requested kind of the scrollbar"]
626 pub const fn is_vertical(&self) -> bool {
627 matches!(self, Self::VerticalRight | Self::VerticalLeft)
628 }
629
630 #[must_use = "returns the requested kind of the scrollbar"]
632 pub const fn is_horizontal(&self) -> bool {
633 matches!(self, Self::HorizontalBottom | Self::HorizontalTop)
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use std::str::FromStr;
640
641 use rstest::{fixture, rstest};
642 use strum::ParseError;
643
644 use super::*;
645 use crate::{text::Text, widgets::Widget};
646
647 #[test]
648 fn scroll_direction_to_string() {
649 assert_eq!(ScrollDirection::Forward.to_string(), "Forward");
650 assert_eq!(ScrollDirection::Backward.to_string(), "Backward");
651 }
652
653 #[test]
654 fn scroll_direction_from_str() {
655 assert_eq!("Forward".parse(), Ok(ScrollDirection::Forward));
656 assert_eq!("Backward".parse(), Ok(ScrollDirection::Backward));
657 assert_eq!(
658 ScrollDirection::from_str(""),
659 Err(ParseError::VariantNotFound)
660 );
661 }
662
663 #[test]
664 fn scrollbar_orientation_to_string() {
665 use ScrollbarOrientation::*;
666 assert_eq!(VerticalRight.to_string(), "VerticalRight");
667 assert_eq!(VerticalLeft.to_string(), "VerticalLeft");
668 assert_eq!(HorizontalBottom.to_string(), "HorizontalBottom");
669 assert_eq!(HorizontalTop.to_string(), "HorizontalTop");
670 }
671
672 #[test]
673 fn scrollbar_orientation_from_str() {
674 use ScrollbarOrientation::*;
675 assert_eq!("VerticalRight".parse(), Ok(VerticalRight));
676 assert_eq!("VerticalLeft".parse(), Ok(VerticalLeft));
677 assert_eq!("HorizontalBottom".parse(), Ok(HorizontalBottom));
678 assert_eq!("HorizontalTop".parse(), Ok(HorizontalTop));
679 assert_eq!(
680 ScrollbarOrientation::from_str(""),
681 Err(ParseError::VariantNotFound)
682 );
683 }
684
685 #[fixture]
686 fn scrollbar_no_arrows() -> Scrollbar<'static> {
687 Scrollbar::new(ScrollbarOrientation::HorizontalTop)
688 .begin_symbol(None)
689 .end_symbol(None)
690 .track_symbol(Some("-"))
691 .thumb_symbol("#")
692 }
693
694 #[rstest]
695 #[case::area_2_position_0("#-", 0, 2)]
696 #[case::area_2_position_1("-#", 1, 2)]
697 fn render_scrollbar_simplest(
698 #[case] expected: &str,
699 #[case] position: usize,
700 #[case] content_length: usize,
701 scrollbar_no_arrows: Scrollbar,
702 ) {
703 let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
704 let mut state = ScrollbarState::new(content_length).position(position);
705 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
706 assert_eq!(buffer, Buffer::with_lines([expected]));
707 }
708
709 #[rstest]
710 #[case::position_0("#####-----", 0, 10)]
711 #[case::position_1("-#####----", 1, 10)]
712 #[case::position_2("-#####----", 2, 10)]
713 #[case::position_3("--#####---", 3, 10)]
714 #[case::position_4("--#####---", 4, 10)]
715 #[case::position_5("---#####--", 5, 10)]
716 #[case::position_6("---#####--", 6, 10)]
717 #[case::position_7("----#####-", 7, 10)]
718 #[case::position_8("----#####-", 8, 10)]
719 #[case::position_9("-----#####", 9, 10)]
720 fn render_scrollbar_simple(
721 #[case] expected: &str,
722 #[case] position: usize,
723 #[case] content_length: usize,
724 scrollbar_no_arrows: Scrollbar,
725 ) {
726 let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
727 let mut state = ScrollbarState::new(content_length).position(position);
728 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
729 assert_eq!(buffer, Buffer::with_lines([expected]));
730 }
731
732 #[rstest]
733 #[case::position_0(" ", 0, 0)]
734 fn render_scrollbar_nobar(
735 #[case] expected: &str,
736 #[case] position: usize,
737 #[case] content_length: usize,
738 scrollbar_no_arrows: Scrollbar,
739 ) {
740 let size = expected.width();
741 let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
742 let mut state = ScrollbarState::new(content_length).position(position);
743 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
744 assert_eq!(buffer, Buffer::with_lines([expected]));
745 }
746
747 #[rstest]
748 #[case::fullbar_position_0("##########", 0, 1)]
749 #[case::almost_fullbar_position_0("#########-", 0, 2)]
750 #[case::almost_fullbar_position_1("-#########", 1, 2)]
751 fn render_scrollbar_fullbar(
752 #[case] expected: &str,
753 #[case] position: usize,
754 #[case] content_length: usize,
755 scrollbar_no_arrows: Scrollbar,
756 ) {
757 let size = expected.width();
758 let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
759 let mut state = ScrollbarState::new(content_length).position(position);
760 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
761 assert_eq!(buffer, Buffer::with_lines([expected]));
762 }
763
764 #[rstest]
765 #[case::position_0("#########-", 0, 2)]
766 #[case::position_1("-#########", 1, 2)]
767 fn render_scrollbar_almost_fullbar(
768 #[case] expected: &str,
769 #[case] position: usize,
770 #[case] content_length: usize,
771 scrollbar_no_arrows: Scrollbar,
772 ) {
773 let size = expected.width();
774 let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
775 let mut state = ScrollbarState::new(content_length).position(position);
776 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
777 assert_eq!(buffer, Buffer::with_lines([expected]));
778 }
779
780 #[rstest]
781 #[case::position_0("█████═════", 0, 10)]
782 #[case::position_1("═█████════", 1, 10)]
783 #[case::position_2("═█████════", 2, 10)]
784 #[case::position_3("══█████═══", 3, 10)]
785 #[case::position_4("══█████═══", 4, 10)]
786 #[case::position_5("═══█████══", 5, 10)]
787 #[case::position_6("═══█████══", 6, 10)]
788 #[case::position_7("════█████═", 7, 10)]
789 #[case::position_8("════█████═", 8, 10)]
790 #[case::position_9("═════█████", 9, 10)]
791 #[case::position_out_of_bounds("═════█████", 100, 10)]
792 fn render_scrollbar_without_symbols(
793 #[case] expected: &str,
794 #[case] position: usize,
795 #[case] content_length: usize,
796 ) {
797 let size = expected.width() as u16;
798 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
799 let mut state = ScrollbarState::new(content_length).position(position);
800 Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
801 .begin_symbol(None)
802 .end_symbol(None)
803 .render(buffer.area, &mut buffer, &mut state);
804 assert_eq!(buffer, Buffer::with_lines([expected]));
805 }
806
807 #[rstest]
808 #[case::position_0("█████ ", 0, 10)]
809 #[case::position_1(" █████ ", 1, 10)]
810 #[case::position_2(" █████ ", 2, 10)]
811 #[case::position_3(" █████ ", 3, 10)]
812 #[case::position_4(" █████ ", 4, 10)]
813 #[case::position_5(" █████ ", 5, 10)]
814 #[case::position_6(" █████ ", 6, 10)]
815 #[case::position_7(" █████ ", 7, 10)]
816 #[case::position_8(" █████ ", 8, 10)]
817 #[case::position_9(" █████", 9, 10)]
818 #[case::position_out_of_bounds(" █████", 100, 10)]
819 fn render_scrollbar_without_track_symbols(
820 #[case] expected: &str,
821 #[case] position: usize,
822 #[case] content_length: usize,
823 ) {
824 let size = expected.width() as u16;
825 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
826 let mut state = ScrollbarState::new(content_length).position(position);
827 Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
828 .track_symbol(None)
829 .begin_symbol(None)
830 .end_symbol(None)
831 .render(buffer.area, &mut buffer, &mut state);
832 assert_eq!(buffer, Buffer::with_lines([expected]));
833 }
834
835 #[rstest]
836 #[case::position_0("█████-----", 0, 10)]
837 #[case::position_1("-█████----", 1, 10)]
838 #[case::position_2("-█████----", 2, 10)]
839 #[case::position_3("--█████---", 3, 10)]
840 #[case::position_4("--█████---", 4, 10)]
841 #[case::position_5("---█████--", 5, 10)]
842 #[case::position_6("---█████--", 6, 10)]
843 #[case::position_7("----█████-", 7, 10)]
844 #[case::position_8("----█████-", 8, 10)]
845 #[case::position_9("-----█████", 9, 10)]
846 #[case::position_out_of_bounds("-----█████", 100, 10)]
847 fn render_scrollbar_without_track_symbols_over_content(
848 #[case] expected: &str,
849 #[case] position: usize,
850 #[case] content_length: usize,
851 ) {
852 let size = expected.width() as u16;
853 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
854 let width = buffer.area.width as usize;
855 let s = "";
856 Text::from(format!("{s:-^width$}")).render(buffer.area, &mut buffer);
857 let mut state = ScrollbarState::new(content_length).position(position);
858 Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
859 .track_symbol(None)
860 .begin_symbol(None)
861 .end_symbol(None)
862 .render(buffer.area, &mut buffer, &mut state);
863 assert_eq!(buffer, Buffer::with_lines([expected]));
864 }
865
866 #[rstest]
867 #[case::position_0("<####---->", 0, 10)]
868 #[case::position_1("<#####--->", 1, 10)]
869 #[case::position_2("<-####--->", 2, 10)]
870 #[case::position_3("<-####--->", 3, 10)]
871 #[case::position_4("<--####-->", 4, 10)]
872 #[case::position_5("<--####-->", 5, 10)]
873 #[case::position_6("<---####->", 6, 10)]
874 #[case::position_7("<---####->", 7, 10)]
875 #[case::position_8("<---#####>", 8, 10)]
876 #[case::position_9("<----####>", 9, 10)]
877 #[case::position_one_out_of_bounds("<----####>", 10, 10)]
878 #[case::position_few_out_of_bounds("<----####>", 15, 10)]
879 #[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
880 fn render_scrollbar_with_symbols(
881 #[case] expected: &str,
882 #[case] position: usize,
883 #[case] content_length: usize,
884 ) {
885 let size = expected.width() as u16;
886 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
887 let mut state = ScrollbarState::new(content_length).position(position);
888 Scrollbar::new(ScrollbarOrientation::HorizontalTop)
889 .begin_symbol(Some("<"))
890 .end_symbol(Some(">"))
891 .track_symbol(Some("-"))
892 .thumb_symbol("#")
893 .render(buffer.area, &mut buffer, &mut state);
894 assert_eq!(buffer, Buffer::with_lines([expected]));
895 }
896
897 #[rstest]
898 #[case::position_0("█████═════", 0, 10)]
899 #[case::position_1("═█████════", 1, 10)]
900 #[case::position_2("═█████════", 2, 10)]
901 #[case::position_3("══█████═══", 3, 10)]
902 #[case::position_4("══█████═══", 4, 10)]
903 #[case::position_5("═══█████══", 5, 10)]
904 #[case::position_6("═══█████══", 6, 10)]
905 #[case::position_7("════█████═", 7, 10)]
906 #[case::position_8("════█████═", 8, 10)]
907 #[case::position_9("═════█████", 9, 10)]
908 #[case::position_out_of_bounds("═════█████", 100, 10)]
909 fn render_scrollbar_horizontal_bottom(
910 #[case] expected: &str,
911 #[case] position: usize,
912 #[case] content_length: usize,
913 ) {
914 let size = expected.width() as u16;
915 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
916 let mut state = ScrollbarState::new(content_length).position(position);
917 Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
918 .begin_symbol(None)
919 .end_symbol(None)
920 .render(buffer.area, &mut buffer, &mut state);
921 let empty_string = " ".repeat(size as usize);
922 assert_eq!(buffer, Buffer::with_lines([&empty_string, expected]));
923 }
924
925 #[rstest]
926 #[case::position_0("█████═════", 0, 10)]
927 #[case::position_1("═█████════", 1, 10)]
928 #[case::position_2("═█████════", 2, 10)]
929 #[case::position_3("══█████═══", 3, 10)]
930 #[case::position_4("══█████═══", 4, 10)]
931 #[case::position_5("═══█████══", 5, 10)]
932 #[case::position_6("═══█████══", 6, 10)]
933 #[case::position_7("════█████═", 7, 10)]
934 #[case::position_8("════█████═", 8, 10)]
935 #[case::position_9("═════█████", 9, 10)]
936 #[case::position_out_of_bounds("═════█████", 100, 10)]
937 fn render_scrollbar_horizontal_top(
938 #[case] expected: &str,
939 #[case] position: usize,
940 #[case] content_length: usize,
941 ) {
942 let size = expected.width() as u16;
943 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
944 let mut state = ScrollbarState::new(content_length).position(position);
945 Scrollbar::new(ScrollbarOrientation::HorizontalTop)
946 .begin_symbol(None)
947 .end_symbol(None)
948 .render(buffer.area, &mut buffer, &mut state);
949 let empty_string = " ".repeat(size as usize);
950 assert_eq!(buffer, Buffer::with_lines([expected, &empty_string]));
951 }
952
953 #[rstest]
954 #[case::position_0("<####---->", 0, 10)]
955 #[case::position_1("<#####--->", 1, 10)]
956 #[case::position_2("<-####--->", 2, 10)]
957 #[case::position_3("<-####--->", 3, 10)]
958 #[case::position_4("<--####-->", 4, 10)]
959 #[case::position_5("<--####-->", 5, 10)]
960 #[case::position_6("<---####->", 6, 10)]
961 #[case::position_7("<---####->", 7, 10)]
962 #[case::position_8("<---#####>", 8, 10)]
963 #[case::position_9("<----####>", 9, 10)]
964 #[case::position_one_out_of_bounds("<----####>", 10, 10)]
965 fn render_scrollbar_vertical_left(
966 #[case] expected: &str,
967 #[case] position: usize,
968 #[case] content_length: usize,
969 ) {
970 let size = expected.width() as u16;
971 let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
972 let mut state = ScrollbarState::new(content_length).position(position);
973 Scrollbar::new(ScrollbarOrientation::VerticalLeft)
974 .begin_symbol(Some("<"))
975 .end_symbol(Some(">"))
976 .track_symbol(Some("-"))
977 .thumb_symbol("#")
978 .render(buffer.area, &mut buffer, &mut state);
979 let bar = expected.chars().map(|c| format!("{c} "));
980 assert_eq!(buffer, Buffer::with_lines(bar));
981 }
982
983 #[rstest]
984 #[case::position_0("<####---->", 0, 10)]
985 #[case::position_1("<#####--->", 1, 10)]
986 #[case::position_2("<-####--->", 2, 10)]
987 #[case::position_3("<-####--->", 3, 10)]
988 #[case::position_4("<--####-->", 4, 10)]
989 #[case::position_5("<--####-->", 5, 10)]
990 #[case::position_6("<---####->", 6, 10)]
991 #[case::position_7("<---####->", 7, 10)]
992 #[case::position_8("<---#####>", 8, 10)]
993 #[case::position_9("<----####>", 9, 10)]
994 #[case::position_one_out_of_bounds("<----####>", 10, 10)]
995 fn render_scrollbar_vertical_rightl(
996 #[case] expected: &str,
997 #[case] position: usize,
998 #[case] content_length: usize,
999 ) {
1000 let size = expected.width() as u16;
1001 let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
1002 let mut state = ScrollbarState::new(content_length).position(position);
1003 Scrollbar::new(ScrollbarOrientation::VerticalRight)
1004 .begin_symbol(Some("<"))
1005 .end_symbol(Some(">"))
1006 .track_symbol(Some("-"))
1007 .thumb_symbol("#")
1008 .render(buffer.area, &mut buffer, &mut state);
1009 let bar = expected.chars().map(|c| format!(" {c}"));
1010 assert_eq!(buffer, Buffer::with_lines(bar));
1011 }
1012
1013 #[rstest]
1014 #[case::position_0("##--------", 0, 10)]
1015 #[case::position_1("-##-------", 1, 10)]
1016 #[case::position_2("--##------", 2, 10)]
1017 #[case::position_3("---##-----", 3, 10)]
1018 #[case::position_4("----#-----", 4, 10)]
1019 #[case::position_5("-----#----", 5, 10)]
1020 #[case::position_6("-----##---", 6, 10)]
1021 #[case::position_7("------##--", 7, 10)]
1022 #[case::position_8("-------##-", 8, 10)]
1023 #[case::position_9("--------##", 9, 10)]
1024 #[case::position_one_out_of_bounds("--------##", 10, 10)]
1025 fn custom_viewport_length(
1026 #[case] expected: &str,
1027 #[case] position: usize,
1028 #[case] content_length: usize,
1029 scrollbar_no_arrows: Scrollbar,
1030 ) {
1031 let size = expected.width() as u16;
1032 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
1033 let mut state = ScrollbarState::new(content_length)
1034 .position(position)
1035 .viewport_content_length(2);
1036 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
1037 assert_eq!(buffer, Buffer::with_lines([expected]));
1038 }
1039
1040 #[rstest]
1043 #[case::position_0("#----", 0, 100)]
1044 #[case::position_10("#----", 10, 100)]
1045 #[case::position_20("-#---", 20, 100)]
1046 #[case::position_30("-#---", 30, 100)]
1047 #[case::position_40("--#--", 40, 100)]
1048 #[case::position_50("--#--", 50, 100)]
1049 #[case::position_60("---#-", 60, 100)]
1050 #[case::position_70("---#-", 70, 100)]
1051 #[case::position_80("----#", 80, 100)]
1052 #[case::position_90("----#", 90, 100)]
1053 #[case::position_one_out_of_bounds("----#", 100, 100)]
1054 fn thumb_visible_on_very_small_track(
1055 #[case] expected: &str,
1056 #[case] position: usize,
1057 #[case] content_length: usize,
1058 scrollbar_no_arrows: Scrollbar,
1059 ) {
1060 let size = expected.width() as u16;
1061 let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
1062 let mut state = ScrollbarState::new(content_length)
1063 .position(position)
1064 .viewport_content_length(2);
1065 scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
1066 assert_eq!(buffer, Buffer::with_lines([expected]));
1067 }
1068}