use crate::console::{ConsoleOptions, RenderResult, Renderable};
use crate::segment::Segment;
use crate::style::Style;
#[derive(Debug, Clone)]
pub struct TreeGuides {
pub space: &'static str,
pub continue_line: &'static str,
pub fork: &'static str,
pub end: &'static str,
}
pub const ASCII_GUIDES: TreeGuides = TreeGuides {
space: " ",
continue_line: "| ",
fork: "+-- ",
end: "`-- ",
};
pub const TREE_GUIDES: TreeGuides = TreeGuides {
space: " ",
continue_line: "│ ",
fork: "├── ",
end: "└── ",
};
#[derive(Debug, Clone)]
pub struct Tree {
pub label: String,
pub style: Style,
pub guide_style: Style,
pub expanded: bool,
pub highlight: bool,
pub hide_root: bool,
pub children: Vec<Tree>,
}
impl Tree {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
style: Style::new(),
guide_style: Style::new(),
expanded: true,
highlight: false,
hide_root: false,
children: Vec::new(),
}
}
pub fn add(&mut self, label: impl Into<String>) -> &mut Tree {
let child = Tree::new(label);
self.children.push(child);
self.children.last_mut().unwrap()
}
pub fn style(mut self, style: Style) -> Self { self.style = style; self }
pub fn guide_style(mut self, style: Style) -> Self { self.guide_style = style; self }
pub fn hide_root(mut self) -> Self { self.hide_root = true; self }
}
impl Renderable for Tree {
fn render(&self, options: &ConsoleOptions) -> RenderResult {
let guides = if options.ascii_only { &ASCII_GUIDES } else { &TREE_GUIDES };
let mut lines: Vec<Vec<Segment>> = Vec::new();
if !self.hide_root {
lines.push(vec![Segment::new(&self.label), Segment::line()]);
}
let last_idx = self.children.len().saturating_sub(1);
for (i, child) in self.children.iter().enumerate() {
let is_last = i == last_idx;
self.render_node(child, &mut lines, guides, "", is_last, options);
}
RenderResult { lines, items: Vec::new() }
}
}
impl Tree {
fn render_node(
&self,
node: &Tree,
lines: &mut Vec<Vec<Segment>>,
guides: &TreeGuides,
prefix: &str,
is_last: bool,
options: &ConsoleOptions,
) {
let connector = if is_last { guides.end } else { guides.fork };
let guide_ansi = self.guide_style.to_ansi();
let guide_reset = if guide_ansi.is_empty() { "" } else { "\x1b[0m" };
let guide_str = format!("{prefix}{connector}");
lines.push(vec![
Segment::new(format!("{guide_ansi}{guide_str}{guide_reset}")),
Segment::new(&node.label),
Segment::line(),
]);
let child_prefix = if is_last {
format!("{prefix}{}", guides.space)
} else {
format!("{prefix}{}", guides.continue_line)
};
let child_prefix_styled = format!("{guide_ansi}{child_prefix}{guide_reset}");
let last_child = node.children.len().saturating_sub(1);
for (i, child) in node.children.iter().enumerate() {
let child_is_last = i == last_child;
self.render_node(child, lines, guides, &child_prefix_styled, child_is_last, options);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::ConsoleOptions;
#[test]
fn test_simple_tree() {
let mut tree = Tree::new("Root");
tree.add("Child 1");
tree.add("Child 2");
let opts = ConsoleOptions::default();
let result = tree.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("Root"));
assert!(ansi.contains("Child 1"));
assert!(ansi.contains("Child 2"));
}
#[test]
fn test_nested_tree() {
let mut tree = Tree::new("Root");
let child = tree.add("A");
child.add("A.1");
tree.add("B");
let opts = ConsoleOptions::default();
let result = tree.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("A.1"));
}
}