Skip to main content

ftui_widgets/
tree.rs

1//! Tree widget for hierarchical display.
2//!
3//! Renders a tree of labeled nodes with configurable guide characters
4//! and styles, suitable for file trees or structured views.
5//!
6//! # Example
7//!
8//! ```
9//! use ftui_widgets::tree::{Tree, TreeNode, TreeGuides};
10//!
11//! let tree = Tree::new(TreeNode::new("root")
12//!     .child(TreeNode::new("src")
13//!         .child(TreeNode::new("main.rs"))
14//!         .child(TreeNode::new("lib.rs")))
15//!     .child(TreeNode::new("Cargo.toml")));
16//!
17//! assert_eq!(tree.root().label(), "root");
18//! assert_eq!(tree.root().children().len(), 2);
19//! ```
20
21use 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/// Guide character styles for tree rendering.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum TreeGuides {
35    /// ASCII guides: `|`, `+--`, `` `-- ``.
36    Ascii,
37    /// Unicode box-drawing characters (default).
38    #[default]
39    Unicode,
40    /// Bold Unicode box-drawing characters.
41    Bold,
42    /// Double-line Unicode characters.
43    Double,
44    /// Rounded Unicode characters.
45    Rounded,
46}
47
48impl TreeGuides {
49    /// Vertical continuation (item has siblings below).
50    #[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    /// Branch guide (item has siblings below).
61    #[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    /// Last-item guide (no siblings below).
73    #[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    /// Empty indentation (no guide needed).
85    #[must_use]
86    pub const fn space(&self) -> &str {
87        "    "
88    }
89
90    /// Width in columns of each guide segment.
91    #[inline]
92    #[must_use]
93    pub fn width(&self) -> usize {
94        4
95    }
96}
97
98/// A node in the tree hierarchy.
99#[derive(Debug, Clone)]
100pub struct TreeNode {
101    label: String,
102    /// Child nodes (crate-visible for undo support).
103    pub(crate) children: Vec<TreeNode>,
104    /// Whether this node is expanded (crate-visible for undo support).
105    pub(crate) expanded: bool,
106}
107
108impl TreeNode {
109    /// Create a new tree node with the given label.
110    #[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    /// Add a child node.
120    #[must_use]
121    pub fn child(mut self, node: TreeNode) -> Self {
122        self.children.push(node);
123        self
124    }
125
126    /// Set children from a vec.
127    #[must_use]
128    pub fn with_children(mut self, nodes: Vec<TreeNode>) -> Self {
129        self.children = nodes;
130        self
131    }
132
133    /// Set whether this node is expanded.
134    #[must_use]
135    pub fn with_expanded(mut self, expanded: bool) -> Self {
136        self.expanded = expanded;
137        self
138    }
139
140    /// Get the label.
141    #[must_use]
142    pub fn label(&self) -> &str {
143        &self.label
144    }
145
146    /// Get the children.
147    #[must_use]
148    pub fn children(&self) -> &[TreeNode] {
149        &self.children
150    }
151
152    /// Whether this node is expanded.
153    #[must_use]
154    pub fn is_expanded(&self) -> bool {
155        self.expanded
156    }
157
158    /// Toggle the expanded state.
159    pub fn toggle_expanded(&mut self) {
160        self.expanded = !self.expanded;
161    }
162
163    /// Count all visible (expanded) nodes, including this one.
164    #[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    /// Collect all expanded node paths into a set.
176    #[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    /// Apply expanded state from a set of paths.
194    #[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/// Tree widget for rendering hierarchical data.
213#[derive(Debug, Clone)]
214pub struct Tree {
215    /// Unique ID for undo tracking.
216    undo_id: UndoWidgetId,
217    root: TreeNode,
218    /// Whether to show the root node.
219    show_root: bool,
220    /// Guide character style.
221    guides: TreeGuides,
222    /// Style for guide characters.
223    guide_style: Style,
224    /// Style for node labels.
225    label_style: Style,
226    /// Style for the root node label.
227    root_style: Style,
228    /// Optional persistence ID for state saving/restoration.
229    persistence_id: Option<String>,
230    /// Optional hit ID for mouse interaction.
231    hit_id: Option<HitId>,
232}
233
234impl Tree {
235    /// Create a tree widget with the given root node.
236    #[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    /// Set whether to show the root node.
252    #[must_use]
253    pub fn with_show_root(mut self, show: bool) -> Self {
254        self.show_root = show;
255        self
256    }
257
258    /// Set the guide character style.
259    #[must_use]
260    pub fn with_guides(mut self, guides: TreeGuides) -> Self {
261        self.guides = guides;
262        self
263    }
264
265    /// Set the style for guide characters.
266    #[must_use]
267    pub fn with_guide_style(mut self, style: Style) -> Self {
268        self.guide_style = style;
269        self
270    }
271
272    /// Set the style for node labels.
273    #[must_use]
274    pub fn with_label_style(mut self, style: Style) -> Self {
275        self.label_style = style;
276        self
277    }
278
279    /// Set the style for the root label.
280    #[must_use]
281    pub fn with_root_style(mut self, style: Style) -> Self {
282        self.root_style = style;
283        self
284    }
285
286    /// Set a persistence ID for state saving.
287    #[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    /// Get the persistence ID, if set.
294    #[must_use]
295    pub fn persistence_id(&self) -> Option<&str> {
296        self.persistence_id.as_deref()
297    }
298
299    /// Set a hit ID for mouse interaction.
300    #[must_use]
301    pub fn hit_id(mut self, id: HitId) -> Self {
302        self.hit_id = Some(id);
303        self
304    }
305
306    /// Get a reference to the root node.
307    #[must_use]
308    pub fn root(&self) -> &TreeNode {
309        &self.root
310    }
311
312    /// Get a mutable reference to the root node.
313    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        // Draw guide characters for each depth level
337        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                    // This is the immediate parent level
342                    if is_last_at_depth {
343                        self.guides.last()
344                    } else {
345                        self.guides.branch()
346                    }
347                } else {
348                    // Ancestor level: show vertical line or blank
349                    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            // Minimal rendering: indent with spaces
360            // Avoid allocation by drawing chunks iteratively
361            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        // Draw label
370        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        // Register hit region for the row
383        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            // If root is hidden but expanded, render children as top-level nodes.
425            // We do NOT push to is_last for the root level, effectively shifting
426            // the hierarchy up by one level.
427            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, // Children become depth 0
433                    &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// ============================================================================
450// Stateful Persistence Implementation
451// ============================================================================
452
453/// Persistable state for a [`Tree`] widget.
454///
455/// Stores the set of expanded node paths to restore tree expansion state.
456#[derive(Clone, Debug, Default, PartialEq)]
457#[cfg_attr(
458    feature = "state-persistence",
459    derive(serde::Serialize, serde::Deserialize)
460)]
461pub struct TreePersistState {
462    /// Set of expanded node paths (e.g., "root/src/main.rs").
463    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
484// ============================================================================
485// Undo Support Implementation
486// ============================================================================
487
488impl 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    /// Get the undo widget ID for this tree.
529    #[must_use]
530    pub fn undo_id(&self) -> UndoWidgetId {
531        self.undo_id
532    }
533
534    /// Get a reference to a node at the given path (indices from root).
535    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    /// Get a mutable reference to a node at the given path (indices from root).
544    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    /// Handle a mouse event for this tree.
553    ///
554    /// # Hit data convention
555    ///
556    /// The hit data (`u64`) encodes the flattened visible row index. When the
557    /// tree renders with a `hit_id`, each visible row registers
558    /// `HitRegion::Content` with `data = visible_row_index as u64`.
559    ///
560    /// Clicking a parent node (one with children) toggles its expanded state
561    /// and returns `Activated`. Clicking a leaf returns `Selected`.
562    ///
563    /// # Arguments
564    ///
565    /// * `event` — the mouse event from the terminal
566    /// * `hit` — result of `frame.hit_test(event.x, event.y)`, if available
567    /// * `expected_id` — the `HitId` this tree was rendered with
568    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    /// Get a mutable reference to the node at the given visible (flattened) index.
595    ///
596    /// The traversal order matches `render_node()`: if `show_root` is true the
597    /// root is row 0; otherwise children of the root are the top-level rows.
598    /// Only expanded nodes' children are visited.
599    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    /// Recursive helper that walks the visible tree to find the node at the
616    /// given flattened index. Returns `Some` if found, `None` otherwise.
617    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// ---------------------------------------------------------------------------
638// Test-only flatten helpers
639// ---------------------------------------------------------------------------
640
641#[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        // root + a + a1 + a2 + b = 5
712        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        // root + a (collapsed, so no a1/a2) + b = 3
726        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        // Root label at (0, 0)
777        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        // Row 1 should be child "a" with branch guide "+-- "
791        // First char of guide at (0, 1)
792        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        // Row 1: "+-- a" (not last)
811        let cell = frame.buffer.get(0, 1).unwrap();
812        assert_eq!(cell.content.as_char(), Some('+'));
813
814        // Row 2: "`-- b" (last child)
815        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); // No panic
825    }
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); // Only first 2 rows render, no panic
834    }
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    // --- Stateful Persistence tests ---
886
887    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        // Create tree with some nodes expanded, some collapsed
904        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        // Verify initial state: root and src expanded, tests collapsed
916        assert!(tree.root().is_expanded());
917        assert!(tree.root().children()[0].is_expanded()); // src
918        assert!(!tree.root().children()[1].is_expanded()); // tests
919
920        let saved = tree.save_state();
921
922        // Verify saved state captures expanded nodes
923        assert!(saved.expanded_paths.contains("root"));
924        assert!(saved.expanded_paths.contains("root/src"));
925        assert!(!saved.expanded_paths.contains("root/tests"));
926
927        // Modify tree state (collapse src)
928        tree.root_mut().children[0].toggle_expanded();
929        assert!(!tree.root().children()[0].is_expanded());
930
931        // Restore
932        tree.restore_state(saved);
933
934        // Verify restored state
935        assert!(tree.root().is_expanded());
936        assert!(tree.root().children()[0].is_expanded()); // src restored
937        assert!(!tree.root().children()[1].is_expanded()); // tests still collapsed
938    }
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")), // leaf has no children
966        );
967
968        let saved = tree.save_state();
969
970        // Only root is expanded (and has children)
971        assert!(saved.expanded_paths.contains("root"));
972        // leaf has no children, so it's not tracked
973        assert!(!saved.expanded_paths.contains("root/leaf"));
974    }
975
976    // ============================================================================
977    // Undo Support Tests
978    // ============================================================================
979
980    #[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        // Nodes must have children for their expanded state to be tracked
990        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        // Create snapshot
1005        let snapshot = tree.create_snapshot();
1006
1007        // Verify initial state
1008        assert!(tree.is_node_expanded(&[0])); // a
1009        assert!(!tree.is_node_expanded(&[1])); // b
1010
1011        // Modify state
1012        tree.collapse_node(&[0]); // collapse a
1013        tree.expand_node(&[1]); // expand b
1014        assert!(!tree.is_node_expanded(&[0]));
1015        assert!(tree.is_node_expanded(&[1]));
1016
1017        // Restore snapshot
1018        assert!(tree.restore_snapshot(&*snapshot));
1019
1020        // Verify restored state
1021        assert!(tree.is_node_expanded(&[0])); // a back to expanded
1022        assert!(!tree.is_node_expanded(&[1])); // b back to collapsed
1023    }
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        // Initial state
1031        assert!(tree.is_node_expanded(&[0]));
1032
1033        // Collapse
1034        tree.collapse_node(&[0]);
1035        assert!(!tree.is_node_expanded(&[0]));
1036
1037        // Expand again
1038        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        // Test path navigation
1055        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()); // Invalid path
1067    }
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    // --- Mouse handling tests ---
1078
1079    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        // Click on row 1 which is node "a" (a parent node)
1096        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()); // toggled to collapsed
1101    }
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        // Row 4 is "b" (a leaf): root=0, a=1, a1=2, a2=3, b=4
1116        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        // Visible order: root=0, a=1, a1=2, a2=3, b=4
1161        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        // Root hidden: a=0, a1=1, b=2
1199        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        // root=0, a=1 (collapsed, so a1/a2 hidden), b=2
1231        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        // Click on "a" (row 1) to expand it
1263        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()); // now expanded
1268    }
1269}