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::*`.
1005 lines
37 KiB
Rust
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()));
|
|
}
|
|
}
|
|
}
|