use retroglyph_core::{Backend, Rect, Style, Terminal};
use super::Widget;
use crate::Theme;
use crate::draw::thumb_geometry;
#[derive(Clone, Copy, Debug)]
pub struct Scrollbar {
total_len: usize,
visible_len: usize,
offset: usize,
track_style: Style,
thumb_style: Style,
}
impl Scrollbar {
#[must_use]
pub fn new(total_len: usize, visible_len: usize) -> Self {
Self {
total_len,
visible_len,
offset: 0,
track_style: Style::new(),
thumb_style: Style::new(),
}
}
#[must_use]
pub const fn offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
#[must_use]
pub const fn track_style(mut self, style: Style) -> Self {
self.track_style = style;
self
}
#[must_use]
pub const fn thumb_style(mut self, style: Style) -> Self {
self.thumb_style = style;
self
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.track_style = Style::new().bg(theme.panel_bg);
self.thumb_style = Style::new().bg(theme.border);
self
}
}
impl<B: Backend> Widget<B> for Scrollbar {
fn render(self, area: Rect, term: &mut Terminal<B>) {
if area.width() == 0 || area.height() == 0 {
return;
}
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
term.put_styled(x, y, ' ', self.track_style);
}
}
let Some((start, len)) =
thumb_geometry(area, self.total_len, self.visible_len, self.offset)
else {
return;
};
for y in (area.top() + start)..(area.top() + start + len) {
for x in area.left()..area.right() {
term.put_styled(x, y, ' ', self.thumb_style);
}
}
}
}
#[cfg(test)]
mod tests {
use retroglyph_core::{Color, Headless};
use super::*;
#[test]
fn draws_a_plain_track_with_no_thumb_when_nothing_to_scroll() {
let area = Rect::new(0, 0, 1, 5);
let mut term = Terminal::new(Headless::new(1, 5));
let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
Scrollbar::new(3, 5)
.track_style(track)
.thumb_style(thumb)
.render(area, &mut term);
for y in 0..5 {
assert_eq!(
term.grid().get(0, y).style().background(),
track.background()
);
}
}
#[test]
fn draws_the_thumb_over_the_track() {
let area = Rect::new(0, 0, 1, 10);
let mut term = Terminal::new(Headless::new(1, 10));
let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
Scrollbar::new(20, 5)
.offset(0)
.track_style(track)
.thumb_style(thumb)
.render(area, &mut term);
let (start, len) = thumb_geometry(area, 20, 5, 0).unwrap();
for y in 0..10 {
let bg = term.grid().get(0, y).style().background();
if y >= start && y < start + len {
assert_eq!(bg, thumb.background());
} else {
assert_eq!(bg, track.background());
}
}
}
#[test]
fn theme_maps_named_roles_onto_track_and_thumb() {
let area = Rect::new(0, 0, 1, 10);
let mut term = Terminal::new(Headless::new(1, 10));
Scrollbar::new(20, 5)
.theme(Theme::DARK)
.render(area, &mut term);
let (start, len) = thumb_geometry(area, 20, 5, 0).unwrap();
for y in 0..10 {
let bg = term.grid().get(0, y).style().background();
if y >= start && y < start + len {
assert_eq!(bg, Theme::DARK.border);
} else {
assert_eq!(bg, Theme::DARK.panel_bg);
}
}
}
#[test]
fn offset_defaults_to_zero() {
let area = Rect::new(0, 0, 1, 10);
let mut term = Terminal::new(Headless::new(1, 10));
let track = Style::new().bg(Color::Rgb { r: 1, g: 1, b: 1 });
let thumb = Style::new().bg(Color::Rgb { r: 2, g: 2, b: 2 });
Scrollbar::new(20, 5)
.track_style(track)
.thumb_style(thumb)
.render(area, &mut term);
let (start, _) = thumb_geometry(area, 20, 5, 0).unwrap();
assert_eq!(start, 0);
}
}