#![warn(clippy::pedantic)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::module_name_repetitions
)]
use std::iter;
use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
widgets::StatefulWidget,
};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Scrollbar<'a> {
orientation: ScrollbarOrientation,
thumb_style: Style,
thumb_symbol: &'a str,
track_style: Style,
track_symbol: Option<&'a str>,
begin_symbol: Option<&'a str>,
begin_style: Style,
end_symbol: Option<&'a str>,
end_style: Style,
}
#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
pub enum ScrollbarOrientation {
#[default]
VerticalRight,
VerticalLeft,
HorizontalBottom,
HorizontalTop,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollbarState {
content_length: usize,
position: usize,
viewport_content_length: usize,
}
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ScrollDirection {
#[default]
Forward,
Backward,
}
impl<'a> Default for Scrollbar<'a> {
fn default() -> Self {
Self::new(ScrollbarOrientation::default())
}
}
impl<'a> Scrollbar<'a> {
#[must_use = "creates the Scrollbar"]
pub const fn new(orientation: ScrollbarOrientation) -> Self {
let symbols = if orientation.is_vertical() {
DOUBLE_VERTICAL
} else {
DOUBLE_HORIZONTAL
};
Self::new_with_symbols(orientation, &symbols)
}
#[must_use = "creates the Scrollbar"]
const fn new_with_symbols(orientation: ScrollbarOrientation, symbols: &Set) -> Self {
Self {
orientation,
thumb_symbol: symbols.thumb,
thumb_style: Style::new(),
track_symbol: Some(symbols.track),
track_style: Style::new(),
begin_symbol: Some(symbols.begin),
begin_style: Style::new(),
end_symbol: Some(symbols.end),
end_style: Style::new(),
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
self.orientation = orientation;
let symbols = if self.orientation.is_vertical() {
DOUBLE_VERTICAL
} else {
DOUBLE_HORIZONTAL
};
self.symbols(symbols)
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn orientation_and_symbol(
mut self,
orientation: ScrollbarOrientation,
symbols: Set,
) -> Self {
self.orientation = orientation;
self.symbols(symbols)
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
self.thumb_symbol = thumb_symbol;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
self.thumb_style = thumb_style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
self.track_symbol = track_symbol;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
self.track_style = track_style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
self.begin_symbol = begin_symbol;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
self.begin_style = begin_style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
self.end_symbol = end_symbol;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
self.end_style = end_style.into();
self
}
#[allow(clippy::needless_pass_by_value)] #[must_use = "method moves the value of self and returns the modified value"]
pub const fn symbols(mut self, symbols: Set) -> Self {
self.thumb_symbol = symbols.thumb;
if self.track_symbol.is_some() {
self.track_symbol = Some(symbols.track);
}
if self.begin_symbol.is_some() {
self.begin_symbol = Some(symbols.begin);
}
if self.end_symbol.is_some() {
self.end_symbol = Some(symbols.end);
}
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
let style = style.into();
self.track_style = style;
self.thumb_style = style;
self.begin_style = style;
self.end_style = style;
self
}
}
impl ScrollbarState {
#[must_use = "creates the ScrollbarState"]
pub const fn new(content_length: usize) -> Self {
Self {
content_length,
position: 0,
viewport_content_length: 0,
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn position(mut self, position: usize) -> Self {
self.position = position;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn content_length(mut self, content_length: usize) -> Self {
self.content_length = content_length;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
self.viewport_content_length = viewport_content_length;
self
}
pub fn prev(&mut self) {
self.position = self.position.saturating_sub(1);
}
pub fn next(&mut self) {
self.position = self
.position
.saturating_add(1)
.min(self.content_length.saturating_sub(1));
}
pub fn first(&mut self) {
self.position = 0;
}
pub fn last(&mut self) {
self.position = self.content_length.saturating_sub(1);
}
pub fn scroll(&mut self, direction: ScrollDirection) {
match direction {
ScrollDirection::Forward => {
self.next();
}
ScrollDirection::Backward => {
self.prev();
}
}
}
}
impl<'a> StatefulWidget for Scrollbar<'a> {
type State = ScrollbarState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if state.content_length == 0 || self.track_length_excluding_arrow_heads(area) == 0 {
return;
}
let mut bar = self.bar_symbols(area, state);
let area = self.scollbar_area(area);
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
if let Some(Some((symbol, style))) = bar.next() {
buf.set_string(x, y, symbol, style);
}
}
}
}
}
impl Scrollbar<'_> {
fn bar_symbols(
&self,
area: Rect,
state: &ScrollbarState,
) -> impl Iterator<Item = Option<(&str, Style)>> {
let (track_start_len, thumb_len, track_end_len) = self.part_lengths(area, state);
let begin = self.begin_symbol.map(|s| Some((s, self.begin_style)));
let track = Some(self.track_symbol.map(|s| (s, self.track_style)));
let thumb = Some(Some((self.thumb_symbol, self.thumb_style)));
let end = self.end_symbol.map(|s| Some((s, self.end_style)));
iter::once(begin)
.chain(iter::repeat(track).take(track_start_len))
.chain(iter::repeat(thumb).take(thumb_len))
.chain(iter::repeat(track).take(track_end_len))
.chain(iter::once(end))
.flatten()
}
fn part_lengths(&self, area: Rect, state: &ScrollbarState) -> (usize, usize, usize) {
let track_length = f64::from(self.track_length_excluding_arrow_heads(area));
let viewport_length = self.viewport_length(state, area) as f64;
let max_position = state.content_length.saturating_sub(1) as f64;
let start_position = (state.position as f64).clamp(0.0, max_position);
let max_viewport_position = max_position + viewport_length;
let end_position = start_position + viewport_length;
let thumb_start = start_position * track_length / max_viewport_position;
let thumb_end = end_position * track_length / max_viewport_position;
let thumb_start = thumb_start.round().clamp(0.0, track_length - 1.0) as usize;
let thumb_end = thumb_end.round().clamp(0.0, track_length) as usize;
let thumb_length = thumb_end.saturating_sub(thumb_start).max(1);
let track_end_length = (track_length as usize).saturating_sub(thumb_start + thumb_length);
(thumb_start, thumb_length, track_end_length)
}
fn scollbar_area(&self, area: Rect) -> Rect {
match self.orientation {
ScrollbarOrientation::VerticalLeft => area.columns().next(),
ScrollbarOrientation::VerticalRight => area.columns().last(),
ScrollbarOrientation::HorizontalTop => area.rows().next(),
ScrollbarOrientation::HorizontalBottom => area.rows().last(),
}
.expect("Scrollbar area is empty") }
fn track_length_excluding_arrow_heads(&self, area: Rect) -> u16 {
let start_len = self.begin_symbol.map_or(0, |s| s.width() as u16);
let end_len = self.end_symbol.map_or(0, |s| s.width() as u16);
let arrows_len = start_len.saturating_add(end_len);
if self.orientation.is_vertical() {
area.height.saturating_sub(arrows_len)
} else {
area.width.saturating_sub(arrows_len)
}
}
const fn viewport_length(&self, state: &ScrollbarState, area: Rect) -> usize {
if state.viewport_content_length != 0 {
state.viewport_content_length
} else if self.orientation.is_vertical() {
area.height as usize
} else {
area.width as usize
}
}
}
impl ScrollbarOrientation {
#[must_use = "returns the requested kind of the scrollbar"]
pub const fn is_vertical(&self) -> bool {
matches!(self, Self::VerticalRight | Self::VerticalLeft)
}
#[must_use = "returns the requested kind of the scrollbar"]
pub const fn is_horizontal(&self) -> bool {
matches!(self, Self::HorizontalBottom | Self::HorizontalTop)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use rstest::{fixture, rstest};
use strum::ParseError;
use super::*;
use crate::{text::Text, widgets::Widget};
#[test]
fn scroll_direction_to_string() {
assert_eq!(ScrollDirection::Forward.to_string(), "Forward");
assert_eq!(ScrollDirection::Backward.to_string(), "Backward");
}
#[test]
fn scroll_direction_from_str() {
assert_eq!("Forward".parse(), Ok(ScrollDirection::Forward));
assert_eq!("Backward".parse(), Ok(ScrollDirection::Backward));
assert_eq!(
ScrollDirection::from_str(""),
Err(ParseError::VariantNotFound)
);
}
#[test]
fn scrollbar_orientation_to_string() {
use ScrollbarOrientation::*;
assert_eq!(VerticalRight.to_string(), "VerticalRight");
assert_eq!(VerticalLeft.to_string(), "VerticalLeft");
assert_eq!(HorizontalBottom.to_string(), "HorizontalBottom");
assert_eq!(HorizontalTop.to_string(), "HorizontalTop");
}
#[test]
fn scrollbar_orientation_from_str() {
use ScrollbarOrientation::*;
assert_eq!("VerticalRight".parse(), Ok(VerticalRight));
assert_eq!("VerticalLeft".parse(), Ok(VerticalLeft));
assert_eq!("HorizontalBottom".parse(), Ok(HorizontalBottom));
assert_eq!("HorizontalTop".parse(), Ok(HorizontalTop));
assert_eq!(
ScrollbarOrientation::from_str(""),
Err(ParseError::VariantNotFound)
);
}
#[fixture]
fn scrollbar_no_arrows() -> Scrollbar<'static> {
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(Some("-"))
.thumb_symbol("#")
}
#[rstest]
#[case::area_2_position_0("#-", 0, 2)]
#[case::area_2_position_1("-#", 1, 2)]
fn render_scrollbar_simplest(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
scrollbar_no_arrows: Scrollbar,
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0("#####-----", 0, 10)]
#[case::position_1("-#####----", 1, 10)]
#[case::position_2("-#####----", 2, 10)]
#[case::position_3("--#####---", 3, 10)]
#[case::position_4("--#####---", 4, 10)]
#[case::position_5("---#####--", 5, 10)]
#[case::position_6("---#####--", 6, 10)]
#[case::position_7("----#####-", 7, 10)]
#[case::position_8("----#####-", 8, 10)]
#[case::position_9("-----#####", 9, 10)]
fn render_scrollbar_simple(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
scrollbar_no_arrows: Scrollbar,
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0(" ", 0, 0)]
fn render_scrollbar_nobar(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::fullbar_position_0("##########", 0, 1)]
#[case::almost_fullbar_position_0("#########-", 0, 2)]
#[case::almost_fullbar_position_1("-#########", 1, 2)]
fn render_scrollbar_fullbar(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0("#########-", 0, 2)]
#[case::position_1("-#########", 1, 2)]
fn render_scrollbar_almost_fullbar(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0("█████═════", 0, 10)]
#[case::position_1("═█████════", 1, 10)]
#[case::position_2("═█████════", 2, 10)]
#[case::position_3("══█████═══", 3, 10)]
#[case::position_4("══█████═══", 4, 10)]
#[case::position_5("═══█████══", 5, 10)]
#[case::position_6("═══█████══", 6, 10)]
#[case::position_7("════█████═", 7, 10)]
#[case::position_8("════█████═", 8, 10)]
#[case::position_9("═════█████", 9, 10)]
#[case::position_out_of_bounds("═════█████", 100, 10)]
fn render_scrollbar_without_symbols(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0("█████ ", 0, 10)]
#[case::position_1(" █████ ", 1, 10)]
#[case::position_2(" █████ ", 2, 10)]
#[case::position_3(" █████ ", 3, 10)]
#[case::position_4(" █████ ", 4, 10)]
#[case::position_5(" █████ ", 5, 10)]
#[case::position_6(" █████ ", 6, 10)]
#[case::position_7(" █████ ", 7, 10)]
#[case::position_8(" █████ ", 8, 10)]
#[case::position_9(" █████", 9, 10)]
#[case::position_out_of_bounds(" █████", 100, 10)]
fn render_scrollbar_without_track_symbols(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0("█████-----", 0, 10)]
#[case::position_1("-█████----", 1, 10)]
#[case::position_2("-█████----", 2, 10)]
#[case::position_3("--█████---", 3, 10)]
#[case::position_4("--█████---", 4, 10)]
#[case::position_5("---█████--", 5, 10)]
#[case::position_6("---█████--", 6, 10)]
#[case::position_7("----█████-", 7, 10)]
#[case::position_8("----█████-", 8, 10)]
#[case::position_9("-----█████", 9, 10)]
#[case::position_out_of_bounds("-----█████", 100, 10)]
fn render_scrollbar_without_track_symbols_over_content(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let width = buffer.area.width as usize;
let s = "";
Text::from(format!("{s:-^width$}")).render(buffer.area, &mut buffer);
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0("<####---->", 0, 10)]
#[case::position_1("<#####--->", 1, 10)]
#[case::position_2("<-####--->", 2, 10)]
#[case::position_3("<-####--->", 3, 10)]
#[case::position_4("<--####-->", 4, 10)]
#[case::position_5("<--####-->", 5, 10)]
#[case::position_6("<---####->", 6, 10)]
#[case::position_7("<---####->", 7, 10)]
#[case::position_8("<---#####>", 8, 10)]
#[case::position_9("<----####>", 9, 10)]
#[case::position_one_out_of_bounds("<----####>", 10, 10)]
#[case::position_few_out_of_bounds("<----####>", 15, 10)]
#[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
fn render_scrollbar_with_symbols(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0("█████═════", 0, 10)]
#[case::position_1("═█████════", 1, 10)]
#[case::position_2("═█████════", 2, 10)]
#[case::position_3("══█████═══", 3, 10)]
#[case::position_4("══█████═══", 4, 10)]
#[case::position_5("═══█████══", 5, 10)]
#[case::position_6("═══█████══", 6, 10)]
#[case::position_7("════█████═", 7, 10)]
#[case::position_8("════█████═", 8, 10)]
#[case::position_9("═════█████", 9, 10)]
#[case::position_out_of_bounds("═════█████", 100, 10)]
fn render_scrollbar_horizontal_bottom(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let empty_string = " ".repeat(size as usize);
assert_eq!(buffer, Buffer::with_lines([&empty_string, expected]));
}
#[rstest]
#[case::position_0("█████═════", 0, 10)]
#[case::position_1("═█████════", 1, 10)]
#[case::position_2("═█████════", 2, 10)]
#[case::position_3("══█████═══", 3, 10)]
#[case::position_4("══█████═══", 4, 10)]
#[case::position_5("═══█████══", 5, 10)]
#[case::position_6("═══█████══", 6, 10)]
#[case::position_7("════█████═", 7, 10)]
#[case::position_8("════█████═", 8, 10)]
#[case::position_9("═════█████", 9, 10)]
#[case::position_out_of_bounds("═════█████", 100, 10)]
fn render_scrollbar_horizontal_top(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let empty_string = " ".repeat(size as usize);
assert_eq!(buffer, Buffer::with_lines([expected, &empty_string]));
}
#[rstest]
#[case::position_0("<####---->", 0, 10)]
#[case::position_1("<#####--->", 1, 10)]
#[case::position_2("<-####--->", 2, 10)]
#[case::position_3("<-####--->", 3, 10)]
#[case::position_4("<--####-->", 4, 10)]
#[case::position_5("<--####-->", 5, 10)]
#[case::position_6("<---####->", 6, 10)]
#[case::position_7("<---####->", 7, 10)]
#[case::position_8("<---#####>", 8, 10)]
#[case::position_9("<----####>", 9, 10)]
#[case::position_one_out_of_bounds("<----####>", 10, 10)]
fn render_scrollbar_vertical_left(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
let bar = expected.chars().map(|c| format!("{c} "));
assert_eq!(buffer, Buffer::with_lines(bar));
}
#[rstest]
#[case::position_0("<####---->", 0, 10)]
#[case::position_1("<#####--->", 1, 10)]
#[case::position_2("<-####--->", 2, 10)]
#[case::position_3("<-####--->", 3, 10)]
#[case::position_4("<--####-->", 4, 10)]
#[case::position_5("<--####-->", 5, 10)]
#[case::position_6("<---####->", 6, 10)]
#[case::position_7("<---####->", 7, 10)]
#[case::position_8("<---#####>", 8, 10)]
#[case::position_9("<----####>", 9, 10)]
#[case::position_one_out_of_bounds("<----####>", 10, 10)]
fn render_scrollbar_vertical_rightl(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
let bar = expected.chars().map(|c| format!(" {c}"));
assert_eq!(buffer, Buffer::with_lines(bar));
}
#[rstest]
#[case::position_0("##--------", 0, 10)]
#[case::position_1("-##-------", 1, 10)]
#[case::position_2("--##------", 2, 10)]
#[case::position_3("---##-----", 3, 10)]
#[case::position_4("----#-----", 4, 10)]
#[case::position_5("-----#----", 5, 10)]
#[case::position_6("-----##---", 6, 10)]
#[case::position_7("------##--", 7, 10)]
#[case::position_8("-------##-", 8, 10)]
#[case::position_9("--------##", 9, 10)]
#[case::position_one_out_of_bounds("--------##", 10, 10)]
fn custom_viewport_length(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::new(content_length)
.position(position)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case::position_0("#----", 0, 100)]
#[case::position_10("#----", 10, 100)]
#[case::position_20("-#---", 20, 100)]
#[case::position_30("-#---", 30, 100)]
#[case::position_40("--#--", 40, 100)]
#[case::position_50("--#--", 50, 100)]
#[case::position_60("---#-", 60, 100)]
#[case::position_70("---#-", 70, 100)]
#[case::position_80("----#", 80, 100)]
#[case::position_90("----#", 90, 100)]
#[case::position_one_out_of_bounds("----#", 100, 100)]
fn thumb_visible_on_very_small_track(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::new(content_length)
.position(position)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
}