pub trait ListNavigation {
fn selected(&self) -> usize;
fn set_selected(&mut self, idx: usize);
fn total(&self) -> usize;
fn set_total(&mut self, total: usize);
fn select_next(&mut self) {
let total = self.total();
let selected = self.selected();
if total > 0 && selected < total.saturating_sub(1) {
self.set_selected(selected + 1);
}
}
fn select_prev(&mut self) {
let selected = self.selected();
if selected > 0 {
self.set_selected(selected - 1);
}
}
fn clamp_selection(&mut self) {
let total = self.total();
let selected = self.selected();
if total == 0 {
self.set_selected(0);
} else if selected >= total {
self.set_selected(total.saturating_sub(1));
}
}
fn page_up(&mut self) {
use super::constants::PAGE_SIZE;
let selected = self.selected();
self.set_selected(selected.saturating_sub(PAGE_SIZE));
}
fn page_down(&mut self) {
use super::constants::PAGE_SIZE;
let total = self.total();
let selected = self.selected();
if total > 0 {
self.set_selected((selected + PAGE_SIZE).min(total.saturating_sub(1)));
}
}
fn go_first(&mut self) {
self.set_selected(0);
}
fn go_last(&mut self) {
let total = self.total();
if total > 0 {
self.set_selected(total.saturating_sub(1));
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ListState {
pub selected: usize,
pub total: usize,
pub scroll_offset: usize,
}
impl ListState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_total(total: usize) -> Self {
Self {
selected: 0,
total,
scroll_offset: 0,
}
}
}
impl ListNavigation for ListState {
fn selected(&self) -> usize {
self.selected
}
fn set_selected(&mut self, idx: usize) {
self.selected = idx;
}
fn total(&self) -> usize {
self.total
}
fn set_total(&mut self, total: usize) {
self.total = total;
}
}
pub trait TreeNavigation: ListNavigation {
fn is_expanded(&self, node_id: &str) -> bool;
fn expand(&mut self, node_id: &str);
fn collapse(&mut self, node_id: &str);
fn toggle_expand(&mut self, node_id: &str) {
if self.is_expanded(node_id) {
self.collapse(node_id);
} else {
self.expand(node_id);
}
}
fn expand_all(&mut self);
fn collapse_all(&mut self);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_state_navigation() {
let mut state = ListState::with_total(10);
assert_eq!(state.selected(), 0);
state.select_next();
assert_eq!(state.selected(), 1);
state.select_prev();
assert_eq!(state.selected(), 0);
state.select_prev();
assert_eq!(state.selected(), 0);
state.go_last();
assert_eq!(state.selected(), 9);
state.select_next();
assert_eq!(state.selected(), 9);
state.go_first();
assert_eq!(state.selected(), 0);
}
#[test]
fn test_list_state_page_navigation() {
let mut state = ListState::with_total(50);
state.page_down();
assert_eq!(state.selected(), 10);
state.page_down();
assert_eq!(state.selected(), 20);
state.page_up();
assert_eq!(state.selected(), 10);
state.page_up();
assert_eq!(state.selected(), 0);
state.page_up();
assert_eq!(state.selected(), 0);
}
#[test]
fn test_list_state_clamp() {
let mut state = ListState::with_total(10);
state.selected = 15;
state.clamp_selection();
assert_eq!(state.selected(), 9);
state.set_total(0);
state.clamp_selection();
assert_eq!(state.selected(), 0);
}
}