1use crate::mouse::MouseResult;
22use crate::stateful::Stateful;
23use crate::undo_support::{TreeUndoExt, UndoSupport, UndoWidgetId};
24use crate::{Widget, clear_text_area, draw_text_span};
25use ftui_core::event::{KeyCode, KeyEvent, 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#[cfg(feature = "tracing")]
32use web_time::Instant;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum TreeGuides {
37 Ascii,
39 #[default]
41 Unicode,
42 Bold,
44 Double,
46 Rounded,
48}
49
50impl TreeGuides {
51 #[must_use]
53 pub const fn vertical(&self) -> &str {
54 match self {
55 Self::Ascii => "| ",
56 Self::Unicode | Self::Rounded => "\u{2502} ",
57 Self::Bold => "\u{2503} ",
58 Self::Double => "\u{2551} ",
59 }
60 }
61
62 #[must_use]
64 pub const fn branch(&self) -> &str {
65 match self {
66 Self::Ascii => "+-- ",
67 Self::Unicode => "\u{251C}\u{2500}\u{2500} ",
68 Self::Bold => "\u{2523}\u{2501}\u{2501} ",
69 Self::Double => "\u{2560}\u{2550}\u{2550} ",
70 Self::Rounded => "\u{251C}\u{2500}\u{2500} ",
71 }
72 }
73
74 #[must_use]
76 pub const fn last(&self) -> &str {
77 match self {
78 Self::Ascii => "`-- ",
79 Self::Unicode => "\u{2514}\u{2500}\u{2500} ",
80 Self::Bold => "\u{2517}\u{2501}\u{2501} ",
81 Self::Double => "\u{255A}\u{2550}\u{2550} ",
82 Self::Rounded => "\u{2570}\u{2500}\u{2500} ",
83 }
84 }
85
86 #[must_use]
88 pub const fn space(&self) -> &str {
89 " "
90 }
91
92 #[inline]
94 #[must_use]
95 pub fn width(&self) -> usize {
96 4
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct TreeNode {
103 label: String,
104 icon: Option<String>,
105 pub(crate) children: Vec<TreeNode>,
107 lazy_children: Option<Vec<TreeNode>>,
109 pub(crate) expanded: bool,
111}
112
113impl TreeNode {
114 #[must_use]
116 pub fn new(label: impl Into<String>) -> Self {
117 Self {
118 label: label.into(),
119 icon: None,
120 children: Vec::new(),
121 lazy_children: None,
122 expanded: true,
123 }
124 }
125
126 #[must_use]
128 pub fn child(mut self, node: TreeNode) -> Self {
129 self.children.push(node);
130 self
131 }
132
133 #[must_use]
135 pub fn with_children(mut self, nodes: Vec<TreeNode>) -> Self {
136 self.children = nodes;
137 self
138 }
139
140 #[must_use]
142 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
143 self.icon = Some(icon.into());
144 self
145 }
146
147 #[must_use]
151 pub fn with_lazy_children(mut self, nodes: Vec<TreeNode>) -> Self {
152 self.lazy_children = Some(nodes);
153 self.expanded = false;
154 self
155 }
156
157 #[must_use]
159 pub fn with_expanded(mut self, expanded: bool) -> Self {
160 if expanded {
161 self.materialize_lazy_children();
162 }
163 self.expanded = expanded;
164 self
165 }
166
167 #[must_use]
169 pub fn label(&self) -> &str {
170 &self.label
171 }
172
173 #[must_use]
175 pub fn children(&self) -> &[TreeNode] {
176 &self.children
177 }
178
179 #[must_use]
181 pub fn icon(&self) -> Option<&str> {
182 self.icon.as_deref()
183 }
184
185 #[must_use]
187 pub fn has_children(&self) -> bool {
188 !self.children.is_empty()
189 || self
190 .lazy_children
191 .as_ref()
192 .is_some_and(|children| !children.is_empty())
193 }
194
195 #[must_use]
197 pub fn is_expanded(&self) -> bool {
198 self.expanded
199 }
200
201 pub fn toggle_expanded(&mut self) {
203 if !self.expanded {
204 self.materialize_lazy_children();
205 }
206 self.expanded = !self.expanded;
207 }
208
209 fn materialize_lazy_children(&mut self) {
210 if let Some(mut lazy) = self.lazy_children.take() {
211 self.children.append(&mut lazy);
212 }
213 }
214
215 fn materialize_all_lazy_children(&mut self) {
216 self.materialize_lazy_children();
217 for child in &mut self.children {
218 child.materialize_all_lazy_children();
219 }
220 }
221
222 #[cfg(feature = "tracing")]
223 fn total_count(&self) -> usize {
224 let mut count = 1usize;
225 for child in &self.children {
226 count = count.saturating_add(child.total_count());
227 }
228 if let Some(lazy) = &self.lazy_children {
229 for child in lazy {
230 count = count.saturating_add(child.total_count());
231 }
232 }
233 count
234 }
235
236 #[cfg(feature = "tracing")]
237 fn expanded_count(&self) -> usize {
238 let mut count = usize::from(self.expanded && self.has_children());
239 for child in &self.children {
240 count = count.saturating_add(child.expanded_count());
241 }
242 if let Some(lazy) = &self.lazy_children {
243 for child in lazy {
244 count = count.saturating_add(child.expanded_count());
245 }
246 }
247 count
248 }
249
250 #[must_use]
252 pub fn visible_count(&self) -> usize {
253 let mut count = 1;
254 if self.expanded {
255 for child in &self.children {
256 count += child.visible_count();
257 }
258 }
259 count
260 }
261
262 #[allow(dead_code)]
264 pub(crate) fn collect_expanded(&self, prefix: &str, out: &mut HashSet<String>) {
265 let path = if prefix.is_empty() {
266 self.label.clone()
267 } else {
268 format!("{}/{}", prefix, self.label)
269 };
270
271 if self.expanded && self.has_children() {
272 out.insert(path.clone());
273 }
274
275 for child in &self.children {
276 child.collect_expanded(&path, out);
277 }
278 }
279
280 #[allow(dead_code)]
282 pub(crate) fn apply_expanded(&mut self, prefix: &str, expanded_paths: &HashSet<String>) {
283 let path = if prefix.is_empty() {
284 self.label.clone()
285 } else {
286 format!("{}/{}", prefix, self.label)
287 };
288
289 if self.has_children() {
290 self.expanded = expanded_paths.contains(&path);
291 if self.expanded {
292 self.materialize_lazy_children();
293 }
294 }
295
296 for child in &mut self.children {
297 child.apply_expanded(&path, expanded_paths);
298 }
299 }
300}
301
302#[derive(Debug, Clone)]
304pub struct Tree {
305 undo_id: UndoWidgetId,
307 root: TreeNode,
308 show_root: bool,
310 guides: TreeGuides,
312 guide_style: Style,
314 label_style: Style,
316 root_style: Style,
318 persistence_id: Option<String>,
320 hit_id: Option<HitId>,
322 search_query: Option<String>,
324}
325
326impl Tree {
327 #[must_use]
329 pub fn new(root: TreeNode) -> Self {
330 Self {
331 undo_id: UndoWidgetId::new(),
332 root,
333 show_root: true,
334 guides: TreeGuides::default(),
335 guide_style: Style::default(),
336 label_style: Style::default(),
337 root_style: Style::default(),
338 persistence_id: None,
339 hit_id: None,
340 search_query: None,
341 }
342 }
343
344 #[must_use]
346 pub fn with_show_root(mut self, show: bool) -> Self {
347 self.show_root = show;
348 self
349 }
350
351 #[must_use]
353 pub fn with_guides(mut self, guides: TreeGuides) -> Self {
354 self.guides = guides;
355 self
356 }
357
358 #[must_use]
360 pub fn with_guide_style(mut self, style: Style) -> Self {
361 self.guide_style = style;
362 self
363 }
364
365 #[must_use]
367 pub fn with_label_style(mut self, style: Style) -> Self {
368 self.label_style = style;
369 self
370 }
371
372 #[must_use]
374 pub fn with_root_style(mut self, style: Style) -> Self {
375 self.root_style = style;
376 self
377 }
378
379 #[must_use]
381 pub fn with_persistence_id(mut self, id: impl Into<String>) -> Self {
382 self.persistence_id = Some(id.into());
383 self
384 }
385
386 #[must_use]
388 pub fn persistence_id(&self) -> Option<&str> {
389 self.persistence_id.as_deref()
390 }
391
392 #[must_use]
394 pub fn hit_id(mut self, id: HitId) -> Self {
395 self.hit_id = Some(id);
396 self
397 }
398
399 #[must_use]
401 pub fn with_search_query(mut self, query: impl Into<String>) -> Self {
402 let query = query.into();
403 self.search_query = if query.trim().is_empty() {
404 None
405 } else {
406 Some(query)
407 };
408 self
409 }
410
411 #[must_use]
413 pub fn without_search_query(mut self) -> Self {
414 self.search_query = None;
415 self
416 }
417
418 #[cfg(feature = "tracing")]
419 fn total_nodes(&self) -> usize {
420 if self.show_root {
421 self.root.total_count()
422 } else if self.root.expanded {
423 self.root
424 .children
425 .iter()
426 .fold(0usize, |acc, child| acc.saturating_add(child.total_count()))
427 } else {
428 0
429 }
430 }
431
432 #[cfg(feature = "tracing")]
433 fn visible_nodes(&self) -> usize {
434 if self.show_root {
435 self.root.visible_count()
436 } else if self.root.expanded {
437 self.root.children.iter().fold(0usize, |acc, child| {
438 acc.saturating_add(child.visible_count())
439 })
440 } else {
441 0
442 }
443 }
444
445 #[cfg(feature = "tracing")]
446 fn expanded_nodes(&self) -> usize {
447 if self.show_root {
448 self.root.expanded_count()
449 } else if self.root.expanded {
450 self.root.children.iter().fold(0usize, |acc, child| {
451 acc.saturating_add(child.expanded_count())
452 })
453 } else {
454 0
455 }
456 }
457
458 #[must_use]
460 pub fn root(&self) -> &TreeNode {
461 &self.root
462 }
463
464 pub fn root_mut(&mut self) -> &mut TreeNode {
466 &mut self.root
467 }
468
469 #[allow(clippy::too_many_arguments)]
470 fn render_node(
471 &self,
472 node: &TreeNode,
473 depth: usize,
474 is_last: &mut Vec<bool>,
475 area: Rect,
476 frame: &mut Frame,
477 current_row: &mut usize,
478 deg: ftui_render::budget::DegradationLevel,
479 ) {
480 if *current_row >= area.height as usize {
481 return;
482 }
483
484 let y = area.y.saturating_add(*current_row as u16);
485 let mut x = area.x;
486 let max_x = area.right();
487
488 if depth > 0 && deg.apply_styling() {
490 for d in 0..depth {
491 let is_last_at_depth = is_last.get(d).copied().unwrap_or(false);
492 let guide = if d == depth - 1 {
493 if is_last_at_depth {
495 self.guides.last()
496 } else {
497 self.guides.branch()
498 }
499 } else {
500 if is_last_at_depth {
502 self.guides.space()
503 } else {
504 self.guides.vertical()
505 }
506 };
507
508 x = draw_text_span(frame, x, y, guide, self.guide_style, max_x);
509 }
510 } else if depth > 0 {
511 for _ in 0..depth {
514 x = draw_text_span(frame, x, y, " ", Style::default(), max_x);
515 if x >= max_x {
516 break;
517 }
518 }
519 }
520
521 let style = if depth == 0 && self.show_root {
523 self.root_style
524 } else {
525 self.label_style
526 };
527 if let Some(icon) = node.icon() {
528 let icon_style = if deg.apply_styling() {
529 style
530 } else {
531 Style::default()
532 };
533 x = draw_text_span(frame, x, y, icon, icon_style, max_x);
534 if x < max_x {
535 x = draw_text_span(frame, x, y, " ", icon_style, max_x);
536 }
537 }
538
539 if deg.apply_styling() {
540 draw_text_span(frame, x, y, &node.label, style, max_x);
541 } else {
542 draw_text_span(frame, x, y, &node.label, Style::default(), max_x);
543 }
544
545 if let Some(id) = self.hit_id {
547 let row_area = Rect::new(area.x, y, area.width, 1);
548 frame.register_hit(row_area, id, HitRegion::Content, *current_row as u64);
549 }
550
551 *current_row += 1;
552
553 if !node.expanded {
554 return;
555 }
556
557 let child_count = node.children.len();
558 for (i, child) in node.children.iter().enumerate() {
559 if *current_row >= area.height as usize {
560 break;
561 }
562 is_last.push(i == child_count - 1);
563 self.render_node(child, depth + 1, is_last, area, frame, current_row, deg);
564 is_last.pop();
565 }
566 }
567}
568
569fn filter_node(node: &TreeNode, query_lower: &str) -> Option<TreeNode> {
570 let label_matches = crate::contains_ignore_case(&node.label, query_lower)
571 || node
572 .icon
573 .as_deref()
574 .is_some_and(|icon| crate::contains_ignore_case(icon, query_lower));
575
576 let mut filtered_children = Vec::new();
577 for child in &node.children {
578 if let Some(filtered) = filter_node(child, query_lower) {
579 filtered_children.push(filtered);
580 }
581 }
582
583 let mut filtered_lazy = Vec::new();
584 if let Some(lazy) = &node.lazy_children {
585 for child in lazy {
586 if let Some(filtered) = filter_node(child, query_lower) {
587 filtered_lazy.push(filtered);
588 }
589 }
590 }
591
592 if !label_matches && filtered_children.is_empty() && filtered_lazy.is_empty() {
593 return None;
594 }
595
596 let mut filtered = node.clone();
597 if label_matches {
598 filtered.materialize_all_lazy_children();
602 filtered.expanded = true;
603 } else {
604 filtered.children = filtered_children;
607 filtered.children.extend(filtered_lazy);
608 filtered.lazy_children = None;
609 filtered.expanded = true;
610 }
611 Some(filtered)
612}
613
614struct FilteredPathNode {
615 expanded: bool,
616 children: Vec<(usize, FilteredPathNode)>,
617}
618
619fn create_unfiltered_path_node(node: &TreeNode) -> FilteredPathNode {
620 let mut children = Vec::new();
621 for (idx, child) in node.children.iter().enumerate() {
622 children.push((idx, create_unfiltered_path_node(child)));
623 }
624 let lazy_offset = node.children.len();
625 if let Some(lazy) = &node.lazy_children {
626 for (idx, child) in lazy.iter().enumerate() {
627 children.push((lazy_offset + idx, create_unfiltered_path_node(child)));
628 }
629 }
630 FilteredPathNode {
631 expanded: node.expanded,
632 children,
633 }
634}
635
636fn filter_node_paths(
637 node: &TreeNode,
638 query_lower: &str,
639) -> Option<(bool, Vec<(usize, FilteredPathNode)>)> {
640 let label_matches = crate::contains_ignore_case(&node.label, query_lower)
641 || node
642 .icon
643 .as_deref()
644 .is_some_and(|icon| crate::contains_ignore_case(icon, query_lower));
645
646 if label_matches {
647 let mut children = Vec::new();
648 for (idx, child) in node.children.iter().enumerate() {
649 children.push((idx, create_unfiltered_path_node(child)));
650 }
651 let lazy_offset = node.children.len();
652 if let Some(lazy) = &node.lazy_children {
653 for (idx, child) in lazy.iter().enumerate() {
654 children.push((lazy_offset + idx, create_unfiltered_path_node(child)));
655 }
656 }
657 return Some((true, children));
658 }
659
660 let mut filtered_children = Vec::new();
661 for (idx, child) in node.children.iter().enumerate() {
662 if let Some(filtered) = filter_node_paths(child, query_lower) {
663 filtered_children.push((
664 idx,
665 FilteredPathNode {
666 expanded: filtered.0,
667 children: filtered.1,
668 },
669 ));
670 }
671 }
672
673 let mut filtered_lazy = Vec::new();
674 let lazy_offset = node.children.len();
675 if let Some(lazy) = &node.lazy_children {
676 for (idx, child) in lazy.iter().enumerate() {
677 if let Some(filtered) = filter_node_paths(child, query_lower) {
678 filtered_lazy.push((
679 lazy_offset + idx,
680 FilteredPathNode {
681 expanded: filtered.0,
682 children: filtered.1,
683 },
684 ));
685 }
686 }
687 }
688
689 if filtered_children.is_empty() && filtered_lazy.is_empty() {
690 return None;
691 }
692
693 filtered_children.extend(filtered_lazy);
694 Some((true, filtered_children))
695}
696
697impl Widget for Tree {
698 fn render(&self, area: Rect, frame: &mut Frame) {
699 if area.width == 0 || area.height == 0 {
700 return;
701 }
702
703 #[cfg(feature = "tracing")]
704 let render_start = Instant::now();
705 #[cfg(feature = "tracing")]
706 let total_nodes = self.total_nodes();
707 #[cfg(feature = "tracing")]
708 let visible_nodes = self.visible_nodes();
709 #[cfg(feature = "tracing")]
710 let expanded_count = self.expanded_nodes();
711 #[cfg(feature = "tracing")]
712 let render_span = tracing::debug_span!(
713 "tree.render",
714 total_nodes,
715 visible_nodes,
716 expanded_count,
717 render_duration_us = tracing::field::Empty,
718 );
719 #[cfg(feature = "tracing")]
720 let _render_guard = render_span.enter();
721
722 let deg = frame.buffer.degradation;
723 let base_style = if deg.apply_styling() {
724 self.label_style
725 } else {
726 Style::default()
727 };
728 clear_text_area(frame, area, base_style);
729 let mut current_row = 0;
730 let mut is_last = Vec::with_capacity(8);
731
732 let search_query = self
733 .search_query
734 .as_deref()
735 .filter(|q| !q.trim().is_empty());
736 let is_searching = search_query.is_some();
737 let filtered_root = if let Some(q) = search_query {
738 let query_lower = q.trim().to_lowercase();
739 filter_node(&self.root, &query_lower)
740 } else {
741 Some(self.root.clone())
742 };
743
744 if is_searching && filtered_root.is_none() {
745 #[cfg(feature = "tracing")]
746 {
747 let elapsed = render_start.elapsed();
748 let elapsed_us = elapsed.as_micros() as u64;
749 render_span.record("render_duration_us", elapsed_us);
750 tracing::debug!(
751 render_duration_us = elapsed_us,
752 "Tree render complete (empty search)"
753 );
754 }
755 return;
756 }
757
758 let root_fallback = self.root.clone();
759 let root = filtered_root.as_ref().unwrap_or(&root_fallback);
760
761 if self.show_root {
762 self.render_node(root, 0, &mut is_last, area, frame, &mut current_row, deg);
763 } else if root.expanded {
764 let child_count = root.children.len();
768 for (i, child) in root.children.iter().enumerate() {
769 is_last.push(i == child_count - 1);
770 self.render_node(
771 child,
772 0, &mut is_last,
774 area,
775 frame,
776 &mut current_row,
777 deg,
778 );
779 is_last.pop();
780 }
781 }
782
783 #[cfg(feature = "tracing")]
784 {
785 let elapsed = render_start.elapsed();
786 let elapsed_us = elapsed.as_micros() as u64;
787 render_span.record("render_duration_us", elapsed_us);
788 tracing::debug!(
789 message = "tree.metrics",
790 tree_render_duration_us = elapsed_us,
791 total_nodes,
792 visible_nodes,
793 expanded_count
794 );
795 }
796 }
797
798 fn is_essential(&self) -> bool {
799 false
800 }
801}
802
803#[derive(Clone, Debug, Default, PartialEq)]
811#[cfg_attr(
812 feature = "state-persistence",
813 derive(serde::Serialize, serde::Deserialize)
814)]
815pub struct TreePersistState {
816 pub expanded_paths: HashSet<String>,
818}
819
820impl crate::stateful::Stateful for Tree {
821 type State = TreePersistState;
822
823 fn state_key(&self) -> crate::stateful::StateKey {
824 crate::stateful::StateKey::new("Tree", self.persistence_id.as_deref().unwrap_or("default"))
825 }
826
827 fn save_state(&self) -> TreePersistState {
828 let mut expanded_paths = HashSet::new();
829 self.root.collect_expanded("", &mut expanded_paths);
830 TreePersistState { expanded_paths }
831 }
832
833 fn restore_state(&mut self, state: TreePersistState) {
834 self.root.apply_expanded("", &state.expanded_paths);
835 }
836}
837
838impl UndoSupport for Tree {
843 fn undo_widget_id(&self) -> UndoWidgetId {
844 self.undo_id
845 }
846
847 fn create_snapshot(&self) -> Box<dyn Any + Send> {
848 Box::new(self.save_state())
849 }
850
851 fn restore_snapshot(&mut self, snapshot: &dyn Any) -> bool {
852 if let Some(snap) = snapshot.downcast_ref::<TreePersistState>() {
853 self.restore_state(snap.clone());
854 true
855 } else {
856 false
857 }
858 }
859}
860
861impl TreeUndoExt for Tree {
862 fn is_node_expanded(&self, path: &[usize]) -> bool {
863 self.get_node_at_path(path)
864 .map(|node| node.is_expanded())
865 .unwrap_or(false)
866 }
867
868 fn expand_node(&mut self, path: &[usize]) {
869 if let Some(node) = self.get_node_at_path_mut(path) {
870 node.materialize_lazy_children();
871 node.expanded = true;
872 }
873 }
874
875 fn collapse_node(&mut self, path: &[usize]) {
876 if let Some(node) = self.get_node_at_path_mut(path) {
877 node.expanded = false;
878 }
879 }
880}
881
882impl Tree {
883 #[must_use]
885 pub fn undo_id(&self) -> UndoWidgetId {
886 self.undo_id
887 }
888
889 fn get_node_at_path(&self, path: &[usize]) -> Option<&TreeNode> {
891 let mut current = &self.root;
892 for &idx in path {
893 current = current.children.get(idx)?;
894 }
895 Some(current)
896 }
897
898 fn get_node_at_path_mut(&mut self, path: &[usize]) -> Option<&mut TreeNode> {
900 let mut current = &mut self.root;
901 for &idx in path {
902 current = current.children.get_mut(idx)?;
903 }
904 Some(current)
905 }
906
907 #[cfg(feature = "tracing")]
908 fn log_expand_collapse(action: &str, source: &str, index: usize, label: &str) {
909 tracing::debug!(
910 message = "tree.toggle",
911 action,
912 source,
913 visible_index = index,
914 label
915 );
916 }
917
918 fn toggle_node_at_visible_index(&mut self, index: usize, source: &str) -> bool {
919 #[cfg(not(feature = "tracing"))]
920 let _ = source;
921 let Some(node) = self.node_at_visible_index_mut(index) else {
922 return false;
923 };
924 if !node.has_children() {
925 return false;
926 }
927 #[cfg(feature = "tracing")]
928 let action = if node.is_expanded() {
929 "collapse"
930 } else {
931 "expand"
932 };
933 #[cfg(feature = "tracing")]
934 let label = node.label().to_owned();
935 node.toggle_expanded();
936 #[cfg(feature = "tracing")]
937 Self::log_expand_collapse(action, source, index, &label);
938 true
939 }
940
941 pub fn handle_key(&mut self, key: &KeyEvent, selected_visible_index: usize) -> bool {
950 match key.code {
951 KeyCode::Enter | KeyCode::Char(' ') => {
952 self.toggle_node_at_visible_index(selected_visible_index, "keyboard")
953 }
954 KeyCode::Right => {
955 if let Some(node) = self.node_at_visible_index_mut(selected_visible_index)
957 && !node.is_expanded()
958 && node.has_children()
959 {
960 #[cfg(feature = "tracing")]
961 let label = node.label().to_owned();
962 node.toggle_expanded();
963 #[cfg(feature = "tracing")]
964 Self::log_expand_collapse("expand", "keyboard", selected_visible_index, &label);
965 return true;
966 }
967 false
968 }
969 KeyCode::Left => {
970 if let Some(node) = self.node_at_visible_index_mut(selected_visible_index)
972 && node.is_expanded()
973 && node.has_children()
974 {
975 #[cfg(feature = "tracing")]
976 let label = node.label().to_owned();
977 node.toggle_expanded();
978 #[cfg(feature = "tracing")]
979 Self::log_expand_collapse(
980 "collapse",
981 "keyboard",
982 selected_visible_index,
983 &label,
984 );
985 return true;
986 }
987 false
988 }
989 _ => false,
990 }
991 }
992
993 pub fn handle_mouse(
1010 &mut self,
1011 event: &MouseEvent,
1012 hit: Option<(HitId, HitRegion, u64)>,
1013 expected_id: HitId,
1014 ) -> MouseResult {
1015 match event.kind {
1016 MouseEventKind::Down(MouseButton::Left) => {
1017 if let Some((id, HitRegion::Content, data)) = hit
1018 && id == expected_id
1019 {
1020 let index = data as usize;
1021 if let Some(node) = self.node_at_visible_index_mut(index)
1022 && !node.has_children()
1023 {
1024 return MouseResult::Selected(index);
1025 }
1026 if self.toggle_node_at_visible_index(index, "mouse") {
1027 return MouseResult::Activated(index);
1028 }
1029 }
1030 MouseResult::Ignored
1031 }
1032 _ => MouseResult::Ignored,
1033 }
1034 }
1035
1036 pub fn node_at_visible_index_mut(&mut self, target: usize) -> Option<&mut TreeNode> {
1042 let path = self.find_path_indices_at_visible_index(target)?;
1043
1044 let mut current = &mut self.root;
1045 for &idx in &path {
1046 current.materialize_lazy_children();
1047 current = current.children.get_mut(idx)?;
1048 }
1049 Some(current)
1050 }
1051
1052 fn find_path_indices_at_visible_index(&self, target: usize) -> Option<Vec<usize>> {
1053 let query = self
1054 .search_query
1055 .as_deref()
1056 .map(str::trim)
1057 .filter(|q| !q.is_empty());
1058 let mut counter = 0usize;
1059 let mut path = Vec::new();
1060
1061 if let Some(q) = query {
1062 let query_lower = q.to_lowercase();
1063 let (expanded, children) = filter_node_paths(&self.root, &query_lower)?;
1064 let root_node = FilteredPathNode { expanded, children };
1065
1066 if self.show_root {
1067 Self::walk_filtered_path(&root_node, target, &mut counter, &mut path)
1068 } else if root_node.expanded {
1069 for &(idx, ref child) in &root_node.children {
1070 path.push(idx);
1071 if let Some(p) =
1072 Self::walk_filtered_path(child, target, &mut counter, &mut path)
1073 {
1074 return Some(p);
1075 }
1076 path.pop();
1077 }
1078 None
1079 } else {
1080 None
1081 }
1082 } else {
1083 if self.show_root {
1084 Self::walk_visible_index_path(&self.root, target, &mut counter, &mut path)
1085 } else if self.root.expanded {
1086 for (idx, child) in self.root.children.iter().enumerate() {
1087 path.push(idx);
1088 if let Some(p) =
1089 Self::walk_visible_index_path(child, target, &mut counter, &mut path)
1090 {
1091 return Some(p);
1092 }
1093 path.pop();
1094 }
1095 None
1096 } else {
1097 None
1098 }
1099 }
1100 }
1101
1102 fn walk_filtered_path(
1103 node: &FilteredPathNode,
1104 target: usize,
1105 counter: &mut usize,
1106 current_path: &mut Vec<usize>,
1107 ) -> Option<Vec<usize>> {
1108 if *counter == target {
1109 return Some(current_path.clone());
1110 }
1111 *counter += 1;
1112 if node.expanded {
1113 for &(idx, ref child) in &node.children {
1114 current_path.push(idx);
1115 if let Some(found) = Self::walk_filtered_path(child, target, counter, current_path)
1116 {
1117 return Some(found);
1118 }
1119 current_path.pop();
1120 }
1121 }
1122 None
1123 }
1124
1125 fn walk_visible_index_path(
1126 node: &TreeNode,
1127 target: usize,
1128 counter: &mut usize,
1129 current_path: &mut Vec<usize>,
1130 ) -> Option<Vec<usize>> {
1131 if *counter == target {
1132 return Some(current_path.clone());
1133 }
1134 *counter += 1;
1135 if node.expanded {
1136 for (idx, child) in node.children.iter().enumerate() {
1137 current_path.push(idx);
1138 if let Some(found) =
1139 Self::walk_visible_index_path(child, target, counter, current_path)
1140 {
1141 return Some(found);
1142 }
1143 current_path.pop();
1144 }
1145 }
1146 None
1147 }
1148}
1149
1150#[cfg(test)]
1155#[derive(Debug, Clone, PartialEq, Eq)]
1156struct FlatNode {
1157 label: String,
1158 depth: usize,
1159}
1160
1161#[cfg(test)]
1162fn flatten_visible(node: &TreeNode, depth: usize, out: &mut Vec<FlatNode>) {
1163 out.push(FlatNode {
1164 label: node.label.clone(),
1165 depth,
1166 });
1167 if node.expanded {
1168 for child in &node.children {
1169 flatten_visible(child, depth + 1, out);
1170 }
1171 }
1172}
1173
1174#[cfg(test)]
1175impl Tree {
1176 fn flatten(&self) -> Vec<FlatNode> {
1177 let mut out = Vec::new();
1178 let search_query = self
1179 .search_query
1180 .as_deref()
1181 .filter(|q| !q.trim().is_empty());
1182 let is_searching = search_query.is_some();
1183 let filtered_root = if let Some(q) = search_query {
1184 let query_lower = q.trim().to_lowercase();
1185 filter_node(&self.root, &query_lower)
1186 } else {
1187 Some(self.root.clone())
1188 };
1189
1190 if is_searching && filtered_root.is_none() {
1191 return out;
1192 }
1193
1194 let root_fallback = self.root.clone();
1195 let root = filtered_root.as_ref().unwrap_or(&root_fallback);
1196 if self.show_root {
1197 flatten_visible(root, 0, &mut out);
1198 } else if root.expanded {
1199 for child in &root.children {
1200 flatten_visible(child, 0, &mut out);
1201 }
1202 }
1203 out
1204 }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210 use ftui_render::frame::Frame;
1211 use ftui_render::grapheme_pool::GraphemePool;
1212 #[cfg(feature = "tracing")]
1213 use std::sync::{Arc, Mutex};
1214 #[cfg(feature = "tracing")]
1215 use tracing::Subscriber;
1216 #[cfg(feature = "tracing")]
1217 use tracing_subscriber::Layer;
1218 #[cfg(feature = "tracing")]
1219 use tracing_subscriber::layer::{Context, SubscriberExt};
1220
1221 fn line_text(frame: &Frame, y: u16, width: u16) -> String {
1222 (0..width)
1223 .map(|x| {
1224 frame
1225 .buffer
1226 .get(x, y)
1227 .and_then(|cell| cell.content.as_char())
1228 .unwrap_or(' ')
1229 })
1230 .collect()
1231 }
1232
1233 fn simple_tree() -> TreeNode {
1234 TreeNode::new("root")
1235 .child(
1236 TreeNode::new("a")
1237 .child(TreeNode::new("a1"))
1238 .child(TreeNode::new("a2")),
1239 )
1240 .child(TreeNode::new("b"))
1241 }
1242
1243 #[cfg(feature = "tracing")]
1244 #[derive(Debug, Default)]
1245 struct TreeTraceState {
1246 tree_render_seen: bool,
1247 has_total_nodes_field: bool,
1248 has_visible_nodes_field: bool,
1249 has_expanded_count_field: bool,
1250 render_duration_recorded: bool,
1251 toggle_events: usize,
1252 }
1253
1254 #[cfg(feature = "tracing")]
1255 struct TreeTraceCapture {
1256 state: Arc<Mutex<TreeTraceState>>,
1257 }
1258
1259 #[cfg(feature = "tracing")]
1260 impl<S> Layer<S> for TreeTraceCapture
1261 where
1262 S: Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
1263 {
1264 fn on_new_span(
1265 &self,
1266 attrs: &tracing::span::Attributes<'_>,
1267 _id: &tracing::Id,
1268 _ctx: Context<'_, S>,
1269 ) {
1270 if attrs.metadata().name() != "tree.render" {
1271 return;
1272 }
1273 let fields = attrs.metadata().fields();
1274 let mut state = self.state.lock().expect("tree trace state lock");
1275 state.tree_render_seen = true;
1276 state.has_total_nodes_field |= fields.field("total_nodes").is_some();
1277 state.has_visible_nodes_field |= fields.field("visible_nodes").is_some();
1278 state.has_expanded_count_field |= fields.field("expanded_count").is_some();
1279 }
1280
1281 fn on_record(
1282 &self,
1283 id: &tracing::Id,
1284 values: &tracing::span::Record<'_>,
1285 ctx: Context<'_, S>,
1286 ) {
1287 let Some(span) = ctx.span(id) else {
1288 return;
1289 };
1290 if span.metadata().name() != "tree.render" {
1291 return;
1292 }
1293
1294 struct DurationVisitor {
1295 saw_duration: bool,
1296 }
1297 impl tracing::field::Visit for DurationVisitor {
1298 fn record_u64(&mut self, field: &tracing::field::Field, _value: u64) {
1299 if field.name() == "render_duration_us" {
1300 self.saw_duration = true;
1301 }
1302 }
1303
1304 fn record_debug(
1305 &mut self,
1306 field: &tracing::field::Field,
1307 _value: &dyn std::fmt::Debug,
1308 ) {
1309 if field.name() == "render_duration_us" {
1310 self.saw_duration = true;
1311 }
1312 }
1313 }
1314
1315 let mut visitor = DurationVisitor {
1316 saw_duration: false,
1317 };
1318 values.record(&mut visitor);
1319 if visitor.saw_duration {
1320 self.state
1321 .lock()
1322 .expect("tree trace state lock")
1323 .render_duration_recorded = true;
1324 }
1325 }
1326
1327 fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
1328 struct MessageVisitor {
1329 message: Option<String>,
1330 }
1331 impl tracing::field::Visit for MessageVisitor {
1332 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
1333 if field.name() == "message" {
1334 self.message = Some(value.to_owned());
1335 }
1336 }
1337
1338 fn record_debug(
1339 &mut self,
1340 field: &tracing::field::Field,
1341 value: &dyn std::fmt::Debug,
1342 ) {
1343 if field.name() == "message" {
1344 self.message = Some(format!("{value:?}").trim_matches('"').to_owned());
1345 }
1346 }
1347 }
1348
1349 let mut visitor = MessageVisitor { message: None };
1350 event.record(&mut visitor);
1351 if visitor.message.as_deref() == Some("tree.toggle") {
1352 let mut state = self.state.lock().expect("tree trace state lock");
1353 state.toggle_events = state.toggle_events.saturating_add(1);
1354 }
1355 }
1356 }
1357
1358 #[test]
1359 fn tree_node_basics() {
1360 let node = TreeNode::new("hello");
1361 assert_eq!(node.label(), "hello");
1362 assert!(node.children().is_empty());
1363 assert!(node.is_expanded());
1364 }
1365
1366 #[test]
1367 fn tree_node_children() {
1368 let root = simple_tree();
1369 assert_eq!(root.children().len(), 2);
1370 assert_eq!(root.children()[0].label(), "a");
1371 assert_eq!(root.children()[0].children().len(), 2);
1372 }
1373
1374 #[test]
1375 fn tree_node_visible_count() {
1376 let root = simple_tree();
1377 assert_eq!(root.visible_count(), 5);
1379 }
1380
1381 #[test]
1382 fn tree_node_collapsed() {
1383 let root = TreeNode::new("root")
1384 .child(
1385 TreeNode::new("a")
1386 .with_expanded(false)
1387 .child(TreeNode::new("a1"))
1388 .child(TreeNode::new("a2")),
1389 )
1390 .child(TreeNode::new("b"));
1391 assert_eq!(root.visible_count(), 3);
1393 }
1394
1395 #[test]
1396 fn tree_node_toggle() {
1397 let mut node = TreeNode::new("x");
1398 assert!(node.is_expanded());
1399 node.toggle_expanded();
1400 assert!(!node.is_expanded());
1401 node.toggle_expanded();
1402 assert!(node.is_expanded());
1403 }
1404
1405 #[test]
1406 fn tree_node_lazy_children_materialize_on_expand() {
1407 let mut node = TreeNode::new("root")
1408 .with_lazy_children(vec![TreeNode::new("child"), TreeNode::new("child2")]);
1409 assert!(!node.is_expanded());
1410 assert_eq!(node.children().len(), 0);
1411 assert!(node.has_children());
1412
1413 node.toggle_expanded();
1414 assert!(node.is_expanded());
1415 assert_eq!(node.children().len(), 2);
1416 }
1417
1418 #[test]
1419 fn tree_guides_unicode() {
1420 let g = TreeGuides::Unicode;
1421 assert!(g.branch().contains('├'));
1422 assert!(g.last().contains('└'));
1423 assert!(g.vertical().contains('│'));
1424 }
1425
1426 #[test]
1427 fn tree_guides_ascii() {
1428 let g = TreeGuides::Ascii;
1429 assert!(g.branch().contains('+'));
1430 assert!(g.vertical().contains('|'));
1431 }
1432
1433 #[test]
1434 fn tree_guides_width() {
1435 for g in [
1436 TreeGuides::Ascii,
1437 TreeGuides::Unicode,
1438 TreeGuides::Bold,
1439 TreeGuides::Double,
1440 TreeGuides::Rounded,
1441 ] {
1442 assert_eq!(g.width(), 4);
1443 }
1444 }
1445
1446 #[test]
1447 fn tree_render_basic() {
1448 let tree = Tree::new(simple_tree());
1449
1450 let mut pool = GraphemePool::new();
1451 let mut frame = Frame::new(40, 10, &mut pool);
1452 let area = Rect::new(0, 0, 40, 10);
1453 tree.render(area, &mut frame);
1454
1455 let cell = frame.buffer.get(0, 0).unwrap();
1457 assert_eq!(cell.content.as_char(), Some('r'));
1458 }
1459
1460 #[test]
1461 fn tree_render_guides_present() {
1462 let tree = Tree::new(simple_tree()).with_guides(TreeGuides::Ascii);
1463
1464 let mut pool = GraphemePool::new();
1465 let mut frame = Frame::new(40, 10, &mut pool);
1466 let area = Rect::new(0, 0, 40, 10);
1467 tree.render(area, &mut frame);
1468
1469 let cell = frame.buffer.get(0, 1).unwrap();
1472 assert_eq!(cell.content.as_char(), Some('+'));
1473 }
1474
1475 #[test]
1476 fn tree_render_last_guide() {
1477 let tree = Tree::new(
1478 TreeNode::new("root")
1479 .child(TreeNode::new("a"))
1480 .child(TreeNode::new("b")),
1481 )
1482 .with_guides(TreeGuides::Ascii);
1483
1484 let mut pool = GraphemePool::new();
1485 let mut frame = Frame::new(40, 10, &mut pool);
1486 let area = Rect::new(0, 0, 40, 10);
1487 tree.render(area, &mut frame);
1488
1489 let cell = frame.buffer.get(0, 1).unwrap();
1491 assert_eq!(cell.content.as_char(), Some('+'));
1492
1493 let cell = frame.buffer.get(0, 2).unwrap();
1495 assert_eq!(cell.content.as_char(), Some('`'));
1496 }
1497
1498 #[test]
1499 fn tree_render_icon_before_label() {
1500 let tree = Tree::new(TreeNode::new("root").with_icon(">"));
1501 let mut pool = GraphemePool::new();
1502 let mut frame = Frame::new(12, 2, &mut pool);
1503 tree.render(Rect::new(0, 0, 12, 2), &mut frame);
1504
1505 assert_eq!(
1506 frame.buffer.get(0, 0).and_then(|c| c.content.as_char()),
1507 Some('>')
1508 );
1509 assert_eq!(
1510 frame.buffer.get(2, 0).and_then(|c| c.content.as_char()),
1511 Some('r')
1512 );
1513 }
1514
1515 #[test]
1516 fn tree_search_query_filters_to_matching_branches() {
1517 let tree = Tree::new(
1518 TreeNode::new("root")
1519 .child(TreeNode::new("alpha").child(TreeNode::new("target-file")))
1520 .child(TreeNode::new("beta")),
1521 )
1522 .with_search_query("target");
1523
1524 let flat = tree.flatten();
1525 assert_eq!(flat.len(), 3);
1526 assert_eq!(flat[0].label, "root");
1527 assert_eq!(flat[1].label, "alpha");
1528 assert_eq!(flat[2].label, "target-file");
1529 }
1530
1531 #[test]
1532 fn tree_search_query_includes_lazy_matching_descendants() {
1533 let tree =
1534 Tree::new(TreeNode::new("root").child(
1535 TreeNode::new("alpha").with_lazy_children(vec![TreeNode::new("target-file")]),
1536 ))
1537 .with_search_query("target");
1538
1539 let flat = tree.flatten();
1540 assert_eq!(flat.len(), 3);
1541 assert_eq!(flat[0].label, "root");
1542 assert_eq!(flat[1].label, "alpha");
1543 assert_eq!(flat[2].label, "target-file");
1544 }
1545
1546 #[test]
1547 fn tree_search_query_on_matching_parent_includes_immediate_lazy_children() {
1548 let tree = Tree::new(TreeNode::new("root").child(
1549 TreeNode::new("alpha").with_lazy_children(vec![
1550 TreeNode::new("lazy-child")
1551 .with_lazy_children(vec![TreeNode::new("deep-grandchild")]),
1552 ]),
1553 ))
1554 .with_search_query("alpha");
1555
1556 let flat = tree.flatten();
1557 assert_eq!(flat.len(), 3);
1558 assert_eq!(flat[0].label, "root");
1559 assert_eq!(flat[1].label, "alpha");
1560 assert_eq!(flat[2].label, "lazy-child");
1561 }
1562
1563 #[test]
1564 fn tree_render_zero_area() {
1565 let tree = Tree::new(simple_tree());
1566 let mut pool = GraphemePool::new();
1567 let mut frame = Frame::new(40, 10, &mut pool);
1568 tree.render(Rect::new(0, 0, 0, 0), &mut frame); }
1570
1571 #[test]
1572 fn tree_render_shorter_label_clears_stale_suffix() {
1573 let mut pool = GraphemePool::new();
1574 let mut frame = Frame::new(20, 3, &mut pool);
1575 let area = Rect::new(0, 0, 20, 3);
1576
1577 Tree::new(TreeNode::new("root-with-long-tail")).render(area, &mut frame);
1578 Tree::new(TreeNode::new("r")).render(area, &mut frame);
1579
1580 assert_eq!(line_text(&frame, 0, 20), "r ");
1581 assert_eq!(line_text(&frame, 1, 20), " ");
1582 assert_eq!(line_text(&frame, 2, 20), " ");
1583 }
1584
1585 #[test]
1586 fn tree_render_truncated_height() {
1587 let tree = Tree::new(simple_tree());
1588 let mut pool = GraphemePool::new();
1589 let mut frame = Frame::new(40, 2, &mut pool);
1590 let area = Rect::new(0, 0, 40, 2);
1591 tree.render(area, &mut frame); }
1593
1594 #[test]
1595 fn is_not_essential() {
1596 let tree = Tree::new(TreeNode::new("x"));
1597 assert!(!tree.is_essential());
1598 }
1599
1600 #[test]
1601 fn tree_root_access() {
1602 let mut tree = Tree::new(TreeNode::new("root"));
1603 assert_eq!(tree.root().label(), "root");
1604 tree.root_mut().toggle_expanded();
1605 assert!(!tree.root().is_expanded());
1606 }
1607
1608 #[test]
1609 fn tree_guides_default() {
1610 let g = TreeGuides::default();
1611 assert_eq!(g, TreeGuides::Unicode);
1612 }
1613
1614 #[test]
1615 fn tree_guides_rounded() {
1616 let g = TreeGuides::Rounded;
1617 assert!(g.last().contains('╰'));
1618 }
1619
1620 #[test]
1621 fn tree_deep_nesting() {
1622 let node = TreeNode::new("d3");
1623 let node = TreeNode::new("d2").child(node);
1624 let node = TreeNode::new("d1").child(node);
1625 let root = TreeNode::new("root").child(node);
1626
1627 let tree = Tree::new(root);
1628 let flat = tree.flatten();
1629 assert_eq!(flat.len(), 4);
1630 assert_eq!(flat[3].depth, 3);
1631 }
1632
1633 #[test]
1634 fn tree_node_with_children_vec() {
1635 let root = TreeNode::new("root").with_children(vec![
1636 TreeNode::new("a"),
1637 TreeNode::new("b"),
1638 TreeNode::new("c"),
1639 ]);
1640 assert_eq!(root.children().len(), 3);
1641 }
1642
1643 use crate::stateful::Stateful;
1646
1647 #[test]
1648 fn tree_with_persistence_id() {
1649 let tree = Tree::new(TreeNode::new("root")).with_persistence_id("file-tree");
1650 assert_eq!(tree.persistence_id(), Some("file-tree"));
1651 }
1652
1653 #[test]
1654 fn tree_default_no_persistence_id() {
1655 let tree = Tree::new(TreeNode::new("root"));
1656 assert_eq!(tree.persistence_id(), None);
1657 }
1658
1659 #[test]
1660 fn tree_save_restore_round_trip() {
1661 let mut tree = Tree::new(
1663 TreeNode::new("root")
1664 .child(
1665 TreeNode::new("src")
1666 .child(TreeNode::new("main.rs"))
1667 .child(TreeNode::new("lib.rs")),
1668 )
1669 .child(TreeNode::new("tests").with_expanded(false)),
1670 )
1671 .with_persistence_id("test");
1672
1673 assert!(tree.root().is_expanded());
1675 assert!(tree.root().children()[0].is_expanded()); assert!(!tree.root().children()[1].is_expanded()); let saved = tree.save_state();
1679
1680 assert!(saved.expanded_paths.contains("root"));
1682 assert!(saved.expanded_paths.contains("root/src"));
1683 assert!(!saved.expanded_paths.contains("root/tests"));
1684
1685 tree.root_mut().children[0].toggle_expanded();
1687 assert!(!tree.root().children()[0].is_expanded());
1688
1689 tree.restore_state(saved);
1691
1692 assert!(tree.root().is_expanded());
1694 assert!(tree.root().children()[0].is_expanded()); assert!(!tree.root().children()[1].is_expanded()); }
1697
1698 #[test]
1699 fn tree_state_key_uses_persistence_id() {
1700 let tree = Tree::new(TreeNode::new("root")).with_persistence_id("project-explorer");
1701 let key = tree.state_key();
1702 assert_eq!(key.widget_type, "Tree");
1703 assert_eq!(key.instance_id, "project-explorer");
1704 }
1705
1706 #[test]
1707 fn tree_state_key_default_when_no_id() {
1708 let tree = Tree::new(TreeNode::new("root"));
1709 let key = tree.state_key();
1710 assert_eq!(key.widget_type, "Tree");
1711 assert_eq!(key.instance_id, "default");
1712 }
1713
1714 #[test]
1715 fn tree_persist_state_default() {
1716 let persist = TreePersistState::default();
1717 assert!(persist.expanded_paths.is_empty());
1718 }
1719
1720 #[test]
1721 fn tree_collect_expanded_only_includes_nodes_with_children() {
1722 let tree = Tree::new(
1723 TreeNode::new("root").child(TreeNode::new("leaf")), );
1725
1726 let saved = tree.save_state();
1727
1728 assert!(saved.expanded_paths.contains("root"));
1730 assert!(!saved.expanded_paths.contains("root/leaf"));
1732 }
1733
1734 #[test]
1739 fn tree_undo_widget_id_unique() {
1740 let tree1 = Tree::new(TreeNode::new("root1"));
1741 let tree2 = Tree::new(TreeNode::new("root2"));
1742 assert_ne!(tree1.undo_id(), tree2.undo_id());
1743 }
1744
1745 #[test]
1746 fn tree_undo_snapshot_and_restore() {
1747 let mut tree = Tree::new(
1749 TreeNode::new("root")
1750 .child(
1751 TreeNode::new("a")
1752 .with_expanded(true)
1753 .child(TreeNode::new("a_child")),
1754 )
1755 .child(
1756 TreeNode::new("b")
1757 .with_expanded(false)
1758 .child(TreeNode::new("b_child")),
1759 ),
1760 );
1761
1762 let snapshot = tree.create_snapshot();
1764
1765 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]));
1773 assert!(tree.is_node_expanded(&[1]));
1774
1775 assert!(tree.restore_snapshot(&*snapshot));
1777
1778 assert!(tree.is_node_expanded(&[0])); assert!(!tree.is_node_expanded(&[1])); }
1782
1783 #[test]
1784 fn tree_expand_collapse_node() {
1785 let mut tree =
1786 Tree::new(TreeNode::new("root").child(TreeNode::new("child").with_expanded(true)));
1787
1788 assert!(tree.is_node_expanded(&[0]));
1790
1791 tree.collapse_node(&[0]);
1793 assert!(!tree.is_node_expanded(&[0]));
1794
1795 tree.expand_node(&[0]);
1797 assert!(tree.is_node_expanded(&[0]));
1798 }
1799
1800 #[test]
1801 fn tree_node_path_navigation() {
1802 let tree = Tree::new(
1803 TreeNode::new("root")
1804 .child(
1805 TreeNode::new("a")
1806 .child(TreeNode::new("a1"))
1807 .child(TreeNode::new("a2")),
1808 )
1809 .child(TreeNode::new("b")),
1810 );
1811
1812 assert_eq!(tree.get_node_at_path(&[]).map(|n| n.label()), Some("root"));
1814 assert_eq!(tree.get_node_at_path(&[0]).map(|n| n.label()), Some("a"));
1815 assert_eq!(tree.get_node_at_path(&[1]).map(|n| n.label()), Some("b"));
1816 assert_eq!(
1817 tree.get_node_at_path(&[0, 0]).map(|n| n.label()),
1818 Some("a1")
1819 );
1820 assert_eq!(
1821 tree.get_node_at_path(&[0, 1]).map(|n| n.label()),
1822 Some("a2")
1823 );
1824 assert!(tree.get_node_at_path(&[5]).is_none()); }
1826
1827 #[test]
1828 fn tree_restore_wrong_snapshot_type_fails() {
1829 use std::any::Any;
1830 let mut tree = Tree::new(TreeNode::new("root"));
1831 let wrong_snapshot: Box<dyn Any + Send> = Box::new(42i32);
1832 assert!(!tree.restore_snapshot(&*wrong_snapshot));
1833 }
1834
1835 use crate::mouse::MouseResult;
1838 use ftui_core::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
1839
1840 #[test]
1841 fn tree_click_expands_parent() {
1842 let mut tree = Tree::new(
1843 TreeNode::new("root")
1844 .child(
1845 TreeNode::new("a")
1846 .child(TreeNode::new("a1"))
1847 .child(TreeNode::new("a2")),
1848 )
1849 .child(TreeNode::new("b")),
1850 );
1851 assert!(tree.root().children()[0].is_expanded());
1852
1853 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 5, 1);
1855 let hit = Some((HitId::new(1), HitRegion::Content, 1u64));
1856 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1857 assert_eq!(result, MouseResult::Activated(1));
1858 assert!(!tree.root().children()[0].is_expanded()); }
1860
1861 #[test]
1862 fn tree_click_selects_leaf() {
1863 let mut tree = Tree::new(
1864 TreeNode::new("root")
1865 .child(
1866 TreeNode::new("a")
1867 .child(TreeNode::new("a1"))
1868 .child(TreeNode::new("a2")),
1869 )
1870 .child(TreeNode::new("b")),
1871 );
1872
1873 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 5, 4);
1875 let hit = Some((HitId::new(1), HitRegion::Content, 4u64));
1876 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1877 assert_eq!(result, MouseResult::Selected(4));
1878 }
1879
1880 #[test]
1881 fn tree_click_wrong_id_ignored() {
1882 let mut tree = Tree::new(TreeNode::new("root").child(TreeNode::new("a")));
1883 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
1884 let hit = Some((HitId::new(99), HitRegion::Content, 0u64));
1885 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1886 assert_eq!(result, MouseResult::Ignored);
1887 }
1888
1889 #[test]
1890 fn tree_click_no_hit_ignored() {
1891 let mut tree = Tree::new(TreeNode::new("root"));
1892 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
1893 let result = tree.handle_mouse(&event, None, HitId::new(1));
1894 assert_eq!(result, MouseResult::Ignored);
1895 }
1896
1897 #[test]
1898 fn tree_right_click_ignored() {
1899 let mut tree = Tree::new(TreeNode::new("root").child(TreeNode::new("a")));
1900 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Right), 0, 0);
1901 let hit = Some((HitId::new(1), HitRegion::Content, 0u64));
1902 let result = tree.handle_mouse(&event, hit, HitId::new(1));
1903 assert_eq!(result, MouseResult::Ignored);
1904 }
1905
1906 #[test]
1907 fn tree_node_at_visible_index_with_show_root() {
1908 let mut tree = Tree::new(
1909 TreeNode::new("root")
1910 .child(
1911 TreeNode::new("a")
1912 .child(TreeNode::new("a1"))
1913 .child(TreeNode::new("a2")),
1914 )
1915 .child(TreeNode::new("b")),
1916 );
1917
1918 assert_eq!(
1920 tree.node_at_visible_index_mut(0)
1921 .map(|n| n.label().to_string()),
1922 Some("root".to_string())
1923 );
1924 assert_eq!(
1925 tree.node_at_visible_index_mut(1)
1926 .map(|n| n.label().to_string()),
1927 Some("a".to_string())
1928 );
1929 assert_eq!(
1930 tree.node_at_visible_index_mut(2)
1931 .map(|n| n.label().to_string()),
1932 Some("a1".to_string())
1933 );
1934 assert_eq!(
1935 tree.node_at_visible_index_mut(3)
1936 .map(|n| n.label().to_string()),
1937 Some("a2".to_string())
1938 );
1939 assert_eq!(
1940 tree.node_at_visible_index_mut(4)
1941 .map(|n| n.label().to_string()),
1942 Some("b".to_string())
1943 );
1944 assert!(tree.node_at_visible_index_mut(5).is_none());
1945 }
1946
1947 #[test]
1948 fn tree_node_at_visible_index_hidden_root() {
1949 let mut tree = Tree::new(
1950 TreeNode::new("root")
1951 .child(TreeNode::new("a").child(TreeNode::new("a1")))
1952 .child(TreeNode::new("b")),
1953 )
1954 .with_show_root(false);
1955
1956 assert_eq!(
1958 tree.node_at_visible_index_mut(0)
1959 .map(|n| n.label().to_string()),
1960 Some("a".to_string())
1961 );
1962 assert_eq!(
1963 tree.node_at_visible_index_mut(1)
1964 .map(|n| n.label().to_string()),
1965 Some("a1".to_string())
1966 );
1967 assert_eq!(
1968 tree.node_at_visible_index_mut(2)
1969 .map(|n| n.label().to_string()),
1970 Some("b".to_string())
1971 );
1972 assert!(tree.node_at_visible_index_mut(3).is_none());
1973 }
1974
1975 #[test]
1976 fn tree_node_at_visible_index_collapsed() {
1977 let mut tree = Tree::new(
1978 TreeNode::new("root")
1979 .child(
1980 TreeNode::new("a")
1981 .with_expanded(false)
1982 .child(TreeNode::new("a1"))
1983 .child(TreeNode::new("a2")),
1984 )
1985 .child(TreeNode::new("b")),
1986 );
1987
1988 assert_eq!(
1990 tree.node_at_visible_index_mut(0)
1991 .map(|n| n.label().to_string()),
1992 Some("root".to_string())
1993 );
1994 assert_eq!(
1995 tree.node_at_visible_index_mut(1)
1996 .map(|n| n.label().to_string()),
1997 Some("a".to_string())
1998 );
1999 assert_eq!(
2000 tree.node_at_visible_index_mut(2)
2001 .map(|n| n.label().to_string()),
2002 Some("b".to_string())
2003 );
2004 assert!(tree.node_at_visible_index_mut(3).is_none());
2005 }
2006
2007 #[test]
2008 fn tree_click_toggles_collapsed_node() {
2009 let mut tree = Tree::new(
2010 TreeNode::new("root")
2011 .child(
2012 TreeNode::new("a")
2013 .with_expanded(false)
2014 .child(TreeNode::new("a1")),
2015 )
2016 .child(TreeNode::new("b")),
2017 );
2018 assert!(!tree.root().children()[0].is_expanded());
2019
2020 let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 1);
2022 let hit = Some((HitId::new(1), HitRegion::Content, 1u64));
2023 let result = tree.handle_mouse(&event, hit, HitId::new(1));
2024 assert_eq!(result, MouseResult::Activated(1));
2025 assert!(tree.root().children()[0].is_expanded()); }
2027
2028 #[test]
2029 fn tree_handle_key_enter_toggles_parent() {
2030 let mut tree = Tree::new(
2031 TreeNode::new("root")
2032 .child(TreeNode::new("a").child(TreeNode::new("a1")))
2033 .child(TreeNode::new("b")),
2034 );
2035
2036 assert!(tree.root().children()[0].is_expanded());
2038 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Enter), 1));
2039 assert!(!tree.root().children()[0].is_expanded());
2040 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Char(' ')), 1));
2041 assert!(tree.root().children()[0].is_expanded());
2042 }
2043
2044 #[test]
2049 fn tree_handle_key_right_expands_collapsed_node() {
2050 let mut tree = Tree::new(
2051 TreeNode::new("root")
2052 .child(
2053 TreeNode::new("a")
2054 .with_expanded(false)
2055 .child(TreeNode::new("a1")),
2056 )
2057 .child(TreeNode::new("b")),
2058 );
2059 assert!(!tree.root().children()[0].is_expanded());
2060
2061 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2063 assert!(tree.root().children()[0].is_expanded());
2064 }
2065
2066 #[test]
2067 fn tree_handle_key_right_on_expanded_node_is_noop() {
2068 let mut tree = Tree::new(
2069 TreeNode::new("root")
2070 .child(TreeNode::new("a").child(TreeNode::new("a1")))
2071 .child(TreeNode::new("b")),
2072 );
2073 assert!(tree.root().children()[0].is_expanded());
2074
2075 assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2077 assert!(tree.root().children()[0].is_expanded());
2078 }
2079
2080 #[test]
2081 fn tree_handle_key_right_on_leaf_is_noop() {
2082 let mut tree = Tree::new(
2083 TreeNode::new("root")
2084 .child(TreeNode::new("a").child(TreeNode::new("a1")))
2085 .child(TreeNode::new("b")),
2086 );
2087
2088 assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Right), 3));
2090 }
2091
2092 #[test]
2093 fn tree_handle_key_left_collapses_expanded_node() {
2094 let mut tree = Tree::new(
2095 TreeNode::new("root")
2096 .child(TreeNode::new("a").child(TreeNode::new("a1")))
2097 .child(TreeNode::new("b")),
2098 );
2099 assert!(tree.root().children()[0].is_expanded());
2100
2101 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2103 assert!(!tree.root().children()[0].is_expanded());
2104 }
2105
2106 #[test]
2107 fn tree_handle_key_left_on_collapsed_node_is_noop() {
2108 let mut tree = Tree::new(
2109 TreeNode::new("root")
2110 .child(
2111 TreeNode::new("a")
2112 .with_expanded(false)
2113 .child(TreeNode::new("a1")),
2114 )
2115 .child(TreeNode::new("b")),
2116 );
2117 assert!(!tree.root().children()[0].is_expanded());
2118
2119 assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2121 assert!(!tree.root().children()[0].is_expanded());
2122 }
2123
2124 #[test]
2125 fn tree_handle_key_left_on_leaf_is_noop() {
2126 let mut tree = Tree::new(
2127 TreeNode::new("root")
2128 .child(TreeNode::new("a").child(TreeNode::new("a1")))
2129 .child(TreeNode::new("b")),
2130 );
2131
2132 assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Left), 3));
2134 }
2135
2136 #[test]
2137 fn tree_handle_key_left_right_round_trip() {
2138 let mut tree = Tree::new(
2139 TreeNode::new("root")
2140 .child(TreeNode::new("a").child(TreeNode::new("a1")))
2141 .child(TreeNode::new("b")),
2142 );
2143
2144 assert!(tree.root().children()[0].is_expanded());
2146 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2147 assert!(!tree.root().children()[0].is_expanded());
2148
2149 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2151 assert!(tree.root().children()[0].is_expanded());
2152 }
2153
2154 #[test]
2155 fn tree_handle_key_unhandled_keys_return_false() {
2156 let mut tree = Tree::new(
2157 TreeNode::new("root")
2158 .child(TreeNode::new("a").child(TreeNode::new("a1")))
2159 .child(TreeNode::new("b")),
2160 );
2161
2162 assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Up), 1));
2164 assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Down), 1));
2165 assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Tab), 1));
2166 assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Escape), 1));
2167 }
2168
2169 #[test]
2170 fn tree_visible_index_navigation_after_collapse() {
2171 let mut tree = Tree::new(
2172 TreeNode::new("root")
2173 .child(
2174 TreeNode::new("a")
2175 .child(TreeNode::new("a1"))
2176 .child(TreeNode::new("a2")),
2177 )
2178 .child(TreeNode::new("b")),
2179 );
2180
2181 assert_eq!(
2183 tree.node_at_visible_index_mut(4)
2184 .map(|n| n.label().to_string()),
2185 Some("b".to_string())
2186 );
2187
2188 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2190
2191 assert_eq!(
2193 tree.node_at_visible_index_mut(2)
2194 .map(|n| n.label().to_string()),
2195 Some("b".to_string())
2196 );
2197 assert!(tree.node_at_visible_index_mut(3).is_none());
2198 }
2199
2200 #[test]
2205 fn tree_lazy_children_materialized_visible_count() {
2206 let mut node = TreeNode::new("root").with_lazy_children(vec![
2207 TreeNode::new("child1").child(TreeNode::new("grandchild")),
2208 TreeNode::new("child2"),
2209 ]);
2210
2211 assert_eq!(node.visible_count(), 1);
2213
2214 node.toggle_expanded();
2216 assert_eq!(node.visible_count(), 4);
2218 }
2219
2220 #[test]
2221 fn tree_lazy_children_second_toggle_collapses() {
2222 let mut node = TreeNode::new("root")
2223 .with_lazy_children(vec![TreeNode::new("child1"), TreeNode::new("child2")]);
2224
2225 node.toggle_expanded();
2227 assert!(node.is_expanded());
2228 assert_eq!(node.children().len(), 2);
2229
2230 node.toggle_expanded();
2232 assert!(!node.is_expanded());
2233 assert_eq!(node.children().len(), 2); assert_eq!(node.visible_count(), 1); }
2236
2237 #[test]
2238 fn tree_lazy_children_with_right_key() {
2239 let mut tree = Tree::new(TreeNode::new("root").child(
2240 TreeNode::new("lazy-parent").with_lazy_children(vec![TreeNode::new("lazy-child")]),
2241 ));
2242
2243 assert!(!tree.root().children()[0].is_expanded());
2245 assert!(tree.root().children()[0].has_children());
2246
2247 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2249 assert!(tree.root().children()[0].is_expanded());
2250 assert_eq!(tree.root().children()[0].children().len(), 1);
2251 assert_eq!(
2252 tree.root().children()[0].children()[0].label(),
2253 "lazy-child"
2254 );
2255 }
2256
2257 #[test]
2262 fn tree_search_case_insensitive() {
2263 let tree = Tree::new(
2264 TreeNode::new("root")
2265 .child(TreeNode::new("Alpha"))
2266 .child(TreeNode::new("Beta")),
2267 )
2268 .with_search_query("alpha");
2269
2270 let flat = tree.flatten();
2271 assert_eq!(flat.len(), 2); assert_eq!(flat[1].label, "Alpha");
2273 }
2274
2275 #[test]
2276 fn tree_search_no_match_returns_empty() {
2277 let tree = Tree::new(
2278 TreeNode::new("root")
2279 .child(TreeNode::new("Alpha"))
2280 .child(TreeNode::new("Beta")),
2281 )
2282 .with_search_query("zzz-no-match");
2283
2284 let flat = tree.flatten();
2285 assert_eq!(flat.len(), 0);
2288 }
2289
2290 #[test]
2291 fn tree_search_no_match_clears_stale_rows() {
2292 let base_tree = Tree::new(
2293 TreeNode::new("root")
2294 .child(TreeNode::new("Alpha"))
2295 .child(TreeNode::new("Beta")),
2296 );
2297 let filtered_tree = Tree::new(
2298 TreeNode::new("root")
2299 .child(TreeNode::new("Alpha"))
2300 .child(TreeNode::new("Beta")),
2301 )
2302 .with_search_query("zzz-no-match");
2303
2304 let mut pool = GraphemePool::new();
2305 let mut frame = Frame::new(20, 4, &mut pool);
2306 let area = Rect::new(0, 0, 20, 4);
2307
2308 base_tree.render(area, &mut frame);
2309 filtered_tree.render(area, &mut frame);
2310
2311 assert_eq!(line_text(&frame, 0, 20), " ");
2312 assert_eq!(line_text(&frame, 1, 20), " ");
2313 assert_eq!(line_text(&frame, 2, 20), " ");
2314 assert_eq!(line_text(&frame, 3, 20), " ");
2315 }
2316
2317 #[test]
2318 fn tree_search_cleared() {
2319 let tree = Tree::new(
2320 TreeNode::new("root")
2321 .child(TreeNode::new("Alpha"))
2322 .child(TreeNode::new("Beta")),
2323 )
2324 .with_search_query("alpha")
2325 .without_search_query();
2326
2327 let flat = tree.flatten();
2328 assert_eq!(flat.len(), 3); }
2330
2331 #[test]
2336 fn tree_expand_deep_nesting() {
2337 let mut tree = Tree::new(
2338 TreeNode::new("root").child(
2339 TreeNode::new("d1").with_expanded(false).child(
2340 TreeNode::new("d2")
2341 .with_expanded(false)
2342 .child(TreeNode::new("d3")),
2343 ),
2344 ),
2345 );
2346
2347 assert_eq!(tree.root().visible_count(), 2);
2349
2350 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2352 assert_eq!(tree.root().visible_count(), 3);
2354
2355 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 2));
2357 assert_eq!(tree.root().visible_count(), 4);
2359 }
2360
2361 #[test]
2362 fn tree_collapse_parent_hides_all_descendants() {
2363 let mut tree = Tree::new(
2364 TreeNode::new("root")
2365 .child(
2366 TreeNode::new("a")
2367 .child(TreeNode::new("a1").child(TreeNode::new("a1x")))
2368 .child(TreeNode::new("a2")),
2369 )
2370 .child(TreeNode::new("b")),
2371 );
2372
2373 assert_eq!(tree.root().visible_count(), 6);
2375
2376 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2378 assert_eq!(tree.root().visible_count(), 3);
2380 }
2381
2382 #[cfg(feature = "tracing")]
2383 #[test]
2384 fn tree_tracing_span_and_toggle_events_are_emitted() {
2385 let trace_state = Arc::new(Mutex::new(TreeTraceState::default()));
2386 let _trace_test_guard = crate::tracing_test_support::acquire();
2387 let subscriber = tracing_subscriber::registry().with(TreeTraceCapture {
2388 state: Arc::clone(&trace_state),
2389 });
2390 let _guard = tracing::subscriber::set_default(subscriber);
2391 tracing::callsite::rebuild_interest_cache();
2392
2393 let mut tree = Tree::new(
2394 TreeNode::new("root")
2395 .child(TreeNode::new("a").child(TreeNode::new("a1")))
2396 .child(TreeNode::new("b")),
2397 );
2398 let mut pool = GraphemePool::new();
2399 let mut frame = Frame::new(20, 6, &mut pool);
2400 tracing::callsite::rebuild_interest_cache();
2401 tree.render(Rect::new(0, 0, 20, 6), &mut frame);
2402 tracing::callsite::rebuild_interest_cache();
2403 assert!(tree.handle_key(&KeyEvent::new(KeyCode::Enter), 1));
2404
2405 tracing::callsite::rebuild_interest_cache();
2406 let snapshot = trace_state.lock().expect("tree trace state lock");
2407 assert!(snapshot.tree_render_seen, "expected tree.render span");
2408 assert!(
2409 snapshot.has_total_nodes_field,
2410 "tree.render missing total_nodes"
2411 );
2412 assert!(
2413 snapshot.has_visible_nodes_field,
2414 "tree.render missing visible_nodes"
2415 );
2416 assert!(
2417 snapshot.has_expanded_count_field,
2418 "tree.render missing expanded_count"
2419 );
2420 assert!(
2421 snapshot.render_duration_recorded,
2422 "tree.render did not record render_duration_us"
2423 );
2424 assert!(
2425 snapshot.toggle_events >= 1,
2426 "expected tree.toggle debug event"
2427 );
2428 }
2429}