use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols::{block::FULL, line},
widgets::Widget,
};
#[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: "→",
};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ScrollDirection {
#[default]
Forward,
Backward,
}
#[derive(Default, Debug, Clone, Copy)]
pub enum ScrollbarOrientation {
#[default]
VerticalRight,
VerticalLeft,
HorizontalBottom,
HorizontalTop,
}
#[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,
position: u16,
offset: u16,
content_length: u16,
viewport_content_length: u16,
}
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(),
position: Default::default(),
offset: Default::default(),
content_length: Default::default(),
viewport_content_length: Default::default(),
}
}
}
impl<'a> Scrollbar<'a> {
pub fn new(orientation: ScrollbarOrientation) -> Self {
Self::default().orientation(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)
}
pub fn show_orientation(&self) -> ScrollbarOrientation {
self.orientation
}
pub fn orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
self.orientation = orientation;
self.symbols(set)
}
pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
self.thumb_symbol = thumb_symbol;
self
}
pub fn thumb_style(mut self, thumb_style: Style) -> Self {
self.thumb_style = thumb_style;
self
}
pub fn track_symbol(mut self, track_symbol: &'a str) -> Self {
self.track_symbol = track_symbol;
self
}
pub fn track_style(mut self, track_style: Style) -> Self {
self.track_style = track_style;
self
}
pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
self.begin_symbol = begin_symbol;
self
}
pub fn begin_style(mut self, begin_style: Style) -> Self {
self.begin_style = begin_style;
self
}
pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
self.end_symbol = end_symbol;
self
}
pub fn end_style(mut self, end_style: Style) -> Self {
self.end_style = end_style;
self
}
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
}
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 {
let area = if self.begin_symbol.is_some() {
if self.is_vertical() {
Rect::new(
area.x,
area.y + 1,
area.width,
area.height.saturating_sub(1),
)
} else {
Rect::new(
area.x + 1,
area.y,
area.width.saturating_sub(1),
area.height,
)
}
} else {
area
};
if self.end_symbol.is_some() {
if self.is_vertical() {
Rect::new(area.x, area.y, area.width, area.height.saturating_sub(1))
} else {
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()),
}
}
fn get_thumb_start_end(&self, track_start_end: (u16, u16)) -> (u16, u16) {
let (track_start, track_end) = track_start_end;
let viewport_content_length = if self.viewport_content_length == 0 {
track_end - track_start
} else {
self.viewport_content_length
};
let scroll_position_ratio = if self.offset == 0 {
self.position as f64 / self.content_length as f64
} else {
let gap = f64::from(self.content_length.saturating_sub(viewport_content_length));
if gap != 0.0 {
f64::from(self.offset) / gap
} else {
0.0
}
}
.min(1.0);
let thumb_size = (((viewport_content_length as f64 / self.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)
}
pub fn offset(&mut self, offset: u16) {
self.offset = offset;
}
pub fn position(&mut self, position: u16) {
self.position = position;
}
pub fn content_length(&mut self, content_length: u16) {
self.content_length = content_length;
}
pub fn viewport_content_length(&mut self, viewport_content_length: u16) {
self.viewport_content_length = viewport_content_length;
}
pub fn prev(&mut self) {
self.position = self.position.saturating_sub(1);
}
pub fn next(&mut self) {
self.position = self
.position
.saturating_add(1)
.clamp(0, self.content_length.saturating_sub(1))
}
pub fn first(&mut self) {
self.position = 0;
}
pub fn last(&mut self) {
self.position = self.content_length.saturating_sub(1)
}
pub fn scroll(&mut self, direction: ScrollDirection) {
match direction {
ScrollDirection::Forward => {
self.next();
}
ScrollDirection::Backward => {
self.prev();
}
}
}
}
impl<'a> Widget for Scrollbar<'a> {
fn render(&mut self, area: Rect, buf: &mut Buffer) {
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, self.content_length) {
return;
}
let (thumb_start, thumb_end) = self.get_thumb_start_end((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 scrollbar = Scrollbar::default();
scrollbar.position(0);
scrollbar.content_length(1);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default();
scrollbar.position(0);
scrollbar.content_length(1);
scrollbar.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 0));
let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
scrollbar.position(0);
scrollbar.content_length(1);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default();
scrollbar.position(0);
scrollbar.content_length(1);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
scrollbar.position(0);
scrollbar.content_length(1);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
scrollbar.position(0);
scrollbar.content_length(1);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default();
scrollbar.position(0);
scrollbar.content_length(1);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default();
scrollbar.position(0);
scrollbar.content_length(0);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
scrollbar.position(0);
scrollbar.content_length(2);
scrollbar.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" █", " █"]));
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
scrollbar.position(0);
scrollbar.content_length(8);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
scrollbar.position(i);
scrollbar.content_length(16);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.orientation(ScrollbarOrientation::HorizontalBottom);
scrollbar.position(i);
scrollbar.content_length(16);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.orientation(ScrollbarOrientation::HorizontalBottom);
scrollbar.position(0);
scrollbar.content_length(1600);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.orientation(ScrollbarOrientation::HorizontalBottom);
scrollbar.position(800);
scrollbar.content_length(1600);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
scrollbar.position(i);
scrollbar.content_length(8);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.symbols(VERTICAL)
.begin_symbol(None)
.end_symbol(None);
scrollbar.position(i);
scrollbar.content_length(8);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None);
scrollbar.position(i);
scrollbar.content_length(8);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.symbols(HORIZONTAL)
.begin_symbol(None)
.end_symbol(None);
scrollbar.position(i);
scrollbar.content_length(8);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(Some(DOUBLE_HORIZONTAL.end));
scrollbar.position(i);
scrollbar.content_length(16);
scrollbar.viewport_content_length(4);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(Some(DOUBLE_HORIZONTAL.end));
scrollbar.position(i);
scrollbar.content_length(16);
scrollbar.viewport_content_length(1);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(Some(DOUBLE_HORIZONTAL.end));
scrollbar.position(i);
scrollbar.content_length(16);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalTop)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(Some(DOUBLE_HORIZONTAL.end));
scrollbar.position(i);
scrollbar.content_length(16);
scrollbar.render(buffer.area, &mut buffer);
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 scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
.end_symbol(None);
scrollbar.position(i);
scrollbar.content_length(16);
scrollbar.render(buffer.area, &mut buffer);
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()));
}
}
}