feat: add scrollbar widget (#228)

Represents a scrollbar widget that renders a track, thumb and arrows
either horizontally or vertically. State is kept in ScrollbarState, and
passed as a parameter to the render function.
This commit is contained in:
Dheepak Krishnamurthy 2023-06-17 15:25:43 -04:00 committed by GitHub
parent 8b7b7881f5
commit 130bdf8337
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 1151 additions and 0 deletions

View file

@ -127,6 +127,11 @@ name = "popup"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "scrollbar"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "sparkline"
required-features = ["crossterm"]

273
examples/scrollbar.rs Normal file
View file

@ -0,0 +1,273 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Margin},
style::{Color, Modifier, Style},
text::{Line, Masked, Span},
widgets::{
scrollbar, Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
},
Frame, Terminal,
};
#[derive(Default)]
struct App {
pub vertical_scroll_state: ScrollbarState,
pub horizontal_scroll_state: ScrollbarState,
pub vertical_scroll: usize,
pub horizontal_scroll: usize,
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::default();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state = app
.vertical_scroll_state
.position(app.vertical_scroll as u16);
}
KeyCode::Char('k') => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state = app
.vertical_scroll_state
.position(app.vertical_scroll as u16);
}
KeyCode::Char('h') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.position(app.horizontal_scroll as u16);
}
KeyCode::Char('l') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.position(app.horizontal_scroll as u16);
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::default().style(Style::default().fg(Color::Black));
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(size);
let text = vec![
Line::from("This is a line "),
Line::from(Span::styled(
"This is a line ",
Style::default().fg(Color::Red),
)),
Line::from(Span::styled(
"This is a line",
Style::default().bg(Color::DarkGray),
)),
Line::from(Span::styled(
"This is a longer line",
Style::default().add_modifier(Modifier::CROSSED_OUT),
)),
Line::from(Span::styled(&long_line, Style::default())),
Line::from(Span::styled("This is a line", Style::default())),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
]),
Line::from("This is a line "),
Line::from(Span::styled(
"This is a line ",
Style::default().fg(Color::Red),
)),
Line::from(Span::styled(
"This is a line",
Style::default().bg(Color::DarkGray),
)),
Line::from(Span::styled(
"This is a longer line",
Style::default().add_modifier(Modifier::CROSSED_OUT),
)),
Line::from(Span::styled(&long_line, Style::default())),
Line::from(Span::styled("This is a line", Style::default())),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
]),
];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len() as u16);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.content_length(long_line.len() as u16);
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let title = Block::default()
.title("Use h j k l to scroll ◄ ▲ ▼ ►")
.title_alignment(Alignment::Center);
f.render_widget(title, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Vertical scrollbar with arrows"))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[1]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
chunks[1],
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block(
"Vertical scrollbar without arrows and mirrored",
))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.end_symbol(None),
chunks[2].inner(&Margin {
vertical: 1,
horizontal: 0,
}),
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block(
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[3]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("🬋")
.end_symbol(None),
chunks[3].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block(
"Horizontal scrollbar without arrows & custom thumb and track symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[4]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(""),
chunks[4].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
}

View file

@ -27,6 +27,7 @@ mod gauge;
mod list;
mod paragraph;
mod reflow;
pub mod scrollbar;
mod sparkline;
mod table;
mod tabs;
@ -43,6 +44,7 @@ pub use self::{
gauge::{Gauge, LineGauge},
list::{List, ListItem, ListState},
paragraph::{Paragraph, Wrap},
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
sparkline::{RenderDirection, Sparkline},
table::{Cell, Row, Table, TableState},
tabs::Tabs,

871
src/widgets/scrollbar.rs Normal file
View file

@ -0,0 +1,871 @@
use super::StatefulWidget;
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols::{block::FULL, line},
};
/// Scrollbar Set
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
#[derive(Debug, Clone)]
pub struct Set {
pub track: &'static str,
pub thumb: &'static str,
pub begin: &'static str,
pub end: &'static str,
}
pub const DOUBLE_VERTICAL: Set = Set {
track: line::DOUBLE_VERTICAL,
thumb: FULL,
begin: "",
end: "",
};
pub const DOUBLE_HORIZONTAL: Set = Set {
track: line::DOUBLE_HORIZONTAL,
thumb: FULL,
begin: "",
end: "",
};
pub const VERTICAL: Set = Set {
track: line::VERTICAL,
thumb: FULL,
begin: "",
end: "",
};
pub const HORIZONTAL: Set = Set {
track: line::HORIZONTAL,
thumb: FULL,
begin: "",
end: "",
};
/// An enum representing the direction of scrolling in a Scrollbar widget.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
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.
///
/// For example, in the following list, assume there are 4 bullet points:
///
/// - the `position` is 0
/// - the `content_length` is 4
/// - the `viewport_content_length` is 2
///
/// ┌───────────────┐
/// │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(Clone, Copy, Debug, Default)]
pub struct ScrollbarState {
// The current position within the scrollable content.
position: u16,
// The total length of the scrollable content.
content_length: u16,
// The length of content in current viewport.
viewport_content_length: u16,
}
impl ScrollbarState {
/// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
pub fn position(mut self, position: u16) -> 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: u16) -> 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: u16) -> 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(Default, Debug, Clone)]
pub enum ScrollbarOrientation {
#[default]
VerticalRight,
VerticalLeft,
HorizontalBottom,
HorizontalTop,
}
/// Scrollbar widget for tui-rs library.
///
/// This widget can be used to display a scrollbar in a terminal user interface.
/// The following components of the scrollbar are customizable in symbol and style.
///
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
#[derive(Debug, Clone)]
pub struct Scrollbar<'a> {
orientation: ScrollbarOrientation,
thumb_style: Style,
thumb_symbol: &'a str,
track_style: Style,
track_symbol: &'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: 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: &'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`].
///
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
///
/// Only sets begin_symbol and end_symbol if they already contain a value.
/// If begin_symbol and/or end_symbol were set to `None` explicitly, this function will respect
/// that choice.
pub fn symbols(mut self, symbol: Set) -> Self {
self.track_symbol = symbol.track;
self.thumb_symbol = symbol.thumb;
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`].
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ 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: u16) -> 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
} 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 {
(self.track_style, self.track_symbol)
};
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 super::*;
use crate::assert_buffer_eq;
#[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()));
}
}
}