#[derive(Debug, Clone, Copy, Default)]
pub struct ScrollState {
offset: usize,
total_rows: usize,
visible_rows: usize,
selected: Option<usize>,
}
impl ScrollState {
pub fn new(total_rows: usize, visible_rows: usize) -> Self {
Self {
offset: 0,
total_rows,
visible_rows,
selected: None,
}
}
#[inline]
pub fn offset(&self) -> usize {
self.offset
}
pub fn set_offset(&mut self, offset: usize) {
self.offset = self.clamp_offset(offset);
}
#[inline]
pub fn total_rows(&self) -> usize {
self.total_rows
}
pub fn set_total_rows(&mut self, total: usize) {
self.total_rows = total;
self.offset = self.clamp_offset(self.offset);
if let Some(sel) = self.selected {
if sel >= total {
self.selected = if total > 0 { Some(total - 1) } else { None };
}
}
}
#[inline]
pub fn visible_rows(&self) -> usize {
self.visible_rows
}
pub fn set_visible_rows(&mut self, visible: usize) {
self.visible_rows = visible;
self.offset = self.clamp_offset(self.offset);
}
#[inline]
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn set_selected(&mut self, row: Option<usize>) {
self.selected = match row {
Some(r) if r >= self.total_rows => {
if self.total_rows > 0 {
Some(self.total_rows - 1)
} else {
None
}
}
other => other,
};
if let Some(sel) = self.selected {
self.ensure_visible(sel);
}
}
pub fn select_next(&mut self) {
let new_sel = match self.selected {
Some(sel) => {
if sel + 1 < self.total_rows {
Some(sel + 1)
} else {
Some(sel)
}
}
None if self.total_rows > 0 => Some(0),
None => None,
};
self.set_selected(new_sel);
}
pub fn select_prev(&mut self) {
let new_sel = match self.selected {
Some(sel) if sel > 0 => Some(sel - 1),
Some(sel) => Some(sel),
None if self.total_rows > 0 => Some(0),
None => None,
};
self.set_selected(new_sel);
}
pub fn scroll_down(&mut self) {
let max_offset = self.max_offset();
if self.offset < max_offset {
self.offset += 1;
}
}
pub fn scroll_up(&mut self) {
self.offset = self.offset.saturating_sub(1);
}
pub fn page_down(&mut self) {
let page_size = self.visible_rows.max(1);
let new_offset = self.offset.saturating_add(page_size);
self.offset = self.clamp_offset(new_offset);
}
pub fn page_up(&mut self) {
let page_size = self.visible_rows.max(1);
self.offset = self.offset.saturating_sub(page_size);
}
pub fn home(&mut self) {
self.offset = 0;
}
pub fn end(&mut self) {
self.offset = self.max_offset();
}
pub fn ensure_visible(&mut self, row: usize) {
if row < self.offset {
self.offset = row;
} else if row >= self.offset + self.visible_rows {
self.offset = row.saturating_sub(self.visible_rows.saturating_sub(1));
}
self.offset = self.clamp_offset(self.offset);
}
pub fn needs_scrollbar(&self) -> bool {
self.total_rows > self.visible_rows
}
#[allow(clippy::cast_precision_loss)]
pub fn scrollbar_position(&self) -> f32 {
if self.total_rows <= self.visible_rows {
return 0.0;
}
let max = self.max_offset();
if max == 0 {
return 0.0;
}
self.offset as f32 / max as f32
}
#[allow(clippy::cast_precision_loss)]
pub fn scrollbar_size(&self) -> f32 {
if self.total_rows == 0 {
return 1.0;
}
(self.visible_rows as f32 / self.total_rows as f32).min(1.0)
}
fn max_offset(&self) -> usize {
self.total_rows.saturating_sub(self.visible_rows)
}
fn clamp_offset(&self, offset: usize) -> usize {
offset.min(self.max_offset())
}
pub fn is_visible(&self, row: usize) -> bool {
row >= self.offset && row < self.offset + self.visible_rows
}
pub fn to_viewport_row(&self, global_row: usize) -> Option<usize> {
if self.is_visible(global_row) {
Some(global_row - self.offset)
} else {
None
}
}
pub fn to_global_row(&self, viewport_row: usize) -> usize {
self.offset + viewport_row
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn f_scroll_new() {
let scroll = ScrollState::new(100, 20);
assert_eq!(scroll.offset(), 0);
assert_eq!(scroll.total_rows(), 100);
assert_eq!(scroll.visible_rows(), 20);
}
#[test]
fn f_scroll_down() {
let mut scroll = ScrollState::new(100, 20);
scroll.scroll_down();
assert_eq!(scroll.offset(), 1);
}
#[test]
fn f_scroll_up() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(10);
scroll.scroll_up();
assert_eq!(scroll.offset(), 9);
}
#[test]
fn f_scroll_up_at_zero() {
let mut scroll = ScrollState::new(100, 20);
scroll.scroll_up();
assert_eq!(scroll.offset(), 0, "FALSIFIED: Should not go negative");
}
#[test]
fn f_scroll_down_at_max() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(80); scroll.scroll_down();
assert_eq!(scroll.offset(), 80, "FALSIFIED: Should not exceed max");
}
#[test]
fn f_scroll_page_down() {
let mut scroll = ScrollState::new(100, 20);
scroll.page_down();
assert_eq!(scroll.offset(), 20);
}
#[test]
fn f_scroll_page_up() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(50);
scroll.page_up();
assert_eq!(scroll.offset(), 30);
}
#[test]
fn f_scroll_home() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(50);
scroll.home();
assert_eq!(scroll.offset(), 0);
}
#[test]
fn f_scroll_end() {
let mut scroll = ScrollState::new(100, 20);
scroll.end();
assert_eq!(scroll.offset(), 80);
}
#[test]
fn f_scroll_ensure_visible_above() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(50);
scroll.ensure_visible(30);
assert_eq!(scroll.offset(), 30);
}
#[test]
fn f_scroll_ensure_visible_below() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(0);
scroll.ensure_visible(30);
assert!(scroll.offset() + scroll.visible_rows() > 30);
}
#[test]
fn f_scroll_ensure_visible_already() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(10);
scroll.ensure_visible(15);
assert_eq!(
scroll.offset(),
10,
"FALSIFIED: Should not change if visible"
);
}
#[test]
fn f_scroll_needs_scrollbar_yes() {
let scroll = ScrollState::new(100, 20);
assert!(scroll.needs_scrollbar());
}
#[test]
fn f_scroll_needs_scrollbar_no() {
let scroll = ScrollState::new(10, 20);
assert!(!scroll.needs_scrollbar());
}
#[test]
fn f_scroll_scrollbar_position() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(40);
let pos = scroll.scrollbar_position();
assert!(pos > 0.4 && pos < 0.6);
}
#[test]
fn f_scroll_scrollbar_size() {
let scroll = ScrollState::new(100, 20);
let size = scroll.scrollbar_size();
assert!((size - 0.2).abs() < 0.01);
}
#[test]
fn f_scroll_is_visible() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(10);
assert!(scroll.is_visible(15));
assert!(!scroll.is_visible(5));
assert!(!scroll.is_visible(35));
}
#[test]
fn f_scroll_to_viewport_row() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(10);
assert_eq!(scroll.to_viewport_row(15), Some(5));
assert_eq!(scroll.to_viewport_row(5), None);
}
#[test]
fn f_scroll_to_global_row() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(10);
assert_eq!(scroll.to_global_row(5), 15);
}
#[test]
fn f_scroll_select_next() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_selected(Some(0));
scroll.select_next();
assert_eq!(scroll.selected(), Some(1));
}
#[test]
fn f_scroll_select_prev() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_selected(Some(5));
scroll.select_prev();
assert_eq!(scroll.selected(), Some(4));
}
#[test]
fn f_scroll_select_prev_at_zero() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_selected(Some(0));
scroll.select_prev();
assert_eq!(scroll.selected(), Some(0));
}
#[test]
fn f_scroll_select_next_at_end() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_selected(Some(99));
scroll.select_next();
assert_eq!(scroll.selected(), Some(99));
}
#[test]
fn f_scroll_select_from_none() {
let mut scroll = ScrollState::new(100, 20);
scroll.select_next();
assert_eq!(scroll.selected(), Some(0));
}
#[test]
fn f_scroll_empty_dataset() {
let scroll = ScrollState::new(0, 20);
assert_eq!(scroll.offset(), 0);
assert!(!scroll.needs_scrollbar());
}
#[test]
fn f_scroll_set_total_rows_shrink() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(80);
scroll.set_selected(Some(90));
scroll.set_total_rows(50);
assert!(scroll.offset() <= 30);
assert!(scroll.selected().unwrap_or(0) < 50);
}
#[test]
fn f_scroll_default() {
let scroll = ScrollState::default();
assert_eq!(scroll.offset(), 0);
assert_eq!(scroll.total_rows(), 0);
assert_eq!(scroll.visible_rows(), 0);
}
#[test]
fn f_scroll_clone() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_offset(50);
let cloned = scroll;
assert_eq!(scroll.offset(), cloned.offset());
}
#[test]
fn f_scroll_set_total_rows_shrink_to_zero() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_selected(Some(50));
scroll.set_total_rows(0);
assert_eq!(scroll.selected(), None);
}
#[test]
fn f_scroll_set_selected_out_of_bounds_empty() {
let mut scroll = ScrollState::new(0, 20);
scroll.set_selected(Some(100));
assert_eq!(scroll.selected(), None);
}
#[test]
fn f_scroll_set_selected_out_of_bounds_clamps() {
let mut scroll = ScrollState::new(50, 20);
scroll.set_selected(Some(100));
assert_eq!(scroll.selected(), Some(49));
}
#[test]
fn f_scroll_set_total_rows_shrink_selection_out_of_bounds() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_selected(Some(90));
scroll.set_total_rows(50);
assert_eq!(scroll.selected(), Some(49));
}
#[test]
fn f_scroll_page_up_at_zero() {
let mut scroll = ScrollState::new(100, 20);
scroll.page_up();
assert_eq!(scroll.offset(), 0);
}
#[test]
fn f_scroll_select_prev_from_none() {
let mut scroll = ScrollState::new(100, 20);
scroll.select_prev();
assert_eq!(scroll.selected(), Some(0));
}
#[test]
fn f_scroll_select_next_empty() {
let mut scroll = ScrollState::new(0, 20);
scroll.select_next();
assert_eq!(scroll.selected(), None);
}
#[test]
fn f_scroll_select_prev_empty() {
let mut scroll = ScrollState::new(0, 20);
scroll.select_prev();
assert_eq!(scroll.selected(), None);
}
#[test]
fn f_scroll_scrollbar_position_small_content() {
let scroll = ScrollState::new(10, 20);
let pos = scroll.scrollbar_position();
assert!((pos - 0.0).abs() < 0.001);
}
#[test]
fn f_scroll_scrollbar_position_max_zero() {
let scroll = ScrollState::new(20, 20);
let pos = scroll.scrollbar_position();
assert!((pos - 0.0).abs() < 0.001);
}
#[test]
fn f_scroll_scrollbar_size_empty() {
let scroll = ScrollState::new(0, 20);
let size = scroll.scrollbar_size();
assert!((size - 1.0).abs() < 0.001);
}
#[test]
fn f_scroll_set_total_rows_with_selection_clamps() {
let mut scroll = ScrollState::new(100, 20);
scroll.set_selected(Some(80));
scroll.set_total_rows(50);
assert_eq!(scroll.selected(), Some(49));
}
}