use ratatui::widgets::ListState;
pub trait ListStateExt {
fn move_up_by(&mut self, count: usize, total_items: usize);
fn move_down_by(&mut self, count: usize, total_items: usize);
fn select_first_item(&mut self, total_items: usize);
fn select_last_item(&mut self, total_items: usize);
fn page_up(&mut self, page_size: usize, total_items: usize);
fn page_down(&mut self, page_size: usize, total_items: usize);
fn select_previous_wrap(&mut self, total_items: usize);
fn select_next_wrap(&mut self, total_items: usize);
fn selected_or_first(&mut self, total_items: usize) -> Option<usize>;
}
impl ListStateExt for ListState {
fn move_up_by(&mut self, count: usize, total_items: usize) {
if total_items == 0 {
return;
}
let current = self.selected().unwrap_or(0);
let new_index = current.saturating_sub(count);
self.select(Some(new_index));
}
fn move_down_by(&mut self, count: usize, total_items: usize) {
if total_items == 0 {
return;
}
let current = self.selected().unwrap_or(0);
let new_index = (current + count).min(total_items.saturating_sub(1));
self.select(Some(new_index));
}
fn select_first_item(&mut self, total_items: usize) {
if total_items > 0 {
self.select(Some(0));
}
}
fn select_last_item(&mut self, total_items: usize) {
if total_items > 0 {
self.select(Some(total_items - 1));
}
}
fn page_up(&mut self, page_size: usize, total_items: usize) {
self.move_up_by(page_size, total_items);
}
fn page_down(&mut self, page_size: usize, total_items: usize) {
self.move_down_by(page_size, total_items);
}
fn select_previous_wrap(&mut self, total_items: usize) {
if total_items == 0 {
return;
}
let current = self.selected().unwrap_or(0);
let new_index = if current == 0 {
total_items - 1
} else {
current - 1
};
self.select(Some(new_index));
}
fn select_next_wrap(&mut self, total_items: usize) {
if total_items == 0 {
return;
}
let current = self.selected().unwrap_or(0);
let new_index = if current >= total_items - 1 {
0
} else {
current + 1
};
self.select(Some(new_index));
}
fn selected_or_first(&mut self, total_items: usize) -> Option<usize> {
if total_items == 0 {
return None;
}
if self.selected().is_none() {
self.select(Some(0));
}
self.selected()
}
}
pub const DEFAULT_PAGE_SIZE: usize = 10;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_move_up_by() {
let mut state = ListState::default();
state.select(Some(5));
state.move_up_by(3, 10);
assert_eq!(state.selected(), Some(2));
state.move_up_by(10, 10);
assert_eq!(state.selected(), Some(0));
}
#[test]
fn test_move_down_by() {
let mut state = ListState::default();
state.select(Some(5));
state.move_down_by(3, 10);
assert_eq!(state.selected(), Some(8));
state.move_down_by(10, 10);
assert_eq!(state.selected(), Some(9));
}
#[test]
fn test_select_first_last() {
let mut state = ListState::default();
state.select(Some(5));
state.select_first_item(10);
assert_eq!(state.selected(), Some(0));
state.select_last_item(10);
assert_eq!(state.selected(), Some(9));
}
#[test]
fn test_wrap_navigation() {
let mut state = ListState::default();
state.select(Some(0));
state.select_previous_wrap(5);
assert_eq!(state.selected(), Some(4));
state.select_next_wrap(5);
assert_eq!(state.selected(), Some(0));
}
#[test]
fn test_empty_list() {
let mut state = ListState::default();
state.move_up_by(1, 0);
assert_eq!(state.selected(), None);
state.move_down_by(1, 0);
assert_eq!(state.selected(), None);
state.select_first_item(0);
assert_eq!(state.selected(), None);
}
#[test]
fn test_selected_or_first() {
let mut state = ListState::default();
let idx = state.selected_or_first(5);
assert_eq!(idx, Some(0));
assert_eq!(state.selected(), Some(0));
state.select(Some(3));
let idx = state.selected_or_first(5);
assert_eq!(idx, Some(3));
}
}