use std::borrow::Cow;
use ratatui::text::{Line, Span};
use ratatui::widgets::Cell;
use smallvec::SmallVec;
use crate::context::TreeRowContext;
use crate::model::TreeModel;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TreeGlyphs<'a> {
pub indent: &'a str,
pub branch_last: &'a str,
pub branch: &'a str,
pub vert: &'a str,
pub empty: &'a str,
pub leaf: &'a str,
pub expanded: &'a str,
pub collapsed: &'a str,
}
impl TreeGlyphs<'static> {
#[must_use]
pub const fn unicode() -> Self {
Self {
indent: " ",
branch_last: "└──",
branch: "├──",
vert: "│ ",
empty: " ",
leaf: "•",
expanded: "▼",
collapsed: "▶",
}
}
#[must_use]
pub const fn ascii() -> Self {
Self {
indent: " ",
branch_last: "`--",
branch: "|--",
vert: "| ",
empty: " ",
leaf: "*",
expanded: "v",
collapsed: ">",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TreeLabelPrefix<'a> {
pub name: &'a str,
pub prefix: Option<Cow<'a, str>>,
}
pub trait TreeLabelProvider<T: TreeModel> {
fn label_parts<'a>(&'a self, model: &'a T, id: T::Id) -> TreeLabelPrefix<'a>;
}
pub trait TreeLabelRenderer<T: TreeModel> {
fn cell<'a>(
&'a self,
model: &'a T,
id: T::Id,
ctx: &TreeRowContext,
glyphs: &TreeGlyphs<'a>,
) -> Cell<'a>;
}
impl<T, P> TreeLabelRenderer<T> for P
where
T: TreeModel,
P: TreeLabelProvider<T>,
{
fn cell<'a>(
&'a self,
model: &'a T,
id: T::Id,
ctx: &TreeRowContext,
glyphs: &TreeGlyphs<'a>,
) -> Cell<'a> {
let parts = self.label_parts(model, id);
tree_name_cell(ctx, parts, glyphs)
}
}
#[must_use]
pub fn tree_label_line<'a>(
ctx: &TreeRowContext<'_>,
parts: TreeLabelPrefix<'a>,
glyphs: &TreeGlyphs<'a>,
) -> Line<'a> {
let TreeLabelPrefix { name, prefix: op } = parts;
let op = op.filter(|value| !value.is_empty());
if ctx.level == 0 || !ctx.render.draw_lines {
let expander = if ctx.node.has_children {
if ctx.node.is_expanded {
glyphs.expanded
} else {
glyphs.collapsed
}
} else if ctx.level == 0 {
""
} else {
glyphs.leaf
};
let mut spans = SmallVec::<[Span; 16]>::with_capacity(ctx.level as usize + 6);
if ctx.level > 0 {
for _ in 0..ctx.level {
spans.push(Span::raw(glyphs.empty));
}
}
if !expander.is_empty() {
spans.push(Span::raw(expander));
}
if let Some(op) = op {
spans.push(Span::raw(op));
}
spans.push(Span::raw(" "));
spans.push(Span::raw(name));
return Line::from(spans.into_vec());
}
let mut name_spans = SmallVec::<[Span; 16]>::with_capacity(ctx.is_tail_stack.len() + 6);
let branch_level = (ctx.level as usize) - 1;
for (level, &is_last) in ctx.is_tail_stack.iter().enumerate() {
let part = if level == branch_level {
if is_last {
glyphs.branch_last
} else {
glyphs.branch
}
} else if is_last {
glyphs.indent
} else {
glyphs.vert
};
name_spans.push(Span::styled(part, ctx.line_style));
}
let expander = if ctx.node.has_children {
if ctx.node.is_expanded {
glyphs.expanded
} else {
glyphs.collapsed
}
} else {
glyphs.leaf
};
if !expander.is_empty() {
name_spans.push(Span::raw(expander));
name_spans.push(Span::raw(" "));
}
if let Some(op) = op {
name_spans.push(Span::raw(op));
name_spans.push(Span::raw(" "));
}
name_spans.push(Span::raw(name));
Line::from(name_spans.into_vec())
}
#[inline]
#[must_use]
pub fn tree_name_cell<'a>(
ctx: &TreeRowContext<'_>,
parts: TreeLabelPrefix<'a>,
glyphs: &TreeGlyphs<'a>,
) -> Cell<'a> {
Cell::from(tree_label_line(ctx, parts, glyphs))
}
#[cfg(test)]
mod tests {
use ratatui::style::Style;
use super::{TreeGlyphs, TreeLabelPrefix, tree_label_line};
use crate::context::{TreeRowContext, TreeRowNodeState, TreeRowRenderState};
#[test]
fn tree_label_line_renders_root_expander() {
let ctx = TreeRowContext {
level: 0,
is_tail_stack: &[],
node: TreeRowNodeState {
is_expanded: false,
has_children: true,
is_marked: false,
},
render: TreeRowRenderState { draw_lines: true },
line_style: Style::default(),
};
let line = tree_label_line(
&ctx,
TreeLabelPrefix {
name: "root",
prefix: None,
},
&TreeGlyphs::unicode(),
);
assert_eq!(line.to_string(), "▶ root");
}
#[test]
fn tree_label_line_renders_nested_leaf_with_guides() {
let tail_stack = [false, true];
let ctx = TreeRowContext {
level: 2,
is_tail_stack: &tail_stack,
node: TreeRowNodeState {
is_expanded: false,
has_children: false,
is_marked: false,
},
render: TreeRowRenderState { draw_lines: true },
line_style: Style::default(),
};
let line = tree_label_line(
&ctx,
TreeLabelPrefix {
name: "leaf",
prefix: None,
},
&TreeGlyphs::unicode(),
);
assert_eq!(line.to_string(), "│ └──• leaf");
}
}