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::(), Ok(ScrollDirection::Forward) ); assert_eq!( "Backward".parse::(), Ok(ScrollDirection::Backward) ); assert_eq!( "".parse::(), 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::(), Ok(ScrollbarOrientation::VerticalRight) ); assert_eq!( "VerticalLeft".parse::(), Ok(ScrollbarOrientation::VerticalLeft) ); assert_eq!( "HorizontalBottom".parse::(), Ok(ScrollbarOrientation::HorizontalBottom) ); assert_eq!( "HorizontalTop".parse::(), Ok(ScrollbarOrientation::HorizontalTop) ); assert_eq!( "".parse::(), 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())); } } }