use ratatui::layout::Rect;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScrollState {
pub current_line: usize,
pub offset: usize,
pub scroll_offset: usize,
pub viewport_height: usize,
pub total_lines: usize,
pub filter: Option<String>,
pub filter_mode: bool,
}
impl ScrollState {
pub fn new() -> Self {
Self {
current_line: 1,
offset: 0,
scroll_offset: 0,
viewport_height: 20,
total_lines: 0,
filter: None,
filter_mode: false,
}
}
pub fn update_total_lines(&mut self, total_lines: usize) {
self.total_lines = total_lines.max(1);
self.current_line = self.current_line.clamp(1, self.total_lines);
self.clamp_offsets();
}
pub fn scroll_to(&mut self, line: usize) {
self.set_current_line(line.saturating_add(1));
}
pub fn set_current_line(&mut self, line: usize) {
self.current_line = line.clamp(1, self.total_lines.max(1));
self.adjust_scroll_for_current_line();
}
pub fn scroll_down(&mut self, amount: usize) {
self.set_offset(self.effective_offset().saturating_add(amount));
}
pub fn scroll_up(&mut self, amount: usize) {
self.set_offset(self.effective_offset().saturating_sub(amount));
}
pub fn move_current_down(&mut self, amount: usize) {
self.set_current_line(self.current_line.saturating_add(amount));
}
pub fn move_current_up(&mut self, amount: usize) {
self.set_current_line(self.current_line.saturating_sub(amount).max(1));
}
pub fn line_down(&mut self) {
self.move_current_down(1);
}
pub fn line_up(&mut self) {
self.move_current_up(1);
}
pub fn scroll_to_top(&mut self) {
self.current_line = 1;
self.set_offset(0);
}
pub fn scroll_to_bottom(&mut self) {
self.current_line = self.total_lines.max(1);
self.set_offset(self.max_scroll_offset());
}
pub fn filter_line_down(&mut self, _filter_text: String) {
self.line_down();
}
pub fn filter_line_up(&mut self, _filter_text: String) {
self.line_up();
}
pub fn update_viewport(&mut self, area: Rect) {
self.viewport_height = area.height as usize;
self.adjust_scroll_for_current_line();
}
pub fn ensure_current_visible(&mut self, height: usize) {
self.viewport_height = height.max(1);
self.adjust_scroll_for_current_line();
}
pub fn visible_range(&self, height: usize) -> std::ops::Range<usize> {
let start = self.effective_offset();
let end = start.saturating_add(height).min(self.total_lines.max(1));
start..end
}
pub fn is_current_line_visible(&self) -> bool {
let first_visible = self.effective_offset().saturating_add(1);
let last_visible = self
.effective_offset()
.saturating_add(self.viewport_height.max(1));
self.current_line >= first_visible && self.current_line <= last_visible
}
pub fn max_scroll_offset(&self) -> usize {
self.total_lines.saturating_sub(self.viewport_height.max(1))
}
pub fn scroll_percentage(&self) -> f64 {
let max_offset = self.max_scroll_offset();
if max_offset == 0 {
0.0
} else {
self.effective_offset() as f64 / max_offset as f64
}
}
pub fn current_line_index(&self) -> usize {
self.current_line.saturating_sub(1)
}
pub fn effective_offset(&self) -> usize {
self.scroll_offset
.max(self.offset)
.min(self.max_scroll_offset())
}
pub fn set_offset(&mut self, offset: usize) {
let offset = offset.min(self.max_scroll_offset());
self.offset = offset;
self.scroll_offset = offset;
}
pub fn adjust_scroll_for_current_line(&mut self) {
const SCROLL_MARGIN: usize = 3;
let viewport_height = self.viewport_height.max(1);
let current = self.current_line_index();
let mut offset = self.effective_offset();
if viewport_height <= SCROLL_MARGIN * 2 {
if current < offset {
offset = current;
} else if current >= offset.saturating_add(viewport_height) {
offset = current.saturating_sub(viewport_height.saturating_sub(1));
}
self.set_offset(offset);
return;
}
let first_visible = offset;
let last_visible = offset.saturating_add(viewport_height.saturating_sub(1));
if current < first_visible.saturating_add(SCROLL_MARGIN) {
offset = current.saturating_sub(SCROLL_MARGIN);
} else if current > last_visible.saturating_sub(SCROLL_MARGIN) {
offset = current
.saturating_add(SCROLL_MARGIN)
.saturating_sub(viewport_height.saturating_sub(1));
}
self.set_offset(offset);
}
fn clamp_offsets(&mut self) {
let offset = self.effective_offset();
self.set_offset(offset);
}
}
impl Default for ScrollState {
fn default() -> Self {
Self::new()
}
}