use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use teksilo_core::signal::Signal;
use crate::dnd_types::{
DragEligibility, DragSource, DropCommit, DropPosition, DropQuery, DropResponse, ItemKey,
};
use crate::tree_data_source::{FlatEntry, TreeDataSource};
#[derive(Debug, Clone)]
pub struct TreeRow<K, T> {
pub key: K,
pub item: T,
pub depth: usize,
pub has_children: Option<bool>,
}
impl<K, T> TreeRow<K, T> {
pub fn new(key: K, item: T, depth: usize) -> Self {
Self {
key,
item,
depth,
has_children: None,
}
}
pub fn with_children(mut self, has_children: bool) -> Self {
self.has_children = Some(has_children);
self
}
}
type ReorderFn<K> = Rc<dyn Fn(K, K, DropPosition) -> bool>;
type DragPolicyFn<K> = Rc<dyn Fn(&K) -> DragEligibility>;
type DropResolverFn<K, T> = Rc<dyn Fn(&K, &K, &T, DropPosition) -> Option<DropPosition>>;
type SourceFn<K, T> = Rc<dyn Fn() -> Vec<TreeRow<K, T>>>;
struct Row<K, T> {
key: K,
item: T,
depth: usize,
parent: Option<K>,
has_children: bool,
declared: bool,
}
struct Built<K, T> {
rows: Vec<Row<K, T>>,
children: HashMap<K, Vec<usize>>,
roots: Vec<usize>,
row_pos: HashMap<K, usize>,
visible: Vec<usize>,
vis_pos: HashMap<K, usize>,
expanded: HashSet<K>,
seen: HashSet<K>,
}
struct Inner<K: ItemKey, T> {
rows: RefCell<Vec<Row<K, T>>>,
children: RefCell<HashMap<K, Vec<usize>>>,
roots: RefCell<Vec<usize>>,
row_pos: RefCell<HashMap<K, usize>>,
visible: RefCell<Vec<usize>>,
vis_pos: RefCell<HashMap<K, usize>>,
expanded: RefCell<HashSet<K>>,
seen: RefCell<HashSet<K>>,
expand_new: Cell<bool>,
all_expanded: Cell<bool>,
version: Signal<u64>,
version_counter: Cell<u64>,
divergence: Cell<Option<usize>>,
source: RefCell<Option<SourceFn<K, T>>>,
reorder: RefCell<Option<ReorderFn<K>>>,
drag_policy: RefCell<Option<DragPolicyFn<K>>>,
drop_resolver: RefCell<Option<DropResolverFn<K, T>>>,
}
pub struct TreeDataSlice<K: ItemKey, T> {
inner: Rc<Inner<K, T>>,
}
impl<K: ItemKey, T> Clone for TreeDataSlice<K, T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<K: ItemKey, T> Default for TreeDataSlice<K, T> {
fn default() -> Self {
Self::new()
}
}
impl<K: ItemKey, T> TreeDataSlice<K, T> {
pub fn new() -> Self {
Self {
inner: Rc::new(Inner {
rows: RefCell::new(Vec::new()),
children: RefCell::new(HashMap::new()),
roots: RefCell::new(Vec::new()),
row_pos: RefCell::new(HashMap::new()),
visible: RefCell::new(Vec::new()),
vis_pos: RefCell::new(HashMap::new()),
expanded: RefCell::new(HashSet::new()),
seen: RefCell::new(HashSet::new()),
expand_new: Cell::new(false),
all_expanded: Cell::new(false),
version: Signal::new(0),
version_counter: Cell::new(0),
divergence: Cell::new(None),
source: RefCell::new(None),
reorder: RefCell::new(None),
drag_policy: RefCell::new(None),
drop_resolver: RefCell::new(None),
}),
}
}
pub fn set_source(&self, f: impl Fn() -> Vec<TreeRow<K, T>> + 'static) {
*self.inner.source.borrow_mut() = Some(Rc::new(f));
}
pub fn set_reorder(&self, f: impl Fn(K, K, DropPosition) -> bool + 'static) {
*self.inner.reorder.borrow_mut() = Some(Rc::new(f));
}
pub fn set_drag_policy(&self, f: impl Fn(&K) -> DragEligibility + 'static) {
*self.inner.drag_policy.borrow_mut() = Some(Rc::new(f));
}
pub fn set_drop_resolver(
&self,
f: impl Fn(&K, &K, &T, DropPosition) -> Option<DropPosition> + 'static,
) {
*self.inner.drop_resolver.borrow_mut() = Some(Rc::new(f));
}
pub fn set_expand_new_nodes(&self, expand: bool) {
self.inner.expand_new.set(expand);
}
pub fn from_rows(rows: Vec<TreeRow<K, T>>) -> Self {
let slice = Self::new();
let built = slice.build(rows);
slice.commit(built);
slice
}
pub fn reload(&self)
where
T: PartialEq,
{
let f = {
let src = self.inner.source.borrow();
match src.as_ref() {
Some(f) => f.clone(),
None => return,
}
};
self.set_rows(f());
}
pub fn set_rows(&self, rows: Vec<TreeRow<K, T>>)
where
T: PartialEq,
{
let built = self.build(rows);
let all = self.inner.all_expanded.get();
let div = {
let old_rows = self.inner.rows.borrow();
let old_visible = self.inner.visible.borrow();
let old_expanded = self.inner.expanded.borrow();
common_prefix(
&old_rows,
&old_visible,
&old_expanded,
all,
&built.rows,
&built.visible,
&built.expanded,
all,
)
};
self.commit(built);
self.inner.divergence.set(Some(div));
self.bump();
}
pub fn visible_count(&self) -> usize {
self.inner.visible.borrow().len()
}
pub fn with_entry<R>(
&self,
flat_index: usize,
f: impl FnOnce(&T, &FlatEntry<K>) -> R,
) -> Option<R> {
let visible = self.inner.visible.borrow();
let &row_idx = visible.get(flat_index)?;
let rows = self.inner.rows.borrow();
let row = rows.get(row_idx)?;
let entry = FlatEntry {
node_id: row.key.clone(),
depth: row.depth,
has_children: row.has_children,
is_expanded: self.inner.all_expanded.get()
|| self.inner.expanded.borrow().contains(&row.key),
};
Some(f(&row.item, &entry))
}
pub fn with_key<R>(&self, key: &K, f: impl FnOnce(&T) -> R) -> Option<R> {
let idx = *self.inner.row_pos.borrow().get(key)?;
let rows = self.inner.rows.borrow();
rows.get(idx).map(|r| f(&r.item))
}
pub fn key_at(&self, flat_index: usize) -> Option<K> {
let visible = self.inner.visible.borrow();
let &row_idx = visible.get(flat_index)?;
self.inner.rows.borrow().get(row_idx).map(|r| r.key.clone())
}
pub fn entry_at(&self, flat_index: usize) -> Option<FlatEntry<K>> {
let visible = self.inner.visible.borrow();
let &row_idx = visible.get(flat_index)?;
let rows = self.inner.rows.borrow();
let row = rows.get(row_idx)?;
Some(FlatEntry {
node_id: row.key.clone(),
depth: row.depth,
has_children: row.has_children,
is_expanded: self.inner.all_expanded.get()
|| self.inner.expanded.borrow().contains(&row.key),
})
}
pub fn depth_at(&self, flat_index: usize) -> usize {
let visible = self.inner.visible.borrow();
visible
.get(flat_index)
.and_then(|&i| self.inner.rows.borrow().get(i).map(|r| r.depth))
.unwrap_or(0)
}
pub fn flat_index_of(&self, key: &K) -> Option<usize> {
self.inner.vis_pos.borrow().get(key).copied()
}
pub fn contains_key(&self, key: &K) -> bool {
self.inner.row_pos.borrow().contains_key(key)
}
pub fn parent_of(&self, key: &K) -> Option<K> {
let idx = *self.inner.row_pos.borrow().get(key)?;
self.inner
.rows
.borrow()
.get(idx)
.and_then(|r| r.parent.clone())
}
pub fn child_keys_of(&self, key: &K) -> Vec<K> {
let children = self.inner.children.borrow();
let rows = self.inner.rows.borrow();
children
.get(key)
.map(|idxs| {
idxs.iter()
.filter_map(|&i| rows.get(i).map(|r| r.key.clone()))
.collect()
})
.unwrap_or_default()
}
pub fn is_expanded(&self, key: &K) -> bool {
self.inner.all_expanded.get() || self.inner.expanded.borrow().contains(key)
}
pub fn expand(&self, key: &K)
where
T: PartialEq,
{
self.set_expanded_flag(key, true);
}
pub fn collapse(&self, key: &K)
where
T: PartialEq,
{
self.set_expanded_flag(key, false);
}
pub fn toggle(&self, key: &K)
where
T: PartialEq,
{
let expanded = self.inner.expanded.borrow().contains(key);
self.set_expanded_flag(key, !expanded);
}
pub fn expand_all(&self)
where
T: PartialEq,
{
let target: HashSet<K> = {
let rows = self.inner.rows.borrow();
rows.iter()
.filter(|r| r.has_children)
.map(|r| r.key.clone())
.collect()
};
self.replace_expanded(target);
}
pub fn collapse_all(&self)
where
T: PartialEq,
{
self.replace_expanded(HashSet::new());
}
pub fn expanded_keys(&self) -> Vec<K> {
self.inner.expanded.borrow().iter().cloned().collect()
}
pub fn set_expanded_keys(&self, keys: &[K])
where
T: PartialEq,
{
self.replace_expanded(keys.iter().cloned().collect());
}
pub fn version_signal(&self) -> Signal<u64> {
self.inner.version.clone()
}
pub fn first_changed_index(&self) -> Option<usize> {
self.inner.divergence.get()
}
fn set_expanded_flag(&self, key: &K, expanded: bool)
where
T: PartialEq,
{
let mut target = self.inner.expanded.borrow().clone();
let changed = if expanded {
target.insert(key.clone())
} else {
target.remove(key)
};
if !changed {
return;
}
self.replace_expanded(target);
}
fn replace_expanded(&self, target: HashSet<K>)
where
T: PartialEq,
{
let all = self.inner.all_expanded.get();
let (visible, vis_pos) = {
let rows = self.inner.rows.borrow();
let children = self.inner.children.borrow();
let roots = self.inner.roots.borrow();
flatten(&rows, &children, &roots, &target, all)
};
let div = {
let rows = self.inner.rows.borrow();
let old_visible = self.inner.visible.borrow();
let old_expanded = self.inner.expanded.borrow();
common_prefix(
&rows,
&old_visible,
&old_expanded,
all,
&rows,
&visible,
&target,
all,
)
};
*self.inner.visible.borrow_mut() = visible;
*self.inner.vis_pos.borrow_mut() = vis_pos;
*self.inner.expanded.borrow_mut() = target;
self.inner.divergence.set(Some(div));
self.bump();
}
pub fn set_all_expanded(&self, on: bool)
where
T: PartialEq,
{
if self.inner.all_expanded.get() == on {
return;
}
let expanded = self.inner.expanded.borrow().clone();
let (visible, vis_pos) = {
let rows = self.inner.rows.borrow();
let children = self.inner.children.borrow();
let roots = self.inner.roots.borrow();
flatten(&rows, &children, &roots, &expanded, on)
};
let div = {
let rows = self.inner.rows.borrow();
let old_visible = self.inner.visible.borrow();
common_prefix(
&rows,
&old_visible,
&expanded,
!on, &rows,
&visible,
&expanded,
on,
)
};
self.inner.all_expanded.set(on);
*self.inner.visible.borrow_mut() = visible;
*self.inner.vis_pos.borrow_mut() = vis_pos;
self.inner.divergence.set(Some(div));
self.bump();
}
pub fn all_expanded(&self) -> bool {
self.inner.all_expanded.get()
}
fn build(&self, input: Vec<TreeRow<K, T>>) -> Built<K, T> {
let mut rows: Vec<Row<K, T>> = Vec::with_capacity(input.len());
let mut stack: Vec<(usize, K, usize)> = Vec::new();
for tr in input {
while let Some((d, _, _)) = stack.last() {
if *d >= tr.depth {
stack.pop();
} else {
break;
}
}
let (parent, struct_depth) = match stack.last() {
Some((_, k, pidx)) => (Some(k.clone()), rows[*pidx].depth + 1),
None => (None, 0),
};
let idx = rows.len();
let key = tr.key.clone();
rows.push(Row {
key: tr.key,
item: tr.item,
depth: struct_depth,
parent,
has_children: tr.has_children.unwrap_or(false),
declared: tr.has_children.is_some(),
});
stack.push((tr.depth, key, idx));
}
let mut children: HashMap<K, Vec<usize>> = HashMap::new();
let mut roots: Vec<usize> = Vec::new();
let mut row_pos: HashMap<K, usize> = HashMap::with_capacity(rows.len());
for (i, r) in rows.iter().enumerate() {
row_pos.insert(r.key.clone(), i);
match &r.parent {
Some(pk) => children.entry(pk.clone()).or_default().push(i),
None => roots.push(i),
}
}
for r in rows.iter_mut() {
if !r.declared {
r.has_children = children.get(&r.key).is_some_and(|v| !v.is_empty());
}
}
let expand_new = self.inner.expand_new.get();
let (mut expanded, mut seen) = {
let old_exp = self.inner.expanded.borrow();
let old_seen = self.inner.seen.borrow();
let mut e = HashSet::new();
let s = old_seen.clone();
for r in &rows {
if old_seen.contains(&r.key) {
if old_exp.contains(&r.key) {
e.insert(r.key.clone());
}
} else if expand_new && r.has_children {
e.insert(r.key.clone());
}
}
(e, s)
};
for r in &rows {
seen.insert(r.key.clone());
}
expanded.retain(|k| row_pos.contains_key(k));
let (visible, vis_pos) = flatten(
&rows,
&children,
&roots,
&expanded,
self.inner.all_expanded.get(),
);
Built {
rows,
children,
roots,
row_pos,
visible,
vis_pos,
expanded,
seen,
}
}
fn commit(&self, built: Built<K, T>) {
*self.inner.rows.borrow_mut() = built.rows;
*self.inner.children.borrow_mut() = built.children;
*self.inner.roots.borrow_mut() = built.roots;
*self.inner.row_pos.borrow_mut() = built.row_pos;
*self.inner.visible.borrow_mut() = built.visible;
*self.inner.vis_pos.borrow_mut() = built.vis_pos;
*self.inner.expanded.borrow_mut() = built.expanded;
*self.inner.seen.borrow_mut() = built.seen;
}
fn bump(&self) {
let next = self.inner.version_counter.get() + 1;
self.inner.version_counter.set(next);
self.inner.version.set(next);
}
fn is_descendant(&self, maybe_descendant: &K, ancestor: &K) -> bool {
let rows = self.inner.rows.borrow();
let row_pos = self.inner.row_pos.borrow();
let mut cur = maybe_descendant.clone();
for _ in 0..rows.len() {
let Some(&idx) = row_pos.get(&cur) else {
return false;
};
let Some(parent) = rows[idx].parent.clone() else {
return false;
};
if &parent == ancestor {
return true;
}
cur = parent;
}
false
}
fn resolve(&self, dragged: &K, target: &K, position: DropPosition) -> Option<DropPosition> {
if dragged == target || self.is_descendant(target, dragged) {
return None;
}
let resolver = self.inner.drop_resolver.borrow().clone();
match resolver {
Some(f) => {
let row_pos = self.inner.row_pos.borrow();
let &idx = row_pos.get(target)?; let rows = self.inner.rows.borrow();
f(dragged, target, &rows[idx].item, position)
}
None => Some(position),
}
}
}
impl<K: ItemKey, T> std::fmt::Debug for TreeDataSlice<K, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TreeDataSlice")
.field("visible_count", &self.visible_count())
.field("row_count", &self.inner.rows.borrow().len())
.field("expanded_count", &self.inner.expanded.borrow().len())
.finish()
}
}
fn flatten<K: ItemKey, T>(
rows: &[Row<K, T>],
children: &HashMap<K, Vec<usize>>,
roots: &[usize],
expanded: &HashSet<K>,
all_expanded: bool,
) -> (Vec<usize>, HashMap<K, usize>) {
let mut visible = Vec::with_capacity(rows.len());
let mut vis_pos = HashMap::with_capacity(rows.len());
for &root in roots {
flatten_node(
root,
rows,
children,
expanded,
all_expanded,
&mut visible,
&mut vis_pos,
);
}
(visible, vis_pos)
}
fn flatten_node<K: ItemKey, T>(
idx: usize,
rows: &[Row<K, T>],
children: &HashMap<K, Vec<usize>>,
expanded: &HashSet<K>,
all_expanded: bool,
visible: &mut Vec<usize>,
vis_pos: &mut HashMap<K, usize>,
) {
let row = &rows[idx];
vis_pos.insert(row.key.clone(), visible.len());
visible.push(idx);
if row.has_children
&& (all_expanded || expanded.contains(&row.key))
&& let Some(kids) = children.get(&row.key)
{
for &child in kids {
flatten_node(
child,
rows,
children,
expanded,
all_expanded,
visible,
vis_pos,
);
}
}
}
#[allow(clippy::too_many_arguments)]
fn common_prefix<K: ItemKey, T: PartialEq>(
old_rows: &[Row<K, T>],
old_visible: &[usize],
old_expanded: &HashSet<K>,
old_all: bool,
new_rows: &[Row<K, T>],
new_visible: &[usize],
new_expanded: &HashSet<K>,
new_all: bool,
) -> usize {
let n = old_visible.len().min(new_visible.len());
for i in 0..n {
let o = &old_rows[old_visible[i]];
let m = &new_rows[new_visible[i]];
let o_exp = old_all || old_expanded.contains(&o.key);
let m_exp = new_all || new_expanded.contains(&m.key);
if o.key != m.key
|| o.depth != m.depth
|| o.has_children != m.has_children
|| o_exp != m_exp
|| o.item != m.item
{
return i;
}
}
n
}
impl<K: ItemKey, T: PartialEq + 'static> TreeDataSource for TreeDataSlice<K, T> {
type Item = T;
type Key = K;
fn visible_count(&self) -> usize {
TreeDataSlice::visible_count(self)
}
fn with_entry<R>(
&self,
flat_index: usize,
f: impl FnOnce(&Self::Item, &FlatEntry<Self::Key>) -> R,
) -> Option<R> {
TreeDataSlice::with_entry(self, flat_index, f)
}
fn key_at(&self, flat_index: usize) -> Option<K> {
TreeDataSlice::key_at(self, flat_index)
}
fn flat_index_of(&self, key: &K) -> Option<usize> {
TreeDataSlice::flat_index_of(self, key)
}
fn parent(&self, key: &K) -> Option<K> {
TreeDataSlice::parent_of(self, key)
}
fn child_keys(&self, key: &K) -> Vec<K> {
TreeDataSlice::child_keys_of(self, key)
}
fn version_signal(&self) -> Signal<u64> {
TreeDataSlice::version_signal(self)
}
fn first_changed_index(&self) -> Option<usize> {
TreeDataSlice::first_changed_index(self)
}
fn contains_key(&self, key: &K) -> bool {
TreeDataSlice::contains_key(self, key)
}
fn is_expanded(&self, key: &K) -> bool {
TreeDataSlice::is_expanded(self, key)
}
fn set_expanded(&self, key: &K, expanded: bool) {
self.set_expanded_flag(key, expanded);
}
fn drag(&self, key: &K) -> DragEligibility {
let policy = self.inner.drag_policy.borrow().clone();
match policy {
Some(f) => f(key),
None => DragEligibility::NoDrag,
}
}
fn can_accept(&self, query: &DropQuery<'_, K>) -> DropResponse {
let dragged = match &query.source {
DragSource::SameView { key } => key,
DragSource::Foreign { .. } => return DropResponse::Reject,
};
match self.resolve(dragged, &query.target, query.position) {
Some(p) if p == query.position => DropResponse::Accept,
Some(p) => DropResponse::Redirect(p),
None => DropResponse::Reject,
}
}
fn accept_drop(&self, commit: DropCommit<'_, K>) -> bool {
let dragged = match &commit.source {
DragSource::SameView { key } => key.clone(),
DragSource::Foreign { .. } => return false,
};
let Some(place) = self.resolve(&dragged, &commit.target, commit.position) else {
return false;
};
let reorder = self.inner.reorder.borrow().clone();
let applied = match reorder {
Some(f) => f(dragged, commit.target.clone(), place),
None => return false,
};
if applied {
self.reload();
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Vec<TreeRow<u64, &'static str>> {
vec![
TreeRow::new(1, "M", 0),
TreeRow::new(101, "Book", 1),
TreeRow::new(102, "Opening", 2),
TreeRow::new(103, "Dawn", 2),
TreeRow::new(104, "Ch2", 1),
TreeRow::new(105, "Fight", 2),
TreeRow::new(2, "N", 0),
TreeRow::new(106, "Sketch", 1),
]
}
fn expanded_slice() -> TreeDataSlice<u64, &'static str> {
let slice = TreeDataSlice::new();
slice.set_expand_new_nodes(true);
slice.set_source(sample);
slice.reload();
slice
}
#[test]
fn a_declared_parent_keeps_its_chevron_with_no_children_in_the_stream() {
let slice = TreeDataSlice::from_rows(vec![
TreeRow::new(1, "Scene one", 0).with_children(true),
TreeRow::new(2, "Scene two", 0).with_children(true),
]);
assert_eq!(slice.visible_count(), 2);
for i in 0..2 {
slice.with_entry(i, |_, e| {
assert!(e.has_children, "a promise the stream cannot corroborate");
});
}
}
#[test]
fn a_declared_leaf_stays_a_leaf() {
let slice = TreeDataSlice::from_rows(vec![
TreeRow::new(1, "Parent", 0).with_children(false),
TreeRow::new(2, "Child", 1),
]);
slice.set_expanded_keys(&[1]);
slice.with_entry(0, |_, e| assert!(!e.has_children));
}
#[test]
fn an_undeclared_row_is_still_derived_from_the_stream() {
let slice = TreeDataSlice::from_rows(sample());
slice.with_entry(0, |item, e| {
assert_eq!(*item, "M");
assert!(e.has_children, "M has Book and Ch2 beneath it");
});
slice.with_entry(2, |item, e| {
assert_eq!(*item, "Opening");
assert!(!e.has_children, "Opening has nothing beneath it");
});
}
#[test]
fn a_declared_parent_can_be_expanded_and_then_filled_in() {
let filled = std::rc::Rc::new(std::cell::Cell::new(false));
let slice = TreeDataSlice::new();
slice.set_source({
let filled = filled.clone();
move || {
let mut rows = vec![TreeRow::new(1, "Scene one", 0).with_children(true)];
if filled.get() {
rows.push(TreeRow::new(11, "hit at 42", 1));
rows.push(TreeRow::new(12, "hit at 91", 1));
}
rows
}
});
slice.reload();
slice.with_entry(0, |_, e| assert!(e.has_children));
assert_eq!(slice.visible_count(), 1);
slice.set_expanded_keys(&[1]);
assert!(slice.is_expanded(&1));
assert_eq!(slice.visible_count(), 1);
filled.set(true);
slice.reload();
assert!(slice.is_expanded(&1), "the expand survived the reload");
assert_eq!(
slice.visible_count(),
3,
"and the branch is now really there"
);
slice.with_entry(0, |_, e| assert!(e.has_children));
}
#[test]
fn structure_derivation() {
let slice = TreeDataSlice::from_rows(sample());
assert_eq!(slice.parent_of(&1), None); assert_eq!(slice.parent_of(&101), Some(1)); assert_eq!(slice.parent_of(&102), Some(101)); assert_eq!(slice.parent_of(&104), Some(1)); assert_eq!(slice.parent_of(&105), Some(104)); assert_eq!(slice.parent_of(&106), Some(2)); assert_eq!(slice.child_keys_of(&1), vec![101, 104]);
assert_eq!(slice.child_keys_of(&101), vec![102, 103]);
assert_eq!(slice.child_keys_of(&102), Vec::<u64>::new()); }
#[test]
fn collapsed_by_default_shows_roots() {
let slice = TreeDataSlice::from_rows(sample());
assert_eq!(slice.visible_count(), 2); assert_eq!(slice.key_at(0), Some(1));
assert_eq!(slice.key_at(1), Some(2));
}
#[test]
fn expand_new_shows_all() {
let slice = expanded_slice();
assert_eq!(slice.visible_count(), 8);
assert_eq!(
slice.with_entry(1, |item, e| {
assert_eq!(*item, "Book");
assert_eq!(e.depth, 1);
assert!(e.has_children);
}),
Some(())
);
}
#[test]
fn collapse_hides_subtree() {
let slice = expanded_slice();
assert_eq!(slice.visible_count(), 8);
slice.collapse(&101); assert_eq!(slice.visible_count(), 6);
slice.expand(&101);
assert_eq!(slice.visible_count(), 8);
}
#[test]
fn toggle_and_flat_index() {
let slice = TreeDataSlice::from_rows(sample());
assert_eq!(slice.flat_index_of(&1), Some(0));
assert_eq!(slice.flat_index_of(&101), None); slice.toggle(&1);
assert_eq!(slice.flat_index_of(&101), Some(1));
assert!(slice.is_expanded(&1));
}
#[test]
fn expand_all_collapse_all() {
let slice = TreeDataSlice::from_rows(sample());
slice.expand_all();
assert_eq!(slice.visible_count(), 8);
slice.collapse_all();
assert_eq!(slice.visible_count(), 2);
}
#[test]
fn set_all_expanded_reveals_then_restores() {
let slice = TreeDataSlice::from_rows(sample()); assert_eq!(slice.visible_count(), 2);
assert!(!slice.all_expanded());
slice.set_all_expanded(true);
assert_eq!(slice.visible_count(), 8); assert!(slice.all_expanded());
assert!(slice.is_expanded(&1));
slice.set_all_expanded(false);
assert_eq!(slice.visible_count(), 2); assert!(!slice.all_expanded());
}
#[test]
fn reveal_preserves_raw_expand_set() {
let slice = TreeDataSlice::from_rows(sample());
slice.expand(&1); assert_eq!(slice.visible_count(), 4);
slice.set_all_expanded(true);
assert_eq!(slice.visible_count(), 8);
slice.set_all_expanded(false);
assert_eq!(slice.visible_count(), 4);
assert_eq!(slice.expanded_keys(), vec![1]);
}
#[test]
fn filter_keepancestors_reveal_shows_matches() {
use crate::{TreeFilterMode, TreeRowFilter};
let sieve = TreeRowFilter::new()
.filter_mode(TreeFilterMode::KeepAncestors)
.filter(|t: &&str| *t == "Dawn");
let slice = TreeDataSlice::from_rows(sieve.apply(sample()));
assert_eq!(slice.visible_count(), 1);
slice.set_all_expanded(true);
assert_eq!(slice.visible_count(), 3);
let titles: Vec<&str> = (0..3)
.map(|i| slice.with_entry(i, |it, _| *it).unwrap())
.collect();
assert_eq!(titles, vec!["M", "Book", "Dawn"]);
}
#[test]
fn two_slices_independent_expand() {
let a = TreeDataSlice::from_rows(sample());
let b = TreeDataSlice::from_rows(sample());
a.expand(&1);
assert_eq!(a.visible_count(), 4); assert_eq!(b.visible_count(), 2); }
#[test]
fn clone_shares_state() {
let a = TreeDataSlice::from_rows(sample());
let b = a.clone();
a.expand(&1);
assert_eq!(b.visible_count(), 4); }
#[test]
fn divergence_none_before_change() {
let slice = TreeDataSlice::from_rows(sample());
assert_eq!(slice.first_changed_index(), None);
}
#[test]
fn divergence_on_expand_is_toggled_row() {
let slice = TreeDataSlice::from_rows(sample());
slice.expand(&1);
assert_eq!(slice.first_changed_index(), Some(0));
}
#[test]
fn divergence_on_deep_expand_is_that_row() {
let slice = TreeDataSlice::from_rows(sample());
slice.expand(&1); slice.expand(&101);
assert_eq!(slice.first_changed_index(), Some(1));
}
#[test]
fn divergence_on_rename_is_that_row() {
let slice = expanded_slice();
let renamed: Vec<TreeRow<u64, &'static str>> = sample()
.into_iter()
.map(|mut r| {
if r.key == 105 {
r.item = "Duel";
}
r
})
.collect();
slice.set_rows(renamed);
assert_eq!(slice.first_changed_index(), Some(5));
}
#[test]
fn divergence_on_append_is_old_len() {
let slice = expanded_slice();
let mut rows = sample();
rows.push(TreeRow::new(107, "Idea", 1)); slice.set_rows(rows);
assert_eq!(slice.first_changed_index(), Some(8));
}
#[test]
fn reload_preserves_expand_by_key() {
let counter = Rc::new(Cell::new(0u32));
let c = counter.clone();
let slice: TreeDataSlice<u64, &'static str> = TreeDataSlice::new();
slice.set_source(move || {
c.set(c.get() + 1);
sample()
});
slice.reload();
slice.expand(&1);
assert_eq!(slice.visible_count(), 4);
slice.reload(); assert_eq!(slice.visible_count(), 4); assert!(slice.is_expanded(&1));
}
#[test]
fn drag_policy_gate() {
let slice = TreeDataSlice::from_rows(sample());
slice.set_drag_policy(|k| {
if *k < 100 {
DragEligibility::NoDrag } else {
DragEligibility::CanDrag
}
});
assert_eq!(slice.drag(&1), DragEligibility::NoDrag);
assert_eq!(slice.drag(&102), DragEligibility::CanDrag);
}
#[test]
fn drag_default_is_nodrag() {
let slice = TreeDataSlice::from_rows(sample());
assert_eq!(slice.drag(&102), DragEligibility::NoDrag);
}
#[test]
fn can_accept_rejects_cycle() {
let slice = expanded_slice();
let q = DropQuery {
source: DragSource::SameView { key: 101 },
target: 102,
position: DropPosition::Into,
};
assert_eq!(slice.can_accept(&q), DropResponse::Reject);
}
#[test]
fn can_accept_default_accepts_sibling() {
let slice = expanded_slice();
let q = DropQuery {
source: DragSource::SameView { key: 102 },
target: 106,
position: DropPosition::Before,
};
assert_eq!(slice.can_accept(&q), DropResponse::Accept);
}
#[test]
fn drop_resolver_redirects() {
let slice = expanded_slice();
slice.set_drop_resolver(|_dragged, target, target_item, pos| match pos {
DropPosition::Into if *target == 103 && *target_item == "Dawn" => {
Some(DropPosition::After)
}
p => Some(p),
});
let q = DropQuery {
source: DragSource::SameView { key: 102 },
target: 103,
position: DropPosition::Into,
};
assert_eq!(
slice.can_accept(&q),
DropResponse::Redirect(DropPosition::After)
);
}
#[test]
fn accept_drop_runs_reorder_then_reloads() {
let moved = Rc::new(Cell::new(false));
let m = moved.clone();
let slice = expanded_slice();
slice.set_reorder(move |dragged, target, _pos| {
assert_eq!(dragged, 102);
assert_eq!(target, 106);
m.set(true);
true
});
let ok = slice.accept_drop(DropCommit {
source: DragSource::SameView { key: 102 },
target: 106,
position: DropPosition::Before,
});
assert!(ok);
assert!(moved.get());
}
#[test]
fn reorder_closure_can_call_back_into_the_slice() {
let slice = expanded_slice();
let reentrant_target = slice.clone();
slice.set_reorder(move |_dragged, _target, _pos| {
reentrant_target.set_reorder(|_, _, _| true);
true
});
let ok = slice.accept_drop(DropCommit {
source: DragSource::SameView { key: 102 },
target: 106,
position: DropPosition::Before,
});
assert!(ok);
}
#[test]
fn source_closure_can_call_back_into_the_slice() {
let slice = TreeDataSlice::<u64, &'static str>::new();
let reentrant = slice.clone();
slice.set_source(move || {
reentrant.set_source(Vec::new);
vec![TreeRow::new(1, "root", 0)]
});
slice.reload();
assert_eq!(slice.visible_count(), 1);
}
#[test]
fn accept_drop_without_reorder_is_refused() {
let slice = expanded_slice();
let ok = slice.accept_drop(DropCommit {
source: DragSource::SameView { key: 102 },
target: 106,
position: DropPosition::Before,
});
assert!(!ok);
}
#[test]
fn foreign_drop_rejected() {
let slice = expanded_slice();
slice.set_reorder(|_, _, _| true);
let ok = slice.accept_drop(DropCommit {
source: DragSource::SameView { key: 1 },
target: 1, position: DropPosition::Into,
});
assert!(!ok);
}
}