#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SelectionWrap {
#[default]
Clamp,
Wrap,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ListState {
selected: Option<usize>,
offset: usize,
wrap: SelectionWrap,
}
impl ListState {
#[must_use]
pub const fn new() -> Self {
Self {
selected: None,
offset: 0,
wrap: SelectionWrap::Clamp,
}
}
#[must_use]
pub const fn selected(&self) -> Option<usize> {
self.selected
}
#[must_use]
pub const fn offset(&self) -> usize {
self.offset
}
#[must_use]
pub const fn wrap(&self) -> SelectionWrap {
self.wrap
}
pub const fn set_wrap(&mut self, wrap: SelectionWrap) {
self.wrap = wrap;
}
pub const fn select(&mut self, index: Option<usize>) {
self.selected = index;
}
pub const fn set_offset(&mut self, offset: usize) {
self.offset = offset;
}
pub const fn reset(&mut self) {
self.selected = None;
self.offset = 0;
}
pub const fn ensure_visible(&mut self, visible_height: usize) {
let Some(selected) = self.selected else {
return;
};
if visible_height == 0 {
return;
}
if selected < self.offset {
self.offset = selected;
} else if selected >= self.offset + visible_height {
self.offset = selected + 1 - visible_height;
}
}
pub fn scroll_by(&mut self, delta: i32) {
let next = i64::from(delta).saturating_add(i64::try_from(self.offset).unwrap_or(i64::MAX));
self.offset = next.max(0).try_into().unwrap_or(usize::MAX);
}
pub fn select_next(&mut self, len: usize) {
self.selected = Self::stepped(self.selected, 1, len, self.wrap);
}
pub fn select_previous(&mut self, len: usize) {
self.selected = Self::stepped(self.selected, -1, len, self.wrap);
}
pub fn select_first(&mut self, len: usize) {
self.selected = (len > 0).then_some(0);
}
pub fn select_last(&mut self, len: usize) {
self.selected = (len > 0).then(|| len - 1);
}
fn stepped(
current: Option<usize>,
delta: i32,
len: usize,
mode: SelectionWrap,
) -> Option<usize> {
if len == 0 {
return None;
}
let Some(i) = current else {
return Some(if delta > 0 { 0 } else { len - 1 });
};
let Ok(len) = i32::try_from(len) else {
return current; };
let next = i32::try_from(i).unwrap_or(0) + delta;
let idx = match mode {
SelectionWrap::Wrap => next.rem_euclid(len),
SelectionWrap::Clamp => next.clamp(0, len - 1),
};
usize::try_from(idx).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_empty() {
let s = ListState::new();
assert_eq!(s.selected(), None);
assert_eq!(s.offset(), 0);
}
#[test]
fn next_from_none_selects_first() {
let mut s = ListState::new();
s.select_next(3);
assert_eq!(s.selected(), Some(0));
}
#[test]
fn previous_from_none_selects_last() {
let mut s = ListState::new();
s.select_previous(3);
assert_eq!(s.selected(), Some(2));
}
#[test]
fn next_clamps_at_the_end_by_default() {
let mut s = ListState::new();
assert_eq!(s.wrap(), SelectionWrap::Clamp);
s.select(Some(2));
s.select_next(3);
assert_eq!(s.selected(), Some(2)); }
#[test]
fn previous_clamps_at_the_start_by_default() {
let mut s = ListState::new();
s.select(Some(0));
s.select_previous(3);
assert_eq!(s.selected(), Some(0)); }
#[test]
fn next_wraps_past_the_end_when_wrap_is_set() {
let mut s = ListState::new();
s.set_wrap(SelectionWrap::Wrap);
s.select(Some(2));
s.select_next(3);
assert_eq!(s.selected(), Some(0));
}
#[test]
fn previous_wraps_past_the_start_when_wrap_is_set() {
let mut s = ListState::new();
s.set_wrap(SelectionWrap::Wrap);
s.select(Some(0));
s.select_previous(3);
assert_eq!(s.selected(), Some(2));
}
#[test]
fn zero_length_clears_selection() {
let mut s = ListState::new();
s.select(Some(0));
s.select_next(0);
assert_eq!(s.selected(), None);
s.select(Some(0));
s.select_previous(0);
assert_eq!(s.selected(), None);
}
#[test]
fn select_first_and_last() {
let mut s = ListState::new();
s.select_last(5);
assert_eq!(s.selected(), Some(4));
s.select_first(5);
assert_eq!(s.selected(), Some(0));
s.select_first(0);
assert_eq!(s.selected(), None);
}
#[test]
fn ensure_visible_is_a_no_op_when_already_in_view() {
let mut s = ListState::new();
s.select(Some(3));
s.set_offset(2);
s.ensure_visible(5); assert_eq!(s.offset(), 2);
}
#[test]
fn ensure_visible_scrolls_down_to_reveal_a_later_selection() {
let mut s = ListState::new();
s.select(Some(10));
s.set_offset(0);
s.ensure_visible(4); assert_eq!(s.offset(), 7); assert!(s.offset() <= 10 && 10 < s.offset() + 4);
}
#[test]
fn ensure_visible_scrolls_up_to_reveal_an_earlier_selection() {
let mut s = ListState::new();
s.select(Some(1));
s.set_offset(5);
s.ensure_visible(3); assert_eq!(s.offset(), 1);
}
#[test]
fn ensure_visible_is_a_no_op_with_nothing_selected_or_zero_height() {
let mut s = ListState::new();
s.set_offset(5);
s.ensure_visible(10); assert_eq!(s.offset(), 5);
s.select(Some(20));
s.ensure_visible(0); assert_eq!(s.offset(), 5);
}
#[test]
fn reset_clears_selection_and_offset() {
let mut s = ListState::new();
s.select(Some(2));
s.set_offset(5);
s.reset();
assert_eq!(s.selected(), None);
assert_eq!(s.offset(), 0);
}
#[test]
fn scroll_by_clamps_at_zero() {
let mut s = ListState::new();
s.scroll_by(-5);
assert_eq!(s.offset(), 0);
s.scroll_by(3);
assert_eq!(s.offset(), 3);
s.scroll_by(-1);
assert_eq!(s.offset(), 2);
}
}