Files
ratatui/src/widgets/scrollbar.rs
Josh McKinney 1414fbcc05 docs: import prelude::* in doc examples (#490)
This commit adds `prelude::*` all doc examples and widget::* to those
that need it. This is done to highlight the use of the prelude and
simplify the examples.

- Examples in Type and module level comments show all imports and use
  `prelude::*` and `widget::*` where possible.
- Function level comments hide imports unless there are imports other
  than `prelude::*` and `widget::*`.
2023-09-11 18:01:57 -07:00

1005 lines
37 KiB
Rust

use strum::{Display, EnumString};
use super::StatefulWidget;
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
};
/// An enum representing the direction of scrolling in a Scrollbar widget.
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ScrollDirection {
/// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
#[default]
Forward,
/// Backward scroll direction, usually corresponds to scrolling upwards or leftwards.
Backward,
}
/// A struct representing the state of a Scrollbar widget.
///
/// # Important
///
/// It's essential to set the `content_length` field when using this struct. This field
/// represents the total length of the scrollable content. The default value is zero
/// which will result in the Scrollbar not rendering.
///
/// For example, in the following list, assume there are 4 bullet points:
///
/// - the `content_length` is 4
/// - the `position` is 0
/// - the `viewport_content_length` is 2
///
/// ```text
/// ┌───────────────┐
/// │1. this is a █
/// │ single item █
/// │2. this is a ║
/// │ second item ║
/// └───────────────┘
/// ```
///
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
/// default of 0 and it'll use the track size as a `viewport_content_length`.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ScrollbarState {
// The total length of the scrollable content.
content_length: usize,
// The current position within the scrollable content.
position: usize,
// The length of content in current viewport.
viewport_content_length: usize,
}
impl ScrollbarState {
/// Constructs a new ScrollbarState with the specified content length.
pub fn new(content_length: usize) -> Self {
Self {
content_length,
..Default::default()
}
}
/// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
pub fn position(mut self, position: usize) -> Self {
self.position = position;
self
}
/// Sets the length of the scrollable content and returns the modified ScrollbarState.
pub fn content_length(mut self, content_length: usize) -> Self {
self.content_length = content_length;
self
}
/// Sets the length of the viewport content and returns the modified ScrollbarState.
pub fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
self.viewport_content_length = viewport_content_length;
self
}
/// Decrements the scroll position by one, ensuring it doesn't go below zero.
pub fn prev(&mut self) {
self.position = self.position.saturating_sub(1);
}
/// Increments the scroll position by one, ensuring it doesn't exceed the length of the content.
pub fn next(&mut self) {
self.position = self
.position
.saturating_add(1)
.clamp(0, self.content_length.saturating_sub(1))
}
/// Sets the scroll position to the start of the scrollable content.
pub fn first(&mut self) {
self.position = 0;
}
/// Sets the scroll position to the end of the scrollable content.
pub fn last(&mut self) {
self.position = self.content_length.saturating_sub(1)
}
/// Changes the scroll position based on the provided ScrollDirection.
pub fn scroll(&mut self, direction: ScrollDirection) {
match direction {
ScrollDirection::Forward => {
self.next();
}
ScrollDirection::Backward => {
self.prev();
}
}
}
}
/// Scrollbar Orientation
#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
pub enum ScrollbarOrientation {
#[default]
VerticalRight,
VerticalLeft,
HorizontalBottom,
HorizontalTop,
}
/// A widget to display a scrollbar
///
/// The following components of the scrollbar are customizable in symbol and style.
///
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
/// ```
///
/// # Examples
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// # fn render_paragraph_with_scrollbar<B: Backend>(frame: &mut Frame<B>, area: Rect) {
///
/// let vertical_scroll = 0; // from app state
///
/// let items = vec![Line::from("Item 1"), Line::from("Item 2"), Line::from("Item 3")];
/// let paragraph = Paragraph::new(items.clone())
/// .scroll((vertical_scroll as u16, 0))
/// .block(Block::new().borders(Borders::RIGHT)); // to show a background for the scrollbar
///
/// let scrollbar = Scrollbar::default()
/// .orientation(ScrollbarOrientation::VerticalRight)
/// .begin_symbol(Some("↑"))
/// .end_symbol(Some("↓"));
/// let mut scrollbar_state = ScrollbarState::new(items.iter().len()).position(vertical_scroll);
///
/// let area = frame.size();
/// frame.render_widget(paragraph, area);
/// frame.render_stateful_widget(scrollbar,
/// area.inner(&Margin {
/// vertical: 1,
/// horizontal: 0,
/// }), // using a inner vertical margin of 1 unit makes the scrollbar inside the block
/// &mut scrollbar_state);
/// # }
/// ```
#[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,
}
impl<'a> Default for Scrollbar<'a> {
fn default() -> Self {
Self {
orientation: ScrollbarOrientation::default(),
thumb_symbol: DOUBLE_VERTICAL.thumb,
thumb_style: Style::default(),
track_symbol: Some(DOUBLE_VERTICAL.track),
track_style: Style::default(),
begin_symbol: Some(DOUBLE_VERTICAL.begin),
begin_style: Style::default(),
end_symbol: Some(DOUBLE_VERTICAL.end),
end_style: Style::default(),
}
}
}
impl<'a> Scrollbar<'a> {
pub fn new(orientation: ScrollbarOrientation) -> Self {
Self::default().orientation(orientation)
}
/// Sets the orientation of the scrollbar.
/// Resets the symbols to [`DOUBLE_VERTICAL`] or [`DOUBLE_HORIZONTAL`] based on orientation
pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
self.orientation = orientation;
let set = if self.is_vertical() {
DOUBLE_VERTICAL
} else {
DOUBLE_HORIZONTAL
};
self.symbols(set)
}
/// Sets the orientation and symbols for the scrollbar from a [`Set`].
pub fn orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
self.orientation = orientation;
self.symbols(set)
}
/// Sets the symbol that represents the thumb of the scrollbar.
pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
self.thumb_symbol = thumb_symbol;
self
}
/// Sets the style that represents the thumb of the scrollbar.
pub fn thumb_style(mut self, thumb_style: Style) -> Self {
self.thumb_style = thumb_style;
self
}
/// Sets the symbol that represents the track of the scrollbar.
pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
self.track_symbol = track_symbol;
self
}
/// Sets the style that is used for the track of the scrollbar.
pub fn track_style(mut self, track_style: Style) -> Self {
self.track_style = track_style;
self
}
/// Sets the symbol that represents the beginning of the scrollbar.
pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
self.begin_symbol = begin_symbol;
self
}
/// Sets the style that is used for the beginning of the scrollbar.
pub fn begin_style(mut self, begin_style: Style) -> Self {
self.begin_style = begin_style;
self
}
/// Sets the symbol that represents the end of the scrollbar.
pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
self.end_symbol = end_symbol;
self
}
/// Sets the style that is used for the end of the scrollbar.
pub fn end_style(mut self, end_style: Style) -> Self {
self.end_style = end_style;
self
}
/// Sets the symbols used for the various parts of the scrollbar from a [`Set`].
///
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
/// ```
///
/// Only sets begin_symbol, end_symbol and track_symbol if they already contain a value.
/// If they were set to `None` explicitly, this function will respect that choice.
pub fn symbols(mut self, symbol: Set) -> Self {
self.thumb_symbol = symbol.thumb;
if self.track_symbol.is_some() {
self.track_symbol = Some(symbol.track);
}
if self.begin_symbol.is_some() {
self.begin_symbol = Some(symbol.begin);
}
if self.end_symbol.is_some() {
self.end_symbol = Some(symbol.end);
}
self
}
/// Sets the style used for the various parts of the scrollbar from a [`Style`].
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
/// ```
pub fn style(mut self, style: Style) -> Self {
self.track_style = style;
self.thumb_style = style;
self.begin_style = style;
self.end_style = style;
self
}
fn is_vertical(&self) -> bool {
match self.orientation {
ScrollbarOrientation::VerticalRight | ScrollbarOrientation::VerticalLeft => true,
ScrollbarOrientation::HorizontalBottom | ScrollbarOrientation::HorizontalTop => false,
}
}
fn get_track_area(&self, area: Rect) -> Rect {
// Decrease track area if a begin arrow is present
let area = if self.begin_symbol.is_some() {
if self.is_vertical() {
// For vertical scrollbar, reduce the height by one
Rect::new(
area.x,
area.y + 1,
area.width,
area.height.saturating_sub(1),
)
} else {
// For horizontal scrollbar, reduce the width by one
Rect::new(
area.x + 1,
area.y,
area.width.saturating_sub(1),
area.height,
)
}
} else {
area
};
// Further decrease scrollbar area if an end arrow is present
if self.end_symbol.is_some() {
if self.is_vertical() {
// For vertical scrollbar, reduce the height by one
Rect::new(area.x, area.y, area.width, area.height.saturating_sub(1))
} else {
// For horizontal scrollbar, reduce the width by one
Rect::new(area.x, area.y, area.width.saturating_sub(1), area.height)
}
} else {
area
}
}
fn should_not_render(&self, track_start: u16, track_end: u16, content_length: usize) -> bool {
if track_end - track_start == 0 || content_length == 0 {
return true;
}
false
}
fn get_track_start_end(&self, area: Rect) -> (u16, u16, u16) {
match self.orientation {
ScrollbarOrientation::VerticalRight => {
(area.top(), area.bottom(), area.right().saturating_sub(1))
}
ScrollbarOrientation::VerticalLeft => (area.top(), area.bottom(), area.left()),
ScrollbarOrientation::HorizontalBottom => {
(area.left(), area.right(), area.bottom().saturating_sub(1))
}
ScrollbarOrientation::HorizontalTop => (area.left(), area.right(), area.top()),
}
}
/// Calculate the starting and ending position of a scrollbar thumb.
///
/// The scrollbar thumb's position and size are determined based on the current state of the
/// scrollbar, and the dimensions of the scrollbar track.
///
/// This function returns a tuple `(thumb_start, thumb_end)` where `thumb_start` is the position
/// at which the scrollbar thumb begins, and `thumb_end` is the position at which the
/// scrollbar thumb ends.
///
/// The size of the thumb (i.e., `thumb_end - thumb_start`) is proportional to the ratio of the
/// viewport content length to the total content length.
///
/// The position of the thumb (i.e., `thumb_start`) is proportional to the ratio of the current
/// scroll position to the total content length.
fn get_thumb_start_end(
&self,
state: &ScrollbarState,
track_start_end: (u16, u16),
) -> (u16, u16) {
// let (track_start, track_end) = track_start_end;
// let track_size = track_end - track_start;
// let thumb_size =
// ((state.viewport_content_length / state.content_length) * track_size).max(1);
// let thumb_start = (state.position / state.content_length) *
// state.viewport_content_length;
// let thumb_end = thumb_size + thumb_start;
// (thumb_start, thumb_end)
let (track_start, track_end) = track_start_end;
let viewport_content_length = if state.viewport_content_length == 0 {
(track_end - track_start) as usize
} else {
state.viewport_content_length
};
let scroll_position_ratio = (state.position as f64 / state.content_length as f64).min(1.0);
let thumb_size = (((viewport_content_length as f64 / state.content_length as f64)
* (track_end - track_start) as f64)
.round() as u16)
.max(1);
let track_size = (track_end - track_start).saturating_sub(thumb_size);
let thumb_start = track_start + (scroll_position_ratio * track_size as f64).round() as u16;
let thumb_end = thumb_start + thumb_size;
(thumb_start, thumb_end)
}
}
impl<'a> StatefulWidget for Scrollbar<'a> {
type State = ScrollbarState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
//
// For ScrollbarOrientation::VerticalRight
//
// ┌───────── track_axis (x)
// v
// ┌───────────────┐
// │ ║<──────── track_start (y1)
// │ █
// │ █
// │ ║
// │ ║<──────── track_end (y2)
// └───────────────┘
//
// For ScrollbarOrientation::HorizontalBottom
//
// ┌───────────────┐
// │ │
// │ │
// │ │
// └═══███═════════┘<──────── track_axis (y)
// ^ ^
// │ └────────── track_end (x2)
// │
// └──────────────────────── track_start (x1)
//
// Find track_start, track_end, and track_axis
let area = self.get_track_area(area);
let (track_start, track_end, track_axis) = self.get_track_start_end(area);
if self.should_not_render(track_start, track_end, state.content_length) {
return;
}
let (thumb_start, thumb_end) = self.get_thumb_start_end(state, (track_start, track_end));
for i in track_start..track_end {
let (style, symbol) = if i >= thumb_start && i < thumb_end {
(self.thumb_style, self.thumb_symbol)
} else if let Some(track_symbol) = self.track_symbol {
(self.track_style, track_symbol)
} else {
continue;
};
if self.is_vertical() {
buf.set_string(track_axis, i, symbol, style);
} else {
buf.set_string(i, track_axis, symbol, style);
}
}
if let Some(s) = self.begin_symbol {
if self.is_vertical() {
buf.set_string(track_axis, track_start - 1, s, self.begin_style);
} else {
buf.set_string(track_start - 1, track_axis, s, self.begin_style);
}
};
if let Some(s) = self.end_symbol {
if self.is_vertical() {
buf.set_string(track_axis, track_end, s, self.end_style);
} else {
buf.set_string(track_end, track_axis, s, self.end_style);
}
}
}
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
use crate::{
assert_buffer_eq,
symbols::scrollbar::{HORIZONTAL, VERTICAL},
};
#[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::<ScrollDirection>(),
Ok(ScrollDirection::Forward)
);
assert_eq!(
"Backward".parse::<ScrollDirection>(),
Ok(ScrollDirection::Backward)
);
assert_eq!(
"".parse::<ScrollDirection>(),
Err(ParseError::VariantNotFound)
);
}
#[test]
fn scrollbar_orientation_to_string() {
assert_eq!(
ScrollbarOrientation::VerticalRight.to_string(),
"VerticalRight"
);
assert_eq!(
ScrollbarOrientation::VerticalLeft.to_string(),
"VerticalLeft"
);
assert_eq!(
ScrollbarOrientation::HorizontalBottom.to_string(),
"HorizontalBottom"
);
assert_eq!(
ScrollbarOrientation::HorizontalTop.to_string(),
"HorizontalTop"
);
}
#[test]
fn scrollbar_orientation_from_str() {
assert_eq!(
"VerticalRight".parse::<ScrollbarOrientation>(),
Ok(ScrollbarOrientation::VerticalRight)
);
assert_eq!(
"VerticalLeft".parse::<ScrollbarOrientation>(),
Ok(ScrollbarOrientation::VerticalLeft)
);
assert_eq!(
"HorizontalBottom".parse::<ScrollbarOrientation>(),
Ok(ScrollbarOrientation::HorizontalBottom)
);
assert_eq!(
"HorizontalTop".parse::<ScrollbarOrientation>(),
Ok(ScrollbarOrientation::HorizontalTop)
);
assert_eq!(
"".parse::<ScrollbarOrientation>(),
Err(ParseError::VariantNotFound)
);
}
#[test]
fn test_renders_empty_with_content_length_is_zero() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
let mut state = ScrollbarState::default().position(0);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![" ", " ", " ", " ", " ", " ", " ", " "])
);
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
let mut state = ScrollbarState::new(8).position(0);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["", "", "", "", "", "", "", ""])
);
}
#[test]
fn test_no_render_when_area_zero() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
let mut state = ScrollbarState::default().position(0).content_length(1);
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
}
#[test]
fn test_no_render_when_height_zero_with_without_arrows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 0));
let mut state = ScrollbarState::default().position(0).content_length(1);
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 0));
let mut state = ScrollbarState::default().position(0).content_length(1);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
}
#[test]
fn test_no_render_when_height_too_small_for_arrows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
let mut state = ScrollbarState::default().position(0).content_length(1);
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", " "]));
}
#[test]
fn test_renders_all_thumbs_at_minimum_height_without_arrows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
let mut state = ScrollbarState::default().position(0).content_length(1);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["", ""]));
}
#[test]
fn test_renders_all_thumbs_at_minimum_height_and_minimum_width_without_arrows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 2));
let mut state = ScrollbarState::default().position(0).content_length(1);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["", ""]));
}
#[test]
fn test_renders_two_arrows_one_thumb_at_minimum_height_with_arrows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 3));
let mut state = ScrollbarState::default().position(0).content_length(1);
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["", "", ""]));
}
#[test]
fn test_no_render_when_content_length_zero() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 2));
let mut state = ScrollbarState::default().position(0).content_length(0);
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", " "]));
}
#[test]
fn test_renders_all_thumbs_when_height_equals_content_length() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 2));
let mut state = ScrollbarState::default().position(0).content_length(2);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["", ""]));
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
let mut state = ScrollbarState::default().position(0).content_length(8);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["", "", "", "", "", "", "", ""])
);
}
#[test]
fn test_renders_single_vertical_thumb_when_content_length_square_of_height() {
for i in 0..=17 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
let mut state = ScrollbarState::default().position(i).content_length(16);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 2 {
vec!["", "", "", ""]
} else if i <= 7 {
vec!["", "", "", ""]
} else if i <= 13 {
vec!["", "", "", ""]
} else {
vec!["", "", "", ""]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_renders_single_horizontal_thumb_when_content_length_square_of_width() {
for i in 0..=17 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
let mut state = ScrollbarState::default().position(i).content_length(16);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.orientation(ScrollbarOrientation::HorizontalBottom)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 2 {
vec![" ", "█═══"]
} else if i <= 7 {
vec![" ", "═█══"]
} else if i <= 13 {
vec![" ", "══█═"]
} else {
vec![" ", "═══█"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_renders_one_thumb_for_large_content_relative_to_height() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
let mut state = ScrollbarState::default().position(0).content_length(1600);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.orientation(ScrollbarOrientation::HorizontalBottom)
.render(buffer.area, &mut buffer, &mut state);
let expected = vec![" ", "█═══"];
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
let mut state = ScrollbarState::default().position(800).content_length(1600);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.orientation(ScrollbarOrientation::HorizontalBottom)
.render(buffer.area, &mut buffer, &mut state);
let expected = vec![" ", "══█═"];
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
#[test]
fn test_renders_two_thumb_default_symbols_for_content_double_height() {
for i in 0..=7 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
let mut state = ScrollbarState::default().position(i).content_length(8);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec!["", "", "", ""]
} else if i <= 5 {
vec!["", "", "", ""]
} else {
vec!["", "", "", ""]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_renders_two_thumb_custom_symbols_for_content_double_height() {
for i in 0..=7 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
let mut state = ScrollbarState::default().position(i).content_length(8);
Scrollbar::default()
.symbols(VERTICAL)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec!["", "", "", ""]
} else if i <= 5 {
vec!["", "", "", ""]
} else {
vec!["", "", "", ""]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_renders_two_thumb_default_symbols_for_content_double_width() {
for i in 0..=7 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
let mut state = ScrollbarState::default().position(i).content_length(8);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec![" ", "██══"]
} else if i <= 5 {
vec![" ", "═██═"]
} else {
vec![" ", "══██"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_renders_two_thumb_custom_symbols_for_content_double_width() {
for i in 0..=7 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
let mut state = ScrollbarState::default().position(i).content_length(8);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.symbols(HORIZONTAL)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec![" ", "██──"]
} else if i <= 5 {
vec![" ", "─██─"]
} else {
vec![" ", "──██"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_rendering_viewport_content_length() {
for i in 0..=16 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
let mut state = ScrollbarState::default()
.position(i)
.content_length(16)
.viewport_content_length(4);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(Some(DOUBLE_HORIZONTAL.end))
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec![" ", "◄██════►"]
} else if i <= 5 {
vec![" ", "◄═██═══►"]
} else if i <= 9 {
vec![" ", "◄══██══►"]
} else if i <= 13 {
vec![" ", "◄═══██═►"]
} else {
vec![" ", "◄════██►"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
for i in 0..=16 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
let mut state = ScrollbarState::default()
.position(i)
.content_length(16)
.viewport_content_length(1);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(Some(DOUBLE_HORIZONTAL.end))
.render(buffer.area, &mut buffer, &mut state);
dbg!(i);
let expected = if i <= 1 {
vec![" ", "◄█═════►"]
} else if i <= 4 {
vec![" ", "◄═█════►"]
} else if i <= 7 {
vec![" ", "◄══█═══►"]
} else if i <= 11 {
vec![" ", "◄═══█══►"]
} else if i <= 14 {
vec![" ", "◄════█═►"]
} else {
vec![" ", "◄═════█►"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_rendering_begin_end_arrows_horizontal_bottom() {
for i in 0..=16 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
let mut state = ScrollbarState::default().position(i).content_length(16);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(Some(DOUBLE_HORIZONTAL.end))
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec![" ", "◄██════►"]
} else if i <= 5 {
vec![" ", "◄═██═══►"]
} else if i <= 9 {
vec![" ", "◄══██══►"]
} else if i <= 13 {
vec![" ", "◄═══██═►"]
} else {
vec![" ", "◄════██►"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_rendering_begin_end_arrows_horizontal_top() {
for i in 0..=16 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
let mut state = ScrollbarState::default().position(i).content_length(16);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalTop)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(Some(DOUBLE_HORIZONTAL.end))
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec!["◄██════►", " "]
} else if i <= 5 {
vec!["◄═██═══►", " "]
} else if i <= 9 {
vec!["◄══██══►", " "]
} else if i <= 13 {
vec!["◄═══██═►", " "]
} else {
vec!["◄════██►", " "]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_rendering_only_begin_arrow_horizontal_bottom() {
for i in 0..=16 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
let mut state = ScrollbarState::default().position(i).content_length(16);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec![" ", "◄███════"]
} else if i <= 5 {
vec![" ", "◄═███═══"]
} else if i <= 9 {
vec![" ", "◄══███══"]
} else if i <= 13 {
vec![" ", "◄═══███═"]
} else {
vec![" ", "◄════███"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_rendering_without_track_horizontal_bottom() {
for i in 0..=16 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
let mut state = ScrollbarState::default().position(i).content_length(16);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec![" ", "◄██ ►"]
} else if i <= 5 {
vec![" ", "◄ ██ ►"]
} else if i <= 9 {
vec![" ", "◄ ██ ►"]
} else if i <= 13 {
vec![" ", "◄ ██ ►"]
} else {
vec![" ", "◄ ██►"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
}