#[derive(Debug, Clone)]
pub struct ListState {
selected_index: usize,
scroll_offset: usize,
group_by_location: bool,
}
impl Default for ListState {
fn default() -> Self {
Self {
selected_index: 0,
scroll_offset: 0,
group_by_location: true,
}
}
}
impl ListState {
pub fn new() -> Self {
Self::default()
}
pub fn selected_index(&self) -> usize {
self.selected_index
}
pub fn set_selected_index(&mut self, index: usize, item_count: usize) {
self.selected_index = clamp_selection(index, item_count);
}
pub fn scroll_offset(&self) -> usize {
self.scroll_offset
}
pub fn set_scroll_offset(&mut self, offset: usize) {
self.scroll_offset = offset;
}
pub fn is_grouped(&self) -> bool {
self.group_by_location
}
pub fn toggle_grouping(&mut self) {
self.group_by_location = !self.group_by_location;
self.reset();
}
pub fn reset(&mut self) {
self.selected_index = 0;
self.scroll_offset = 0;
}
}
pub fn clamp_selection(index: usize, item_count: usize) -> usize {
if item_count == 0 {
0
} else {
index.min(item_count - 1)
}
}
pub fn calculate_visible_range(
scroll_offset: usize,
viewport_height: usize,
total_items: usize,
) -> std::ops::Range<usize> {
let start = scroll_offset;
let end = (scroll_offset + viewport_height).min(total_items);
start..end
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_state() {
let state = ListState::default();
assert_eq!(state.selected_index(), 0);
assert_eq!(state.scroll_offset(), 0);
assert!(state.is_grouped());
}
#[test]
fn test_clamp_selection_empty() {
assert_eq!(clamp_selection(5, 0), 0);
}
#[test]
fn test_clamp_selection_within_bounds() {
assert_eq!(clamp_selection(3, 10), 3);
}
#[test]
fn test_clamp_selection_exceeds_bounds() {
assert_eq!(clamp_selection(15, 10), 9);
}
#[test]
fn test_clamp_selection_at_boundary() {
assert_eq!(clamp_selection(9, 10), 9);
assert_eq!(clamp_selection(10, 10), 9);
}
#[test]
fn test_set_selected_index_clamps() {
let mut state = ListState::new();
state.set_selected_index(100, 10);
assert_eq!(state.selected_index(), 9);
}
#[test]
fn test_set_selected_index_empty_list() {
let mut state = ListState::new();
state.set_selected_index(5, 0);
assert_eq!(state.selected_index(), 0);
}
#[test]
fn test_reset() {
let mut state = ListState::new();
state.set_selected_index(5, 10);
state.set_scroll_offset(3);
state.reset();
assert_eq!(state.selected_index(), 0);
assert_eq!(state.scroll_offset(), 0);
}
#[test]
fn test_scroll_offset() {
let mut state = ListState::new();
state.set_scroll_offset(10);
assert_eq!(state.scroll_offset(), 10);
}
#[test]
fn test_toggle_grouping_resets_position() {
let mut state = ListState::new();
state.set_selected_index(5, 10);
state.set_scroll_offset(3);
state.toggle_grouping();
assert!(!state.is_grouped());
assert_eq!(state.selected_index(), 0);
assert_eq!(state.scroll_offset(), 0);
}
#[test]
fn test_calculate_visible_range_normal() {
let range = calculate_visible_range(0, 10, 100);
assert_eq!(range, 0..10);
}
#[test]
fn test_calculate_visible_range_with_offset() {
let range = calculate_visible_range(5, 10, 100);
assert_eq!(range, 5..15);
}
#[test]
fn test_calculate_visible_range_clamped() {
let range = calculate_visible_range(95, 10, 100);
assert_eq!(range, 95..100);
}
#[test]
fn test_calculate_visible_range_empty() {
let range = calculate_visible_range(0, 10, 0);
assert_eq!(range, 0..0);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn selection_always_valid(
input_index in 0usize..10000,
item_count in 0usize..1000
) {
let mut state = ListState::new();
state.set_selected_index(input_index, item_count);
let selected = state.selected_index();
if item_count == 0 {
prop_assert_eq!(selected, 0);
} else {
prop_assert!(selected < item_count, "Selected {} >= count {}", selected, item_count);
}
}
#[test]
fn clamp_is_idempotent(
index in 0usize..10000,
item_count in 0usize..1000
) {
let once = clamp_selection(index, item_count);
let twice = clamp_selection(once, item_count);
prop_assert_eq!(once, twice);
}
#[test]
fn clamp_output_always_valid(
index in 0usize..10000,
item_count in 0usize..1000
) {
let result = clamp_selection(index, item_count);
if item_count == 0 {
prop_assert_eq!(result, 0);
} else {
prop_assert!(result < item_count);
}
}
#[test]
fn visible_range_end_capped(
scroll_offset in 0usize..100,
viewport_height in 0usize..100,
total_items in 0usize..200
) {
let range = calculate_visible_range(scroll_offset, viewport_height, total_items);
prop_assert!(range.end <= total_items);
}
#[test]
fn visible_range_valid_with_bounded_scroll(
total_items in 1usize..200,
viewport_height in 0usize..100,
scroll_factor in 0.0f64..1.0
) {
let scroll_offset = (scroll_factor * total_items as f64) as usize;
let range = calculate_visible_range(scroll_offset, viewport_height, total_items);
prop_assert!(range.start <= range.end, "start {} > end {} with scroll={}, total={}", range.start, range.end, scroll_offset, total_items);
prop_assert!(range.end <= total_items);
}
#[test]
fn reset_always_zeros(
selected in 0usize..1000,
scroll in 0usize..1000
) {
let mut state = ListState::new();
state.set_selected_index(selected, 1000);
state.set_scroll_offset(scroll);
state.reset();
prop_assert_eq!(state.selected_index(), 0);
prop_assert_eq!(state.scroll_offset(), 0);
}
}
}