use ratatui::{
Frame,
layout::Rect,
text::Span,
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
};
use super::nav::ScrollView;
use super::style;
use crate::theme::Skin;
pub fn render_scrollbar(
frame: &mut Frame,
area: Rect,
skin: &Skin,
view: ScrollView,
) {
render(frame, area, skin, ScrollbarOrientation::VerticalRight, view);
}
pub fn render_hscrollbar(
frame: &mut Frame,
area: Rect,
skin: &Skin,
view: ScrollView,
) {
render(
frame,
area,
skin,
ScrollbarOrientation::HorizontalBottom,
view,
);
}
const THUMB: &str = "\u{2588}";
const TRACK: &str = "\u{2502}";
#[must_use]
pub fn row_indicator(
row: usize,
view: ScrollView,
skin: &Skin,
) -> Span<'static> {
if scrollable_length(view).is_none() {
return Span::raw(" ".to_string());
}
let rows = view.viewport.max(1);
let max_offset = view.total.saturating_sub(rows).max(1);
let thumb = view.offset * rows.saturating_sub(1) / max_offset;
if row == thumb {
Span::styled(THUMB.to_string(), style::fg(skin.palette.foreground_dim))
} else {
Span::styled(TRACK.to_string(), style::fg(skin.palette.border))
}
}
fn scrollable_length(view: ScrollView) -> Option<usize> {
if view.total <= view.viewport {
return None;
}
Some(view.total.saturating_sub(view.viewport).saturating_add(1))
}
fn render(
frame: &mut Frame,
area: Rect,
skin: &Skin,
orientation: ScrollbarOrientation,
view: ScrollView,
) {
let Some(content_length) = scrollable_length(view) else {
return;
};
if area.width == 0 || area.height == 0 {
return;
}
let mut state = ScrollbarState::new(content_length)
.viewport_content_length(view.viewport)
.position(view.offset);
let scrollbar = Scrollbar::new(orientation)
.begin_symbol(None)
.end_symbol(None)
.thumb_style(style::fg(skin.palette.foreground_dim))
.track_style(style::fg(skin.palette.border));
frame.render_stateful_widget(scrollbar, area, &mut state);
}
#[cfg(test)]
mod tests {
use super::*;
fn view(total: usize, offset: usize, viewport: usize) -> ScrollView {
ScrollView {
total,
offset,
viewport,
}
}
#[test]
fn no_scrollbar_when_content_fits() {
assert_eq!(scrollable_length(view(3, 0, 3)), None);
assert_eq!(scrollable_length(view(2, 0, 5)), None);
}
#[test]
fn scrollable_length_is_total_minus_viewport_plus_one() {
assert_eq!(scrollable_length(view(10, 0, 3)), Some(8));
assert_eq!(scrollable_length(view(4, 0, 3)), Some(2));
}
}