Skip to main content

rich_rs/
tree.rs

1//! Tree: hierarchical tree rendering.
2//!
3//! Tree renders tree structures with connecting lines (guides) to show
4//! parent-child relationships. This is useful for displaying file trees,
5//! hierarchical data structures, and nested content.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use rich_rs::{Tree, Text, Style, Console};
11//!
12//! let mut tree = Tree::new(Box::new(Text::plain("Root")));
13//! tree.add(Box::new(Text::plain("Child 1")));
14//! let child2 = tree.add(Box::new(Text::plain("Child 2")));
15//! child2.add(Box::new(Text::plain("Grandchild")));
16//!
17//! let mut console = Console::new();
18//! console.print(&tree, None, None, None, false, "\n").unwrap();
19//! ```
20
21use std::io::Stdout;
22
23use crate::console::ConsoleOptions;
24use crate::measure::Measurement;
25use crate::segment::{Segment, Segments};
26use crate::style::Style;
27use crate::{Console, Renderable};
28
29// ============================================================================
30// Tree Guides
31// ============================================================================
32
33/// Guide characters for tree structure rendering.
34///
35/// Contains the four types of guide characters:
36/// - `space`: Empty space for alignment (4 chars wide)
37/// - `vertical`: Vertical continuation line
38/// - `branch`: Branch connector (for non-last children)
39/// - `end`: End connector (for last child)
40#[derive(Debug, Clone, Copy)]
41pub struct TreeGuides {
42    /// Space for alignment (where no vertical line continues).
43    pub space: &'static str,
44    /// Vertical continuation line.
45    pub vertical: &'static str,
46    /// Branch connector for non-last children.
47    pub branch: &'static str,
48    /// End connector for last child.
49    pub end: &'static str,
50}
51
52/// Unicode box-drawing tree guides.
53pub const TREE_GUIDES: TreeGuides = TreeGuides {
54    space: "    ",
55    vertical: "\u{2502}   ",             // "│   "
56    branch: "\u{251c}\u{2500}\u{2500} ", // "├── "
57    end: "\u{2514}\u{2500}\u{2500} ",    // "└── "
58};
59
60/// Bold Unicode box-drawing tree guides.
61///
62/// Uses heavy-weight box characters (┃, ┣━━, ┗━━).
63/// In Python Rich, these are selected when `guide_style` has `bold=True`.
64pub const BOLD_TREE_GUIDES: TreeGuides = TreeGuides {
65    space: "    ",
66    vertical: "\u{2503}   ",             // "┃   "
67    branch: "\u{2523}\u{2501}\u{2501} ", // "┣━━ "
68    end: "\u{2517}\u{2501}\u{2501} ",    // "┗━━ "
69};
70
71/// Double-line Unicode tree guides.
72///
73/// Uses double-line box characters (║, ╠══, ╚══).
74/// In Python Rich, these are selected when `guide_style` has `underline2=True`.
75pub const DOUBLE_TREE_GUIDES: TreeGuides = TreeGuides {
76    space: "    ",
77    vertical: "\u{2551}   ",             // "║   "
78    branch: "\u{2560}\u{2550}\u{2550} ", // "╠══ "
79    end: "\u{255a}\u{2550}\u{2550} ",    // "╚══ "
80};
81
82/// ASCII tree guides for non-Unicode terminals.
83pub const ASCII_GUIDES: TreeGuides = TreeGuides {
84    space: "    ",
85    vertical: "|   ",
86    branch: "+-- ",
87    end: "`-- ",
88};
89
90// ============================================================================
91// Tree
92// ============================================================================
93
94/// A tree node that can be rendered with guide lines.
95///
96/// Tree is a hierarchical data structure where each node has a label (content)
97/// and zero or more children. When rendered, guide lines connect the nodes
98/// to show the tree structure.
99///
100/// # Example
101///
102/// ```ignore
103/// use rich_rs::{Tree, Text};
104///
105/// let mut root = Tree::new(Box::new(Text::plain("Documents")));
106/// let mut projects = root.add(Box::new(Text::plain("Projects")));
107/// projects.add(Box::new(Text::plain("project1")));
108/// projects.add(Box::new(Text::plain("project2")));
109/// root.add(Box::new(Text::plain("notes.txt")));
110/// ```
111/// Options for adding a child node to a tree.
112///
113/// Used with `Tree::add_with_options()` to specify per-node overrides.
114#[derive(Debug, Clone, Default)]
115pub struct TreeNodeOptions {
116    /// Style for this node's label. If `None`, inherits from parent.
117    pub style: Option<Style>,
118    /// Style for guide lines from this node. If `None`, inherits from parent.
119    pub guide_style: Option<Style>,
120    /// Whether children are shown. Defaults to `true`.
121    pub expanded: Option<bool>,
122    /// Whether to apply highlighting. If `None`, inherits from parent.
123    pub highlight: Option<bool>,
124}
125
126impl TreeNodeOptions {
127    /// Create new default options.
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Set the node style.
133    pub fn with_style(mut self, style: Style) -> Self {
134        self.style = Some(style);
135        self
136    }
137
138    /// Set the guide style.
139    pub fn with_guide_style(mut self, style: Style) -> Self {
140        self.guide_style = Some(style);
141        self
142    }
143
144    /// Set whether children are expanded.
145    pub fn with_expanded(mut self, expanded: bool) -> Self {
146        self.expanded = Some(expanded);
147        self
148    }
149
150    /// Set whether to highlight.
151    pub fn with_highlight(mut self, highlight: bool) -> Self {
152        self.highlight = Some(highlight);
153        self
154    }
155}
156
157pub struct Tree {
158    /// The label/content of this node.
159    label: Box<dyn Renderable + Send + Sync>,
160    /// Child nodes.
161    children: Vec<Tree>,
162    /// Style for the label.
163    style: Style,
164    /// Style for the guide lines.
165    guide_style: Style,
166    /// Whether children are visible when rendered.
167    expanded: bool,
168    /// Whether to highlight labels (for future use with highlighters).
169    highlight: bool,
170    /// Whether to hide the root node when rendering.
171    hide_root: bool,
172}
173
174impl std::fmt::Debug for Tree {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.debug_struct("Tree")
177            .field("children_count", &self.children.len())
178            .field("style", &self.style)
179            .field("guide_style", &self.guide_style)
180            .field("expanded", &self.expanded)
181            .field("highlight", &self.highlight)
182            .field("hide_root", &self.hide_root)
183            .finish_non_exhaustive()
184    }
185}
186
187impl Tree {
188    fn guides_for(options: &ConsoleOptions, guide_style: Style) -> &'static TreeGuides {
189        if options.ascii_only() {
190            &ASCII_GUIDES
191        } else if guide_style.bold == Some(true) {
192            &BOLD_TREE_GUIDES
193        } else if guide_style.underline == Some(true) {
194            // Use double guides for underline (Rust doesn't have underline2,
195            // so we use underline as the trigger, matching the spirit of Python Rich).
196            &DOUBLE_TREE_GUIDES
197        } else {
198            &TREE_GUIDES
199        }
200    }
201
202    /// Create a new tree node with the given label.
203    ///
204    /// # Arguments
205    ///
206    /// * `label` - The content to display for this node.
207    ///
208    /// # Example
209    ///
210    /// ```ignore
211    /// use rich_rs::{Tree, Text};
212    ///
213    /// let tree = Tree::new(Box::new(Text::plain("Root")));
214    /// ```
215    pub fn new(label: Box<dyn Renderable + Send + Sync>) -> Self {
216        Tree {
217            label,
218            children: Vec::new(),
219            style: Style::default(),
220            guide_style: Style::default(),
221            expanded: true,
222            highlight: false,
223            hide_root: false,
224        }
225    }
226
227    /// Add a child node with the given label.
228    ///
229    /// Returns a mutable reference to the newly created child, allowing
230    /// for chaining to build nested structures.
231    ///
232    /// # Arguments
233    ///
234    /// * `label` - The content to display for the child node.
235    ///
236    /// # Returns
237    ///
238    /// A mutable reference to the newly added child Tree.
239    ///
240    /// # Example
241    ///
242    /// ```ignore
243    /// use rich_rs::{Tree, Text};
244    ///
245    /// let mut root = Tree::new(Box::new(Text::plain("Root")));
246    /// let child = root.add(Box::new(Text::plain("Child")));
247    /// child.add(Box::new(Text::plain("Grandchild")));
248    /// ```
249    pub fn add(&mut self, label: Box<dyn Renderable + Send + Sync>) -> &mut Tree {
250        let child = Tree {
251            label,
252            children: Vec::new(),
253            style: self.style,
254            guide_style: self.guide_style,
255            expanded: true,
256            highlight: self.highlight,
257            hide_root: false,
258        };
259        self.children.push(child);
260        self.children.last_mut().unwrap()
261    }
262
263    /// Add a child node with the given label and options.
264    ///
265    /// Options allow overriding style, guide_style, expanded, and highlight
266    /// on a per-node basis. Fields set to `None` inherit from the parent.
267    ///
268    /// # Arguments
269    ///
270    /// * `label` - The content to display for the child node.
271    /// * `options` - Per-node overrides.
272    ///
273    /// # Returns
274    ///
275    /// A mutable reference to the newly added child Tree.
276    pub fn add_with_options(
277        &mut self,
278        label: Box<dyn Renderable + Send + Sync>,
279        options: TreeNodeOptions,
280    ) -> &mut Tree {
281        let child = Tree {
282            label,
283            children: Vec::new(),
284            style: options.style.unwrap_or(self.style),
285            guide_style: options.guide_style.unwrap_or(self.guide_style),
286            expanded: options.expanded.unwrap_or(true),
287            highlight: options.highlight.unwrap_or(self.highlight),
288            hide_root: false,
289        };
290        self.children.push(child);
291        self.children.last_mut().unwrap()
292    }
293
294    /// Add an existing tree as a child.
295    ///
296    /// This is useful for combining pre-built subtrees.
297    ///
298    /// # Arguments
299    ///
300    /// * `tree` - An existing Tree to add as a child.
301    ///
302    /// # Example
303    ///
304    /// ```ignore
305    /// use rich_rs::{Tree, Text};
306    ///
307    /// let mut subtree = Tree::new(Box::new(Text::plain("Subtree")));
308    /// subtree.add(Box::new(Text::plain("Leaf")));
309    ///
310    /// let mut root = Tree::new(Box::new(Text::plain("Root")));
311    /// root.add_tree(subtree);
312    /// ```
313    pub fn add_tree(&mut self, tree: Tree) {
314        self.children.push(tree);
315    }
316
317    /// Set the style for the label.
318    ///
319    /// # Arguments
320    ///
321    /// * `style` - Style to apply to the node's label.
322    pub fn with_style(mut self, style: Style) -> Self {
323        self.style = style;
324        self
325    }
326
327    /// Set the style for guide lines.
328    ///
329    /// # Arguments
330    ///
331    /// * `style` - Style to apply to guide characters.
332    pub fn with_guide_style(mut self, style: Style) -> Self {
333        self.guide_style = style;
334        self
335    }
336
337    /// Set whether children are expanded (visible).
338    ///
339    /// # Arguments
340    ///
341    /// * `expanded` - If true, children are rendered; if false, only this node is shown.
342    pub fn with_expanded(mut self, expanded: bool) -> Self {
343        self.expanded = expanded;
344        self
345    }
346
347    /// Set whether to highlight labels.
348    ///
349    /// # Arguments
350    ///
351    /// * `highlight` - If true, labels may be highlighted (for future use).
352    pub fn with_highlight(mut self, highlight: bool) -> Self {
353        self.highlight = highlight;
354        self
355    }
356
357    /// Set whether to hide the root node.
358    ///
359    /// When true, only children are rendered (no root label or root guide lines).
360    ///
361    /// # Arguments
362    ///
363    /// * `hide` - If true, the root node's label is hidden.
364    pub fn with_hide_root(mut self, hide: bool) -> Self {
365        self.hide_root = hide;
366        self
367    }
368
369    /// Get the number of direct children.
370    pub fn children_count(&self) -> usize {
371        self.children.len()
372    }
373
374    /// Check if this node has any children.
375    pub fn has_children(&self) -> bool {
376        !self.children.is_empty()
377    }
378
379    /// Check if children are expanded.
380    pub fn is_expanded(&self) -> bool {
381        self.expanded
382    }
383
384    /// Get the style for labels.
385    pub fn style(&self) -> Style {
386        self.style
387    }
388
389    /// Get the style for guide lines.
390    pub fn guide_style(&self) -> Style {
391        self.guide_style
392    }
393
394    /// Get a reference to the children.
395    pub fn children(&self) -> &[Tree] {
396        &self.children
397    }
398
399    /// Get a mutable reference to the children.
400    pub fn children_mut(&mut self) -> &mut Vec<Tree> {
401        &mut self.children
402    }
403}
404
405/// State for each node during stack-based traversal.
406struct TraversalState<'a> {
407    /// Reference to the current tree node.
408    node: &'a Tree,
409    /// Index of the next child to process.
410    child_index: usize,
411    /// Whether this is the last sibling at its level.
412    is_last: bool,
413    /// Depth in the tree (0 = root).
414    depth: usize,
415}
416
417impl Renderable for Tree {
418    fn render(&self, _console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
419        let mut result = Segments::new();
420
421        // When hide_root is true, render children as if they are root-level.
422        // We push each child at depth 0 instead of the root.
423        let mut stack: Vec<TraversalState> = if self.hide_root {
424            // Push children in reverse so they render in order
425            self.children
426                .iter()
427                .enumerate()
428                .rev()
429                .map(|(i, child)| TraversalState {
430                    node: child,
431                    child_index: 0,
432                    is_last: i == self.children.len() - 1,
433                    depth: 0,
434                })
435                .collect()
436        } else {
437            vec![TraversalState {
438                node: self,
439                child_index: 0,
440                is_last: true,
441                depth: 0,
442            }]
443        };
444
445        // Track "is_last" for each depth level for guide prefix calculation
446        // levels[i] = true means the node at depth i was the last child
447        let mut levels: Vec<bool> = Vec::new();
448
449        // Create temp console for nested rendering
450        let temp_console = Console::<Stdout>::with_options(options.clone());
451
452        while let Some(state) = stack.last_mut() {
453            let TraversalState {
454                node,
455                child_index,
456                is_last,
457                depth,
458            } = state;
459
460            if *child_index == 0 {
461                // First time visiting this node - render its label
462                let node_guides = Self::guides_for(options, node.guide_style);
463
464                // Ensure levels vec is the right size
465                while levels.len() < *depth {
466                    levels.push(false);
467                }
468                if *depth > 0 {
469                    if *depth <= levels.len() {
470                        levels[*depth - 1] = *is_last;
471                    } else {
472                        levels.push(*is_last);
473                    }
474                }
475
476                // Build guide prefix for this node
477                // Don't add guides for root-level nodes (depth 0)
478                if *depth > 0 {
479                    let mut prefix = String::new();
480
481                    // Add continuation guides for all ancestor levels (except current)
482                    for i in 0..(*depth - 1) {
483                        let ancestor_is_last = levels.get(i).copied().unwrap_or(false);
484                        if ancestor_is_last {
485                            prefix.push_str(node_guides.space);
486                        } else {
487                            prefix.push_str(node_guides.vertical);
488                        }
489                    }
490
491                    // Add the connector for this node
492                    if *is_last {
493                        prefix.push_str(node_guides.end);
494                    } else {
495                        prefix.push_str(node_guides.branch);
496                    }
497
498                    // Add styled guide prefix
499                    if !prefix.is_empty() {
500                        result.push(Segment::styled(prefix, node.guide_style));
501                    }
502                }
503
504                // Calculate available width for label (subtract guide prefix width)
505                let guide_width = if *depth > 0 { *depth * 4 } else { 0 };
506                let label_width = options.max_width.saturating_sub(guide_width);
507                let mut label_options = options.update_width(label_width);
508                label_options.highlight = Some(node.highlight);
509
510                // Render the label - may produce multiple lines
511                let label_segments = node.label.render(&temp_console, &label_options);
512
513                // Split into lines and handle multi-line labels
514                let lines = Segment::split_lines(label_segments);
515
516                for (line_idx, line) in lines.iter().enumerate() {
517                    if line_idx > 0 {
518                        // Continuation lines need guide prefix too
519                        if *depth > 0 {
520                            let mut prefix = String::new();
521                            for i in 0..*depth {
522                                let ancestor_is_last = levels.get(i).copied().unwrap_or(false);
523                                if ancestor_is_last {
524                                    prefix.push_str(node_guides.space);
525                                } else {
526                                    prefix.push_str(node_guides.vertical);
527                                }
528                            }
529                            result.push(Segment::styled(prefix, node.guide_style));
530                        }
531                    }
532
533                    // Add line segments
534                    for seg in line {
535                        // Apply node style to label if not already styled
536                        if !node.style.is_null() && seg.style.is_none() {
537                            result.push(Segment::styled(seg.text.clone(), node.style));
538                        } else if !node.style.is_null() {
539                            // Combine styles
540                            let combined = node.style.combine(&seg.style.unwrap_or_default());
541                            result.push(Segment::styled(seg.text.clone(), combined));
542                        } else {
543                            result.push(seg.clone());
544                        }
545                    }
546
547                    // Add newline
548                    result.push(Segment::line());
549                }
550
551                // If the label had no content, still add a newline
552                if lines.is_empty() {
553                    result.push(Segment::line());
554                }
555            }
556
557            // Process children
558            if node.expanded && *child_index < node.children.len() {
559                let child_node = &node.children[*child_index];
560                let child_is_last = *child_index == node.children.len() - 1;
561                let child_depth = *depth + 1;
562                *child_index += 1;
563
564                stack.push(TraversalState {
565                    node: child_node,
566                    child_index: 0,
567                    is_last: child_is_last,
568                    depth: child_depth,
569                });
570            } else {
571                // Done with this node
572                stack.pop();
573            }
574        }
575
576        result
577    }
578
579    fn measure(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
580        // Stack-based measurement traversal
581        let mut stack: Vec<(&Tree, usize)> = vec![(self, 0)];
582        let mut minimum: usize = 0;
583        let mut maximum: usize = 0;
584
585        while let Some((node, depth)) = stack.pop() {
586            // Measure the label
587            let label_measurement = node.label.measure(console, options);
588            let indent = depth * 4;
589
590            minimum = minimum.max(label_measurement.minimum + indent);
591            maximum = maximum.max(label_measurement.maximum + indent);
592
593            // Add children to stack if expanded
594            if node.expanded {
595                for child in node.children.iter().rev() {
596                    stack.push((child, depth + 1));
597                }
598            }
599        }
600
601        Measurement::new(minimum, maximum)
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    use crate::Renderable;
609    use crate::text::Text;
610    use std::io::Stdout;
611
612    struct HighlightAwareLabel;
613
614    impl Renderable for HighlightAwareLabel {
615        fn render(&self, _console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
616            if options.highlight == Some(true) {
617                Segments::one(Segment::styled(
618                    "highlighted",
619                    Style::parse("yellow").unwrap(),
620                ))
621            } else {
622                Segments::one(Segment::new("plain".to_string()))
623            }
624        }
625    }
626
627    // ==================== TreeGuides tests ====================
628
629    #[test]
630    fn test_tree_guides_unicode() {
631        assert_eq!(TREE_GUIDES.space, "    ");
632        assert_eq!(TREE_GUIDES.vertical, "│   ");
633        assert_eq!(TREE_GUIDES.branch, "├── ");
634        assert_eq!(TREE_GUIDES.end, "└── ");
635    }
636
637    #[test]
638    fn test_tree_guides_ascii() {
639        assert_eq!(ASCII_GUIDES.space, "    ");
640        assert_eq!(ASCII_GUIDES.vertical, "|   ");
641        assert_eq!(ASCII_GUIDES.branch, "+-- ");
642        assert_eq!(ASCII_GUIDES.end, "`-- ");
643    }
644
645    // ==================== Tree creation tests ====================
646
647    #[test]
648    fn test_tree_new() {
649        let tree = Tree::new(Box::new(Text::plain("Root")));
650        assert_eq!(tree.children_count(), 0);
651        assert!(!tree.has_children());
652        assert!(tree.is_expanded());
653    }
654
655    #[test]
656    fn test_tree_add_child() {
657        let mut tree = Tree::new(Box::new(Text::plain("Root")));
658        tree.add(Box::new(Text::plain("Child")));
659        assert_eq!(tree.children_count(), 1);
660        assert!(tree.has_children());
661    }
662
663    #[test]
664    fn test_tree_add_returns_child() {
665        let mut tree = Tree::new(Box::new(Text::plain("Root")));
666        let child = tree.add(Box::new(Text::plain("Child")));
667        child.add(Box::new(Text::plain("Grandchild")));
668
669        assert_eq!(tree.children_count(), 1);
670        assert_eq!(tree.children()[0].children_count(), 1);
671    }
672
673    #[test]
674    fn test_tree_add_tree() {
675        let mut subtree = Tree::new(Box::new(Text::plain("Subtree")));
676        subtree.add(Box::new(Text::plain("Leaf")));
677
678        let mut root = Tree::new(Box::new(Text::plain("Root")));
679        root.add_tree(subtree);
680
681        assert_eq!(root.children_count(), 1);
682        assert_eq!(root.children()[0].children_count(), 1);
683    }
684
685    #[test]
686    fn test_tree_chained_add() {
687        let mut root = Tree::new(Box::new(Text::plain("Root")));
688        root.add(Box::new(Text::plain("A")))
689            .add(Box::new(Text::plain("A1")))
690            .add(Box::new(Text::plain("A1a")));
691
692        // Root -> A -> A1 -> A1a
693        assert_eq!(root.children_count(), 1);
694        assert_eq!(root.children()[0].children_count(), 1);
695        assert_eq!(root.children()[0].children()[0].children_count(), 1);
696    }
697
698    // ==================== Tree builder tests ====================
699
700    #[test]
701    fn test_tree_with_style() {
702        let style = Style::new().with_bold(true);
703        let tree = Tree::new(Box::new(Text::plain("Root"))).with_style(style);
704        assert_eq!(tree.style().bold, Some(true));
705    }
706
707    #[test]
708    fn test_tree_with_guide_style() {
709        let style = Style::new().with_dim(true);
710        let tree = Tree::new(Box::new(Text::plain("Root"))).with_guide_style(style);
711        assert_eq!(tree.guide_style().dim, Some(true));
712    }
713
714    #[test]
715    fn test_tree_with_expanded() {
716        let tree = Tree::new(Box::new(Text::plain("Root"))).with_expanded(false);
717        assert!(!tree.is_expanded());
718    }
719
720    #[test]
721    fn test_tree_with_highlight() {
722        let tree = Tree::new(Box::new(Text::plain("Root"))).with_highlight(true);
723        assert!(tree.highlight);
724    }
725
726    // ==================== hide_root tests ====================
727
728    #[test]
729    fn test_tree_hide_root() {
730        let mut tree = Tree::new(Box::new(Text::plain("Root"))).with_hide_root(true);
731        tree.add(Box::new(Text::plain("Child 1")));
732        tree.add(Box::new(Text::plain("Child 2")));
733
734        let console = Console::with_options(ConsoleOptions::default());
735        let options = console.options().clone();
736        let segments = tree.render(&console, &options);
737        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
738
739        // Root should not appear
740        assert!(!output.contains("Root"));
741        // Children should appear
742        assert!(output.contains("Child 1"));
743        assert!(output.contains("Child 2"));
744    }
745
746    #[test]
747    fn test_tree_hide_root_no_children() {
748        let tree = Tree::new(Box::new(Text::plain("Root"))).with_hide_root(true);
749
750        let console = Console::with_options(ConsoleOptions::default());
751        let options = console.options().clone();
752        let segments = tree.render(&console, &options);
753        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
754
755        // Nothing should appear
756        assert!(!output.contains("Root"));
757    }
758
759    // ==================== add_with_options tests ====================
760
761    #[test]
762    fn test_tree_add_with_options() {
763        let mut tree = Tree::new(Box::new(Text::plain("Root")));
764        let opts = TreeNodeOptions::new()
765            .with_style(Style::new().with_bold(true))
766            .with_expanded(false);
767        let child = tree.add_with_options(Box::new(Text::plain("Child")), opts);
768        assert!(!child.is_expanded());
769        assert_eq!(child.style().bold, Some(true));
770    }
771
772    #[test]
773    fn test_tree_add_with_options_inherits() {
774        let style = Style::new().with_dim(true);
775        let mut tree = Tree::new(Box::new(Text::plain("Root"))).with_style(style);
776        let child = tree.add_with_options(Box::new(Text::plain("Child")), TreeNodeOptions::new());
777        // Should inherit parent style when options don't override
778        assert_eq!(child.style().dim, Some(true));
779    }
780
781    #[test]
782    fn test_tree_add_with_options_highlight_affects_render() {
783        let mut tree = Tree::new(Box::new(Text::plain("Root")));
784        tree.add_with_options(
785            Box::new(HighlightAwareLabel),
786            TreeNodeOptions::new().with_highlight(true),
787        );
788
789        let console = Console::with_options(ConsoleOptions::default());
790        let options = console.options().clone();
791        let segments = tree.render(&console, &options);
792
793        assert!(
794            segments.iter().any(|seg| seg.text == "highlighted"),
795            "child label should render with highlight=true",
796        );
797        assert!(!segments.iter().any(|seg| seg.text == "plain"));
798        assert!(
799            segments
800                .iter()
801                .any(|seg| seg.text == "highlighted" && seg.style.and_then(|s| s.color).is_some()),
802            "highlighted label should include style from highlight-aware renderable",
803        );
804    }
805
806    #[test]
807    fn test_tree_add_with_options_guide_style_affects_guide_chars() {
808        let mut tree = Tree::new(Box::new(Text::plain("Root")));
809        tree.add_with_options(
810            Box::new(Text::plain("Bold child")),
811            TreeNodeOptions::new().with_guide_style(Style::new().with_bold(true)),
812        );
813
814        let console = Console::with_options(ConsoleOptions::default());
815        let options = console.options().clone();
816        let segments = tree.render(&console, &options);
817        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
818
819        assert!(
820            output.contains("┗━━ "),
821            "child with bold guide_style should use bold guide connector",
822        );
823    }
824
825    // ==================== Guide variant tests ====================
826
827    #[test]
828    fn test_bold_tree_guides() {
829        assert_eq!(BOLD_TREE_GUIDES.vertical, "┃   ");
830        assert_eq!(BOLD_TREE_GUIDES.branch, "┣━━ ");
831        assert_eq!(BOLD_TREE_GUIDES.end, "┗━━ ");
832    }
833
834    #[test]
835    fn test_double_tree_guides() {
836        assert_eq!(DOUBLE_TREE_GUIDES.vertical, "║   ");
837        assert_eq!(DOUBLE_TREE_GUIDES.branch, "╠══ ");
838        assert_eq!(DOUBLE_TREE_GUIDES.end, "╚══ ");
839    }
840
841    #[test]
842    fn test_tree_renders_bold_guides() {
843        let mut tree =
844            Tree::new(Box::new(Text::plain("Root"))).with_guide_style(Style::new().with_bold(true));
845        tree.add(Box::new(Text::plain("Child")));
846
847        let console = Console::with_options(ConsoleOptions::default());
848        let options = console.options().clone();
849        let segments = tree.render(&console, &options);
850        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
851
852        assert!(output.contains("┗━━ "), "Should use bold guides");
853    }
854
855    // ==================== Tree render tests ====================
856
857    #[test]
858    fn test_tree_render_single_node() {
859        let tree = Tree::new(Box::new(Text::plain("Root")));
860        let console = Console::with_options(ConsoleOptions::default());
861        let options = console.options().clone();
862
863        let segments = tree.render(&console, &options);
864        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
865
866        assert!(output.contains("Root"));
867        assert!(output.ends_with('\n'));
868    }
869
870    #[test]
871    fn test_tree_render_with_children() {
872        let mut tree = Tree::new(Box::new(Text::plain("Root")));
873        tree.add(Box::new(Text::plain("Child 1")));
874        tree.add(Box::new(Text::plain("Child 2")));
875
876        let console = Console::with_options(ConsoleOptions::default());
877        let options = console.options().clone();
878
879        let segments = tree.render(&console, &options);
880        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
881
882        assert!(output.contains("Root"));
883        assert!(output.contains("Child 1"));
884        assert!(output.contains("Child 2"));
885        // Should contain branch and end guides
886        assert!(output.contains("├── ") || output.contains("+-- ")); // branch
887        assert!(output.contains("└── ") || output.contains("`-- ")); // end
888    }
889
890    #[test]
891    fn test_tree_render_nested() {
892        let mut tree = Tree::new(Box::new(Text::plain("Root")));
893        let child = tree.add(Box::new(Text::plain("Child")));
894        child.add(Box::new(Text::plain("Grandchild")));
895
896        let console = Console::with_options(ConsoleOptions::default());
897        let options = console.options().clone();
898
899        let segments = tree.render(&console, &options);
900        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
901
902        assert!(output.contains("Root"));
903        assert!(output.contains("Child"));
904        assert!(output.contains("Grandchild"));
905    }
906
907    #[test]
908    fn test_tree_render_ascii_guides() {
909        let mut tree = Tree::new(Box::new(Text::plain("Root")));
910        tree.add(Box::new(Text::plain("Child")));
911
912        let console = Console::with_options(ConsoleOptions {
913            encoding: "ascii".to_string(),
914            ..Default::default()
915        });
916        let options = console.options().clone();
917
918        let segments = tree.render(&console, &options);
919        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
920
921        // Should use ASCII guides
922        assert!(output.contains("`-- ")); // ASCII end guide
923        assert!(!output.contains("└")); // No Unicode
924    }
925
926    #[test]
927    fn test_tree_render_unicode_guides() {
928        let mut tree = Tree::new(Box::new(Text::plain("Root")));
929        tree.add(Box::new(Text::plain("Child")));
930
931        let console = Console::with_options(ConsoleOptions {
932            encoding: "utf-8".to_string(),
933            ..Default::default()
934        });
935        let options = console.options().clone();
936
937        let segments = tree.render(&console, &options);
938        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
939
940        // Should use Unicode guides
941        assert!(output.contains("└── ")); // Unicode end guide
942    }
943
944    #[test]
945    fn test_tree_render_collapsed() {
946        let mut tree = Tree::new(Box::new(Text::plain("Root"))).with_expanded(false);
947        tree.add(Box::new(Text::plain("Child")));
948
949        let console = Console::with_options(ConsoleOptions::default());
950        let options = console.options().clone();
951
952        let segments = tree.render(&console, &options);
953        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
954
955        assert!(output.contains("Root"));
956        assert!(!output.contains("Child")); // Children hidden
957    }
958
959    #[test]
960    fn test_tree_render_complex_structure() {
961        // Build a more complex tree
962        let mut root = Tree::new(Box::new(Text::plain("Documents")));
963
964        let projects = root.add(Box::new(Text::plain("Projects")));
965        projects.add(Box::new(Text::plain("project1")));
966        projects.add(Box::new(Text::plain("project2")));
967
968        root.add(Box::new(Text::plain("notes.txt")));
969
970        let console = Console::with_options(ConsoleOptions::default());
971        let options = console.options().clone();
972
973        let segments = root.render(&console, &options);
974        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
975
976        assert!(output.contains("Documents"));
977        assert!(output.contains("Projects"));
978        assert!(output.contains("project1"));
979        assert!(output.contains("project2"));
980        assert!(output.contains("notes.txt"));
981    }
982
983    // ==================== Tree measure tests ====================
984
985    #[test]
986    fn test_tree_measure_single_node() {
987        let tree = Tree::new(Box::new(Text::plain("Root")));
988        let console = Console::with_options(ConsoleOptions::default());
989        let options = console.options().clone();
990
991        let measurement = tree.measure(&console, &options);
992        // "Root" is 4 characters
993        assert!(measurement.minimum >= 4);
994        assert!(measurement.maximum >= measurement.minimum);
995    }
996
997    #[test]
998    fn test_tree_measure_with_children() {
999        let mut tree = Tree::new(Box::new(Text::plain("R"))); // 1 char
1000        tree.add(Box::new(Text::plain("Child"))); // 5 chars + 4 indent = 9
1001
1002        let console = Console::with_options(ConsoleOptions::default());
1003        let options = console.options().clone();
1004
1005        let measurement = tree.measure(&console, &options);
1006        // Maximum should be at least 9 (longest line with indent)
1007        assert!(measurement.maximum >= 9);
1008    }
1009
1010    // ==================== Send + Sync tests ====================
1011
1012    #[test]
1013    fn test_tree_is_send_sync() {
1014        fn assert_send<T: Send>() {}
1015        fn assert_sync<T: Sync>() {}
1016        assert_send::<Tree>();
1017        assert_sync::<Tree>();
1018    }
1019
1020    #[test]
1021    fn test_tree_guides_is_send_sync() {
1022        fn assert_send<T: Send>() {}
1023        fn assert_sync<T: Sync>() {}
1024        assert_send::<TreeGuides>();
1025        assert_sync::<TreeGuides>();
1026    }
1027
1028    // ==================== Debug tests ====================
1029
1030    #[test]
1031    fn test_tree_debug() {
1032        let tree = Tree::new(Box::new(Text::plain("Root")));
1033        let debug_str = format!("{:?}", tree);
1034        assert!(debug_str.contains("Tree"));
1035        assert!(debug_str.contains("children_count"));
1036    }
1037}