use std::cell::{Cell, RefCell};
use std::collections::BTreeSet;
use std::rc::Rc;
use teksilo_core::signal::Signal;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelectionMode {
None,
Single,
Multi,
}
pub struct SelectionModel {
mode: SelectionMode,
selection: Signal<BTreeSet<usize>>,
anchor: Rc<Cell<Option<usize>>>,
base: Rc<RefCell<BTreeSet<usize>>>,
extending: Rc<Cell<bool>>,
#[cfg(debug_assertions)]
debug_adapter_holder: Rc<RefCell<Option<Rc<dyn crate::debug_registry::ModelDebug>>>>,
}
impl SelectionModel {
pub fn new(mode: SelectionMode) -> Self {
Self {
mode,
selection: Signal::new(BTreeSet::new()),
anchor: Rc::new(Cell::new(None)),
base: Rc::new(RefCell::new(BTreeSet::new())),
extending: Rc::new(Cell::new(false)),
#[cfg(debug_assertions)]
debug_adapter_holder: Rc::new(RefCell::new(None)),
}
}
fn commit_base(&self, base: BTreeSet<usize>) {
*self.base.borrow_mut() = base;
self.extending.set(false);
}
pub fn mode(&self) -> SelectionMode {
self.mode
}
pub fn selection_signal(&self) -> Signal<BTreeSet<usize>> {
self.selection.clone()
}
pub fn is_selected(&self, index: usize) -> bool {
self.selection.get().contains(&index)
}
pub fn selected_indices(&self) -> Vec<usize> {
self.selection.get().into_iter().collect()
}
pub fn count(&self) -> usize {
self.selection.get().len()
}
pub fn select(&self, index: usize) {
if self.mode == SelectionMode::None {
return;
}
let mut set = BTreeSet::new();
set.insert(index);
self.selection.set(set);
self.anchor.set(Some(index));
self.commit_base(BTreeSet::new());
}
pub fn toggle(&self, index: usize) {
match self.mode {
SelectionMode::None => {}
SelectionMode::Single => self.select(index),
SelectionMode::Multi => {
let mut set = self.selection.get();
if set.contains(&index) {
set.remove(&index);
} else {
set.insert(index);
}
self.selection.set(set.clone());
self.anchor.set(Some(index));
self.commit_base(set);
}
}
}
pub fn extend_to(&self, index: usize) {
self.extend_from_base(index, false);
}
pub fn extend_to_additive(&self, index: usize) {
self.extend_from_base(index, true);
}
fn extend_from_base(&self, index: usize, additive: bool) {
match self.mode {
SelectionMode::None => {}
SelectionMode::Single => self.select(index),
SelectionMode::Multi => {
if additive && !self.extending.get() {
*self.base.borrow_mut() = self.selection.get();
}
let anchor = self.anchor.get().unwrap_or(index);
let start = anchor.min(index);
let end = anchor.max(index);
let mut set = self.base.borrow().clone();
set.extend(start..=end);
self.selection.set(set);
self.extending.set(true);
}
}
}
pub fn select_indices(&self, indices: impl IntoIterator<Item = usize>, additive: bool) {
if self.mode == SelectionMode::None {
return;
}
let mut set = if additive {
self.selection.get()
} else {
BTreeSet::new()
};
set.extend(indices);
if self.mode == SelectionMode::Single {
let last = set.iter().next_back().copied();
set = last.into_iter().collect();
}
self.selection.set(set.clone());
self.commit_base(if additive { set } else { BTreeSet::new() });
}
pub fn select_all(&self, count: usize) {
if self.mode == SelectionMode::None || self.mode == SelectionMode::Single {
return;
}
let set: BTreeSet<usize> = (0..count).collect();
self.selection.set(set.clone());
self.commit_base(set);
}
pub fn clear(&self) {
self.selection.set(BTreeSet::new());
self.anchor.set(None);
self.commit_base(BTreeSet::new());
}
pub fn adjust_for_insert(&self, start: usize, count: usize) {
let old = self.selection.get();
let mut new_set = BTreeSet::new();
for &idx in &old {
if idx >= start {
new_set.insert(idx + count);
} else {
new_set.insert(idx);
}
}
if new_set != old {
self.selection.set(new_set);
}
if let Some(a) = self.anchor.get()
&& a >= start
{
self.anchor.set(Some(a + count));
}
self.remap_base(|idx| Some(if idx >= start { idx + count } else { idx }));
}
pub fn adjust_for_remove(&self, start: usize, count: usize) {
let old = self.selection.get();
let end = start + count;
let mut new_set = BTreeSet::new();
for &idx in &old {
if idx < start {
new_set.insert(idx);
} else if idx >= end {
new_set.insert(idx - count);
}
}
if new_set != old {
self.selection.set(new_set);
}
if let Some(a) = self.anchor.get() {
if a >= end {
self.anchor.set(Some(a - count));
} else if a >= start {
self.anchor.set(None);
}
}
self.remap_base(|idx| {
if idx < start {
Some(idx)
} else if idx >= end {
Some(idx - count)
} else {
None
}
});
}
pub fn adjust_for_move(&self, from: usize, to: usize, count: usize) {
if from == to || count == 0 {
return;
}
let old = self.selection.get();
let new_set: BTreeSet<usize> = old
.iter()
.map(|&idx| crate::map_index_after_move(idx, from, to, count))
.collect();
if new_set != old {
self.selection.set(new_set);
}
if let Some(a) = self.anchor.get() {
self.anchor
.set(Some(crate::map_index_after_move(a, from, to, count)));
}
self.remap_base(|idx| Some(crate::map_index_after_move(idx, from, to, count)));
}
fn remap_base(&self, map: impl Fn(usize) -> Option<usize>) {
let mut base = self.base.borrow_mut();
if base.is_empty() {
return;
}
*base = base.iter().filter_map(|&idx| map(idx)).collect();
}
pub fn invalidate_anchor_from(&self, first_changed: usize) {
if self.anchor.get().is_some_and(|a| a >= first_changed) {
self.anchor.set(None);
}
self.remap_base(|idx| (idx < first_changed).then_some(idx));
}
}
impl Clone for SelectionModel {
fn clone(&self) -> Self {
Self {
mode: self.mode,
selection: self.selection.clone(),
anchor: self.anchor.clone(),
base: self.base.clone(),
extending: self.extending.clone(),
#[cfg(debug_assertions)]
debug_adapter_holder: self.debug_adapter_holder.clone(),
}
}
}
impl SelectionModel {
pub fn debug_named(self, _name: impl Into<String>) -> Self {
#[cfg(debug_assertions)]
{
let adapter: Rc<dyn crate::debug_registry::ModelDebug> = Rc::new(SelectionModelDebug {
selection: self.selection.clone(),
mode: self.mode,
});
crate::debug_registry::register(_name.into(), Rc::downgrade(&adapter));
*self.debug_adapter_holder.borrow_mut() = Some(adapter);
}
self
}
}
#[cfg(debug_assertions)]
struct SelectionModelDebug {
selection: Signal<BTreeSet<usize>>,
mode: SelectionMode,
}
#[cfg(debug_assertions)]
impl crate::debug_registry::ModelDebug for SelectionModelDebug {
fn kind(&self) -> &'static str {
"SelectionModel"
}
fn len(&self) -> usize {
self.selection.get().len()
}
fn debug_dump(&self, out: &mut dyn std::fmt::Write) {
let _ = writeln!(out, "mode = {:?}", self.mode);
let sel = self.selection.get();
if sel.is_empty() {
let _ = writeln!(out, "(empty)");
return;
}
for i in sel.iter() {
let _ = writeln!(out, "[{}]", i);
}
}
}
impl std::fmt::Debug for SelectionModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SelectionModel")
.field("mode", &self.mode)
.field("selected_count", &self.selection.get().len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_select() {
let model = SelectionModel::new(SelectionMode::Single);
model.select(2);
assert!(model.is_selected(2));
assert!(!model.is_selected(0));
assert_eq!(model.selected_indices(), vec![2]);
model.select(5);
assert!(!model.is_selected(2));
assert!(model.is_selected(5));
}
#[test]
fn multi_select_toggle() {
let model = SelectionModel::new(SelectionMode::Multi);
model.toggle(1);
model.toggle(3);
assert!(model.is_selected(1));
assert!(model.is_selected(3));
assert_eq!(model.count(), 2);
model.toggle(1);
assert!(!model.is_selected(1));
assert!(model.is_selected(3));
}
#[test]
fn multi_select_extend_range() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(2); model.extend_to(5); assert_eq!(model.selected_indices(), vec![2, 3, 4, 5]);
}
#[test]
fn extend_backwards() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(5);
model.extend_to(2);
assert_eq!(model.selected_indices(), vec![2, 3, 4, 5]);
}
#[test]
fn reversing_a_shift_gesture_shrinks_the_range() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(2);
model.extend_to(6);
assert_eq!(model.selected_indices(), vec![2, 3, 4, 5, 6]);
model.extend_to(4);
assert_eq!(model.selected_indices(), vec![2, 3, 4]);
model.extend_to(2);
assert_eq!(model.selected_indices(), vec![2]);
}
#[test]
fn extending_across_the_anchor_replaces_rather_than_unions() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(5);
model.extend_to(8);
assert_eq!(model.selected_indices(), vec![5, 6, 7, 8]);
model.extend_to(3);
assert_eq!(model.selected_indices(), vec![3, 4, 5]);
}
#[test]
fn shift_end_then_shift_home_selects_one_row_not_the_whole_list() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(4);
model.extend_to(9); model.extend_to(0); assert_eq!(model.selected_indices(), vec![0, 1, 2, 3, 4]);
}
#[test]
fn a_ctrl_toggle_moves_the_anchor_and_survives_the_next_shift_range() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(1);
model.toggle(5);
model.extend_to(7);
assert_eq!(model.selected_indices(), vec![1, 5, 6, 7]);
model.extend_to(6);
assert_eq!(model.selected_indices(), vec![1, 5, 6]);
}
#[test]
fn the_anchor_moves_when_a_toggle_deselects_too() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(3);
model.toggle(3); model.extend_to(5);
assert_eq!(model.selected_indices(), vec![3, 4, 5]);
}
#[test]
fn an_additive_extend_keeps_the_range_built_by_the_previous_gesture() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(0);
model.extend_to(2);
assert_eq!(model.selected_indices(), vec![0, 1, 2]);
model.toggle(6); model.extend_to_additive(8);
assert_eq!(model.selected_indices(), vec![0, 1, 2, 6, 7, 8]);
model.extend_to_additive(7);
assert_eq!(model.selected_indices(), vec![0, 1, 2, 6, 7]);
}
#[test]
fn a_plain_extend_after_a_click_discards_everything_else() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select_indices([1, 2, 8], false);
model.select(4); model.extend_to(6);
assert_eq!(model.selected_indices(), vec![4, 5, 6]);
}
#[test]
fn an_additive_marquee_is_kept_by_a_following_shift_range() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(0);
model.select_indices([7, 8], true);
model.toggle(2);
model.extend_to(4);
assert_eq!(model.selected_indices(), vec![0, 2, 3, 4, 7, 8]);
}
#[test]
fn the_gesture_base_follows_an_insert_and_a_remove() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(1);
model.toggle(5); model.adjust_for_insert(0, 2); model.extend_to(9); assert_eq!(model.selected_indices(), vec![3, 7, 8, 9]);
let model = SelectionModel::new(SelectionMode::Multi);
model.select(1);
model.toggle(5);
model.adjust_for_remove(0, 1); model.extend_to(6);
assert_eq!(model.selected_indices(), vec![0, 4, 5, 6]);
}
#[test]
fn a_reprojection_drops_an_anchor_it_has_renumbered() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(2);
model.toggle(6);
model.invalidate_anchor_from(4);
model.extend_to(8);
assert_eq!(model.selected_indices(), vec![2, 8]);
}
#[test]
fn single_mode_ignores_the_additive_extend_like_every_other_mutator() {
let model = SelectionModel::new(SelectionMode::Single);
model.select(3);
model.extend_to_additive(7);
assert_eq!(model.selected_indices(), vec![7]);
}
#[test]
fn select_all() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select_all(5);
assert_eq!(model.selected_indices(), vec![0, 1, 2, 3, 4]);
}
#[test]
fn select_indices_replaces_then_adds() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select(1);
model.select_indices([4, 5], false);
assert_eq!(model.selected_indices(), vec![4, 5]);
model.select_indices([2], true);
assert_eq!(model.selected_indices(), vec![2, 4, 5]);
}
#[test]
fn clear() {
let model = SelectionModel::new(SelectionMode::Multi);
model.select_all(3);
model.clear();
assert_eq!(model.count(), 0);
}
#[test]
fn none_mode_ignores_all() {
let model = SelectionModel::new(SelectionMode::None);
model.select(1);
assert_eq!(model.count(), 0);
model.toggle(2);
assert_eq!(model.count(), 0);
model.select_all(10);
assert_eq!(model.count(), 0);
}
#[test]
fn adjust_for_insert() {
let model = SelectionModel::new(SelectionMode::Multi);
model.toggle(1);
model.toggle(3);
model.adjust_for_insert(2, 2);
assert_eq!(model.selected_indices(), vec![1, 5]);
}
#[test]
fn adjust_for_remove() {
let model = SelectionModel::new(SelectionMode::Multi);
model.toggle(1);
model.toggle(3);
model.toggle(5);
model.adjust_for_remove(3, 1);
assert_eq!(model.selected_indices(), vec![1, 4]);
}
#[test]
fn adjust_for_move_follows_the_moved_item() {
let model = SelectionModel::new(SelectionMode::Multi);
model.toggle(0);
model.adjust_for_move(0, 2, 1);
assert_eq!(model.selected_indices(), vec![2], "selection followed A");
}
#[test]
fn adjust_for_move_shifts_a_bystander_selection() {
let model = SelectionModel::new(SelectionMode::Multi);
model.toggle(1);
model.adjust_for_move(0, 2, 1);
assert_eq!(model.selected_indices(), vec![0], "B shifted down to 0");
}
#[test]
fn adjust_for_move_backwards() {
let model = SelectionModel::new(SelectionMode::Multi);
model.toggle(3);
model.adjust_for_move(3, 1, 1);
assert_eq!(model.selected_indices(), vec![1]);
}
#[test]
fn signal_reactivity() {
use std::cell::Cell;
use std::rc::Rc;
let model = SelectionModel::new(SelectionMode::Single);
let signal = model.selection_signal();
let changed = Rc::new(Cell::new(false));
let c = changed.clone();
let _handle = signal.observe(move |_| c.set(true));
model.select(3);
assert!(changed.get());
}
#[test]
fn single_mode_extend_acts_as_select() {
let model = SelectionModel::new(SelectionMode::Single);
model.select(1);
model.extend_to(5);
assert_eq!(model.selected_indices(), vec![5]);
}
}