use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, List, ListItem, Widget},
};
#[derive(Debug, Clone)]
pub struct TreeNode {
pub label: String,
pub expandable: bool,
pub expanded: bool,
pub children: Vec<TreeNode>,
pub depth: usize,
pub value: Option<String>,
pub node_type: String,
}
impl TreeNode {
pub fn new(label: impl Into<String>, node_type: impl Into<String>) -> Self {
Self {
label: label.into(),
expandable: false,
expanded: false,
children: Vec::new(),
depth: 0,
value: None,
node_type: node_type.into(),
}
}
pub fn with_children(
label: impl Into<String>,
node_type: impl Into<String>,
children: Vec<TreeNode>,
) -> Self {
let has_children = !children.is_empty();
Self {
label: label.into(),
expandable: has_children,
expanded: false,
children,
depth: 0,
value: None,
node_type: node_type.into(),
}
}
pub fn with_value(
label: impl Into<String>,
value: impl Into<String>,
node_type: impl Into<String>,
) -> Self {
Self {
label: label.into(),
expandable: false,
expanded: false,
children: Vec::new(),
depth: 0,
value: Some(value.into()),
node_type: node_type.into(),
}
}
pub fn toggle(&mut self) {
if self.expandable {
self.expanded = !self.expanded;
}
}
pub fn expand(&mut self) {
if self.expandable {
self.expanded = true;
}
}
pub fn collapse(&mut self) {
if self.expandable {
self.expanded = false;
}
}
pub fn add_child(&mut self, child: TreeNode) {
self.children.push(child);
self.expandable = true;
}
pub fn flatten(&self, include_self: bool) -> Vec<&TreeNode> {
let mut result = Vec::new();
if include_self {
result.push(self);
}
if self.expanded {
for child in &self.children {
result.extend(child.flatten(true));
}
}
result
}
pub fn flatten_mut(&mut self) -> Vec<*mut TreeNode> {
let mut result = Vec::new();
result.push(self as *mut TreeNode);
if self.expanded {
for child in &mut self.children {
result.extend(child.flatten_mut());
}
}
result
}
}
#[derive(Debug, Clone, Default)]
pub struct TreeState {
pub selected: usize,
pub offset: usize,
}
impl TreeState {
pub fn new() -> Self {
Self::default()
}
pub fn select_next(&mut self, max: usize) {
if self.selected < max.saturating_sub(1) {
self.selected += 1;
}
}
pub fn select_previous(&mut self) {
if self.selected > 0 {
self.selected = self.selected.saturating_sub(1);
}
}
pub fn select_first(&mut self) {
self.selected = 0;
self.offset = 0;
}
pub fn select_last(&mut self, max: usize) {
self.selected = max.saturating_sub(1);
}
pub fn update_offset(&mut self, height: usize) {
if self.selected < self.offset {
self.offset = self.selected;
} else if self.selected >= self.offset + height {
self.offset = self.selected.saturating_sub(height - 1);
}
}
}
pub struct TreeWidget<'a> {
pub roots: &'a [TreeNode],
pub state: &'a TreeState,
pub block: Option<Block<'a>>,
pub highlight_style: Style,
}
impl<'a> TreeWidget<'a> {
pub fn new(roots: &'a [TreeNode], state: &'a TreeState) -> Self {
Self {
roots,
state,
block: None,
highlight_style: Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD),
}
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
fn get_visible_nodes(&self) -> Vec<&TreeNode> {
let mut nodes = Vec::new();
for root in self.roots {
nodes.extend(root.flatten(true));
}
nodes
}
fn render_node(node: &TreeNode, is_selected: bool, _highlight_style: Style) -> ListItem<'_> {
let indent = " ".repeat(node.depth);
let icon = if node.expandable {
if node.expanded {
"▼ "
} else {
"▶ "
}
} else if node.value.is_some() {
"→ "
} else {
" "
};
let mut spans = Vec::new();
if !indent.is_empty() {
spans.push(Span::raw(indent));
}
spans.push(Span::styled(
icon,
Style::default().fg(if is_selected {
Color::Yellow
} else {
Color::DarkGray
}),
));
if node.value.is_some() {
spans.push(Span::styled("[", Style::default().fg(Color::DarkGray)));
spans.push(Span::styled(
&node.node_type,
Style::default().fg(if is_selected {
Color::Cyan
} else {
Color::Blue
}),
));
spans.push(Span::styled("] ", Style::default().fg(Color::DarkGray)));
spans.push(Span::styled(&node.label, Style::default().fg(Color::White)));
if let Some(ref val) = node.value {
spans.push(Span::styled(": ", Style::default().fg(Color::DarkGray)));
spans.push(Span::styled(
val,
Style::default().fg(if is_selected {
Color::Green
} else {
Color::Gray
}),
));
}
} else {
spans.push(Span::styled(
&node.label,
Style::default().fg(if is_selected {
Color::White
} else {
Color::Gray
}),
));
}
if is_selected {
for span in &mut spans {
span.style = span.style.bg(Color::DarkGray);
}
}
ListItem::new(Line::from(spans))
}
}
impl<'a> Widget for TreeWidget<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let block = self.block.take();
let visible_nodes = self.get_visible_nodes();
let items: Vec<ListItem> = visible_nodes
.iter()
.enumerate()
.map(|(idx, node)| {
Self::render_node(node, idx == self.state.selected, self.highlight_style)
})
.collect();
let mut list = List::new(items);
if let Some(block) = block {
list = list.block(block);
}
Widget::render(list, area, buf);
}
}