use std::ops::Range;
pub const SCROLL_MARGIN: usize = 3;
#[derive(Debug, Clone, Default)]
pub struct PanelScrollState {
pub offset: usize,
pub cursor: usize,
pub total: usize,
pub visible: usize,
}
impl PanelScrollState {
pub fn new() -> Self {
Self::default()
}
pub fn with_total(total: usize) -> Self {
Self {
total,
..Default::default()
}
}
pub fn ensure_cursor_visible(&mut self) {
if self.visible == 0 || self.total == 0 {
return;
}
let margin = SCROLL_MARGIN.min(self.visible / 2);
if self.cursor < self.offset.saturating_add(margin) {
self.offset = self.cursor.saturating_sub(margin);
}
let bottom_threshold = self.offset + self.visible.saturating_sub(margin);
if self.cursor >= bottom_threshold && self.total > self.visible {
self.offset = (self.cursor + margin + 1).saturating_sub(self.visible);
let max_offset = self.total.saturating_sub(self.visible);
self.offset = self.offset.min(max_offset);
}
}
pub fn cursor_down(&mut self) {
if self.cursor + 1 < self.total {
self.cursor += 1;
self.ensure_cursor_visible();
}
}
pub fn cursor_up(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
self.ensure_cursor_visible();
}
}
pub fn cursor_first(&mut self) {
self.cursor = 0;
self.ensure_cursor_visible();
}
pub fn cursor_last(&mut self) {
if self.total > 0 {
self.cursor = self.total - 1;
self.ensure_cursor_visible();
}
}
pub fn page_down(&mut self) {
if self.total > 0 {
self.cursor = (self.cursor + self.visible).min(self.total - 1);
self.ensure_cursor_visible();
}
}
pub fn page_up(&mut self) {
self.cursor = self.cursor.saturating_sub(self.visible);
self.ensure_cursor_visible();
}
pub fn scroll_down(&mut self) {
let max_offset = if self.visible > 0 {
self.total.saturating_sub(self.visible)
} else {
self.total.saturating_sub(1)
};
if self.offset < max_offset {
self.offset += 1;
}
}
pub fn scroll_up(&mut self) {
if self.offset > 0 {
self.offset -= 1;
}
}
pub fn scroll_to_top(&mut self) {
self.offset = 0;
self.cursor = 0;
}
pub fn scroll_to_bottom(&mut self) {
if self.total > self.visible {
self.offset = self.total - self.visible;
}
if self.total > 0 {
self.cursor = self.total - 1;
}
}
pub fn set_total(&mut self, total: usize) {
self.total = total;
if self.total > 0 && self.cursor >= self.total {
self.cursor = self.total - 1;
}
if self.total > 0 && self.offset + self.visible > self.total {
self.offset = self.total.saturating_sub(self.visible);
}
}
pub fn set_visible(&mut self, visible: usize) {
self.visible = visible;
self.ensure_cursor_visible();
}
pub fn selected(&self) -> Option<usize> {
if self.total > 0 {
Some(self.cursor)
} else {
None
}
}
pub fn is_selected(&self, index: usize) -> bool {
self.cursor == index
}
pub fn percentage(&self) -> f64 {
if self.total <= self.visible {
0.0
} else {
self.offset as f64 / (self.total - self.visible) as f64
}
}
pub fn visible_range(&self) -> Range<usize> {
let start = self.offset;
let end = (self.offset + self.visible).min(self.total);
start..end
}
pub fn at_top(&self) -> bool {
self.offset == 0
}
pub fn at_bottom(&self) -> bool {
self.total <= self.visible || self.offset >= self.total - self.visible
}
pub fn indicator(&self) -> Option<String> {
if self.total <= self.visible {
return None;
}
let arrow = if self.at_top() {
"↓"
} else if self.at_bottom() {
"↑"
} else {
"↕"
};
let position = if self.at_top() {
"Top".to_string()
} else if self.at_bottom() {
"Bot".to_string()
} else {
let pct = (self.percentage() * 100.0).round() as u8;
format!("{}%", pct)
};
Some(format!(" {} {} ", arrow, position))
}
}