1use std::cell::RefCell;
21use std::rc::Rc;
22
23use teksilo_core::drag_payload::DragPayload;
24use teksilo_core::signal::Signal;
25use teksilo_core::widget::{EventContext, Widget};
26use teksilo_data::{
27 DragEligibility, DragSource, DropCommit, DropPosition, DropQuery, DropResponse, RowState,
28 TreeDataSource,
29};
30
31use crate::data_views::{RowDragData, ViewId};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct TreeRowMeta {
36 pub depth: usize,
38 pub has_children: bool,
40 pub is_expanded: bool,
42}
43
44pub struct TreeRow {
50 pub depth: usize,
52 pub has_children: bool,
54 pub is_expanded: bool,
56 toggle: Rc<dyn Fn(&mut EventContext)>,
57}
58
59impl TreeRow {
60 pub fn toggle_callback(&self) -> Rc<dyn Fn(&mut EventContext)> {
63 self.toggle.clone()
64 }
65}
66
67type RootIndexCache = RefCell<Option<(u64, Rc<Vec<usize>>)>>;
77
78fn root_indices<S: TreeDataSource>(source: &S, cache: &RootIndexCache) -> Rc<Vec<usize>> {
81 let version = source.version_signal().get();
82 {
83 let cached = cache.borrow();
84 if let Some((v, flat)) = cached.as_ref()
85 && *v == version
86 {
87 return flat.clone();
88 }
89 }
90 let n = source.visible_count();
91 let flat = Rc::new(
92 (0..n)
93 .filter(|&j| source.with_entry(j, |_it, e| e.depth == 0).unwrap_or(false))
94 .collect::<Vec<usize>>(),
95 );
96 *cache.borrow_mut() = Some((version, flat.clone()));
97 flat
98}
99
100pub(crate) struct TreeDndLazy {
105 pub(crate) drag_fn: Rc<dyn Fn(usize) -> DragEligibility>,
107 pub(crate) can_accept_fn: Rc<dyn Fn(&DragPayload, usize, DropPosition, ViewId) -> DropResponse>,
109 pub(crate) accept_drop_fn: Rc<dyn Fn(&DragPayload, usize, DropPosition, ViewId) -> bool>,
111 pub(crate) snapshot_out_fn: crate::data_views::SnapshotOutFn,
117 pub(crate) stash_drag_keys_fn: Rc<dyn Fn(&[usize])>,
123 pub(crate) row_state_fn: Rc<dyn Fn(usize) -> RowState>,
125 pub(crate) request_window_fn: Rc<dyn Fn(std::ops::Range<usize>)>,
127 pub(crate) can_fetch_more_fn: Rc<dyn Fn() -> bool>,
129 pub(crate) fetch_more_fn: Rc<dyn Fn()>,
131}
132
133impl TreeDndLazy {
134 fn from_source<T: 'static, S: TreeDataSource<Item = T> + 'static>(s: Rc<S>) -> Self {
135 let drag_keys: Rc<RefCell<Option<Vec<S::Key>>>> = Rc::new(RefCell::new(None));
144 let (keys_ca, keys_ad, keys_snap, keys_stash) = (
145 drag_keys.clone(),
146 drag_keys.clone(),
147 drag_keys.clone(),
148 drag_keys,
149 );
150 let (s1, s2, s3, s4, s5, s6, s7, s8, s9) = (
151 s.clone(),
152 s.clone(),
153 s.clone(),
154 s.clone(),
155 s.clone(),
156 s.clone(),
157 s.clone(),
158 s.clone(),
159 s,
160 );
161 Self {
162 drag_fn: Rc::new(move |index| match s1.key_at(index) {
163 Some(k) => s1.drag(&k),
164 None => DragEligibility::NoDrag,
165 }),
166 can_accept_fn: Rc::new(move |payload, target_index, position, view_id| {
167 let Some(target_key) = s2.key_at(target_index) else {
168 return DropResponse::Reject;
169 };
170 if let Some(rd) = payload.get_typed::<RowDragData<T>>()
171 && rd.source == view_id
172 {
173 let source_key = {
174 let stash = keys_ca.borrow();
175 let Some(keys) = stash.as_ref().filter(|k| !k.is_empty()) else {
176 debug_assert!(false, "same-view drag without a drag-start key stash");
177 return DropResponse::Reject;
178 };
179 if keys.contains(&target_key) {
182 return DropResponse::Reject;
183 }
184 keys[0].clone()
185 };
186 return s2.can_accept(&DropQuery {
187 source: DragSource::SameView { key: source_key },
188 target: target_key,
189 position,
190 });
191 }
192 s2.can_accept(&DropQuery {
193 source: DragSource::Foreign { payload },
194 target: target_key,
195 position,
196 })
197 }),
198 accept_drop_fn: Rc::new(move |payload, target_index, position, view_id| {
199 let Some(target_key) = s3.key_at(target_index) else {
200 return false;
201 };
202 if let Some(rd) = payload.get_typed::<RowDragData<T>>()
203 && rd.source == view_id
204 {
205 let taken = keys_ad.borrow_mut().take();
209 let Some(keys) = taken.filter(|k| !k.is_empty()) else {
210 debug_assert!(false, "same-view drop without a drag-start key stash");
211 return false;
212 };
213 if keys.contains(&target_key) {
214 return false;
215 }
216 return s3.reorder_within(&keys, &target_key, position);
219 }
220 s3.accept_drop(DropCommit {
221 source: DragSource::Foreign { payload },
222 target: target_key,
223 position,
224 })
225 }),
226 snapshot_out_fn: Rc::new(move |indices: &[usize]| {
227 let mut pairs: Vec<(usize, S::Key)> = indices
230 .iter()
231 .filter_map(|&i| s4.key_at(i).map(|k| (i, k)))
232 .collect();
233 *keys_snap.borrow_mut() = Some(pairs.iter().map(|(_, k)| k.clone()).collect());
234 pairs.sort_by_key(|&(i, _)| std::cmp::Reverse(i));
235 let s = s4.clone();
236 Box::new(move || {
237 for (_, k) in &pairs {
238 s.on_drag_out(k);
239 }
240 }) as Box<dyn Fn()>
241 }),
242 stash_drag_keys_fn: Rc::new(move |indices: &[usize]| {
243 *keys_stash.borrow_mut() =
244 Some(indices.iter().filter_map(|&i| s9.key_at(i)).collect());
245 }),
246 row_state_fn: Rc::new(move |index| s5.row_state(index)),
247 request_window_fn: Rc::new(move |range| s6.request_window(range)),
248 can_fetch_more_fn: Rc::new(move || s7.can_fetch_more()),
249 fetch_more_fn: Rc::new(move || s8.fetch_more()),
250 }
251 }
252}
253
254pub(crate) struct TreeSource<T: 'static> {
258 visible_count_fn: Rc<dyn Fn() -> usize>,
259 with_row_fn:
262 Rc<dyn Fn(usize, &dyn Fn(&T, &TreeRowMeta) -> Box<dyn Widget>) -> Option<Box<dyn Widget>>>,
263 with_row_str_fn: Rc<dyn Fn(usize, &dyn Fn(&T) -> String) -> Option<String>>,
267 pub(crate) read_item_fn: Rc<dyn Fn(usize, &mut dyn FnMut(&T)) -> bool>,
271 meta_fn: Rc<dyn Fn(usize) -> Option<TreeRowMeta>>,
273 set_expanded_at_fn: Rc<dyn Fn(usize, bool)>,
275 is_expanded_at_fn: Rc<dyn Fn(usize) -> bool>,
277 parent_index_fn: Rc<dyn Fn(usize) -> Option<usize>>,
279 sibling_pos_fn: Rc<dyn Fn(usize) -> (usize, usize)>,
281 sibling_move_fn: Rc<dyn Fn(usize, crate::common::ordered_move::OrderedMove) -> Option<usize>>,
286 reparent_fn: Rc<dyn Fn(usize, crate::common::ordered_move::TreeMove) -> Option<usize>>,
290 anchor_fn: Rc<dyn Fn(usize) -> crate::data_views::RowAnchor>,
294 version_fn: Rc<dyn Fn() -> Signal<u64>>,
295 first_changed_fn: Rc<dyn Fn() -> Option<usize>>,
296 pub(crate) dnd: TreeDndLazy,
297}
298
299impl<T: 'static> TreeSource<T> {
300 pub(crate) fn from_data_source<S: TreeDataSource<Item = T> + 'static>(s: Rc<S>) -> Self {
303 let dnd = TreeDndLazy::from_source(s.clone());
304 let root_cache: Rc<RootIndexCache> = Rc::new(RefCell::new(None));
308 let (root_cache_sib, root_cache_kbd, root_cache_reparent) =
309 (root_cache.clone(), root_cache.clone(), root_cache);
310 let (s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14) = (
311 s.clone(),
312 s.clone(),
313 s.clone(),
314 s.clone(),
315 s.clone(),
316 s.clone(),
317 s.clone(),
318 s.clone(),
319 s.clone(),
320 s.clone(),
321 s.clone(),
322 s.clone(),
323 s.clone(),
324 s,
325 );
326 Self {
327 visible_count_fn: Rc::new(move || s1.visible_count()),
328 anchor_fn: Rc::new(move |index| match s13.key_at(index) {
329 Some(key) => {
330 let src = s13.clone();
331 crate::data_views::RowAnchor::new(Rc::new(move || {
332 if src.key_at(index).as_ref() == Some(&key) {
334 return Some(index);
335 }
336 src.flat_index_of(&key)
337 }))
338 }
339 None => crate::data_views::RowAnchor::fixed(index),
340 }),
341 with_row_fn: Rc::new(move |index, build| {
342 s2.with_entry(index, |item, entry| {
343 let meta = TreeRowMeta {
344 depth: entry.depth,
345 has_children: entry.has_children,
346 is_expanded: entry.is_expanded,
347 };
348 build(item, &meta)
349 })
350 }),
351 with_row_str_fn: Rc::new(move |index, f| s11.with_entry(index, |item, _entry| f(item))),
352 read_item_fn: Rc::new(move |index, f| {
353 s12.with_entry(index, |item, _entry| f(item)).is_some()
354 }),
355 meta_fn: Rc::new(move |index| {
356 s3.with_entry(index, |_item, entry| TreeRowMeta {
357 depth: entry.depth,
358 has_children: entry.has_children,
359 is_expanded: entry.is_expanded,
360 })
361 }),
362 set_expanded_at_fn: Rc::new(move |index, expanded| {
363 if let Some(k) = s4.key_at(index) {
364 s4.set_expanded(&k, expanded);
365 }
366 }),
367 is_expanded_at_fn: Rc::new(move |index| {
368 s5.key_at(index)
369 .map(|k| s5.is_expanded(&k))
370 .unwrap_or(false)
371 }),
372 parent_index_fn: Rc::new(move |index| {
373 let k = s6.key_at(index)?;
374 let p = s6.parent(&k)?;
375 s6.flat_index_of(&p)
376 }),
377 sibling_pos_fn: Rc::new(move |index| {
378 let Some(k) = s7.key_at(index) else {
379 return (1, 1);
380 };
381 match s7.parent(&k) {
382 Some(p) => {
383 let sibs = s7.child_keys(&p);
384 let pos = sibs.iter().position(|x| *x == k).unwrap_or(0) + 1;
385 (pos, sibs.len().max(1))
386 }
387 None => {
388 let roots = root_indices(&*s7, &root_cache_sib);
392 let pos = roots.binary_search(&index).map(|p| p + 1).unwrap_or(1);
393 (pos, roots.len().max(1))
394 }
395 }
396 }),
397 sibling_move_fn: Rc::new(move |index, mv| {
398 let k = s10.key_at(index)?;
399 let siblings: Vec<S::Key> = match s10.parent(&k) {
403 Some(p) => s10.child_keys(&p),
404 None => root_indices(&*s10, &root_cache_kbd)
405 .iter()
406 .filter_map(|&j| s10.key_at(j))
407 .collect(),
408 };
409 let pos = siblings.iter().position(|x| *x == k)?;
410 let drop = mv.as_row_drop(pos, siblings.len())?;
415 let target = siblings[drop.target].clone();
416 let applied = s10.accept_drop(DropCommit {
417 source: DragSource::SameView { key: k.clone() },
418 target,
419 position: drop.position,
420 });
421 if applied { s10.flat_index_of(&k) } else { None }
422 }),
423 reparent_fn: Rc::new(move |index, mv| {
424 use crate::common::ordered_move::TreeMove;
425 let k = s14.key_at(index)?;
426 let (target, position) = match mv {
427 TreeMove::Indent => {
428 let siblings: Vec<S::Key> = match s14.parent(&k) {
431 Some(p) => s14.child_keys(&p),
432 None => root_indices(&*s14, &root_cache_reparent)
433 .iter()
434 .filter_map(|&j| s14.key_at(j))
435 .collect(),
436 };
437 let pos = siblings.iter().position(|x| *x == k)?;
438 let target = siblings[pos.checked_sub(1)?].clone();
439 s14.set_expanded(&target, true);
445 (target, DropPosition::Into)
446 }
447 TreeMove::Outdent => (s14.parent(&k)?, DropPosition::After),
450 };
451 let applied = s14.accept_drop(DropCommit {
452 source: DragSource::SameView { key: k.clone() },
453 target,
454 position,
455 });
456 if applied { s14.flat_index_of(&k) } else { None }
457 }),
458 version_fn: Rc::new(move || s8.version_signal()),
459 first_changed_fn: Rc::new(move || s9.first_changed_index()),
460 dnd,
461 }
462 }
463
464 pub(crate) fn anchor(&self, index: usize) -> crate::data_views::RowAnchor {
466 (self.anchor_fn)(index)
467 }
468
469 pub(crate) fn visible_count(&self) -> usize {
470 (self.visible_count_fn)()
471 }
472
473 pub(crate) fn with_row(
474 &self,
475 index: usize,
476 build: &dyn Fn(&T, &TreeRowMeta) -> Box<dyn Widget>,
477 ) -> Option<Box<dyn Widget>> {
478 (self.with_row_fn)(index, build)
479 }
480
481 pub(crate) fn meta(&self, index: usize) -> Option<TreeRowMeta> {
482 (self.meta_fn)(index)
483 }
484
485 pub(crate) fn with_row_str(&self, index: usize, f: &dyn Fn(&T) -> String) -> Option<String> {
487 (self.with_row_str_fn)(index, f)
488 }
489
490 pub(crate) fn depth(&self, index: usize) -> usize {
495 self.meta(index).map(|m| m.depth).unwrap_or(0)
496 }
497
498 pub(crate) fn set_expanded_at(&self, index: usize, expanded: bool) {
499 (self.set_expanded_at_fn)(index, expanded)
500 }
501
502 pub(crate) fn is_expanded_at(&self, index: usize) -> bool {
503 (self.is_expanded_at_fn)(index)
504 }
505
506 pub(crate) fn toggle_at(&self, index: usize) {
507 let expanded = (self.is_expanded_at_fn)(index);
508 (self.set_expanded_at_fn)(index, !expanded);
509 }
510
511 pub(crate) fn parent_index(&self, index: usize) -> Option<usize> {
512 (self.parent_index_fn)(index)
513 }
514
515 pub(crate) fn sibling_pos(&self, index: usize) -> (usize, usize) {
516 (self.sibling_pos_fn)(index)
517 }
518
519 pub(crate) fn sibling_move(
523 &self,
524 index: usize,
525 mv: crate::common::ordered_move::OrderedMove,
526 ) -> Option<usize> {
527 (self.sibling_move_fn)(index, mv)
528 }
529
530 pub(crate) fn reparent(
532 &self,
533 index: usize,
534 mv: crate::common::ordered_move::TreeMove,
535 ) -> Option<usize> {
536 (self.reparent_fn)(index, mv)
537 }
538
539 pub(crate) fn sibling_position(&self, index: usize) -> (usize, usize) {
542 (self.sibling_pos_fn)(index)
543 }
544
545 pub(crate) fn version_signal(&self) -> Signal<u64> {
546 (self.version_fn)()
547 }
548
549 pub(crate) fn first_changed_index(&self) -> Option<usize> {
550 (self.first_changed_fn)()
551 }
552
553 pub(crate) fn row_context(self_rc: &Rc<TreeSource<T>>, index: usize) -> TreeRow {
556 let meta = self_rc.meta(index).unwrap_or(TreeRowMeta {
557 depth: 0,
558 has_children: false,
559 is_expanded: false,
560 });
561 let src = self_rc.clone();
562 let anchor = self_rc.anchor(index);
566 TreeRow {
567 depth: meta.depth,
568 has_children: meta.has_children,
569 is_expanded: meta.is_expanded,
570 toggle: Rc::new(move |_ctx| {
571 if let Some(i) = anchor.index() {
572 src.toggle_at(i);
573 }
574 }),
575 }
576 }
577}
578
579#[cfg(test)]
580mod drag_identity_tests {
581 use super::*;
582 use std::cell::RefCell;
583 use teksilo_data::{TreeDataSlice, TreeRow};
584
585 use crate::data_views::{RowDragData, ViewId, ViewKind};
586
587 fn slice_of(keys: &[u64]) -> TreeDataSlice<u64, u64> {
588 let slice = TreeDataSlice::<u64, u64>::new();
589 let owned: Vec<u64> = keys.to_vec();
590 slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
591 slice.reload();
592 slice
593 }
594
595 fn reshape(slice: &TreeDataSlice<u64, u64>, keys: &[u64]) {
596 let owned: Vec<u64> = keys.to_vec();
597 slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
598 slice.reload();
599 }
600
601 fn same_view_payload(view_id: ViewId, rows: Vec<usize>) -> DragPayload {
602 DragPayload::typed(RowDragData::<u64> {
603 source: view_id,
604 rows,
605 items: None,
606 })
607 }
608
609 #[test]
610 fn a_reorder_moves_the_node_dragged_not_the_slot_it_left() {
611 let slice = slice_of(&[10, 20, 30]);
616 let recorded: Rc<RefCell<Vec<(u64, u64, DropPosition)>>> =
617 Rc::new(RefCell::new(Vec::new()));
618 let rec = recorded.clone();
619 slice.set_reorder(move |dragged, target, pos| {
620 rec.borrow_mut().push((dragged, target, pos));
621 true
622 });
623 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
624 let vid = ViewId::next(ViewKind::Tree);
625
626 let _thunk = (src.dnd.snapshot_out_fn)(&[2]); let payload = same_view_payload(vid, vec![2]);
628
629 reshape(&slice, &[1, 2, 10, 20, 30]); assert_eq!(
632 (src.dnd.can_accept_fn)(&payload, 0, DropPosition::Before, vid),
633 DropResponse::Accept
634 );
635 assert!((src.dnd.accept_drop_fn)(
636 &payload,
637 0,
638 DropPosition::Before,
639 vid
640 ));
641 assert_eq!(
642 recorded.borrow().as_slice(),
643 &[(30, 1, DropPosition::Before)],
644 "the dragged node's key must move, not whichever node slid into its old index"
645 );
646 }
647
648 #[test]
649 fn a_reflowed_own_node_still_rejects_a_drop_onto_itself() {
650 let slice = slice_of(&[10, 20, 30]);
653 slice.set_reorder(|_, _, _| true);
654 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
655 let vid = ViewId::next(ViewKind::Tree);
656
657 let _thunk = (src.dnd.snapshot_out_fn)(&[2]); let payload = same_view_payload(vid, vec![2]);
659
660 reshape(&slice, &[1, 2, 10, 20, 30]); assert_eq!(
663 (src.dnd.can_accept_fn)(&payload, 4, DropPosition::Before, vid),
664 DropResponse::Reject
665 );
666 assert!(!(src.dnd.accept_drop_fn)(
667 &payload,
668 4,
669 DropPosition::Before,
670 vid
671 ));
672 }
673}
674
675#[cfg(test)]
676mod anchor_tests {
677 use super::*;
678 use teksilo_data::{TreeDataSlice, TreeRow};
679
680 fn slice_of(keys: &[u64]) -> TreeDataSlice<u64, u64> {
681 let slice = TreeDataSlice::<u64, u64>::new();
682 let owned: Vec<u64> = keys.to_vec();
683 slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
684 slice.reload();
685 slice
686 }
687
688 #[test]
689 fn an_anchor_follows_its_row_when_rows_shift_above_it() {
690 let slice = slice_of(&[10, 20, 30]);
694 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
695 let anchor = src.anchor(2);
696 assert_eq!(anchor.index(), Some(2));
697
698 let shifted: Vec<u64> = vec![1, 2, 10, 20, 30];
699 slice.set_source(move || shifted.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
700 slice.reload();
701
702 assert_eq!(
703 anchor.index(),
704 Some(4),
705 "the anchor must track row 30 to its new index, not stay at 2"
706 );
707 }
708
709 #[test]
710 fn an_anchor_reports_none_once_its_row_is_gone() {
711 let slice = slice_of(&[10, 20, 30]);
714 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
715 let anchor = src.anchor(1); let remaining: Vec<u64> = vec![10, 30];
718 slice.set_source(move || remaining.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
719 slice.reload();
720
721 assert_eq!(anchor.index(), None, "row 20 is gone");
722 assert!(!anchor.is_live());
723 }
724
725 #[test]
726 fn a_keyless_source_degrades_to_a_fixed_anchor() {
727 let anchor = crate::data_views::RowAnchor::fixed(7);
730 assert_eq!(anchor.index(), Some(7));
731 assert!(anchor.is_live());
732 }
733
734 #[test]
735 fn an_editing_reconcile_converges_in_one_pass() {
736 use std::cell::RefCell;
741 use teksilo_core::signal::Signal;
742
743 let slice = slice_of(&[10, 20, 30]);
744 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
745 let editing: Signal<Option<(usize, usize)>> = Signal::new(Some((2, 0)));
746 let slot = Rc::new(RefCell::new(None));
747 let anchor_of = |i: usize| src.anchor(i);
748
749 crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
751 assert_eq!(editing.get(), Some((2, 0)));
752
753 let shifted: Vec<u64> = vec![1, 2, 10, 20, 30];
755 slice.set_source(move || shifted.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
756 slice.reload();
757
758 crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
759 assert_eq!(editing.get(), Some((4, 0)), "corrected once");
760
761 let before = editing.get();
763 crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
764 assert_eq!(editing.get(), before, "second pass must write nothing");
765 }
766}