use crate::console::Justify;
use crate::console::{Console, ConsoleOptions};
use crate::measure::Measurement;
use crate::protocol::Renderable;
use crate::segment::Segment;
use crate::style::{Style, StyleType};
use crate::table::Cell;
pub const ASCII_GUIDES: [&str; 4] = [" ", "| ", "+-- ", "`-- "];
pub const TREE_GUIDES: [[&str; 4]; 3] = [
[" ", "│ ", "├── ", "└── "],
[" ", "┃ ", "┣━━ ", "┗━━ "],
[" ", "║ ", "╠══ ", "╚══ "],
];
const SPACE: usize = 0;
const CONTINUE: usize = 1;
const FORK: usize = 2;
const END: usize = 3;
pub struct Tree {
label: Cell,
style: StyleType,
guide_style: StyleType,
children: Vec<Tree>,
expanded: bool,
highlight: bool,
hide_root: bool,
}
impl Tree {
pub fn new(label: impl Into<Cell>) -> Self {
Tree {
label: label.into(),
style: StyleType::Name("tree".to_string()),
guide_style: StyleType::Name("tree.line".to_string()),
children: Vec::new(),
expanded: true,
highlight: false,
hide_root: false,
}
}
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
pub fn style(mut self, style: impl Into<StyleType>) -> Self {
self.style = style.into();
self
}
pub fn guide_style(mut self, style: impl Into<StyleType>) -> Self {
self.guide_style = style.into();
self
}
pub fn expanded(mut self, expanded: bool) -> Self {
self.expanded = expanded;
self
}
pub fn hide_root(mut self, hide_root: bool) -> Self {
self.hide_root = hide_root;
self
}
pub fn set_style(&mut self, style: impl Into<StyleType>) -> &mut Self {
self.style = style.into();
self
}
pub fn set_guide_style(&mut self, style: impl Into<StyleType>) -> &mut Self {
self.guide_style = style.into();
self
}
pub fn set_expanded(&mut self, expanded: bool) -> &mut Self {
self.expanded = expanded;
self
}
pub fn set_hide_root(&mut self, hide_root: bool) -> &mut Self {
self.hide_root = hide_root;
self
}
pub fn set_highlight(&mut self, highlight: bool) -> &mut Self {
self.highlight = highlight;
self
}
pub fn set_label(&mut self, label: impl Into<Cell>) -> &mut Self {
self.label = label.into();
self
}
pub fn label(&self) -> &Cell {
&self.label
}
pub fn get_style(&self) -> &StyleType {
&self.style
}
pub fn get_guide_style(&self) -> &StyleType {
&self.guide_style
}
pub fn is_expanded(&self) -> bool {
self.expanded
}
pub fn is_root_hidden(&self) -> bool {
self.hide_root
}
pub fn is_highlighted(&self) -> bool {
self.highlight
}
pub fn children(&self) -> &[Tree] {
&self.children
}
pub fn children_mut(&mut self) -> &mut Vec<Tree> {
&mut self.children
}
pub fn add(&mut self, label: impl Into<Cell>) -> &mut Tree {
let mut child = Tree::new(label);
child.style = self.style.clone();
child.guide_style = self.guide_style.clone();
self.add_tree(child)
}
pub fn add_tree(&mut self, child: Tree) -> &mut Tree {
self.children.push(child);
self.children.last_mut().expect("just pushed a child")
}
fn make_guide(options: &ConsoleOptions, index: usize, style: Style) -> Segment {
let line = if options.ascii_only() {
ASCII_GUIDES[index]
} else {
let guide = if style.attr(BOLD) == Some(true) {
1
} else if style.attr(UNDERLINE2) == Some(true) {
2
} else {
0
};
TREE_GUIDES[if options.legacy_windows { 0 } else { guide }][index]
};
Segment::new(line, Some(style))
}
}
const BOLD: usize = 0;
const UNDERLINE2: usize = 9;
struct Label<'a> {
label: &'a Cell,
style: &'a Style,
highlight: bool,
}
impl Renderable for Label<'_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let segments = match self.label {
Cell::Renderable(renderable) => renderable.rich_render(console, options),
cell => cell
.to_text(console, Some(self.highlight))
.unwrap_or_default()
.rich_render(console, options),
};
if self.style.is_null() {
segments
} else {
Segment::apply_style(&segments, self.style)
}
}
}
impl Tree {
fn render_lines_into(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
let get_style = |style: &StyleType| console.get_style(style).unwrap_or_default();
let guide_style = get_style(&self.guide_style);
let mut levels: Vec<Segment> =
vec![Self::make_guide(options, CONTINUE, guide_style.clone())];
let mut stack: Vec<(&[Tree], usize)> = vec![(std::slice::from_ref(self), 0)];
let mut guide_style_stack = vec![guide_style];
let mut style_stack = vec![get_style(&self.style)];
let remove_guide_styles = Style::parse("not bold not underline2").unwrap_or_default();
let offset = if self.hide_root { 2 } else { 1 };
let pad = options.justify != Justify::Default;
let mut depth = 0usize;
let mut lines = Vec::new();
let level_style = |segment: &Segment| segment.style.clone().unwrap_or_default();
while let Some((siblings, next)) = stack.last_mut() {
let siblings: &[Tree] = siblings;
let Some(node) = siblings.get(*next) else {
stack.pop();
levels.pop();
if let Some(level) = levels.last_mut() {
let style = level_style(level);
*level = Self::make_guide(options, FORK, style);
guide_style_stack.pop();
style_stack.pop();
}
continue;
};
*next += 1;
let last = *next == siblings.len();
if last {
let level = levels.last_mut().expect("a level per open stack entry");
*level = Self::make_guide(options, END, level_style(level));
}
let current_guide = guide_style_stack.last().cloned().unwrap_or_default();
let current_style = style_stack.last().cloned().unwrap_or_default();
let node_guide_style = current_guide.combine(&get_style(&node.guide_style));
let style = current_style.combine(&get_style(&node.style));
let prefix_width = levels.len().saturating_sub(offset) * 4;
if !(depth == 0 && self.hide_root) && prefix_width < options.max_width {
let mut prefix: Vec<Segment> = levels[offset.min(levels.len())..].to_vec();
let mut label_options = options.update_width(options.max_width - prefix_width);
label_options.highlight = Some(self.highlight);
label_options.height = None;
let label = Label {
label: &node.label,
style: &style,
highlight: self.highlight,
};
let background = Style::from_color(None, style.bgcolor().cloned());
for (index, label_line) in console
.render_lines(&label, &label_options, pad)
.into_iter()
.enumerate()
{
let mut line = Vec::new();
if !prefix.is_empty() {
line.extend(prefix.iter().map(|segment| {
let own = segment.style.clone().unwrap_or_default();
let own = if background.is_null() {
own
} else {
background.combine(&own)
};
let style = if own.is_null() {
remove_guide_styles.clone()
} else {
own.combine(&remove_guide_styles)
};
Segment::new(segment.text.clone(), Some(style))
}));
}
line.extend(label_line);
lines.push(line);
if index == 0 && !prefix.is_empty() {
let level = prefix.last_mut().expect("non-empty prefix");
*level = Self::make_guide(
options,
if last { SPACE } else { CONTINUE },
level_style(level),
);
}
}
}
if node.expanded && !node.children.is_empty() {
let level = levels.last_mut().expect("a level per open stack entry");
*level = Self::make_guide(
options,
if last { SPACE } else { CONTINUE },
level_style(level),
);
levels.push(Self::make_guide(
options,
if node.children.len() == 1 { END } else { FORK },
node_guide_style,
));
style_stack.push(current_style.combine(&get_style(&node.style)));
guide_style_stack.push(current_guide.combine(&get_style(&node.guide_style)));
stack.push((&node.children, 0));
depth += 1;
}
}
lines
}
}
impl Drop for Tree {
fn drop(&mut self) {
let mut pending = std::mem::take(&mut self.children);
while let Some(mut child) = pending.pop() {
pending.append(&mut child.children);
}
}
}
impl Renderable for Tree {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let lines = self.render_lines_into(console, options);
let mut segments = Vec::new();
let last = lines.len().saturating_sub(1);
for (index, line) in lines.into_iter().enumerate() {
if index == last && line.is_empty() {
segments.push(Segment::new("", None));
}
segments.extend(line);
if index != last {
segments.push(Segment::line());
}
}
segments
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
let mut width = (0, 0);
let mut pending: Vec<(&Tree, usize)> = vec![(self, 0)];
while let Some((tree, level)) = pending.pop() {
let label = tree.label.measure_cell(console, options);
let indent = level * 4;
width.0 = width.0.max(label.minimum + indent);
width.1 = width.1.max(label.maximum + indent);
if tree.expanded {
pending.extend(tree.children.iter().map(|child| (child, level + 1)));
}
}
Measurement::new(width.0, width.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
fn console() -> Console {
Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(40)
.build()
}
#[test]
fn nested_tree() {
let mut tree = Tree::new("root");
let a = tree.add("child A");
a.add("leaf A1");
a.add("leaf A2");
tree.add("child B");
let out = console().render_export(&tree);
let expected = concat!(
"root\n",
"├── child A\n",
"│ ├── leaf A1\n",
"│ └── leaf A2\n",
"└── child B\n",
);
assert_eq!(out, expected);
}
#[test]
fn deep_tree_renders_without_recursion() {
let handle = std::thread::Builder::new()
.stack_size(2 * 1024 * 1024)
.spawn(|| {
let mut tree = Tree::new("0");
let mut node = &mut tree;
for depth in 1..20_000 {
node = node.add(depth.to_string());
}
let console = Console::builder().width(12).build();
let out = console.render_export(&tree);
let measured = Measurement::get(&console, &console.options(), &tree);
(out, measured)
})
.expect("spawn");
let (out, measured) = handle.join().expect("deep tree render overflowed");
assert_eq!(out, "0\n└── 1\n └── 2\n");
assert_eq!(measured, Measurement::new(12, 12));
}
}