#[derive(Debug, Clone)]
pub struct PageInfo {
pub visible_indices: Vec<usize>,
pub first_visible: usize,
pub last_visible: usize,
pub items_per_page: usize,
pub show_above_indicator: bool,
pub items_above: usize,
pub show_below_indicator: bool,
pub items_below: usize,
pub current_page: usize,
pub total_pages: usize,
}
impl PageInfo {
pub fn empty() -> Self {
Self {
visible_indices: vec![],
first_visible: 0,
last_visible: 0,
items_per_page: 0,
show_above_indicator: false,
items_above: 0,
show_below_indicator: false,
items_below: 0,
current_page: 0,
total_pages: 0,
}
}
}
#[derive(Clone)]
pub struct Page {
pub total_items: usize,
pub scroll_offset: usize,
}
impl Page {
pub fn new(total_items: usize) -> Self {
Self {
total_items,
scroll_offset: 0,
}
}
pub fn set_total_items(&mut self, total_items: usize) {
self.total_items = total_items;
if self.scroll_offset >= total_items && total_items > 0 {
self.scroll_offset = total_items.saturating_sub(1);
}
}
pub fn get_page_info(&self, viewport_height: usize) -> PageInfo {
if self.total_items == 0 || viewport_height == 0 {
return PageInfo::empty();
}
let render_start = self.scroll_offset;
let visible_indices: Vec<usize> = (0..viewport_height)
.map(|i| render_start + i)
.filter(|&idx| idx < self.total_items)
.collect();
let first_visible = visible_indices.first().copied().unwrap_or(0);
let last_visible = visible_indices.last().copied().unwrap_or(0);
let items_above = render_start;
let items_below = if visible_indices.is_empty() {
self.total_items.saturating_sub(render_start)
} else {
self.total_items.saturating_sub(last_visible + 1)
};
let has_items_above = render_start > 0;
let has_items_below = (render_start + viewport_height) < self.total_items;
let total_pages = if viewport_height > 0 {
self.total_items.div_ceil(viewport_height)
} else {
0
};
let current_page = if viewport_height > 0 {
self.scroll_offset / viewport_height
} else {
0
};
PageInfo {
visible_indices,
first_visible,
last_visible,
items_per_page: viewport_height,
show_above_indicator: has_items_above,
items_above,
show_below_indicator: has_items_below,
items_below,
current_page,
total_pages,
}
}
pub fn get_adjusted_viewport_height(&self, raw_viewport_height: usize) -> usize {
if self.total_items == 0 || raw_viewport_height == 0 {
return raw_viewport_height;
}
let above = if self.scroll_offset > 0 { 1 } else { 0 };
let available = raw_viewport_height.saturating_sub(above);
let below = if self.scroll_offset + available < self.total_items {
1
} else {
0
};
available.saturating_sub(below)
}
pub fn set_scroll_offset(&mut self, offset: usize) {
self.scroll_offset = offset.min(self.total_items.saturating_sub(1));
}
pub fn scroll_to_visible(&mut self, item_idx: usize, viewport_height: usize) {
if viewport_height == 0 {
return;
}
let scroll_start = self.scroll_offset;
let scroll_end = scroll_start + viewport_height;
if item_idx < scroll_start {
self.scroll_offset = item_idx;
} else if item_idx >= scroll_end {
self.scroll_offset = item_idx.saturating_sub(viewport_height - 1);
}
}
pub fn navigate_up(&self, current_idx: usize) -> usize {
current_idx.saturating_sub(1)
}
pub fn navigate_down(&self, current_idx: usize) -> usize {
if current_idx >= self.total_items.saturating_sub(1) {
current_idx
} else {
current_idx + 1
}
}
pub fn is_empty(&self) -> bool {
self.total_items == 0
}
}
impl Default for Page {
fn default() -> Self {
Self::new(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_page_info_empty() {
let page = Page::new(0);
let info = page.get_page_info(10);
assert!(info.visible_indices.is_empty());
assert!(!info.show_above_indicator);
assert!(!info.show_below_indicator);
}
#[test]
fn test_single_page_fits_all() {
let page = Page::new(5);
let info = page.get_page_info(10);
assert_eq!(info.visible_indices, vec![0, 1, 2, 3, 4]);
assert!(!info.show_above_indicator);
assert!(!info.show_below_indicator);
}
#[test]
fn test_first_page_multi_page() {
let page = Page::new(20);
let info = page.get_page_info(5);
assert_eq!(info.visible_indices, vec![0, 1, 2, 3, 4]);
assert!(!info.show_above_indicator);
assert!(info.show_below_indicator);
assert_eq!(info.items_below, 15);
}
#[test]
fn test_middle_page() {
let mut page = Page::new(20);
page.set_scroll_offset(5);
let info = page.get_page_info(5);
assert_eq!(info.visible_indices, vec![5, 6, 7, 8, 9]);
assert!(info.show_above_indicator);
assert!(info.show_below_indicator);
assert_eq!(info.items_above, 5);
assert_eq!(info.items_below, 10);
}
#[test]
fn test_last_page() {
let mut page = Page::new(20);
page.set_scroll_offset(15);
let info = page.get_page_info(5);
assert_eq!(info.visible_indices, vec![15, 16, 17, 18, 19]);
assert!(info.show_above_indicator);
assert!(!info.show_below_indicator);
}
#[test]
fn test_scroll_to_visible() {
let mut page = Page::new(20);
page.scroll_to_visible(15, 5);
assert_eq!(page.scroll_offset, 11); let info = page.get_page_info(5);
assert!(info.visible_indices.contains(&15));
}
#[test]
fn test_navigate_up_down() {
let page = Page::new(10);
let idx = page.navigate_down(0);
assert_eq!(idx, 1);
let idx = page.navigate_up(idx);
assert_eq!(idx, 0);
let idx = page.navigate_up(0);
assert_eq!(idx, 0);
let idx = page.navigate_down(9);
assert_eq!(idx, 9);
}
#[test]
fn test_set_total_items_clamps_scroll() {
let mut page = Page::new(20);
page.set_scroll_offset(15);
page.set_total_items(10);
assert_eq!(page.scroll_offset, 9);
}
#[test]
fn test_adjusted_viewport_height_empty_list() {
let page = Page::new(0);
assert_eq!(page.get_adjusted_viewport_height(5), 5);
}
#[test]
fn test_adjusted_viewport_height_zero_raw() {
let page = Page::new(10);
assert_eq!(page.get_adjusted_viewport_height(0), 0);
}
#[test]
fn test_adjusted_viewport_height_all_fit() {
let page = Page::new(5);
assert_eq!(page.get_adjusted_viewport_height(10), 10);
}
#[test]
fn test_adjusted_viewport_height_at_top_with_below() {
let page = Page::new(10);
assert_eq!(page.get_adjusted_viewport_height(5), 4);
}
#[test]
fn test_adjusted_viewport_height_middle_both_indicators() {
let mut page = Page::new(10);
page.set_scroll_offset(3);
assert_eq!(page.get_adjusted_viewport_height(5), 3);
}
#[test]
fn test_adjusted_viewport_height_near_end_above_only() {
let mut page = Page::new(10);
page.set_scroll_offset(6);
assert_eq!(page.get_adjusted_viewport_height(5), 4);
}
}