use std::cmp::Ordering;
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortDir {
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SelectionMode {
#[default]
None,
Single,
Multi,
}
pub fn cycle_sort(current: Option<(usize, SortDir)>, column: usize) -> Option<(usize, SortDir)> {
match current {
Some((col, SortDir::Asc)) if col == column => Some((column, SortDir::Desc)),
Some((col, SortDir::Desc)) if col == column => None,
_ => Some((column, SortDir::Asc)),
}
}
pub fn identity_order(len: usize) -> Vec<usize> {
(0..len).collect()
}
pub fn sorted_order<T>(rows: &[T], dir: SortDir, cmp: &dyn Fn(&T, &T) -> Ordering) -> Vec<usize> {
let mut order = identity_order(rows.len());
order.sort_by(|&a, &b| match dir {
SortDir::Asc => cmp(&rows[a], &rows[b]),
SortDir::Desc => cmp(&rows[b], &rows[a]),
});
order
}
fn position_of(order: &[usize], source: usize) -> Option<usize> {
order.iter().position(|&s| s == source)
}
#[derive(Debug, Clone, Default)]
pub struct SelectionState {
selected: BTreeSet<usize>,
anchor: Option<usize>,
cursor: Option<usize>,
}
impl SelectionState {
pub fn selected(&self) -> Vec<usize> {
self.selected.iter().copied().collect()
}
pub fn is_selected(&self, source: usize) -> bool {
self.selected.contains(&source)
}
pub fn cursor(&self) -> Option<usize> {
self.cursor
}
pub fn clear(&mut self) -> bool {
self.anchor = None;
self.cursor = None;
if self.selected.is_empty() {
return false;
}
self.selected.clear();
true
}
pub fn retain_below(&mut self, len: usize) -> bool {
let before = self.selected.len();
self.selected.retain(|&s| s < len);
self.anchor = self.anchor.filter(|&s| s < len);
self.cursor = self.cursor.filter(|&s| s < len);
self.selected.len() != before
}
pub fn click(
&mut self,
mode: SelectionMode,
order: &[usize],
display: usize,
toggle: bool,
range: bool,
) {
let Some(&source) = order.get(display) else {
return;
};
match mode {
SelectionMode::None => {}
SelectionMode::Single => {
if toggle && self.selected.contains(&source) {
self.selected.clear();
} else {
self.selected.clear();
self.selected.insert(source);
}
self.anchor = Some(source);
self.cursor = Some(source);
}
SelectionMode::Multi => {
if range {
let from = match self.anchor.and_then(|a| position_of(order, a)) {
Some(pos) => pos,
None => {
self.anchor = Some(source);
display
}
};
self.select_span(order, from, display);
} else if toggle {
if !self.selected.remove(&source) {
self.selected.insert(source);
}
self.anchor = Some(source);
} else {
self.selected.clear();
self.selected.insert(source);
self.anchor = Some(source);
}
self.cursor = Some(source);
}
}
}
pub fn step(
&mut self,
mode: SelectionMode,
order: &[usize],
delta: isize,
extend: bool,
) -> Option<usize> {
if matches!(mode, SelectionMode::None) || order.is_empty() {
return None;
}
let last = order.len() - 1;
let display = match self.cursor.and_then(|c| position_of(order, c)) {
Some(pos) => (pos as isize + delta).clamp(0, last as isize) as usize,
None if delta < 0 => last,
None => 0,
};
let source = order[display];
if extend && matches!(mode, SelectionMode::Multi) {
let from = match self.anchor.and_then(|a| position_of(order, a)) {
Some(pos) => pos,
None => {
self.anchor = Some(source);
display
}
};
self.select_span(order, from, display);
} else {
self.selected.clear();
self.selected.insert(source);
self.anchor = Some(source);
}
self.cursor = Some(source);
Some(display)
}
fn select_span(&mut self, order: &[usize], a: usize, b: usize) {
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
self.selected.clear();
self.selected.extend(order[lo..=hi].iter().copied());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort_cycles_asc_desc_none() {
let s1 = cycle_sort(None, 2);
assert_eq!(s1, Some((2, SortDir::Asc)));
let s2 = cycle_sort(s1, 2);
assert_eq!(s2, Some((2, SortDir::Desc)));
assert_eq!(cycle_sort(s2, 2), None);
}
#[test]
fn sort_switching_column_restarts_ascending() {
let current = Some((0, SortDir::Desc));
assert_eq!(cycle_sort(current, 1), Some((1, SortDir::Asc)));
}
#[test]
fn sorted_order_never_touches_the_source() {
let rows = vec![3, 1, 2];
let order = sorted_order(&rows, SortDir::Asc, &|a, b| a.cmp(b));
assert_eq!(order, vec![1, 2, 0]);
assert_eq!(rows, vec![3, 1, 2]);
}
#[test]
fn sorted_order_is_stable_in_both_directions() {
let rows = vec![(1, "a"), (0, "b"), (1, "c"), (0, "d")];
let cmp = |a: &(i32, &str), b: &(i32, &str)| a.0.cmp(&b.0);
assert_eq!(sorted_order(&rows, SortDir::Asc, &cmp), vec![1, 3, 0, 2]);
assert_eq!(sorted_order(&rows, SortDir::Desc, &cmp), vec![0, 2, 1, 3]);
}
#[test]
fn single_mode_holds_one_row_and_cmd_click_deselects() {
let order = identity_order(4);
let mut sel = SelectionState::default();
sel.click(SelectionMode::Single, &order, 1, false, false);
assert_eq!(sel.selected(), vec![1]);
sel.click(SelectionMode::Single, &order, 3, false, false);
assert_eq!(sel.selected(), vec![3]);
sel.click(SelectionMode::Single, &order, 3, true, false);
assert_eq!(sel.selected(), Vec::<usize>::new());
}
#[test]
fn none_mode_ignores_clicks_and_steps() {
let order = identity_order(3);
let mut sel = SelectionState::default();
sel.click(SelectionMode::None, &order, 0, false, false);
assert!(sel.selected().is_empty());
assert_eq!(sel.step(SelectionMode::None, &order, 1, false), None);
}
#[test]
fn multi_mode_cmd_click_toggles() {
let order = identity_order(4);
let mut sel = SelectionState::default();
sel.click(SelectionMode::Multi, &order, 0, false, false);
sel.click(SelectionMode::Multi, &order, 2, true, false);
assert_eq!(sel.selected(), vec![0, 2]);
sel.click(SelectionMode::Multi, &order, 0, true, false);
assert_eq!(sel.selected(), vec![2]);
}
#[test]
fn multi_mode_shift_click_selects_a_range_from_the_anchor() {
let order = identity_order(6);
let mut sel = SelectionState::default();
sel.click(SelectionMode::Multi, &order, 1, false, false);
sel.click(SelectionMode::Multi, &order, 4, false, true);
assert_eq!(sel.selected(), vec![1, 2, 3, 4]);
sel.click(SelectionMode::Multi, &order, 0, false, true);
assert_eq!(sel.selected(), vec![0, 1]);
}
#[test]
fn shift_click_first_establishes_the_anchor() {
let order = identity_order(6);
let mut sel = SelectionState::default();
sel.click(SelectionMode::Multi, &order, 2, false, true);
assert_eq!(sel.selected(), vec![2]);
sel.click(SelectionMode::Multi, &order, 5, false, true);
assert_eq!(sel.selected(), vec![2, 3, 4, 5]);
}
#[test]
fn shift_step_first_extends_from_the_entry_row() {
let order = identity_order(4);
let mut sel = SelectionState::default();
assert_eq!(sel.step(SelectionMode::Multi, &order, 1, true), Some(0));
assert_eq!(sel.selected(), vec![0]);
assert_eq!(sel.step(SelectionMode::Multi, &order, 1, true), Some(1));
assert_eq!(sel.selected(), vec![0, 1]);
}
#[test]
fn shift_range_follows_display_order_but_stores_source_indices() {
let order = vec![3, 2, 1, 0];
let mut sel = SelectionState::default();
sel.click(SelectionMode::Multi, &order, 0, false, false);
assert_eq!(sel.selected(), vec![3]);
sel.click(SelectionMode::Multi, &order, 2, false, true);
assert_eq!(sel.selected(), vec![1, 2, 3]);
}
#[test]
fn selection_survives_resorting() {
let mut sel = SelectionState::default();
sel.click(SelectionMode::Single, &identity_order(4), 2, false, false);
assert_eq!(sel.selected(), vec![2]);
assert!(sel.is_selected(2));
let order = vec![3, 2, 1, 0]; let display = sel.step(SelectionMode::Single, &order, 1, false);
assert_eq!(display, Some(2));
assert_eq!(sel.selected(), vec![1]);
}
#[test]
fn step_enters_at_the_edges_and_clamps() {
let order = identity_order(3);
let mut sel = SelectionState::default();
assert_eq!(sel.step(SelectionMode::Single, &order, 1, false), Some(0));
assert_eq!(sel.step(SelectionMode::Single, &order, -1, false), Some(0));
assert_eq!(sel.selected(), vec![0]);
let mut sel = SelectionState::default();
assert_eq!(sel.step(SelectionMode::Single, &order, -1, false), Some(2));
assert_eq!(sel.selected(), vec![2]);
}
#[test]
fn shift_step_extends_from_the_anchor() {
let order = identity_order(5);
let mut sel = SelectionState::default();
sel.click(SelectionMode::Multi, &order, 2, false, false);
sel.step(SelectionMode::Multi, &order, 1, true);
sel.step(SelectionMode::Multi, &order, 1, true);
assert_eq!(sel.selected(), vec![2, 3, 4]);
sel.step(SelectionMode::Multi, &order, -1, true);
assert_eq!(sel.selected(), vec![2, 3]);
}
#[test]
fn clear_and_retain_report_changes() {
let order = identity_order(4);
let mut sel = SelectionState::default();
assert!(!sel.clear());
sel.click(SelectionMode::Multi, &order, 1, false, false);
sel.click(SelectionMode::Multi, &order, 3, true, false);
assert!(sel.retain_below(2)); assert_eq!(sel.selected(), vec![1]);
assert!(!sel.retain_below(2));
assert!(sel.clear());
assert_eq!(sel.cursor(), None);
}
}