use std::cmp::min;
#[derive(Clone, Debug)]
pub struct ScrollManager {
offset: usize,
max_offset: usize,
total_rows: usize,
viewport_rows: u16,
metrics_dirty: bool,
}
#[allow(dead_code)]
impl ScrollManager {
pub fn new(viewport_rows: u16) -> Self {
Self {
offset: 0,
max_offset: 0,
total_rows: 0,
viewport_rows: viewport_rows.max(1),
metrics_dirty: true,
}
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn set_offset(&mut self, offset: usize) {
self.offset = min(offset, self.max_offset);
}
pub fn max_offset(&self) -> usize {
self.max_offset
}
pub fn viewport_rows(&self) -> u16 {
self.viewport_rows
}
pub fn set_viewport_rows(&mut self, rows: u16) {
let rows = rows.max(1);
if self.viewport_rows != rows {
self.viewport_rows = rows;
self.metrics_dirty = true;
}
}
pub fn total_rows(&self) -> usize {
self.total_rows
}
pub fn last_known_total(&self) -> Option<usize> {
if self.metrics_dirty {
None
} else {
Some(self.total_rows)
}
}
pub fn set_total_rows(&mut self, total: usize) -> bool {
let changed = self.total_rows != total;
if changed {
self.total_rows = total;
}
if changed || self.metrics_dirty {
self.update_max_offset();
self.metrics_dirty = false;
}
changed
}
pub fn invalidate_metrics(&mut self) {
self.metrics_dirty = true;
}
pub fn metrics_valid(&self) -> bool {
!self.metrics_dirty
}
pub fn scroll_up(&mut self, lines: usize) {
self.offset = self.offset.saturating_sub(lines);
}
pub fn scroll_down(&mut self, lines: usize) {
self.offset = min(self.offset + lines, self.max_offset);
}
pub fn scroll_page_up(&mut self) {
self.scroll_up(self.viewport_rows.saturating_sub(1) as usize);
}
pub fn scroll_page_down(&mut self) {
self.scroll_down(self.viewport_rows.saturating_sub(1) as usize);
}
pub fn scroll_to_top(&mut self) {
self.offset = 0;
}
pub fn scroll_to_bottom(&mut self) {
self.offset = self.max_offset;
}
pub fn at_top(&self) -> bool {
self.offset == 0
}
pub fn at_bottom(&self) -> bool {
self.offset >= self.max_offset
}
pub fn progress_percent(&self) -> u8 {
if self.max_offset == 0 {
100
} else {
((self.offset as f32 / self.max_offset as f32) * 100.0) as u8
}
}
fn update_max_offset(&mut self) {
let viewport = self.viewport_rows as usize;
self.max_offset = self.total_rows.saturating_sub(viewport).max(0);
}
pub fn clamp_offset(&mut self) {
if self.offset > self.max_offset {
self.offset = self.max_offset;
}
}
pub fn visible_range(&self) -> (usize, usize) {
let start = self.offset;
let end = min(self.offset + self.viewport_rows as usize, self.total_rows);
(start, end)
}
pub fn visible_count(&self) -> usize {
let (start, end) = self.visible_range();
end - start
}
}
impl Default for ScrollManager {
fn default() -> Self {
Self::new(10)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_scroll_manager() {
let manager = ScrollManager::new(10);
assert_eq!(manager.offset(), 0);
assert_eq!(manager.viewport_rows(), 10);
assert!(manager.at_top());
}
#[test]
fn scroll_down() {
let mut manager = ScrollManager::new(10);
manager.set_total_rows(100);
manager.scroll_down(5);
assert_eq!(manager.offset(), 5);
}
#[test]
fn scroll_down_clamped_at_max() {
let mut manager = ScrollManager::new(10);
manager.set_total_rows(20);
manager.scroll_down(100);
assert_eq!(manager.offset(), manager.max_offset());
assert!(manager.at_bottom());
}
#[test]
fn viewport_resize_recomputes_max_offset_when_total_rows_stay_the_same() {
let mut manager = ScrollManager::new(2);
manager.set_total_rows(6);
assert_eq!(manager.max_offset(), 4);
manager.set_viewport_rows(8);
assert!(manager.metrics_dirty);
manager.set_total_rows(6);
assert_eq!(manager.max_offset(), 0);
assert!(manager.metrics_valid());
}
#[test]
fn scroll_up() {
let mut manager = ScrollManager::new(10);
manager.set_total_rows(100);
manager.set_offset(50);
manager.scroll_up(20);
assert_eq!(manager.offset(), 30);
}
#[test]
fn scroll_up_clamped_at_zero() {
let mut manager = ScrollManager::new(10);
manager.set_total_rows(100);
manager.set_offset(50);
manager.scroll_up(100);
assert_eq!(manager.offset(), 0);
assert!(manager.at_top());
}
#[test]
fn page_navigation() {
let mut manager = ScrollManager::new(10);
manager.set_total_rows(100);
manager.scroll_page_down();
assert_eq!(manager.offset(), 9);
manager.scroll_page_up();
assert_eq!(manager.offset(), 0);
}
#[test]
fn visible_range() {
let mut manager = ScrollManager::new(10);
manager.set_total_rows(100);
manager.set_offset(20);
let (start, end) = manager.visible_range();
assert_eq!(start, 20);
assert_eq!(end, 30);
assert_eq!(manager.visible_count(), 10);
}
#[test]
fn progress_calculation() {
let mut manager = ScrollManager::new(10);
manager.set_total_rows(100);
assert_eq!(manager.progress_percent(), 0);
manager.set_offset(50);
assert_eq!(manager.progress_percent(), 55);
manager.scroll_to_bottom();
assert_eq!(manager.progress_percent(), 100);
}
#[test]
fn metrics_invalidation() {
let mut manager = ScrollManager::new(10);
assert!(manager.metrics_dirty);
manager.set_total_rows(50);
assert!(!manager.metrics_dirty);
manager.invalidate_metrics();
assert!(manager.metrics_dirty);
}
#[test]
fn viewport_change() {
let mut manager = ScrollManager::new(10);
manager.set_total_rows(100);
manager.set_offset(50);
manager.set_viewport_rows(20);
assert!(manager.metrics_dirty);
assert_eq!(manager.viewport_rows(), 20);
}
}