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