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 keyboard_reorder_fn: Rc<dyn Fn(usize, bool) -> Option<usize>>,
284 anchor_fn: Rc<dyn Fn(usize) -> crate::data_views::RowAnchor>,
288 version_fn: Rc<dyn Fn() -> Signal<u64>>,
289 first_changed_fn: Rc<dyn Fn() -> Option<usize>>,
290 pub(crate) dnd: TreeDndLazy,
291}
292
293impl<T: 'static> TreeSource<T> {
294 pub(crate) fn from_data_source<S: TreeDataSource<Item = T> + 'static>(s: Rc<S>) -> Self {
297 let dnd = TreeDndLazy::from_source(s.clone());
298 let root_cache: Rc<RootIndexCache> = Rc::new(RefCell::new(None));
302 let (root_cache_sib, root_cache_kbd) = (root_cache.clone(), root_cache);
303 let (s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13) = (
304 s.clone(),
305 s.clone(),
306 s.clone(),
307 s.clone(),
308 s.clone(),
309 s.clone(),
310 s.clone(),
311 s.clone(),
312 s.clone(),
313 s.clone(),
314 s.clone(),
315 s.clone(),
316 s,
317 );
318 Self {
319 visible_count_fn: Rc::new(move || s1.visible_count()),
320 anchor_fn: Rc::new(move |index| match s13.key_at(index) {
321 Some(key) => {
322 let src = s13.clone();
323 crate::data_views::RowAnchor::new(Rc::new(move || {
324 if src.key_at(index).as_ref() == Some(&key) {
326 return Some(index);
327 }
328 src.flat_index_of(&key)
329 }))
330 }
331 None => crate::data_views::RowAnchor::fixed(index),
332 }),
333 with_row_fn: Rc::new(move |index, build| {
334 s2.with_entry(index, |item, entry| {
335 let meta = TreeRowMeta {
336 depth: entry.depth,
337 has_children: entry.has_children,
338 is_expanded: entry.is_expanded,
339 };
340 build(item, &meta)
341 })
342 }),
343 with_row_str_fn: Rc::new(move |index, f| s11.with_entry(index, |item, _entry| f(item))),
344 read_item_fn: Rc::new(move |index, f| {
345 s12.with_entry(index, |item, _entry| f(item)).is_some()
346 }),
347 meta_fn: Rc::new(move |index| {
348 s3.with_entry(index, |_item, entry| TreeRowMeta {
349 depth: entry.depth,
350 has_children: entry.has_children,
351 is_expanded: entry.is_expanded,
352 })
353 }),
354 set_expanded_at_fn: Rc::new(move |index, expanded| {
355 if let Some(k) = s4.key_at(index) {
356 s4.set_expanded(&k, expanded);
357 }
358 }),
359 is_expanded_at_fn: Rc::new(move |index| {
360 s5.key_at(index)
361 .map(|k| s5.is_expanded(&k))
362 .unwrap_or(false)
363 }),
364 parent_index_fn: Rc::new(move |index| {
365 let k = s6.key_at(index)?;
366 let p = s6.parent(&k)?;
367 s6.flat_index_of(&p)
368 }),
369 sibling_pos_fn: Rc::new(move |index| {
370 let Some(k) = s7.key_at(index) else {
371 return (1, 1);
372 };
373 match s7.parent(&k) {
374 Some(p) => {
375 let sibs = s7.child_keys(&p);
376 let pos = sibs.iter().position(|x| *x == k).unwrap_or(0) + 1;
377 (pos, sibs.len().max(1))
378 }
379 None => {
380 let roots = root_indices(&*s7, &root_cache_sib);
384 let pos = roots.binary_search(&index).map(|p| p + 1).unwrap_or(1);
385 (pos, roots.len().max(1))
386 }
387 }
388 }),
389 keyboard_reorder_fn: Rc::new(move |index, down| {
390 let k = s10.key_at(index)?;
391 let siblings: Vec<S::Key> = match s10.parent(&k) {
395 Some(p) => s10.child_keys(&p),
396 None => root_indices(&*s10, &root_cache_kbd)
397 .iter()
398 .filter_map(|&j| s10.key_at(j))
399 .collect(),
400 };
401 let pos = siblings.iter().position(|x| *x == k)?;
402 let (target, position) = if down {
403 if pos + 1 >= siblings.len() {
404 return None;
405 }
406 (siblings[pos + 1].clone(), DropPosition::After)
407 } else {
408 if pos == 0 {
409 return None;
410 }
411 (siblings[pos - 1].clone(), DropPosition::Before)
412 };
413 let applied = s10.accept_drop(DropCommit {
414 source: DragSource::SameView { key: k.clone() },
415 target,
416 position,
417 });
418 if applied { s10.flat_index_of(&k) } else { None }
419 }),
420 version_fn: Rc::new(move || s8.version_signal()),
421 first_changed_fn: Rc::new(move || s9.first_changed_index()),
422 dnd,
423 }
424 }
425
426 pub(crate) fn anchor(&self, index: usize) -> crate::data_views::RowAnchor {
428 (self.anchor_fn)(index)
429 }
430
431 pub(crate) fn visible_count(&self) -> usize {
432 (self.visible_count_fn)()
433 }
434
435 pub(crate) fn with_row(
436 &self,
437 index: usize,
438 build: &dyn Fn(&T, &TreeRowMeta) -> Box<dyn Widget>,
439 ) -> Option<Box<dyn Widget>> {
440 (self.with_row_fn)(index, build)
441 }
442
443 pub(crate) fn meta(&self, index: usize) -> Option<TreeRowMeta> {
444 (self.meta_fn)(index)
445 }
446
447 pub(crate) fn with_row_str(&self, index: usize, f: &dyn Fn(&T) -> String) -> Option<String> {
449 (self.with_row_str_fn)(index, f)
450 }
451
452 pub(crate) fn depth(&self, index: usize) -> usize {
457 self.meta(index).map(|m| m.depth).unwrap_or(0)
458 }
459
460 pub(crate) fn set_expanded_at(&self, index: usize, expanded: bool) {
461 (self.set_expanded_at_fn)(index, expanded)
462 }
463
464 pub(crate) fn is_expanded_at(&self, index: usize) -> bool {
465 (self.is_expanded_at_fn)(index)
466 }
467
468 pub(crate) fn toggle_at(&self, index: usize) {
469 let expanded = (self.is_expanded_at_fn)(index);
470 (self.set_expanded_at_fn)(index, !expanded);
471 }
472
473 pub(crate) fn parent_index(&self, index: usize) -> Option<usize> {
474 (self.parent_index_fn)(index)
475 }
476
477 pub(crate) fn sibling_pos(&self, index: usize) -> (usize, usize) {
478 (self.sibling_pos_fn)(index)
479 }
480
481 pub(crate) fn keyboard_reorder(&self, index: usize, down: bool) -> Option<usize> {
485 (self.keyboard_reorder_fn)(index, down)
486 }
487
488 pub(crate) fn version_signal(&self) -> Signal<u64> {
489 (self.version_fn)()
490 }
491
492 pub(crate) fn first_changed_index(&self) -> Option<usize> {
493 (self.first_changed_fn)()
494 }
495
496 pub(crate) fn row_context(self_rc: &Rc<TreeSource<T>>, index: usize) -> TreeRow {
499 let meta = self_rc.meta(index).unwrap_or(TreeRowMeta {
500 depth: 0,
501 has_children: false,
502 is_expanded: false,
503 });
504 let src = self_rc.clone();
505 let anchor = self_rc.anchor(index);
509 TreeRow {
510 depth: meta.depth,
511 has_children: meta.has_children,
512 is_expanded: meta.is_expanded,
513 toggle: Rc::new(move |_ctx| {
514 if let Some(i) = anchor.index() {
515 src.toggle_at(i);
516 }
517 }),
518 }
519 }
520}
521
522#[cfg(test)]
523mod drag_identity_tests {
524 use super::*;
525 use std::cell::RefCell;
526 use teksilo_data::{TreeDataSlice, TreeRow};
527
528 use crate::data_views::{RowDragData, ViewId, ViewKind};
529
530 fn slice_of(keys: &[u64]) -> TreeDataSlice<u64, u64> {
531 let slice = TreeDataSlice::<u64, u64>::new();
532 let owned: Vec<u64> = keys.to_vec();
533 slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
534 slice.reload();
535 slice
536 }
537
538 fn reshape(slice: &TreeDataSlice<u64, u64>, keys: &[u64]) {
539 let owned: Vec<u64> = keys.to_vec();
540 slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
541 slice.reload();
542 }
543
544 fn same_view_payload(view_id: ViewId, rows: Vec<usize>) -> DragPayload {
545 DragPayload::typed(RowDragData::<u64> {
546 source: view_id,
547 rows,
548 items: None,
549 })
550 }
551
552 #[test]
553 fn a_reorder_moves_the_node_dragged_not_the_slot_it_left() {
554 let slice = slice_of(&[10, 20, 30]);
559 let recorded: Rc<RefCell<Vec<(u64, u64, DropPosition)>>> =
560 Rc::new(RefCell::new(Vec::new()));
561 let rec = recorded.clone();
562 slice.set_reorder(move |dragged, target, pos| {
563 rec.borrow_mut().push((dragged, target, pos));
564 true
565 });
566 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
567 let vid = ViewId::next(ViewKind::Tree);
568
569 let _thunk = (src.dnd.snapshot_out_fn)(&[2]); let payload = same_view_payload(vid, vec![2]);
571
572 reshape(&slice, &[1, 2, 10, 20, 30]); assert_eq!(
575 (src.dnd.can_accept_fn)(&payload, 0, DropPosition::Before, vid),
576 DropResponse::Accept
577 );
578 assert!((src.dnd.accept_drop_fn)(
579 &payload,
580 0,
581 DropPosition::Before,
582 vid
583 ));
584 assert_eq!(
585 recorded.borrow().as_slice(),
586 &[(30, 1, DropPosition::Before)],
587 "the dragged node's key must move, not whichever node slid into its old index"
588 );
589 }
590
591 #[test]
592 fn a_reflowed_own_node_still_rejects_a_drop_onto_itself() {
593 let slice = slice_of(&[10, 20, 30]);
596 slice.set_reorder(|_, _, _| true);
597 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
598 let vid = ViewId::next(ViewKind::Tree);
599
600 let _thunk = (src.dnd.snapshot_out_fn)(&[2]); let payload = same_view_payload(vid, vec![2]);
602
603 reshape(&slice, &[1, 2, 10, 20, 30]); assert_eq!(
606 (src.dnd.can_accept_fn)(&payload, 4, DropPosition::Before, vid),
607 DropResponse::Reject
608 );
609 assert!(!(src.dnd.accept_drop_fn)(
610 &payload,
611 4,
612 DropPosition::Before,
613 vid
614 ));
615 }
616}
617
618#[cfg(test)]
619mod anchor_tests {
620 use super::*;
621 use teksilo_data::{TreeDataSlice, TreeRow};
622
623 fn slice_of(keys: &[u64]) -> TreeDataSlice<u64, u64> {
624 let slice = TreeDataSlice::<u64, u64>::new();
625 let owned: Vec<u64> = keys.to_vec();
626 slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
627 slice.reload();
628 slice
629 }
630
631 #[test]
632 fn an_anchor_follows_its_row_when_rows_shift_above_it() {
633 let slice = slice_of(&[10, 20, 30]);
637 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
638 let anchor = src.anchor(2);
639 assert_eq!(anchor.index(), Some(2));
640
641 let shifted: Vec<u64> = vec![1, 2, 10, 20, 30];
642 slice.set_source(move || shifted.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
643 slice.reload();
644
645 assert_eq!(
646 anchor.index(),
647 Some(4),
648 "the anchor must track row 30 to its new index, not stay at 2"
649 );
650 }
651
652 #[test]
653 fn an_anchor_reports_none_once_its_row_is_gone() {
654 let slice = slice_of(&[10, 20, 30]);
657 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
658 let anchor = src.anchor(1); let remaining: Vec<u64> = vec![10, 30];
661 slice.set_source(move || remaining.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
662 slice.reload();
663
664 assert_eq!(anchor.index(), None, "row 20 is gone");
665 assert!(!anchor.is_live());
666 }
667
668 #[test]
669 fn a_keyless_source_degrades_to_a_fixed_anchor() {
670 let anchor = crate::data_views::RowAnchor::fixed(7);
673 assert_eq!(anchor.index(), Some(7));
674 assert!(anchor.is_live());
675 }
676
677 #[test]
678 fn an_editing_reconcile_converges_in_one_pass() {
679 use std::cell::RefCell;
684 use teksilo_core::signal::Signal;
685
686 let slice = slice_of(&[10, 20, 30]);
687 let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
688 let editing: Signal<Option<(usize, usize)>> = Signal::new(Some((2, 0)));
689 let slot = Rc::new(RefCell::new(None));
690 let anchor_of = |i: usize| src.anchor(i);
691
692 crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
694 assert_eq!(editing.get(), Some((2, 0)));
695
696 let shifted: Vec<u64> = vec![1, 2, 10, 20, 30];
698 slice.set_source(move || shifted.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
699 slice.reload();
700
701 crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
702 assert_eq!(editing.get(), Some((4, 0)), "corrected once");
703
704 let before = editing.get();
706 crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
707 assert_eq!(editing.get(), before, "second pass must write nothing");
708 }
709}