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 let indent = " ".repeat(depth);
361 x = draw_text_span(frame, x, y, &indent, Style::default(), max_x);
362 }
363
364 let style = if depth == 0 && self.show_root {
366 self.root_style
367 } else {
368 self.label_style
369 };
370
371 if deg.apply_styling() {
372 draw_text_span(frame, x, y, &node.label, style, max_x);
373 } else {
374 draw_text_span(frame, x, y, &node.label, Style::default(), max_x);
375 }
376
377 if let Some(id) = self.hit_id {
379 let row_area = Rect::new(area.x, y, area.width, 1);
380 frame.register_hit(row_area, id, HitRegion::Content, *current_row as u64);
381 }
382
383 *current_row += 1;
384
385 if !node.expanded {
386 return;
387 }
388
389 let child_count = node.children.len();
390 for (i, child) in node.children.iter().enumerate() {
391 is_last.push(i == child_count - 1);
392 self.render_node(child, depth + 1, is_last, area, frame, current_row, deg);
393 is_last.pop();
394 }
395 }
396}
397
398impl Widget for Tree {
399 fn render(&self, area: Rect, frame: &mut Frame) {
400 if area.width == 0 || area.height == 0 {
401 return;
402 }
403
404 let deg = frame.buffer.degradation;
405 let mut current_row = 0;
406 let mut is_last = Vec::with_capacity(8);
407
408 if self.show_root {
409 self.render_node(
410 &self.root,
411 0,
412 &mut is_last,
413 area,
414 frame,
415 &mut current_row,
416 deg,
417 );
418 } else if self.root.expanded {
419 let child_count = self.root.children.len();
423 for (i, child) in self.root.children.iter().enumerate() {
424 is_last.push(i == child_count - 1);
425 self.render_node(
426 child,
427 0, &mut is_last,
429 area,
430 frame,
431 &mut current_row,
432 deg,
433 );
434 is_last.pop();
435 }
436 }
437 }
438
439 fn is_essential(&self) -> bool {
440 false
441 }
442}
443
444#[derive(Clone, Debug, Default, PartialEq)]
452#[cfg_attr(
453 feature = "state-persistence",
454 derive(serde::Serialize, serde::Deserialize)
455)]
456pub struct TreePersistState {
457 pub expanded_paths: HashSet<String>,
459}
460
461impl crate::stateful::Stateful for Tree {
462 type State = TreePersistState;
463
464 fn state_key(&self) -> crate::stateful::StateKey {
465 crate::stateful::StateKey::new("Tree", self.persistence_id.as_deref().unwrap_or("default"))
466 }
467
468 fn save_state(&self) -> TreePersistState {
469 let mut expanded_paths = HashSet::new();
470 self.root.collect_expanded("", &mut expanded_paths);
471 TreePersistState { expanded_paths }
472 }
473
474 fn restore_state(&mut self, state: TreePersistState) {
475 self.root.apply_expanded("", &state.expanded_paths);
476 }
477}
478
479impl UndoSupport for Tree {
484 fn undo_widget_id(&self) -> UndoWidgetId {
485 self.undo_id
486 }
487
488 fn create_snapshot(&self) -> Box<dyn Any + Send> {
489 Box::new(self.save_state())
490 }
491
492 fn restore_snapshot(&mut self, snapshot: &dyn Any) -> bool {
493 if let Some(snap) = snapshot.downcast_ref::<TreePersistState>() {
494 self.restore_state(snap.clone());
495 true
496 } else {
497 false
498 }
499 }
500}
501
502impl TreeUndoExt for Tree {
503 fn is_node_expanded(&self, path: &[usize]) -> bool {
504 self.get_node_at_path(path)
505 .map(|node| node.is_expanded())
506 .unwrap_or(false)
507 }
508
509 fn expand_node(&mut self, path: &[usize]) {
510 if let Some(node) = self.get_node_at_path_mut(path) {
511 node.expanded = true;
512 }
513 }
514
515 fn collapse_node(&mut self, path: &[usize]) {
516 if let Some(node) = self.get_node_at_path_mut(path) {
517 node.expanded = false;
518 }
519 }
520}
521
522impl Tree {
523 #[must_use]
525 pub fn undo_id(&self) -> UndoWidgetId {
526 self.undo_id
527 }
528
529 fn get_node_at_path(&self, path: &[usize]) -> Option<&TreeNode> {
531 let mut current = &self.root;
532 for &idx in path {
533 current = current.children.get(idx)?;
534 }
535 Some(current)
536 }
537
538 fn get_node_at_path_mut(&mut self, path: &[usize]) -> Option<&mut TreeNode> {
540 let mut current = &mut self.root;
541 for &idx in path {
542 current = current.children.get_mut(idx)?;
543 }
544 Some(current)
545 }
546
547 pub fn handle_mouse(
564 &mut self,
565 event: &MouseEvent,
566 hit: Option<(HitId, HitRegion, u64)>,
567 expected_id: HitId,
568 ) -> MouseResult {
569 match event.kind {
570 MouseEventKind::Down(MouseButton::Left) => {
571 if let Some((id, HitRegion::Content, data)) = hit
572 && id == expected_id
573 {
574 let index = data as usize;
575 if let Some(node) = self.node_at_visible_index_mut(index) {
576 if node.children.is_empty() {
577 return MouseResult::Selected(index);
578 }
579 node.toggle_expanded();
580 return MouseResult::Activated(index);
581 }
582 }
583 MouseResult::Ignored
584 }
585 _ => MouseResult::Ignored,
586 }
587 }
588
589 pub fn node_at_visible_index_mut(&mut self, target: usize) -> Option<&mut TreeNode> {
595 let mut counter = 0usize;
596 if self.show_root {
597 Self::walk_visible_mut(&mut self.root, target, &mut counter)
598 } else if self.root.expanded {
599 for child in &mut self.root.children {
600 if let Some(node) = Self::walk_visible_mut(child, target, &mut counter) {
601 return Some(node);
602 }
603 }
604 None
605 } else {
606 None
607 }
608 }
609
610 fn walk_visible_mut<'a>(
613 node: &'a mut TreeNode,
614 target: usize,
615 counter: &mut usize,
616 ) -> Option<&'a mut TreeNode> {
617 if *counter == target {
618 return Some(node);
619 }
620 *counter += 1;
621 if node.expanded {
622 for child in &mut node.children {
623 if let Some(found) = Self::walk_visible_mut(child, target, counter) {
624 return Some(found);
625 }
626 }
627 }
628 None
629 }
630}
631
632#[cfg(test)]
637#[derive(Debug, Clone, PartialEq, Eq)]
638struct FlatNode {
639 label: String,
640 depth: usize,
641}
642
643#[cfg(test)]
644fn flatten_visible(node: &TreeNode, depth: usize, out: &mut Vec<FlatNode>) {
645 out.push(FlatNode {
646 label: node.label.clone(),
647 depth,
648 });
649 if node.expanded {
650 for child in &node.children {
651 flatten_visible(child, depth + 1, out);
652 }
653 }
654}
655
656#[cfg(test)]
657impl Tree {
658 fn flatten(&self) -> Vec<FlatNode> {
659 let mut out = Vec::new();
660 if self.show_root {
661 flatten_visible(&self.root, 0, &mut out);
662 } else if self.root.expanded {
663 for child in &self.root.children {
664 flatten_visible(child, 0, &mut out);
665 }
666 }
667 out
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use ftui_render::frame::Frame;
675 use ftui_render::grapheme_pool::GraphemePool;
676
677 fn simple_tree() -> TreeNode {
678 TreeNode::new("root")
679 .child(
680 TreeNode::new("a")
681 .child(TreeNode::new("a1"))
682 .child(TreeNode::new("a2")),
683 )
684 .child(TreeNode::new("b"))
685 }
686
687 #[test]
688 fn tree_node_basics() {
689 let node = TreeNode::new("hello");
690 assert_eq!(node.label(), "hello");
691 assert!(node.children().is_empty());
692 assert!(node.is_expanded());
693 }
694
695 #[test]
696 fn tree_node_children() {
697 let root = simple_tree();
698 assert_eq!(root.children().len(), 2);
699 assert_eq!(root.children()[0].label(), "a");
700 assert_eq!(root.children()[0].children().len(), 2);
701 }
702
703 #[test]
704 fn tree_node_visible_count() {
705 let root = simple_tree();
706 assert_eq!(root.visible_count(), 5);
708 }
709
710 #[test]
711 fn tree_node_collapsed() {
712 let root = TreeNode::new("root")
713 .child(
714 TreeNode::new("a")
715 .with_expanded(false)
716 .child(TreeNode::new("a1"))
717 .child(TreeNode::new("a2")),
718 )
719 .child(TreeNode::new("b"));
720 assert_eq!(root.visible_count(), 3);
722 }
723
724 #[test]
725 fn tree_node_toggle() {
726 let mut node = TreeNode::new("x");
727 assert!(node.is_expanded());
728 node.toggle_expanded();
729 assert!(!node.is_expanded());
730 node.toggle_expanded();
731 assert!(node.is_expanded());
732 }
733
734 #[test]
735 fn tree_guides_unicode() {
736 let g = TreeGuides::Unicode;
737 assert!(g.branch().contains('├'));
738 assert!(g.last().contains('└'));
739 assert!(g.vertical().contains('│'));
740 }
741
742 #[test]
743 fn tree_guides_ascii() {
744 let g = TreeGuides::Ascii;
745 assert!(g.branch().contains('+'));
746 assert!(g.vertical().contains('|'));
747 }
748
749 #[test]
750 fn tree_guides_width() {
751 for g in [
752 TreeGuides::Ascii,
753 TreeGuides::Unicode,
754 TreeGuides::Bold,
755 TreeGuides::Double,
756 TreeGuides::Rounded,
757 ] {
758 assert_eq!(g.width(), 4);
759 }
760 }
761
762 #[test]
763 fn tree_render_basic() {
764 let tree = Tree::new(simple_tree());
765
766 let mut pool = GraphemePool::new();
767 let mut frame = Frame::new(40, 10, &mut pool);
768 let area = Rect::new(0, 0, 40, 10);
769 tree.render(area, &mut frame);
770
771 let cell = frame.buffer.get(0, 0).unwrap();
773 assert_eq!(cell.content.as_char(), Some('r'));
774 }
775
776 #[test]
777 fn tree_render_guides_present() {
778 let tree = Tree::new(simple_tree()).with_guides(TreeGuides::Ascii);
779
780 let mut pool = GraphemePool::new();
781 let mut frame = Frame::new(40, 10, &mut pool);
782 let area = Rect::new(0, 0, 40, 10);
783 tree.render(area, &mut frame);
784
785 let cell = frame.buffer.get(0, 1).unwrap();
788 assert_eq!(cell.content.as_char(), Some('+'));
789 }
790
791 #[test]
792 fn tree_render_last_guide() {
793 let tree = Tree::new(
794 TreeNode::new("root")
795 .child(TreeNode::new("a"))
796 .child(TreeNode::new("b")),
797 )
798 .with_guides(TreeGuides::Ascii);
799
800 let mut pool = GraphemePool::new();
801 let mut frame = Frame::new(40, 10, &mut pool);
802 let area = Rect::new(0, 0, 40, 10);
803 tree.render(area, &mut frame);
804
805 let cell = frame.buffer.get(0, 1).unwrap();
807 assert_eq!(cell.content.as_char(), Some('+'));
808
809 let cell = frame.buffer.get(0, 2).unwrap();
811 assert_eq!(cell.content.as_char(), Some('`'));
812 }
813
814 #[test]
815 fn tree_render_zero_area() {
816 let tree = Tree::new(simple_tree());
817 let mut pool = GraphemePool::new();
818 let mut frame = Frame::new(40, 10, &mut pool);
819 tree.render(Rect::new(0, 0, 0, 0), &mut frame); }
821
822 #[test]
823 fn tree_render_truncated_height() {
824 let tree = Tree::new(simple_tree());
825 let mut pool = GraphemePool::new();
826 let mut frame = Frame::new(40, 2, &mut pool);
827 let area = Rect::new(0, 0, 40, 2);
828 tree.render(area, &mut frame); }
830
831 #[test]
832 fn is_not_essential() {
833 let tree = Tree::new(TreeNode::new("x"));
834 assert!(!tree.is_essential());
835 }
836
837 #[test]
838 fn tree_root_access() {
839 let mut tree = Tree::new(TreeNode::new("root"));
840 assert_eq!(tree.root().label(), "root");
841 tree.root_mut().toggle_expanded();
842 assert!(!tree.root().is_expanded());
843 }
844
845 #[test]
846 fn tree_guides_default() {
847 let g = TreeGuides::default();
848 assert_eq!(g, TreeGuides::Unicode);
849 }
850
851 #[test]
852 fn tree_guides_rounded() {
853 let g = TreeGuides::Rounded;
854 assert!(g.last().contains('╰'));
855 }
856
857 #[test]
858 fn tree_deep_nesting() {
859 let node = TreeNode::new("d3");
860 let node = TreeNode::new("d2").child(node);
861 let node = TreeNode::new("d1").child(node);
862 let root = TreeNode::new("root").child(node);
863
864 let tree = Tree::new(root);
865 let flat = tree.flatten();
866 assert_eq!(flat.len(), 4);
867 assert_eq!(flat[3].depth, 3);
868 }
869
870 #[test]
871 fn tree_node_with_children_vec() {
872 let root = TreeNode::new("root").with_children(vec![
873 TreeNode::new("a"),
874 TreeNode::new("b"),
875 TreeNode::new("c"),
876 ]);
877 assert_eq!(root.children().len(), 3);
878 }
879
880 use crate::stateful::Stateful;
883
884 #[test]
885 fn tree_with_persistence_id() {
886 let tree = Tree::new(TreeNode::new("root")).with_persistence_id("file-tree");
887 assert_eq!(tree.persistence_id(), Some("file-tree"));
888 }
889
890 #[test]
891 fn tree_default_no_persistence_id() {
892 let tree = Tree::new(TreeNode::new("root"));
893 assert_eq!(tree.persistence_id(), None);
894 }
895
896 #[test]
897 fn tree_save_restore_round_trip() {
898 let mut tree = Tree::new(
900 TreeNode::new("root")
901 .child(
902 TreeNode::new("src")
903 .child(TreeNode::new("main.rs"))
904 .child(TreeNode::new("lib.rs")),
905 )
906 .child(TreeNode::new("tests").with_expanded(false)),
907 )
908 .with_persistence_id("test");
909
910 assert!(tree.root().is_expanded());
912 assert!(tree.root().children()[0].is_expanded()); assert!(!tree.root().children()[1].is_expanded()); let saved = tree.save_state();
916
917 assert!(saved.expanded_paths.contains("root"));
919 assert!(saved.expanded_paths.contains("root/src"));
920 assert!(!saved.expanded_paths.contains("root/tests"));
921
922 tree.root_mut().children[0].toggle_expanded();
924 assert!(!tree.root().children()[0].is_expanded());
925
926 tree.restore_state(saved);
928
929 assert!(tree.root().is_expanded());
931 assert!(tree.root().children()[0].is_expanded()); assert!(!tree.root().children()[1].is_expanded()); }
934
935 #[test]
936 fn tree_state_key_uses_persistence_id() {
937 let tree = Tree::new(TreeNode::new("root")).with_persistence_id("project-explorer");
938 let key = tree.state_key();
939 assert_eq!(key.widget_type, "Tree");
940 assert_eq!(key.instance_id, "project-explorer");
941 }
942
943 #[test]
944 fn tree_state_key_default_when_no_id() {
945 let tree = Tree::new(TreeNode::new("root"));
946 let key = tree.state_key();
947 assert_eq!(key.widget_type, "Tree");
948 assert_eq!(key.instance_id, "default");
949 }
950
951 #[test]
952 fn tree_persist_state_default() {
953 let persist = TreePersistState::default();
954 assert!(persist.expanded_paths.is_empty());
955 }
956
957 #[test]
958 fn tree_collect_expanded_only_includes_nodes_with_children() {
959 let tree = Tree::new(
960 TreeNode::new("root").child(TreeNode::new("leaf")), );
962
963 let saved = tree.save_state();
964
965 assert!(saved.expanded_paths.contains("root"));
967 assert!(!saved.expanded_paths.contains("root/leaf"));
969 }
970
971 #[test]
976 fn tree_undo_widget_id_unique() {
977 let tree1 = Tree::new(TreeNode::new("root1"));
978 let tree2 = Tree::new(TreeNode::new("root2"));
979 assert_ne!(tree1.undo_id(), tree2.undo_id());
980 }
981
982 #[test]
983 fn tree_undo_snapshot_and_restore() {
984 let mut tree = Tree::new(
986 TreeNode::new("root")
987 .child(
988 TreeNode::new("a")
989 .with_expanded(true)
990 .child(TreeNode::new("a_child")),
991 )
992 .child(
993 TreeNode::new("b")
994 .with_expanded(false)
995 .child(TreeNode::new("b_child")),
996 ),
997 );
998
999 let snapshot = tree.create_snapshot();
1001
1002 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]));
1010 assert!(tree.is_node_expanded(&[1]));
1011
1012 assert!(tree.restore_snapshot(&*snapshot));
1014
1015 assert!(tree.is_node_expanded(&[0])); assert!(!tree.is_node_expanded(&[1])); }
1019
1020 #[test]
1021 fn tree_expand_collapse_node() {
1022 let mut tree =
1023 Tree::new(TreeNode::new("root").child(TreeNode::new("child").with_expanded(true)));
1024
1025 assert!(tree.is_node_expanded(&[0]));
1027
1028 tree.collapse_node(&[0]);
1030 assert!(!tree.is_node_expanded(&[0]));
1031
1032 tree.expand_node(&[0]);
1034 assert!(tree.is_node_expanded(&[0]));
1035 }
1036
1037 #[test]
1038 fn tree_node_path_navigation() {
1039 let tree = Tree::new(
1040 TreeNode::new("root")
1041 .child(
1042 TreeNode::new("a")
1043 .child(TreeNode::new("a1"))
1044 .child(TreeNode::new("a2")),
1045 )
1046 .child(TreeNode::new("b")),
1047 );
1048
1049 assert_eq!(tree.get_node_at_path(&[]).map(|n| n.label()), Some("root"));
1051 assert_eq!(tree.get_node_at_path(&[0]).map(|n| n.label()), Some("a"));
1052 assert_eq!(tree.get_node_at_path(&[1]).map(|n| n.label()), Some("b"));
1053 assert_eq!(
1054 tree.get_node_at_path(&[0, 0]).map(|n| n.label()),
1055 Some("a1")
1056 );
1057 assert_eq!(
1058 tree.get_node_at_path(&[0, 1]).map(|n| n.label()),
1059 Some("a2")
1060 );
1061 assert!(tree.get_node_at_path(&[5]).is_none()); }
1063
1064 #[test]
1065 fn tree_restore_wrong_snapshot_type_fails() {
1066 use std::any::Any;
1067 let mut tree = Tree::new(TreeNode::new("root"));
1068 let wrong_snapshot: Box<dyn Any + Send> = Box::new(42i32);
1069 assert!(!tree.restore_snapshot(&*wrong_snapshot));
1070 }
1071
1072 use crate::mouse::MouseResult;
1075 use ftui_core::event::{MouseButton, MouseEvent, MouseEventKind};
1076
1077 #[test]
1078 fn tree_click_expands_parent() {
1079 let mut tree = Tree::new(
1080 TreeNode::new("root")
1081 .child(
1082 TreeNode::new("a")
1083 .child(TreeNode::new("a1"))
1084 .child(TreeNode::new("a2")),
1085 )
1086 .child(TreeNode::new("b")),
1087 );
1088 assert!(tree.root().children()[0].is_expanded());
1089
1090 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 5, 1);
1092 let hit = Some((HitId::new(1), HitRegion::Content, 1u64));
1093 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1094 assert_eq!(result, MouseResult::Activated(1));
1095 assert!(!tree.root().children()[0].is_expanded()); }
1097
1098 #[test]
1099 fn tree_click_selects_leaf() {
1100 let mut tree = Tree::new(
1101 TreeNode::new("root")
1102 .child(
1103 TreeNode::new("a")
1104 .child(TreeNode::new("a1"))
1105 .child(TreeNode::new("a2")),
1106 )
1107 .child(TreeNode::new("b")),
1108 );
1109
1110 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 5, 4);
1112 let hit = Some((HitId::new(1), HitRegion::Content, 4u64));
1113 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1114 assert_eq!(result, MouseResult::Selected(4));
1115 }
1116
1117 #[test]
1118 fn tree_click_wrong_id_ignored() {
1119 let mut tree = Tree::new(TreeNode::new("root").child(TreeNode::new("a")));
1120 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
1121 let hit = Some((HitId::new(99), HitRegion::Content, 0u64));
1122 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1123 assert_eq!(result, MouseResult::Ignored);
1124 }
1125
1126 #[test]
1127 fn tree_click_no_hit_ignored() {
1128 let mut tree = Tree::new(TreeNode::new("root"));
1129 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
1130 let result = tree.handle_mouse(&event, None, HitId::new(1));
1131 assert_eq!(result, MouseResult::Ignored);
1132 }
1133
1134 #[test]
1135 fn tree_right_click_ignored() {
1136 let mut tree = Tree::new(TreeNode::new("root").child(TreeNode::new("a")));
1137 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Right), 0, 0);
1138 let hit = Some((HitId::new(1), HitRegion::Content, 0u64));
1139 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1140 assert_eq!(result, MouseResult::Ignored);
1141 }
1142
1143 #[test]
1144 fn tree_node_at_visible_index_with_show_root() {
1145 let mut tree = Tree::new(
1146 TreeNode::new("root")
1147 .child(
1148 TreeNode::new("a")
1149 .child(TreeNode::new("a1"))
1150 .child(TreeNode::new("a2")),
1151 )
1152 .child(TreeNode::new("b")),
1153 );
1154
1155 assert_eq!(
1157 tree.node_at_visible_index_mut(0)
1158 .map(|n| n.label().to_string()),
1159 Some("root".to_string())
1160 );
1161 assert_eq!(
1162 tree.node_at_visible_index_mut(1)
1163 .map(|n| n.label().to_string()),
1164 Some("a".to_string())
1165 );
1166 assert_eq!(
1167 tree.node_at_visible_index_mut(2)
1168 .map(|n| n.label().to_string()),
1169 Some("a1".to_string())
1170 );
1171 assert_eq!(
1172 tree.node_at_visible_index_mut(3)
1173 .map(|n| n.label().to_string()),
1174 Some("a2".to_string())
1175 );
1176 assert_eq!(
1177 tree.node_at_visible_index_mut(4)
1178 .map(|n| n.label().to_string()),
1179 Some("b".to_string())
1180 );
1181 assert!(tree.node_at_visible_index_mut(5).is_none());
1182 }
1183
1184 #[test]
1185 fn tree_node_at_visible_index_hidden_root() {
1186 let mut tree = Tree::new(
1187 TreeNode::new("root")
1188 .child(TreeNode::new("a").child(TreeNode::new("a1")))
1189 .child(TreeNode::new("b")),
1190 )
1191 .with_show_root(false);
1192
1193 assert_eq!(
1195 tree.node_at_visible_index_mut(0)
1196 .map(|n| n.label().to_string()),
1197 Some("a".to_string())
1198 );
1199 assert_eq!(
1200 tree.node_at_visible_index_mut(1)
1201 .map(|n| n.label().to_string()),
1202 Some("a1".to_string())
1203 );
1204 assert_eq!(
1205 tree.node_at_visible_index_mut(2)
1206 .map(|n| n.label().to_string()),
1207 Some("b".to_string())
1208 );
1209 assert!(tree.node_at_visible_index_mut(3).is_none());
1210 }
1211
1212 #[test]
1213 fn tree_node_at_visible_index_collapsed() {
1214 let mut tree = Tree::new(
1215 TreeNode::new("root")
1216 .child(
1217 TreeNode::new("a")
1218 .with_expanded(false)
1219 .child(TreeNode::new("a1"))
1220 .child(TreeNode::new("a2")),
1221 )
1222 .child(TreeNode::new("b")),
1223 );
1224
1225 assert_eq!(
1227 tree.node_at_visible_index_mut(0)
1228 .map(|n| n.label().to_string()),
1229 Some("root".to_string())
1230 );
1231 assert_eq!(
1232 tree.node_at_visible_index_mut(1)
1233 .map(|n| n.label().to_string()),
1234 Some("a".to_string())
1235 );
1236 assert_eq!(
1237 tree.node_at_visible_index_mut(2)
1238 .map(|n| n.label().to_string()),
1239 Some("b".to_string())
1240 );
1241 assert!(tree.node_at_visible_index_mut(3).is_none());
1242 }
1243
1244 #[test]
1245 fn tree_click_toggles_collapsed_node() {
1246 let mut tree = Tree::new(
1247 TreeNode::new("root")
1248 .child(
1249 TreeNode::new("a")
1250 .with_expanded(false)
1251 .child(TreeNode::new("a1")),
1252 )
1253 .child(TreeNode::new("b")),
1254 );
1255 assert!(!tree.root().children()[0].is_expanded());
1256
1257 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 1);
1259 let hit = Some((HitId::new(1), HitRegion::Content, 1u64));
1260 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1261 assert_eq!(result, MouseResult::Activated(1));
1262 assert!(tree.root().children()[0].is_expanded()); }
1264}