use ratatui_core::layout::Rect;
use ratatui_core::style::{Color, Style};
use crate::glyphs::GlyphSet;
mod interaction;
mod render;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollBarOrientation {
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackClickBehavior {
Page,
JumpToClick,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ScrollBarArrows {
#[default]
None,
Start,
End,
Both,
}
impl ScrollBarArrows {
const fn has_start(self) -> bool {
matches!(self, Self::Start | Self::Both)
}
const fn has_end(self) -> bool {
matches!(self, Self::End | Self::Both)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ArrowHit {
Start,
End,
}
#[derive(Debug, Clone, Copy)]
struct ArrowLayout {
track_area: Rect,
start: Option<(u16, u16)>,
end: Option<(u16, u16)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScrollBar {
orientation: ScrollBarOrientation,
content_len: usize,
viewport_len: usize,
offset: usize,
track_style: Style,
thumb_style: Style,
arrow_style: Option<Style>,
glyph_set: GlyphSet,
arrows: ScrollBarArrows,
track_click_behavior: TrackClickBehavior,
scroll_step: usize,
}
impl ScrollBar {
pub fn new(orientation: ScrollBarOrientation, lengths: crate::ScrollLengths) -> Self {
Self {
orientation,
content_len: lengths.content_len,
viewport_len: lengths.viewport_len,
offset: 0,
track_style: Style::new().bg(Color::DarkGray),
thumb_style: Style::new().fg(Color::White).bg(Color::DarkGray),
arrow_style: Some(Style::new().fg(Color::White).bg(Color::DarkGray)),
glyph_set: GlyphSet::default(),
arrows: ScrollBarArrows::default(),
track_click_behavior: TrackClickBehavior::Page,
scroll_step: 1,
}
}
pub fn vertical(lengths: crate::ScrollLengths) -> Self {
Self::new(ScrollBarOrientation::Vertical, lengths)
}
pub fn horizontal(lengths: crate::ScrollLengths) -> Self {
Self::new(ScrollBarOrientation::Horizontal, lengths)
}
pub const fn orientation(mut self, orientation: ScrollBarOrientation) -> Self {
self.orientation = orientation;
self
}
pub const fn content_len(mut self, content_len: usize) -> Self {
self.content_len = content_len;
self
}
pub const fn viewport_len(mut self, viewport_len: usize) -> Self {
self.viewport_len = viewport_len;
self
}
pub const fn offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
pub const fn track_style(mut self, style: Style) -> Self {
self.track_style = style;
self
}
pub const fn thumb_style(mut self, style: Style) -> Self {
self.thumb_style = style;
self
}
pub const fn arrow_style(mut self, style: Style) -> Self {
self.arrow_style = Some(style);
self
}
pub const fn glyph_set(mut self, glyph_set: GlyphSet) -> Self {
self.glyph_set = glyph_set;
self
}
pub const fn arrows(mut self, arrows: ScrollBarArrows) -> Self {
self.arrows = arrows;
self
}
pub const fn track_click_behavior(mut self, behavior: TrackClickBehavior) -> Self {
self.track_click_behavior = behavior;
self
}
pub fn scroll_step(mut self, step: usize) -> Self {
self.scroll_step = step.max(1);
self
}
fn arrow_layout(&self, area: Rect) -> ArrowLayout {
let mut track_area = area;
let (start, end) = match self.orientation {
ScrollBarOrientation::Vertical => {
let start_enabled = self.arrows.has_start() && area.height > 0;
let end_enabled = self.arrows.has_end() && area.height > start_enabled as u16;
let start = start_enabled.then_some((area.x, area.y));
let end = end_enabled
.then_some((area.x, area.y.saturating_add(area.height).saturating_sub(1)));
if start_enabled {
track_area.y = track_area.y.saturating_add(1);
track_area.height = track_area.height.saturating_sub(1);
}
if end_enabled {
track_area.height = track_area.height.saturating_sub(1);
}
(start, end)
}
ScrollBarOrientation::Horizontal => {
let start_enabled = self.arrows.has_start() && area.width > 0;
let end_enabled = self.arrows.has_end() && area.width > start_enabled as u16;
let start = start_enabled.then_some((area.x, area.y));
let end = end_enabled
.then_some((area.x.saturating_add(area.width).saturating_sub(1), area.y));
if start_enabled {
track_area.x = track_area.x.saturating_add(1);
track_area.width = track_area.width.saturating_sub(1);
}
if end_enabled {
track_area.width = track_area.width.saturating_sub(1);
}
(start, end)
}
};
ArrowLayout {
track_area,
start,
end,
}
}
}
#[cfg(test)]
mod tests {
use ratatui_core::style::{Color, Style};
use super::*;
use crate::glyphs::GlyphSet;
use crate::ScrollLengths;
#[test]
fn builder_methods_update_fields() {
let lengths = ScrollLengths {
content_len: 10,
viewport_len: 4,
};
let track_style = Style::new().fg(Color::Red);
let thumb_style = Style::new().bg(Color::Blue);
let arrow_style = Style::new().fg(Color::Green);
let glyphs = GlyphSet::unicode();
let scrollbar = ScrollBar::new(ScrollBarOrientation::Vertical, lengths)
.orientation(ScrollBarOrientation::Horizontal)
.content_len(20)
.viewport_len(5)
.offset(3)
.track_style(track_style)
.thumb_style(thumb_style)
.arrow_style(arrow_style)
.glyph_set(glyphs.clone())
.arrows(ScrollBarArrows::End)
.track_click_behavior(TrackClickBehavior::JumpToClick)
.scroll_step(0);
assert_eq!(scrollbar.orientation, ScrollBarOrientation::Horizontal);
assert_eq!(scrollbar.content_len, 20);
assert_eq!(scrollbar.viewport_len, 5);
assert_eq!(scrollbar.offset, 3);
assert_eq!(scrollbar.track_style, track_style);
assert_eq!(scrollbar.thumb_style, thumb_style);
assert_eq!(scrollbar.arrow_style, Some(arrow_style));
assert_eq!(scrollbar.glyph_set, glyphs);
assert_eq!(scrollbar.arrows, ScrollBarArrows::End);
assert_eq!(
scrollbar.track_click_behavior,
TrackClickBehavior::JumpToClick
);
assert_eq!(scrollbar.scroll_step, 1);
}
#[test]
fn constructors_set_orientation() {
let lengths = ScrollLengths {
content_len: 10,
viewport_len: 4,
};
let vertical = ScrollBar::vertical(lengths);
let horizontal = ScrollBar::horizontal(lengths);
assert_eq!(vertical.orientation, ScrollBarOrientation::Vertical);
assert_eq!(horizontal.orientation, ScrollBarOrientation::Horizontal);
}
#[test]
fn reserves_track_cells_for_arrows() {
let lengths = ScrollLengths {
content_len: 10,
viewport_len: 4,
};
let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::Both);
let area = Rect::new(0, 0, 1, 5);
let layout = scrollbar.arrow_layout(area);
assert_eq!(layout.track_area.height, 3);
assert_eq!(layout.start, Some((area.x, area.y)));
assert_eq!(
layout.end,
Some((area.x, area.y.saturating_add(area.height).saturating_sub(1)))
);
}
#[test]
fn reserves_track_cells_for_horizontal_arrows() {
let lengths = ScrollLengths {
content_len: 10,
viewport_len: 4,
};
let scrollbar = ScrollBar::horizontal(lengths).arrows(ScrollBarArrows::Both);
let area = Rect::new(0, 0, 5, 1);
let layout = scrollbar.arrow_layout(area);
assert_eq!(layout.track_area.width, 3);
assert_eq!(layout.start, Some((area.x, area.y)));
assert_eq!(
layout.end,
Some((area.x.saturating_add(area.width).saturating_sub(1), area.y))
);
}
}