Skip to main content

rich/
tree.rs

1//! Trees.
2//!
3//! Port of upstream `rich/tree.py`. A [`Tree`] renders a hierarchy with the
4//! familiar `├──`/`└──` guide lines.
5//!
6//! Slice scope: the default (thin) guides, with string (markup), `Text` or
7//! renderable labels. Custom `guide_style`/label styles, `hide_root`,
8//! `expanded` and the ASCII/heavy guide sets are deferred with the rest of
9//! `tree.py`.
10
11use crate::console::{Console, ConsoleOptions};
12use crate::measure::Measurement;
13use crate::protocol::Renderable;
14use crate::segment::Segment;
15use crate::style::Style;
16use crate::table::Cell;
17
18// Default (thin) guide segments, matching `TREE_GUIDES[0]`.
19const SPACE: &str = "    ";
20const CONTINUE: &str = "│   ";
21const FORK: &str = "├── ";
22const END: &str = "└── ";
23
24/// A node in a hierarchy. Mirrors `rich.tree.Tree`.
25pub struct Tree {
26    label: Cell,
27    children: Vec<Tree>,
28    highlight: bool,
29}
30
31impl Tree {
32    /// A new tree/subtree with the given label. A string label is console
33    /// markup, as upstream's `Tree("[b]root")` is; pass a
34    /// [`Text`](crate::text::Text) for a literal one.
35    pub fn new(label: impl Into<Cell>) -> Self {
36        Tree {
37            label: label.into(),
38            children: Vec::new(),
39            highlight: false,
40        }
41    }
42
43    /// Highlight string labels (upstream `Tree(highlight=…)`, default off).
44    /// Upstream renders every label with the root's setting.
45    pub fn highlight(mut self, highlight: bool) -> Self {
46        self.highlight = highlight;
47        self
48    }
49
50    /// Add a child with `label`, returning a mutable reference to it so further
51    /// descendants can be attached. Mirrors `Tree.add`.
52    pub fn add(&mut self, label: impl Into<Cell>) -> &mut Tree {
53        self.children.push(Tree::new(label));
54        self.children.last_mut().expect("just pushed a child")
55    }
56
57    /// Render this node's label into `lines`. `prefix_first` precedes the
58    /// label's first line; `prefix_rest` precedes wrapped continuation lines.
59    #[allow(clippy::too_many_arguments)]
60    fn render_label(
61        &self,
62        console: &Console,
63        options: &ConsoleOptions,
64        highlight: bool,
65        lines: &mut Vec<Vec<Segment>>,
66        prefix_first: &str,
67        prefix_rest: &str,
68        available: usize,
69    ) {
70        let guide_style = Some(Style::new());
71        // The label renders with `options.update(highlight=self.highlight)`,
72        // so a string goes through `render_str` with the root's setting.
73        let mut label_lines = if let Cell::Renderable(renderable) = &self.label {
74            let mut label_options = options.update_width(available);
75            label_options.height = None;
76            console.render_lines(renderable.as_ref(), &label_options, false)
77        } else {
78            self.label
79                .to_text(console, Some(highlight))
80                .unwrap_or_default()
81                .render_lines(console.theme(), &Style::new(), Some(available))
82        };
83        if label_lines.is_empty() {
84            label_lines.push(Vec::new());
85        }
86
87        for (index, label_line) in label_lines.into_iter().enumerate() {
88            let prefix = if index == 0 {
89                prefix_first
90            } else {
91                prefix_rest
92            };
93            let mut line = Vec::new();
94            if !prefix.is_empty() {
95                line.push(Segment::new(prefix.to_string(), guide_style.clone()));
96            }
97            line.extend(label_line);
98            lines.push(line);
99        }
100    }
101
102    /// Render the whole hierarchy into `lines`, depth first.
103    ///
104    /// Like upstream's `__rich_console__`, this walks an explicit stack rather
105    /// than recursing, so a very deep tree cannot overflow the thread stack.
106    /// The guides are kept as one `is_last` flag per level and only turned
107    /// into prefix strings for nodes that still have room to render: a guide
108    /// is four cells per level, so materialising every prefix would cost
109    /// quadratic memory on a deep chain.
110    fn render_into(
111        &self,
112        console: &Console,
113        options: &ConsoleOptions,
114        highlight: bool,
115        lines: &mut Vec<Vec<Segment>>,
116        width: usize,
117    ) {
118        // `(node, index of the next child to visit)`; `levels[d]` is whether
119        // the ancestor at depth `d + 1` on the current path is a last child.
120        let mut stack: Vec<(&Tree, usize)> = vec![(self, 0)];
121        let mut levels: Vec<bool> = Vec::new();
122        let visit = |node: &Tree, levels: &[bool], lines: &mut Vec<Vec<Segment>>| {
123            // Upstream renders the label at `options.max_width - sum(guide
124            // widths)`; with no room left, `Console.render` yields nothing, so
125            // neither the label nor its guides are emitted (its children
126            // still render, and get no room either).
127            let guide_width = levels.len() * 4;
128            if guide_width >= width {
129                return;
130            }
131            let mut prefix_rest = String::new();
132            for &last in levels {
133                prefix_rest.push_str(if last { SPACE } else { CONTINUE });
134            }
135            let mut prefix_first = String::new();
136            if let Some((&last, parents)) = levels.split_last() {
137                for &parent_last in parents {
138                    prefix_first.push_str(if parent_last { SPACE } else { CONTINUE });
139                }
140                prefix_first.push_str(if last { END } else { FORK });
141            }
142            node.render_label(
143                console,
144                options,
145                highlight,
146                lines,
147                &prefix_first,
148                &prefix_rest,
149                width - guide_width,
150            );
151        };
152        visit(self, &levels, lines);
153        while let Some((node, next)) = stack.last_mut() {
154            let node: &Tree = node;
155            if let Some(child) = node.children.get(*next) {
156                *next += 1;
157                levels.push(*next == node.children.len());
158                visit(child, &levels, lines);
159                stack.push((child, 0));
160            } else {
161                stack.pop();
162                levels.pop();
163            }
164        }
165    }
166}
167
168impl Drop for Tree {
169    /// Drop descendants from an explicit stack: the derived drop recurses
170    /// once per level and overflows the stack on a very deep tree.
171    fn drop(&mut self) {
172        let mut pending = std::mem::take(&mut self.children);
173        while let Some(mut child) = pending.pop() {
174            pending.append(&mut child.children);
175        }
176    }
177}
178
179impl Renderable for Tree {
180    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
181        let mut lines: Vec<Vec<Segment>> = Vec::new();
182        self.render_into(
183            console,
184            options,
185            self.highlight,
186            &mut lines,
187            options.max_width,
188        );
189
190        let mut segments = Vec::new();
191        let last = lines.len().saturating_sub(1);
192        for (index, line) in lines.into_iter().enumerate() {
193            segments.extend(line);
194            if index != last {
195                segments.push(Segment::line());
196            }
197        }
198        segments
199    }
200
201    /// Port of `Tree.__rich_measure__`: the widest label plus its indent of
202    /// four cells per level, for both bounds.
203    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
204        // Iterative, like the render: `(node, level)` pairs to visit.
205        let mut width = (0, 0);
206        let mut pending: Vec<(&Tree, usize)> = vec![(self, 0)];
207        while let Some((tree, level)) = pending.pop() {
208            let label = tree.label.measure_cell(console, options);
209            let indent = level * 4;
210            width.0 = width.0.max(label.minimum + indent);
211            width.1 = width.1.max(label.maximum + indent);
212            pending.extend(tree.children.iter().map(|child| (child, level + 1)));
213        }
214        Measurement::new(width.0, width.1)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::color::ColorSystem;
222
223    fn console() -> Console {
224        Console::builder()
225            .force_terminal(true)
226            .color_system(Some(ColorSystem::Truecolor))
227            .width(40)
228            .build()
229    }
230
231    #[test]
232    fn nested_tree() {
233        let mut tree = Tree::new("root");
234        let a = tree.add("child A");
235        a.add("leaf A1");
236        a.add("leaf A2");
237        tree.add("child B");
238        let out = console().render_export(&tree);
239        let expected = concat!(
240            "root\n",
241            "├── child A\n",
242            "│   ├── leaf A1\n",
243            "│   └── leaf A2\n",
244            "└── child B\n",
245        );
246        assert_eq!(out, expected);
247    }
248
249    #[test]
250    fn deep_tree_renders_without_recursion() {
251        // Upstream renders from an explicit stack, so a 20 000-level chain is
252        // fine; a recursive render (or drop, or measure) overflows a normal
253        // 2 MiB thread stack long before that.
254        let handle = std::thread::Builder::new()
255            .stack_size(2 * 1024 * 1024)
256            .spawn(|| {
257                let mut tree = Tree::new("0");
258                let mut node = &mut tree;
259                for depth in 1..20_000 {
260                    node = node.add(depth.to_string());
261                }
262                let console = Console::builder().width(12).build();
263                let out = console.render_export(&tree);
264                let measured = Measurement::get(&console, &console.options(), &tree);
265                (out, measured)
266            })
267            .expect("spawn");
268        let (out, measured) = handle.join().expect("deep tree render overflowed");
269        // Labels render while the guides leave room (4 cells per level at
270        // width 12: depths 0-2); deeper nodes have no room and emit nothing.
271        assert_eq!(out, "0\n└── 1\n    └── 2\n");
272        // `Measurement.get` clamps the 80 001-cell measure to the width.
273        assert_eq!(measured, Measurement::new(12, 12));
274    }
275}