use std::collections::BTreeSet;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SelectionMode {
Single,
Multi,
Range,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Selection {
mode: SelectionMode,
selected: BTreeSet<usize>,
anchor: Option<usize>,
pivot: Option<usize>,
}
impl Selection {
pub fn new(mode: SelectionMode) -> Self {
Self {
mode,
selected: BTreeSet::new(),
anchor: None,
pivot: None,
}
}
pub fn select(&mut self, index: usize) {
if self.mode == SelectionMode::Single {
self.selected.clear();
}
self.selected.insert(index);
self.anchor = Some(index);
self.pivot = Some(index);
}
pub fn toggle(&mut self, index: usize) {
if self.mode != SelectionMode::Multi {
self.select(index);
return;
}
if self.selected.contains(&index) {
self.selected.remove(&index);
} else {
self.selected.insert(index);
}
self.anchor = Some(index);
}
pub fn extend_to(&mut self, index: usize) {
if self.mode == SelectionMode::Single {
self.select(index);
return;
}
let anchor = self.anchor.unwrap_or(0);
if let Some(prev_pivot) = self.pivot {
let old_start = anchor.min(prev_pivot);
let old_end = anchor.max(prev_pivot);
for i in old_start..=old_end {
self.selected.remove(&i);
}
}
let start = anchor.min(index);
let end = anchor.max(index);
for i in start..=end {
self.selected.insert(i);
}
self.pivot = Some(index);
}
pub fn select_all(&mut self, count: usize) {
self.selected.clear();
for i in 0..count {
self.selected.insert(i);
}
}
pub fn clear(&mut self) {
self.selected.clear();
self.anchor = None;
self.pivot = None;
}
pub fn is_selected(&self, index: usize) -> bool {
self.selected.contains(&index)
}
pub fn selected_indices(&self) -> &BTreeSet<usize> {
&self.selected
}
pub fn count(&self) -> usize {
self.selected.len()
}
}
pub trait VirtualDataSource: Send + Sync {
type Item;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn item_at(&self, index: usize) -> Option<&Self::Item>;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CollectionChange {
Insert {
index: usize,
},
Remove {
index: usize,
},
Update {
index: usize,
},
Move {
from: usize,
to: usize,
},
Reset,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CollectionDiff {
changes: Vec<CollectionChange>,
}
impl CollectionDiff {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, change: CollectionChange) {
self.changes.push(change);
}
pub fn changes(&self) -> &[CollectionChange] {
&self.changes
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn len(&self) -> usize {
self.changes.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisibleRange {
start: usize,
end: usize,
prefetch: usize,
}
impl VisibleRange {
pub fn new(start: usize, end: usize, prefetch: usize) -> Self {
Self {
start,
end: end.max(start),
prefetch,
}
}
pub fn contains(&self, index: usize) -> bool {
index >= self.start && index < self.end
}
pub fn prefetch_range(&self) -> (usize, usize) {
let start = self.start.saturating_sub(self.prefetch);
let end = self.end.saturating_add(self.prefetch);
(start, end)
}
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn set_range(&mut self, start: usize, end: usize) {
self.start = start;
self.end = end.max(start);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SortDirection {
Ascending,
Descending,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnDescriptor {
pub id: String,
pub label: String,
pub width: f32,
pub min_width: f32,
pub max_width: f32,
pub resizable: bool,
pub sortable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSort {
pub column_id: String,
pub direction: SortDirection,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VirtualTableModel {
columns: Vec<ColumnDescriptor>,
row_count: usize,
sort: Option<TableSort>,
selection: Selection,
visible_range: VisibleRange,
}
impl VirtualTableModel {
pub fn new(columns: Vec<ColumnDescriptor>, row_count: usize) -> Self {
Self {
columns,
row_count,
sort: None,
selection: Selection::new(SelectionMode::Single),
visible_range: VisibleRange::new(0, 0, 0),
}
}
pub fn columns(&self) -> &[ColumnDescriptor] {
&self.columns
}
pub fn row_count(&self) -> usize {
self.row_count
}
pub fn set_row_count(&mut self, count: usize) {
self.row_count = count;
}
pub fn sort(&self) -> Option<&TableSort> {
self.sort.as_ref()
}
pub fn set_sort(&mut self, sort: Option<TableSort>) {
self.sort = sort;
}
pub fn selection(&self) -> &Selection {
&self.selection
}
pub fn selection_mut(&mut self) -> &mut Selection {
&mut self.selection
}
pub fn visible_range(&self) -> &VisibleRange {
&self.visible_range
}
pub fn visible_range_mut(&mut self) -> &mut VisibleRange {
&mut self.visible_range
}
pub fn resize_column(&mut self, id: &str, width: f32) -> Result<()> {
let col = self
.columns
.iter_mut()
.find(|c| c.id == id)
.ok_or_else(|| anyhow!("column '{}' not found", id))?;
if !col.resizable {
return Err(anyhow!("column '{}' is not resizable", id));
}
col.width = width.clamp(col.min_width, col.max_width);
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreeNode<T> {
pub data: T,
pub children: Vec<TreeNode<T>>,
pub expanded: bool,
pub depth: usize,
}
#[derive(Debug, Clone)]
pub struct FlattenedTreeItem<'a, T> {
pub data: &'a T,
pub depth: usize,
pub expanded: bool,
pub has_children: bool,
pub index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreeModel<T> {
roots: Vec<TreeNode<T>>,
}
impl<T> Default for TreeModel<T> {
fn default() -> Self {
Self { roots: Vec::new() }
}
}
impl<T> TreeModel<T> {
pub fn new() -> Self {
Self::default()
}
pub fn add_root(&mut self, node: TreeNode<T>) {
self.roots.push(node);
}
pub fn roots(&self) -> &[TreeNode<T>] {
&self.roots
}
pub fn flatten(&self) -> Vec<FlattenedTreeItem<'_, T>> {
let mut items = Vec::new();
for root in &self.roots {
Self::flatten_node(root, &mut items);
}
items
}
fn flatten_node<'a>(node: &'a TreeNode<T>, items: &mut Vec<FlattenedTreeItem<'a, T>>) {
let index = items.len();
items.push(FlattenedTreeItem {
data: &node.data,
depth: node.depth,
expanded: node.expanded,
has_children: !node.children.is_empty(),
index,
});
if node.expanded {
for child in &node.children {
Self::flatten_node(child, items);
}
}
}
pub fn toggle_expanded(&mut self, path: &[usize]) -> bool {
if let Some(node) = Self::node_at_path_mut(&mut self.roots, path) {
node.expanded = !node.expanded;
node.expanded
} else {
false
}
}
pub fn total_visible_count(&self) -> usize {
self.roots.iter().map(Self::visible_count_node).sum()
}
fn visible_count_node(node: &TreeNode<T>) -> usize {
let mut count = 1;
if node.expanded {
for child in &node.children {
count += Self::visible_count_node(child);
}
}
count
}
fn node_at_path_mut<'a>(
nodes: &'a mut [TreeNode<T>],
path: &[usize],
) -> Option<&'a mut TreeNode<T>> {
if path.is_empty() {
return None;
}
let idx = path[0];
let node = nodes.get_mut(idx)?;
if path.len() == 1 {
Some(node)
} else {
Self::node_at_path_mut(&mut node.children, &path[1..])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
mod selection_tests {
use super::*;
#[test]
fn single_mode_replaces_previous() {
let mut sel = Selection::new(SelectionMode::Single);
sel.select(3);
sel.select(5);
assert!(!sel.is_selected(3));
assert!(sel.is_selected(5));
assert_eq!(sel.count(), 1);
}
#[test]
fn multi_mode_keeps_all() {
let mut sel = Selection::new(SelectionMode::Multi);
sel.select(1);
sel.select(3);
assert!(sel.is_selected(1));
assert!(sel.is_selected(3));
assert_eq!(sel.count(), 2);
}
#[test]
fn toggle_adds_and_removes() {
let mut sel = Selection::new(SelectionMode::Multi);
sel.toggle(2);
assert!(sel.is_selected(2));
sel.toggle(2);
assert!(!sel.is_selected(2));
assert_eq!(sel.count(), 0);
}
#[test]
fn toggle_in_single_mode_acts_like_select() {
let mut sel = Selection::new(SelectionMode::Single);
sel.toggle(1);
assert!(sel.is_selected(1));
sel.toggle(3);
assert!(!sel.is_selected(1));
assert!(sel.is_selected(3));
}
#[test]
fn extend_to_creates_range() {
let mut sel = Selection::new(SelectionMode::Range);
sel.select(2);
sel.extend_to(5);
for i in 2..=5 {
assert!(sel.is_selected(i), "expected {} to be selected", i);
}
assert!(!sel.is_selected(1));
assert!(!sel.is_selected(6));
}
#[test]
fn extend_to_replaces_old_range() {
let mut sel = Selection::new(SelectionMode::Range);
sel.select(5);
sel.extend_to(8);
assert_eq!(sel.count(), 4);
sel.extend_to(3);
for i in 3..=5 {
assert!(sel.is_selected(i), "expected {} selected", i);
}
assert!(!sel.is_selected(6));
assert!(!sel.is_selected(8));
}
#[test]
fn extend_to_in_single_mode_selects_endpoint() {
let mut sel = Selection::new(SelectionMode::Single);
sel.select(2);
sel.extend_to(7);
assert_eq!(sel.count(), 1);
assert!(sel.is_selected(7));
}
#[test]
fn select_all_and_clear() {
let mut sel = Selection::new(SelectionMode::Multi);
sel.select_all(10);
assert_eq!(sel.count(), 10);
sel.clear();
assert_eq!(sel.count(), 0);
}
#[test]
fn selected_indices_returns_ordered_set() {
let mut sel = Selection::new(SelectionMode::Multi);
sel.select(5);
sel.select(1);
sel.select(3);
let indices: Vec<_> = sel.selected_indices().iter().copied().collect();
assert_eq!(indices, vec![1, 3, 5]);
}
#[test]
fn empty_selection_defaults() {
let sel = Selection::new(SelectionMode::Single);
assert_eq!(sel.count(), 0);
assert!(!sel.is_selected(0));
assert!(sel.selected_indices().is_empty());
}
}
mod collection_diff_tests {
use super::*;
#[test]
fn new_diff_is_empty() {
let diff = CollectionDiff::new();
assert!(diff.is_empty());
assert_eq!(diff.len(), 0);
}
#[test]
fn push_and_query() {
let mut diff = CollectionDiff::new();
diff.push(CollectionChange::Insert { index: 0 });
diff.push(CollectionChange::Remove { index: 5 });
diff.push(CollectionChange::Update { index: 3 });
diff.push(CollectionChange::Move { from: 1, to: 4 });
diff.push(CollectionChange::Reset);
assert_eq!(diff.len(), 5);
assert!(!diff.is_empty());
assert_eq!(diff.changes()[0], CollectionChange::Insert { index: 0 });
}
}
mod visible_range_tests {
use super::*;
#[test]
fn contains_checks_exclusive_end() {
let vr = VisibleRange::new(10, 20, 5);
assert!(!vr.contains(9));
assert!(vr.contains(10));
assert!(vr.contains(19));
assert!(!vr.contains(20));
}
#[test]
fn prefetch_range_expands_both_directions() {
let vr = VisibleRange::new(10, 20, 5);
assert_eq!(vr.prefetch_range(), (5, 25));
}
#[test]
fn prefetch_clamps_at_zero() {
let vr = VisibleRange::new(2, 8, 10);
assert_eq!(vr.prefetch_range(), (0, 18));
}
#[test]
fn len_and_is_empty() {
let vr = VisibleRange::new(5, 5, 0);
assert_eq!(vr.len(), 0);
assert!(vr.is_empty());
let vr2 = VisibleRange::new(0, 10, 0);
assert_eq!(vr2.len(), 10);
assert!(!vr2.is_empty());
}
#[test]
fn set_range_updates() {
let mut vr = VisibleRange::new(0, 10, 3);
vr.set_range(20, 30);
assert!(vr.contains(25));
assert!(!vr.contains(10));
}
#[test]
fn end_clamped_to_start() {
let vr = VisibleRange::new(10, 5, 0);
assert_eq!(vr.len(), 0);
assert!(vr.is_empty());
}
}
mod virtual_table_tests {
use super::*;
fn sample_columns() -> Vec<ColumnDescriptor> {
vec![
ColumnDescriptor {
id: "name".into(),
label: "Name".into(),
width: 200.0,
min_width: 50.0,
max_width: 500.0,
resizable: true,
sortable: true,
},
ColumnDescriptor {
id: "size".into(),
label: "Size".into(),
width: 100.0,
min_width: 60.0,
max_width: 200.0,
resizable: false,
sortable: true,
},
]
}
#[test]
fn basic_construction() {
let table = VirtualTableModel::new(sample_columns(), 100);
assert_eq!(table.columns().len(), 2);
assert_eq!(table.row_count(), 100);
assert!(table.sort().is_none());
}
#[test]
fn set_and_get_sort() {
let mut table = VirtualTableModel::new(sample_columns(), 50);
table.set_sort(Some(TableSort {
column_id: "name".into(),
direction: SortDirection::Ascending,
}));
let sort = table.sort().unwrap();
assert_eq!(sort.column_id, "name");
assert_eq!(sort.direction, SortDirection::Ascending);
}
#[test]
fn resize_column_clamps() {
let mut table = VirtualTableModel::new(sample_columns(), 10);
table.resize_column("name", 1000.0).unwrap();
assert_eq!(table.columns()[0].width, 500.0);
table.resize_column("name", 10.0).unwrap();
assert_eq!(table.columns()[0].width, 50.0);
}
#[test]
fn resize_nonexistent_column_errors() {
let mut table = VirtualTableModel::new(sample_columns(), 10);
assert!(table.resize_column("missing", 100.0).is_err());
}
#[test]
fn resize_non_resizable_errors() {
let mut table = VirtualTableModel::new(sample_columns(), 10);
assert!(table.resize_column("size", 100.0).is_err());
}
#[test]
fn selection_and_visible_range_access() {
let mut table = VirtualTableModel::new(sample_columns(), 100);
table.selection_mut().select(5);
assert!(table.selection().is_selected(5));
table.visible_range_mut().set_range(10, 30);
assert!(table.visible_range().contains(20));
}
#[test]
fn set_row_count() {
let mut table = VirtualTableModel::new(sample_columns(), 0);
table.set_row_count(999);
assert_eq!(table.row_count(), 999);
}
}
mod tree_model_tests {
use super::*;
fn sample_tree() -> TreeModel<&'static str> {
let mut tree = TreeModel::new();
tree.add_root(TreeNode {
data: "root1",
children: vec![
TreeNode {
data: "child1",
children: vec![],
expanded: false,
depth: 1,
},
TreeNode {
data: "child2",
children: vec![TreeNode {
data: "grandchild",
children: vec![],
expanded: false,
depth: 2,
}],
expanded: true,
depth: 1,
},
],
expanded: true,
depth: 0,
});
tree.add_root(TreeNode {
data: "root2",
children: vec![],
expanded: false,
depth: 0,
});
tree
}
#[test]
fn flatten_respects_expanded() {
let tree = sample_tree();
let flat = tree.flatten();
let labels: Vec<_> = flat.iter().map(|f| *f.data).collect();
assert_eq!(
labels,
vec!["root1", "child1", "child2", "grandchild", "root2"]
);
}
#[test]
fn flatten_indices_are_sequential() {
let tree = sample_tree();
let flat = tree.flatten();
for (i, item) in flat.iter().enumerate() {
assert_eq!(item.index, i);
}
}
#[test]
fn flatten_has_children_flag() {
let tree = sample_tree();
let flat = tree.flatten();
assert!(flat[0].has_children);
assert!(!flat[1].has_children);
assert!(flat[2].has_children);
assert!(!flat[3].has_children);
assert!(!flat[4].has_children);
}
#[test]
fn total_visible_count_matches_flatten() {
let tree = sample_tree();
assert_eq!(tree.total_visible_count(), tree.flatten().len());
}
#[test]
fn toggle_expanded_collapses_node() {
let mut tree = sample_tree();
let new_state = tree.toggle_expanded(&[0]);
assert!(!new_state);
let flat = tree.flatten();
let labels: Vec<_> = flat.iter().map(|f| *f.data).collect();
assert_eq!(labels, vec!["root1", "root2"]);
}
#[test]
fn toggle_expanded_on_child() {
let mut tree = sample_tree();
let new_state = tree.toggle_expanded(&[0, 1]);
assert!(!new_state);
let flat = tree.flatten();
let labels: Vec<_> = flat.iter().map(|f| *f.data).collect();
assert_eq!(labels, vec!["root1", "child1", "child2", "root2"]);
}
#[test]
fn toggle_expanded_invalid_path_returns_false() {
let mut tree = sample_tree();
assert!(!tree.toggle_expanded(&[99]));
assert!(!tree.toggle_expanded(&[]));
}
#[test]
fn empty_tree() {
let tree: TreeModel<i32> = TreeModel::new();
assert!(tree.roots().is_empty());
assert!(tree.flatten().is_empty());
assert_eq!(tree.total_visible_count(), 0);
}
}
mod virtual_data_source_tests {
use super::*;
struct VecSource(Vec<String>);
impl VirtualDataSource for VecSource {
type Item = String;
fn len(&self) -> usize {
self.0.len()
}
fn item_at(&self, index: usize) -> Option<&String> {
self.0.get(index)
}
}
#[test]
fn basic_source() {
let src = VecSource(vec!["a".into(), "b".into(), "c".into()]);
assert_eq!(src.len(), 3);
assert!(!src.is_empty());
assert_eq!(src.item_at(0).unwrap(), "a");
assert_eq!(src.item_at(2).unwrap(), "c");
assert!(src.item_at(3).is_none());
}
#[test]
fn empty_source() {
let src = VecSource(vec![]);
assert_eq!(src.len(), 0);
assert!(src.is_empty());
assert!(src.item_at(0).is_none());
}
}
}