#[cfg(test)]
mod tests;
use std::ops::Range;
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState};
use crate::theme::Theme;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct ScrollState {
offset: usize,
content_length: usize,
viewport_height: usize,
}
impl ScrollState {
pub fn new(content_length: usize) -> Self {
Self {
offset: 0,
content_length,
viewport_height: 0,
}
}
pub fn set_content_length(&mut self, len: usize) {
self.content_length = len;
self.clamp_offset();
}
pub fn set_viewport_height(&mut self, height: usize) {
self.viewport_height = height;
self.clamp_offset();
}
pub fn set_offset(&mut self, offset: usize) {
self.offset = offset;
self.clamp_offset();
}
pub fn scroll_up(&mut self) -> bool {
self.scroll_up_by(1)
}
pub fn scroll_down(&mut self) -> bool {
self.scroll_down_by(1)
}
pub fn scroll_up_by(&mut self, n: usize) -> bool {
let old = self.offset;
self.offset = self.offset.saturating_sub(n);
self.offset != old
}
pub fn scroll_down_by(&mut self, n: usize) -> bool {
let old = self.offset;
self.offset = self.offset.saturating_add(n);
self.clamp_offset();
self.offset != old
}
pub fn page_up(&mut self, page_size: usize) -> bool {
self.scroll_up_by(page_size)
}
pub fn page_down(&mut self, page_size: usize) -> bool {
self.scroll_down_by(page_size)
}
pub fn scroll_to_start(&mut self) -> bool {
let old = self.offset;
self.offset = 0;
self.offset != old
}
pub fn scroll_to_end(&mut self) -> bool {
let old = self.offset;
self.offset = self.max_offset();
self.offset != old
}
pub fn ensure_visible(&mut self, index: usize) -> bool {
let old = self.offset;
if self.viewport_height == 0 {
return false;
}
if index < self.offset {
self.offset = index;
} else if index >= self.offset + self.viewport_height {
self.offset = index.saturating_sub(self.viewport_height.saturating_sub(1));
}
self.clamp_offset();
self.offset != old
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn content_length(&self) -> usize {
self.content_length
}
pub fn viewport_height(&self) -> usize {
self.viewport_height
}
pub fn visible_range(&self) -> Range<usize> {
let end = (self.offset + self.viewport_height).min(self.content_length);
self.offset..end
}
pub fn max_offset(&self) -> usize {
self.content_length.saturating_sub(self.viewport_height)
}
pub fn can_scroll(&self) -> bool {
self.content_length > self.viewport_height
}
pub fn at_start(&self) -> bool {
self.offset == 0
}
pub fn at_end(&self) -> bool {
self.offset >= self.max_offset()
}
pub fn scrollbar_state(&self) -> ScrollbarState {
ScrollbarState::default()
.content_length(self.content_length.saturating_sub(self.viewport_height))
.viewport_content_length(self.viewport_height)
.position(self.offset)
}
fn clamp_offset(&mut self) {
let max = self.max_offset();
if self.offset > max {
self.offset = max;
}
}
}
pub fn render_scrollbar(scroll: &ScrollState, frame: &mut Frame, area: Rect, theme: &Theme) {
if !scroll.can_scroll() {
return;
}
let mut scrollbar_state = scroll.scrollbar_state();
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_style(theme.normal_style())
.track_style(theme.disabled_style());
frame.render_stateful_widget(scrollbar, area, &mut scrollbar_state);
}
pub fn render_scrollbar_inside_border(
scroll: &ScrollState,
frame: &mut Frame,
area: Rect,
theme: &Theme,
) {
if !scroll.can_scroll() || area.height < 3 {
return;
}
let inner = Rect::new(
area.x,
area.y + 1,
area.width,
area.height.saturating_sub(2),
);
let mut scrollbar_state = scroll.scrollbar_state();
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_style(theme.normal_style())
.track_style(theme.disabled_style());
frame.render_stateful_widget(scrollbar, inner, &mut scrollbar_state);
}