fast_rich/
tree.rs

1//! Tree rendering for hierarchical data.
2//!
3//! Renders tree-like structures with guide lines.
4
5use crate::console::RenderContext;
6use crate::renderable::{Renderable, Segment};
7use crate::style::Style;
8use crate::text::{Span, Text};
9
10/// Guide style for tree connections.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum GuideStyle {
13    /// ASCII characters
14    Ascii,
15    /// Unicode box drawing (default)
16    #[default]
17    Unicode,
18    /// Bold Unicode
19    Bold,
20    /// Double line
21    Double,
22}
23
24impl GuideStyle {
25    fn chars(&self) -> TreeGuideChars {
26        match self {
27            GuideStyle::Ascii => TreeGuideChars {
28                vertical: '|',
29                horizontal: '-',
30                branch: '+',
31                last_branch: '\\',
32                space: ' ',
33            },
34            GuideStyle::Unicode => TreeGuideChars {
35                vertical: '│',
36                horizontal: '─',
37                branch: '├',
38                last_branch: '└',
39                space: ' ',
40            },
41            GuideStyle::Bold => TreeGuideChars {
42                vertical: '┃',
43                horizontal: '━',
44                branch: '┣',
45                last_branch: '┗',
46                space: ' ',
47            },
48            GuideStyle::Double => TreeGuideChars {
49                vertical: '║',
50                horizontal: '═',
51                branch: '╠',
52                last_branch: '╚',
53                space: ' ',
54            },
55        }
56    }
57}
58
59#[derive(Debug, Clone, Copy)]
60struct TreeGuideChars {
61    vertical: char,
62    horizontal: char,
63    branch: char,
64    last_branch: char,
65    #[allow(dead_code)]
66    space: char,
67}
68
69/// A node in a tree.
70#[derive(Debug, Clone)]
71pub struct TreeNode {
72    /// Label for this node
73    label: Text,
74    /// Child nodes
75    children: Vec<TreeNode>,
76    /// Style for the label
77    style: Style,
78    /// Whether this node is expanded
79    expanded: bool,
80}
81
82impl TreeNode {
83    /// Create a new tree node with a label.
84    pub fn new<T: Into<Text>>(label: T) -> Self {
85        TreeNode {
86            label: label.into(),
87            children: Vec::new(),
88            style: Style::new(),
89            expanded: true,
90        }
91    }
92
93    /// Add a child node.
94    pub fn add<T: Into<TreeNode>>(&mut self, child: T) -> &mut Self {
95        self.children.push(child.into());
96        self
97    }
98
99    /// Add a child node and return self (builder pattern).
100    pub fn with_child<T: Into<TreeNode>>(mut self, child: T) -> Self {
101        self.children.push(child.into());
102        self
103    }
104
105    /// Add multiple children.
106    pub fn with_children<I, T>(mut self, children: I) -> Self
107    where
108        I: IntoIterator<Item = T>,
109        T: Into<TreeNode>,
110    {
111        for child in children {
112            self.children.push(child.into());
113        }
114        self
115    }
116
117    /// Set the style.
118    pub fn style(mut self, style: Style) -> Self {
119        self.style = style;
120        self
121    }
122
123    /// Set whether the node is expanded.
124    pub fn expanded(mut self, expanded: bool) -> Self {
125        self.expanded = expanded;
126        self
127    }
128
129    /// Check if this node has children.
130    pub fn has_children(&self) -> bool {
131        !self.children.is_empty()
132    }
133}
134
135impl<T: Into<Text>> From<T> for TreeNode {
136    fn from(label: T) -> Self {
137        TreeNode::new(label)
138    }
139}
140
141/// A tree structure for hierarchical data.
142#[derive(Debug, Clone)]
143pub struct Tree {
144    /// Root node
145    root: TreeNode,
146    /// Guide style
147    guide_style: GuideStyle,
148    /// Style for guide lines
149    style: Style,
150    /// Hide the root node
151    hide_root: bool,
152}
153
154impl Tree {
155    /// Create a new tree with a root label.
156    pub fn new<T: Into<TreeNode>>(root: T) -> Self {
157        Tree {
158            root: root.into(),
159            guide_style: GuideStyle::Unicode,
160            style: Style::new(),
161            hide_root: false,
162        }
163    }
164
165    /// Set the guide style.
166    pub fn guide_style(mut self, style: GuideStyle) -> Self {
167        self.guide_style = style;
168        self
169    }
170
171    /// Set the style for guide lines.
172    pub fn style(mut self, style: Style) -> Self {
173        self.style = style;
174        self
175    }
176
177    /// Hide the root node.
178    pub fn hide_root(mut self, hide: bool) -> Self {
179        self.hide_root = hide;
180        self
181    }
182
183    /// Add a child to the root.
184    pub fn add<T: Into<TreeNode>>(&mut self, child: T) -> &mut Self {
185        self.root.children.push(child.into());
186        self
187    }
188
189    fn render_node(
190        &self,
191        node: &TreeNode,
192        prefix: &str,
193        is_last: bool,
194        is_root: bool,
195        chars: &TreeGuideChars,
196        segments: &mut Vec<Segment>,
197    ) {
198        if !is_root || !self.hide_root {
199            let mut spans = Vec::new();
200
201            if !is_root {
202                // Add the prefix and branch character
203                spans.push(Span::styled(prefix.to_string(), self.style));
204
205                let branch_char = if is_last {
206                    chars.last_branch
207                } else {
208                    chars.branch
209                };
210
211                spans.push(Span::styled(
212                    format!("{}{}{} ", branch_char, chars.horizontal, chars.horizontal),
213                    self.style,
214                ));
215            }
216
217            // Add the label
218            for span in &node.label.spans {
219                let combined_style = node.style.combine(&span.style);
220                spans.push(Span::styled(span.text.to_string(), combined_style));
221            }
222
223            segments.push(Segment::line(spans));
224        }
225
226        // Render children
227        if node.expanded {
228            let child_count = node.children.len();
229            for (i, child) in node.children.iter().enumerate() {
230                let is_last_child = i == child_count - 1;
231
232                let new_prefix = if is_root {
233                    String::new()
234                } else {
235                    let connector = if is_last {
236                        "    ".to_string()
237                    } else {
238                        format!("{}   ", chars.vertical)
239                    };
240                    format!("{}{}", prefix, connector)
241                };
242
243                self.render_node(child, &new_prefix, is_last_child, false, chars, segments);
244            }
245        }
246    }
247}
248
249impl Renderable for Tree {
250    fn render(&self, _context: &RenderContext) -> Vec<Segment> {
251        let chars = self.guide_style.chars();
252        let mut segments = Vec::new();
253
254        self.render_node(&self.root, "", true, true, &chars, &mut segments);
255
256        segments
257    }
258}
259
260/// Create a tree from a directory path (example helper).
261#[cfg(feature = "std")]
262pub fn from_directory(path: &std::path::Path) -> std::io::Result<Tree> {
263    fn build_node(path: &std::path::Path) -> std::io::Result<TreeNode> {
264        let name = path
265            .file_name()
266            .map(|s| s.to_string_lossy().to_string())
267            .unwrap_or_else(|| path.to_string_lossy().to_string());
268
269        let mut node = TreeNode::new(name);
270
271        if path.is_dir() {
272            let mut entries: Vec<_> = std::fs::read_dir(path)?.filter_map(|e| e.ok()).collect();
273
274            entries.sort_by_key(|e| e.file_name());
275
276            for entry in entries {
277                let child = build_node(&entry.path())?;
278                node.children.push(child);
279            }
280        }
281
282        Ok(node)
283    }
284
285    let root = build_node(path)?;
286    Ok(Tree::new(root))
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_tree_simple() {
295        let tree = Tree::new("root").guide_style(GuideStyle::Unicode);
296
297        let context = RenderContext { width: 40, height: None };
298        let segments = tree.render(&context);
299
300        assert_eq!(segments.len(), 1);
301        assert!(segments[0].plain_text().contains("root"));
302    }
303
304    #[test]
305    fn test_tree_with_children() {
306        let mut tree = Tree::new("root");
307        tree.add(TreeNode::new("child1"));
308        tree.add(TreeNode::new("child2"));
309
310        let context = RenderContext { width: 40, height: None };
311        let segments = tree.render(&context);
312
313        assert_eq!(segments.len(), 3);
314    }
315
316    #[test]
317    fn test_tree_nested() {
318        let child1 = TreeNode::new("child1")
319            .with_child("grandchild1")
320            .with_child("grandchild2");
321
322        let tree = Tree::new(TreeNode::new("root").with_child(child1));
323
324        let context = RenderContext { width: 40, height: None };
325        let segments = tree.render(&context);
326
327        assert_eq!(segments.len(), 4);
328
329        // Check guide characters are present
330        let text: String = segments.iter().map(|s| s.plain_text()).collect();
331        assert!(text.contains("├"));
332        assert!(text.contains("└"));
333    }
334
335    #[test]
336    fn test_tree_hide_root() {
337        let mut tree = Tree::new("root").hide_root(true);
338        tree.add("child1");
339        tree.add("child2");
340
341        let context = RenderContext { width: 40, height: None };
342        let segments = tree.render(&context);
343
344        // Should not contain root
345        let text: String = segments.iter().map(|s| s.plain_text()).collect();
346        assert!(!text.contains("root"));
347        assert!(text.contains("child1"));
348    }
349}