1use crate::mouse::MouseResult;
22use crate::stateful::Stateful;
23use crate::undo_support::{TreeUndoExt, UndoSupport, UndoWidgetId};
24use crate::{Widget, draw_text_span};
25use ftui_core::event::{MouseButton, MouseEvent, MouseEventKind};
26use ftui_core::geometry::Rect;
27use ftui_render::frame::{Frame, HitId, HitRegion};
28use ftui_style::Style;
29use std::any::Any;
30use std::collections::HashSet;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum TreeGuides {
35 Ascii,
37 #[default]
39 Unicode,
40 Bold,
42 Double,
44 Rounded,
46}
47
48impl TreeGuides {
49 #[must_use]
51 pub const fn vertical(&self) -> &str {
52 match self {
53 Self::Ascii => "| ",
54 Self::Unicode | Self::Rounded => "\u{2502} ",
55 Self::Bold => "\u{2503} ",
56 Self::Double => "\u{2551} ",
57 }
58 }
59
60 #[must_use]
62 pub const fn branch(&self) -> &str {
63 match self {
64 Self::Ascii => "+-- ",
65 Self::Unicode => "\u{251C}\u{2500}\u{2500} ",
66 Self::Bold => "\u{2523}\u{2501}\u{2501} ",
67 Self::Double => "\u{2560}\u{2550}\u{2550} ",
68 Self::Rounded => "\u{251C}\u{2500}\u{2500} ",
69 }
70 }
71
72 #[must_use]
74 pub const fn last(&self) -> &str {
75 match self {
76 Self::Ascii => "`-- ",
77 Self::Unicode => "\u{2514}\u{2500}\u{2500} ",
78 Self::Bold => "\u{2517}\u{2501}\u{2501} ",
79 Self::Double => "\u{255A}\u{2550}\u{2550} ",
80 Self::Rounded => "\u{2570}\u{2500}\u{2500} ",
81 }
82 }
83
84 #[must_use]
86 pub const fn space(&self) -> &str {
87 " "
88 }
89
90 #[inline]
92 #[must_use]
93 pub fn width(&self) -> usize {
94 4
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct TreeNode {
101 label: String,
102 pub(crate) children: Vec<TreeNode>,
104 pub(crate) expanded: bool,
106}
107
108impl TreeNode {
109 #[must_use]
111 pub fn new(label: impl Into<String>) -> Self {
112 Self {
113 label: label.into(),
114 children: Vec::new(),
115 expanded: true,
116 }
117 }
118
119 #[must_use]
121 pub fn child(mut self, node: TreeNode) -> Self {
122 self.children.push(node);
123 self
124 }
125
126 #[must_use]
128 pub fn with_children(mut self, nodes: Vec<TreeNode>) -> Self {
129 self.children = nodes;
130 self
131 }
132
133 #[must_use]
135 pub fn with_expanded(mut self, expanded: bool) -> Self {
136 self.expanded = expanded;
137 self
138 }
139
140 #[must_use]
142 pub fn label(&self) -> &str {
143 &self.label
144 }
145
146 #[must_use]
148 pub fn children(&self) -> &[TreeNode] {
149 &self.children
150 }
151
152 #[must_use]
154 pub fn is_expanded(&self) -> bool {
155 self.expanded
156 }
157
158 pub fn toggle_expanded(&mut self) {
160 self.expanded = !self.expanded;
161 }
162
163 #[must_use]
165 pub fn visible_count(&self) -> usize {
166 let mut count = 1;
167 if self.expanded {
168 for child in &self.children {
169 count += child.visible_count();
170 }
171 }
172 count
173 }
174
175 #[allow(dead_code)]
177 pub(crate) fn collect_expanded(&self, prefix: &str, out: &mut HashSet<String>) {
178 let path = if prefix.is_empty() {
179 self.label.clone()
180 } else {
181 format!("{}/{}", prefix, self.label)
182 };
183
184 if self.expanded && !self.children.is_empty() {
185 out.insert(path.clone());
186 }
187
188 for child in &self.children {
189 child.collect_expanded(&path, out);
190 }
191 }
192
193 #[allow(dead_code)]
195 pub(crate) fn apply_expanded(&mut self, prefix: &str, expanded_paths: &HashSet<String>) {
196 let path = if prefix.is_empty() {
197 self.label.clone()
198 } else {
199 format!("{}/{}", prefix, self.label)
200 };
201
202 if !self.children.is_empty() {
203 self.expanded = expanded_paths.contains(&path);
204 }
205
206 for child in &mut self.children {
207 child.apply_expanded(&path, expanded_paths);
208 }
209 }
210}
211
212#[derive(Debug, Clone)]
214pub struct Tree {
215 undo_id: UndoWidgetId,
217 root: TreeNode,
218 show_root: bool,
220 guides: TreeGuides,
222 guide_style: Style,
224 label_style: Style,
226 root_style: Style,
228 persistence_id: Option<String>,
230 hit_id: Option<HitId>,
232}
233
234impl Tree {
235 #[must_use]
237 pub fn new(root: TreeNode) -> Self {
238 Self {
239 undo_id: UndoWidgetId::new(),
240 root,
241 show_root: true,
242 guides: TreeGuides::default(),
243 guide_style: Style::default(),
244 label_style: Style::default(),
245 root_style: Style::default(),
246 persistence_id: None,
247 hit_id: None,
248 }
249 }
250
251 #[must_use]
253 pub fn with_show_root(mut self, show: bool) -> Self {
254 self.show_root = show;
255 self
256 }
257
258 #[must_use]
260 pub fn with_guides(mut self, guides: TreeGuides) -> Self {
261 self.guides = guides;
262 self
263 }
264
265 #[must_use]
267 pub fn with_guide_style(mut self, style: Style) -> Self {
268 self.guide_style = style;
269 self
270 }
271
272 #[must_use]
274 pub fn with_label_style(mut self, style: Style) -> Self {
275 self.label_style = style;
276 self
277 }
278
279 #[must_use]
281 pub fn with_root_style(mut self, style: Style) -> Self {
282 self.root_style = style;
283 self
284 }
285
286 #[must_use]
288 pub fn with_persistence_id(mut self, id: impl Into<String>) -> Self {
289 self.persistence_id = Some(id.into());
290 self
291 }
292
293 #[must_use]
295 pub fn persistence_id(&self) -> Option<&str> {
296 self.persistence_id.as_deref()
297 }
298
299 #[must_use]
301 pub fn hit_id(mut self, id: HitId) -> Self {
302 self.hit_id = Some(id);
303 self
304 }
305
306 #[must_use]
308 pub fn root(&self) -> &TreeNode {
309 &self.root
310 }
311
312 pub fn root_mut(&mut self) -> &mut TreeNode {
314 &mut self.root
315 }
316
317 #[allow(clippy::too_many_arguments)]
318 fn render_node(
319 &self,
320 node: &TreeNode,
321 depth: usize,
322 is_last: &mut Vec<bool>,
323 area: Rect,
324 frame: &mut Frame,
325 current_row: &mut usize,
326 deg: ftui_render::budget::DegradationLevel,
327 ) {
328 if *current_row >= area.height as usize {
329 return;
330 }
331
332 let y = area.y.saturating_add(*current_row as u16);
333 let mut x = area.x;
334 let max_x = area.right();
335
336 if depth > 0 && deg.apply_styling() {
338 for d in 0..depth {
339 let is_last_at_depth = is_last.get(d).copied().unwrap_or(false);
340 let guide = if d == depth - 1 {
341 if is_last_at_depth {
343 self.guides.last()
344 } else {
345 self.guides.branch()
346 }
347 } else {
348 if is_last_at_depth {
350 self.guides.space()
351 } else {
352 self.guides.vertical()
353 }
354 };
355
356 x = draw_text_span(frame, x, y, guide, self.guide_style, max_x);
357 }
358 } else if depth > 0 {
359 for _ in 0..depth {
362 x = draw_text_span(frame, x, y, " ", Style::default(), max_x);
363 if x >= max_x {
364 break;
365 }
366 }
367 }
368
369 let style = if depth == 0 && self.show_root {
371 self.root_style
372 } else {
373 self.label_style
374 };
375
376 if deg.apply_styling() {
377 draw_text_span(frame, x, y, &node.label, style, max_x);
378 } else {
379 draw_text_span(frame, x, y, &node.label, Style::default(), max_x);
380 }
381
382 if let Some(id) = self.hit_id {
384 let row_area = Rect::new(area.x, y, area.width, 1);
385 frame.register_hit(row_area, id, HitRegion::Content, *current_row as u64);
386 }
387
388 *current_row += 1;
389
390 if !node.expanded {
391 return;
392 }
393
394 let child_count = node.children.len();
395 for (i, child) in node.children.iter().enumerate() {
396 is_last.push(i == child_count - 1);
397 self.render_node(child, depth + 1, is_last, area, frame, current_row, deg);
398 is_last.pop();
399 }
400 }
401}
402
403impl Widget for Tree {
404 fn render(&self, area: Rect, frame: &mut Frame) {
405 if area.width == 0 || area.height == 0 {
406 return;
407 }
408
409 let deg = frame.buffer.degradation;
410 let mut current_row = 0;
411 let mut is_last = Vec::with_capacity(8);
412
413 if self.show_root {
414 self.render_node(
415 &self.root,
416 0,
417 &mut is_last,
418 area,
419 frame,
420 &mut current_row,
421 deg,
422 );
423 } else if self.root.expanded {
424 let child_count = self.root.children.len();
428 for (i, child) in self.root.children.iter().enumerate() {
429 is_last.push(i == child_count - 1);
430 self.render_node(
431 child,
432 0, &mut is_last,
434 area,
435 frame,
436 &mut current_row,
437 deg,
438 );
439 is_last.pop();
440 }
441 }
442 }
443
444 fn is_essential(&self) -> bool {
445 false
446 }
447}
448
449#[derive(Clone, Debug, Default, PartialEq)]
457#[cfg_attr(
458 feature = "state-persistence",
459 derive(serde::Serialize, serde::Deserialize)
460)]
461pub struct TreePersistState {
462 pub expanded_paths: HashSet<String>,
464}
465
466impl crate::stateful::Stateful for Tree {
467 type State = TreePersistState;
468
469 fn state_key(&self) -> crate::stateful::StateKey {
470 crate::stateful::StateKey::new("Tree", self.persistence_id.as_deref().unwrap_or("default"))
471 }
472
473 fn save_state(&self) -> TreePersistState {
474 let mut expanded_paths = HashSet::new();
475 self.root.collect_expanded("", &mut expanded_paths);
476 TreePersistState { expanded_paths }
477 }
478
479 fn restore_state(&mut self, state: TreePersistState) {
480 self.root.apply_expanded("", &state.expanded_paths);
481 }
482}
483
484impl UndoSupport for Tree {
489 fn undo_widget_id(&self) -> UndoWidgetId {
490 self.undo_id
491 }
492
493 fn create_snapshot(&self) -> Box<dyn Any + Send> {
494 Box::new(self.save_state())
495 }
496
497 fn restore_snapshot(&mut self, snapshot: &dyn Any) -> bool {
498 if let Some(snap) = snapshot.downcast_ref::<TreePersistState>() {
499 self.restore_state(snap.clone());
500 true
501 } else {
502 false
503 }
504 }
505}
506
507impl TreeUndoExt for Tree {
508 fn is_node_expanded(&self, path: &[usize]) -> bool {
509 self.get_node_at_path(path)
510 .map(|node| node.is_expanded())
511 .unwrap_or(false)
512 }
513
514 fn expand_node(&mut self, path: &[usize]) {
515 if let Some(node) = self.get_node_at_path_mut(path) {
516 node.expanded = true;
517 }
518 }
519
520 fn collapse_node(&mut self, path: &[usize]) {
521 if let Some(node) = self.get_node_at_path_mut(path) {
522 node.expanded = false;
523 }
524 }
525}
526
527impl Tree {
528 #[must_use]
530 pub fn undo_id(&self) -> UndoWidgetId {
531 self.undo_id
532 }
533
534 fn get_node_at_path(&self, path: &[usize]) -> Option<&TreeNode> {
536 let mut current = &self.root;
537 for &idx in path {
538 current = current.children.get(idx)?;
539 }
540 Some(current)
541 }
542
543 fn get_node_at_path_mut(&mut self, path: &[usize]) -> Option<&mut TreeNode> {
545 let mut current = &mut self.root;
546 for &idx in path {
547 current = current.children.get_mut(idx)?;
548 }
549 Some(current)
550 }
551
552 pub fn handle_mouse(
569 &mut self,
570 event: &MouseEvent,
571 hit: Option<(HitId, HitRegion, u64)>,
572 expected_id: HitId,
573 ) -> MouseResult {
574 match event.kind {
575 MouseEventKind::Down(MouseButton::Left) => {
576 if let Some((id, HitRegion::Content, data)) = hit
577 && id == expected_id
578 {
579 let index = data as usize;
580 if let Some(node) = self.node_at_visible_index_mut(index) {
581 if node.children.is_empty() {
582 return MouseResult::Selected(index);
583 }
584 node.toggle_expanded();
585 return MouseResult::Activated(index);
586 }
587 }
588 MouseResult::Ignored
589 }
590 _ => MouseResult::Ignored,
591 }
592 }
593
594 pub fn node_at_visible_index_mut(&mut self, target: usize) -> Option<&mut TreeNode> {
600 let mut counter = 0usize;
601 if self.show_root {
602 Self::walk_visible_mut(&mut self.root, target, &mut counter)
603 } else if self.root.expanded {
604 for child in &mut self.root.children {
605 if let Some(node) = Self::walk_visible_mut(child, target, &mut counter) {
606 return Some(node);
607 }
608 }
609 None
610 } else {
611 None
612 }
613 }
614
615 fn walk_visible_mut<'a>(
618 node: &'a mut TreeNode,
619 target: usize,
620 counter: &mut usize,
621 ) -> Option<&'a mut TreeNode> {
622 if *counter == target {
623 return Some(node);
624 }
625 *counter += 1;
626 if node.expanded {
627 for child in &mut node.children {
628 if let Some(found) = Self::walk_visible_mut(child, target, counter) {
629 return Some(found);
630 }
631 }
632 }
633 None
634 }
635}
636
637#[cfg(test)]
642#[derive(Debug, Clone, PartialEq, Eq)]
643struct FlatNode {
644 label: String,
645 depth: usize,
646}
647
648#[cfg(test)]
649fn flatten_visible(node: &TreeNode, depth: usize, out: &mut Vec<FlatNode>) {
650 out.push(FlatNode {
651 label: node.label.clone(),
652 depth,
653 });
654 if node.expanded {
655 for child in &node.children {
656 flatten_visible(child, depth + 1, out);
657 }
658 }
659}
660
661#[cfg(test)]
662impl Tree {
663 fn flatten(&self) -> Vec<FlatNode> {
664 let mut out = Vec::new();
665 if self.show_root {
666 flatten_visible(&self.root, 0, &mut out);
667 } else if self.root.expanded {
668 for child in &self.root.children {
669 flatten_visible(child, 0, &mut out);
670 }
671 }
672 out
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use ftui_render::frame::Frame;
680 use ftui_render::grapheme_pool::GraphemePool;
681
682 fn simple_tree() -> TreeNode {
683 TreeNode::new("root")
684 .child(
685 TreeNode::new("a")
686 .child(TreeNode::new("a1"))
687 .child(TreeNode::new("a2")),
688 )
689 .child(TreeNode::new("b"))
690 }
691
692 #[test]
693 fn tree_node_basics() {
694 let node = TreeNode::new("hello");
695 assert_eq!(node.label(), "hello");
696 assert!(node.children().is_empty());
697 assert!(node.is_expanded());
698 }
699
700 #[test]
701 fn tree_node_children() {
702 let root = simple_tree();
703 assert_eq!(root.children().len(), 2);
704 assert_eq!(root.children()[0].label(), "a");
705 assert_eq!(root.children()[0].children().len(), 2);
706 }
707
708 #[test]
709 fn tree_node_visible_count() {
710 let root = simple_tree();
711 assert_eq!(root.visible_count(), 5);
713 }
714
715 #[test]
716 fn tree_node_collapsed() {
717 let root = TreeNode::new("root")
718 .child(
719 TreeNode::new("a")
720 .with_expanded(false)
721 .child(TreeNode::new("a1"))
722 .child(TreeNode::new("a2")),
723 )
724 .child(TreeNode::new("b"));
725 assert_eq!(root.visible_count(), 3);
727 }
728
729 #[test]
730 fn tree_node_toggle() {
731 let mut node = TreeNode::new("x");
732 assert!(node.is_expanded());
733 node.toggle_expanded();
734 assert!(!node.is_expanded());
735 node.toggle_expanded();
736 assert!(node.is_expanded());
737 }
738
739 #[test]
740 fn tree_guides_unicode() {
741 let g = TreeGuides::Unicode;
742 assert!(g.branch().contains('├'));
743 assert!(g.last().contains('└'));
744 assert!(g.vertical().contains('│'));
745 }
746
747 #[test]
748 fn tree_guides_ascii() {
749 let g = TreeGuides::Ascii;
750 assert!(g.branch().contains('+'));
751 assert!(g.vertical().contains('|'));
752 }
753
754 #[test]
755 fn tree_guides_width() {
756 for g in [
757 TreeGuides::Ascii,
758 TreeGuides::Unicode,
759 TreeGuides::Bold,
760 TreeGuides::Double,
761 TreeGuides::Rounded,
762 ] {
763 assert_eq!(g.width(), 4);
764 }
765 }
766
767 #[test]
768 fn tree_render_basic() {
769 let tree = Tree::new(simple_tree());
770
771 let mut pool = GraphemePool::new();
772 let mut frame = Frame::new(40, 10, &mut pool);
773 let area = Rect::new(0, 0, 40, 10);
774 tree.render(area, &mut frame);
775
776 let cell = frame.buffer.get(0, 0).unwrap();
778 assert_eq!(cell.content.as_char(), Some('r'));
779 }
780
781 #[test]
782 fn tree_render_guides_present() {
783 let tree = Tree::new(simple_tree()).with_guides(TreeGuides::Ascii);
784
785 let mut pool = GraphemePool::new();
786 let mut frame = Frame::new(40, 10, &mut pool);
787 let area = Rect::new(0, 0, 40, 10);
788 tree.render(area, &mut frame);
789
790 let cell = frame.buffer.get(0, 1).unwrap();
793 assert_eq!(cell.content.as_char(), Some('+'));
794 }
795
796 #[test]
797 fn tree_render_last_guide() {
798 let tree = Tree::new(
799 TreeNode::new("root")
800 .child(TreeNode::new("a"))
801 .child(TreeNode::new("b")),
802 )
803 .with_guides(TreeGuides::Ascii);
804
805 let mut pool = GraphemePool::new();
806 let mut frame = Frame::new(40, 10, &mut pool);
807 let area = Rect::new(0, 0, 40, 10);
808 tree.render(area, &mut frame);
809
810 let cell = frame.buffer.get(0, 1).unwrap();
812 assert_eq!(cell.content.as_char(), Some('+'));
813
814 let cell = frame.buffer.get(0, 2).unwrap();
816 assert_eq!(cell.content.as_char(), Some('`'));
817 }
818
819 #[test]
820 fn tree_render_zero_area() {
821 let tree = Tree::new(simple_tree());
822 let mut pool = GraphemePool::new();
823 let mut frame = Frame::new(40, 10, &mut pool);
824 tree.render(Rect::new(0, 0, 0, 0), &mut frame); }
826
827 #[test]
828 fn tree_render_truncated_height() {
829 let tree = Tree::new(simple_tree());
830 let mut pool = GraphemePool::new();
831 let mut frame = Frame::new(40, 2, &mut pool);
832 let area = Rect::new(0, 0, 40, 2);
833 tree.render(area, &mut frame); }
835
836 #[test]
837 fn is_not_essential() {
838 let tree = Tree::new(TreeNode::new("x"));
839 assert!(!tree.is_essential());
840 }
841
842 #[test]
843 fn tree_root_access() {
844 let mut tree = Tree::new(TreeNode::new("root"));
845 assert_eq!(tree.root().label(), "root");
846 tree.root_mut().toggle_expanded();
847 assert!(!tree.root().is_expanded());
848 }
849
850 #[test]
851 fn tree_guides_default() {
852 let g = TreeGuides::default();
853 assert_eq!(g, TreeGuides::Unicode);
854 }
855
856 #[test]
857 fn tree_guides_rounded() {
858 let g = TreeGuides::Rounded;
859 assert!(g.last().contains('╰'));
860 }
861
862 #[test]
863 fn tree_deep_nesting() {
864 let node = TreeNode::new("d3");
865 let node = TreeNode::new("d2").child(node);
866 let node = TreeNode::new("d1").child(node);
867 let root = TreeNode::new("root").child(node);
868
869 let tree = Tree::new(root);
870 let flat = tree.flatten();
871 assert_eq!(flat.len(), 4);
872 assert_eq!(flat[3].depth, 3);
873 }
874
875 #[test]
876 fn tree_node_with_children_vec() {
877 let root = TreeNode::new("root").with_children(vec![
878 TreeNode::new("a"),
879 TreeNode::new("b"),
880 TreeNode::new("c"),
881 ]);
882 assert_eq!(root.children().len(), 3);
883 }
884
885 use crate::stateful::Stateful;
888
889 #[test]
890 fn tree_with_persistence_id() {
891 let tree = Tree::new(TreeNode::new("root")).with_persistence_id("file-tree");
892 assert_eq!(tree.persistence_id(), Some("file-tree"));
893 }
894
895 #[test]
896 fn tree_default_no_persistence_id() {
897 let tree = Tree::new(TreeNode::new("root"));
898 assert_eq!(tree.persistence_id(), None);
899 }
900
901 #[test]
902 fn tree_save_restore_round_trip() {
903 let mut tree = Tree::new(
905 TreeNode::new("root")
906 .child(
907 TreeNode::new("src")
908 .child(TreeNode::new("main.rs"))
909 .child(TreeNode::new("lib.rs")),
910 )
911 .child(TreeNode::new("tests").with_expanded(false)),
912 )
913 .with_persistence_id("test");
914
915 assert!(tree.root().is_expanded());
917 assert!(tree.root().children()[0].is_expanded()); assert!(!tree.root().children()[1].is_expanded()); let saved = tree.save_state();
921
922 assert!(saved.expanded_paths.contains("root"));
924 assert!(saved.expanded_paths.contains("root/src"));
925 assert!(!saved.expanded_paths.contains("root/tests"));
926
927 tree.root_mut().children[0].toggle_expanded();
929 assert!(!tree.root().children()[0].is_expanded());
930
931 tree.restore_state(saved);
933
934 assert!(tree.root().is_expanded());
936 assert!(tree.root().children()[0].is_expanded()); assert!(!tree.root().children()[1].is_expanded()); }
939
940 #[test]
941 fn tree_state_key_uses_persistence_id() {
942 let tree = Tree::new(TreeNode::new("root")).with_persistence_id("project-explorer");
943 let key = tree.state_key();
944 assert_eq!(key.widget_type, "Tree");
945 assert_eq!(key.instance_id, "project-explorer");
946 }
947
948 #[test]
949 fn tree_state_key_default_when_no_id() {
950 let tree = Tree::new(TreeNode::new("root"));
951 let key = tree.state_key();
952 assert_eq!(key.widget_type, "Tree");
953 assert_eq!(key.instance_id, "default");
954 }
955
956 #[test]
957 fn tree_persist_state_default() {
958 let persist = TreePersistState::default();
959 assert!(persist.expanded_paths.is_empty());
960 }
961
962 #[test]
963 fn tree_collect_expanded_only_includes_nodes_with_children() {
964 let tree = Tree::new(
965 TreeNode::new("root").child(TreeNode::new("leaf")), );
967
968 let saved = tree.save_state();
969
970 assert!(saved.expanded_paths.contains("root"));
972 assert!(!saved.expanded_paths.contains("root/leaf"));
974 }
975
976 #[test]
981 fn tree_undo_widget_id_unique() {
982 let tree1 = Tree::new(TreeNode::new("root1"));
983 let tree2 = Tree::new(TreeNode::new("root2"));
984 assert_ne!(tree1.undo_id(), tree2.undo_id());
985 }
986
987 #[test]
988 fn tree_undo_snapshot_and_restore() {
989 let mut tree = Tree::new(
991 TreeNode::new("root")
992 .child(
993 TreeNode::new("a")
994 .with_expanded(true)
995 .child(TreeNode::new("a_child")),
996 )
997 .child(
998 TreeNode::new("b")
999 .with_expanded(false)
1000 .child(TreeNode::new("b_child")),
1001 ),
1002 );
1003
1004 let snapshot = tree.create_snapshot();
1006
1007 assert!(tree.is_node_expanded(&[0])); assert!(!tree.is_node_expanded(&[1])); tree.collapse_node(&[0]); tree.expand_node(&[1]); assert!(!tree.is_node_expanded(&[0]));
1015 assert!(tree.is_node_expanded(&[1]));
1016
1017 assert!(tree.restore_snapshot(&*snapshot));
1019
1020 assert!(tree.is_node_expanded(&[0])); assert!(!tree.is_node_expanded(&[1])); }
1024
1025 #[test]
1026 fn tree_expand_collapse_node() {
1027 let mut tree =
1028 Tree::new(TreeNode::new("root").child(TreeNode::new("child").with_expanded(true)));
1029
1030 assert!(tree.is_node_expanded(&[0]));
1032
1033 tree.collapse_node(&[0]);
1035 assert!(!tree.is_node_expanded(&[0]));
1036
1037 tree.expand_node(&[0]);
1039 assert!(tree.is_node_expanded(&[0]));
1040 }
1041
1042 #[test]
1043 fn tree_node_path_navigation() {
1044 let tree = Tree::new(
1045 TreeNode::new("root")
1046 .child(
1047 TreeNode::new("a")
1048 .child(TreeNode::new("a1"))
1049 .child(TreeNode::new("a2")),
1050 )
1051 .child(TreeNode::new("b")),
1052 );
1053
1054 assert_eq!(tree.get_node_at_path(&[]).map(|n| n.label()), Some("root"));
1056 assert_eq!(tree.get_node_at_path(&[0]).map(|n| n.label()), Some("a"));
1057 assert_eq!(tree.get_node_at_path(&[1]).map(|n| n.label()), Some("b"));
1058 assert_eq!(
1059 tree.get_node_at_path(&[0, 0]).map(|n| n.label()),
1060 Some("a1")
1061 );
1062 assert_eq!(
1063 tree.get_node_at_path(&[0, 1]).map(|n| n.label()),
1064 Some("a2")
1065 );
1066 assert!(tree.get_node_at_path(&[5]).is_none()); }
1068
1069 #[test]
1070 fn tree_restore_wrong_snapshot_type_fails() {
1071 use std::any::Any;
1072 let mut tree = Tree::new(TreeNode::new("root"));
1073 let wrong_snapshot: Box<dyn Any + Send> = Box::new(42i32);
1074 assert!(!tree.restore_snapshot(&*wrong_snapshot));
1075 }
1076
1077 use crate::mouse::MouseResult;
1080 use ftui_core::event::{MouseButton, MouseEvent, MouseEventKind};
1081
1082 #[test]
1083 fn tree_click_expands_parent() {
1084 let mut tree = Tree::new(
1085 TreeNode::new("root")
1086 .child(
1087 TreeNode::new("a")
1088 .child(TreeNode::new("a1"))
1089 .child(TreeNode::new("a2")),
1090 )
1091 .child(TreeNode::new("b")),
1092 );
1093 assert!(tree.root().children()[0].is_expanded());
1094
1095 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 5, 1);
1097 let hit = Some((HitId::new(1), HitRegion::Content, 1u64));
1098 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1099 assert_eq!(result, MouseResult::Activated(1));
1100 assert!(!tree.root().children()[0].is_expanded()); }
1102
1103 #[test]
1104 fn tree_click_selects_leaf() {
1105 let mut tree = Tree::new(
1106 TreeNode::new("root")
1107 .child(
1108 TreeNode::new("a")
1109 .child(TreeNode::new("a1"))
1110 .child(TreeNode::new("a2")),
1111 )
1112 .child(TreeNode::new("b")),
1113 );
1114
1115 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 5, 4);
1117 let hit = Some((HitId::new(1), HitRegion::Content, 4u64));
1118 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1119 assert_eq!(result, MouseResult::Selected(4));
1120 }
1121
1122 #[test]
1123 fn tree_click_wrong_id_ignored() {
1124 let mut tree = Tree::new(TreeNode::new("root").child(TreeNode::new("a")));
1125 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
1126 let hit = Some((HitId::new(99), HitRegion::Content, 0u64));
1127 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1128 assert_eq!(result, MouseResult::Ignored);
1129 }
1130
1131 #[test]
1132 fn tree_click_no_hit_ignored() {
1133 let mut tree = Tree::new(TreeNode::new("root"));
1134 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
1135 let result = tree.handle_mouse(&event, None, HitId::new(1));
1136 assert_eq!(result, MouseResult::Ignored);
1137 }
1138
1139 #[test]
1140 fn tree_right_click_ignored() {
1141 let mut tree = Tree::new(TreeNode::new("root").child(TreeNode::new("a")));
1142 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Right), 0, 0);
1143 let hit = Some((HitId::new(1), HitRegion::Content, 0u64));
1144 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1145 assert_eq!(result, MouseResult::Ignored);
1146 }
1147
1148 #[test]
1149 fn tree_node_at_visible_index_with_show_root() {
1150 let mut tree = Tree::new(
1151 TreeNode::new("root")
1152 .child(
1153 TreeNode::new("a")
1154 .child(TreeNode::new("a1"))
1155 .child(TreeNode::new("a2")),
1156 )
1157 .child(TreeNode::new("b")),
1158 );
1159
1160 assert_eq!(
1162 tree.node_at_visible_index_mut(0)
1163 .map(|n| n.label().to_string()),
1164 Some("root".to_string())
1165 );
1166 assert_eq!(
1167 tree.node_at_visible_index_mut(1)
1168 .map(|n| n.label().to_string()),
1169 Some("a".to_string())
1170 );
1171 assert_eq!(
1172 tree.node_at_visible_index_mut(2)
1173 .map(|n| n.label().to_string()),
1174 Some("a1".to_string())
1175 );
1176 assert_eq!(
1177 tree.node_at_visible_index_mut(3)
1178 .map(|n| n.label().to_string()),
1179 Some("a2".to_string())
1180 );
1181 assert_eq!(
1182 tree.node_at_visible_index_mut(4)
1183 .map(|n| n.label().to_string()),
1184 Some("b".to_string())
1185 );
1186 assert!(tree.node_at_visible_index_mut(5).is_none());
1187 }
1188
1189 #[test]
1190 fn tree_node_at_visible_index_hidden_root() {
1191 let mut tree = Tree::new(
1192 TreeNode::new("root")
1193 .child(TreeNode::new("a").child(TreeNode::new("a1")))
1194 .child(TreeNode::new("b")),
1195 )
1196 .with_show_root(false);
1197
1198 assert_eq!(
1200 tree.node_at_visible_index_mut(0)
1201 .map(|n| n.label().to_string()),
1202 Some("a".to_string())
1203 );
1204 assert_eq!(
1205 tree.node_at_visible_index_mut(1)
1206 .map(|n| n.label().to_string()),
1207 Some("a1".to_string())
1208 );
1209 assert_eq!(
1210 tree.node_at_visible_index_mut(2)
1211 .map(|n| n.label().to_string()),
1212 Some("b".to_string())
1213 );
1214 assert!(tree.node_at_visible_index_mut(3).is_none());
1215 }
1216
1217 #[test]
1218 fn tree_node_at_visible_index_collapsed() {
1219 let mut tree = Tree::new(
1220 TreeNode::new("root")
1221 .child(
1222 TreeNode::new("a")
1223 .with_expanded(false)
1224 .child(TreeNode::new("a1"))
1225 .child(TreeNode::new("a2")),
1226 )
1227 .child(TreeNode::new("b")),
1228 );
1229
1230 assert_eq!(
1232 tree.node_at_visible_index_mut(0)
1233 .map(|n| n.label().to_string()),
1234 Some("root".to_string())
1235 );
1236 assert_eq!(
1237 tree.node_at_visible_index_mut(1)
1238 .map(|n| n.label().to_string()),
1239 Some("a".to_string())
1240 );
1241 assert_eq!(
1242 tree.node_at_visible_index_mut(2)
1243 .map(|n| n.label().to_string()),
1244 Some("b".to_string())
1245 );
1246 assert!(tree.node_at_visible_index_mut(3).is_none());
1247 }
1248
1249 #[test]
1250 fn tree_click_toggles_collapsed_node() {
1251 let mut tree = Tree::new(
1252 TreeNode::new("root")
1253 .child(
1254 TreeNode::new("a")
1255 .with_expanded(false)
1256 .child(TreeNode::new("a1")),
1257 )
1258 .child(TreeNode::new("b")),
1259 );
1260 assert!(!tree.root().children()[0].is_expanded());
1261
1262 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 1);
1264 let hit = Some((HitId::new(1), HitRegion::Content, 1u64));
1265 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1266 assert_eq!(result, MouseResult::Activated(1));
1267 assert!(tree.root().children()[0].is_expanded()); }
1269}