This change simplifys UI code that uses the Frame type. E.g.:
```rust
fn draw<B: Backend>(frame: &mut Frame<B>) {
// ...
}
```
Frame was generic over Backend because it stored a reference to the
terminal in the field. Instead it now directly stores the viewport area
and current buffer. These are provided at creation time and are valid
for the duration of the frame.
BREAKING CHANGE: Frame is no longer generic over Backend. Code that
accepted `Frame<Backend>` will now need to accept `Frame`. To migrate
existing code, remove any generic parameters from code that uses an
instance of a Frame. E.g. the above code becomes:
```rust
fn draw(frame: &mut Frame) {
// ...
}
```
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(frame: &mut Frame, 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()));
|
|
}
|
|
}
|
|
}
|