use crate::component::{Component, EventCx};
use crate::event::Event;
use crate::geom::{Pos, Rect, Size};
use crate::render::RenderCx;
use crate::style::Style;
use crate::text::Text;
pub struct TreeNode {
pub label: Text,
pub children: Vec<TreeNode>,
pub expanded: bool,
}
struct VisibleEntry {
node_path: Vec<usize>, depth: usize,
is_last: bool,
has_children: bool,
expanded: bool,
label: Text,
}
pub struct Tree {
nodes: Vec<TreeNode>,
selected: usize,
show_guides: bool,
scroll_offset: usize,
rect: Rect,
style: Style,
select_style: Style,
}
impl Tree {
pub fn new(nodes: Vec<TreeNode>) -> Self {
Self {
nodes,
selected: 0,
show_guides: true,
scroll_offset: 0,
rect: Rect::default(),
style: Style::default(),
select_style: Style::default(),
}
}
pub fn show_guides(mut self, show: bool) -> Self {
self.show_guides = show;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn select_style(mut self, style: Style) -> Self {
self.select_style = style;
self
}
pub fn selected_index(&self) -> usize {
self.selected
}
fn flatten(&self) -> Vec<VisibleEntry> {
let mut entries = Vec::new();
for (i, node) in self.nodes.iter().enumerate() {
self.flatten_node(node, &vec![i], 0, i == self.nodes.len() - 1, &mut entries);
}
entries
}
fn flatten_node(
&self,
node: &TreeNode,
path: &[usize],
depth: usize,
is_last: bool,
entries: &mut Vec<VisibleEntry>,
) {
let has_children = !node.children.is_empty();
entries.push(VisibleEntry {
node_path: path.to_vec(),
depth,
is_last,
has_children,
expanded: node.expanded,
label: node.label.clone(),
});
if node.expanded {
let child_count = node.children.len();
for (i, child) in node.children.iter().enumerate() {
let mut child_path = path.to_vec();
child_path.push(i);
self.flatten_node(
child,
&child_path,
depth + 1,
i == child_count - 1,
entries,
);
}
}
}
fn node_at_path(&mut self, path: &[usize]) -> Option<&mut TreeNode> {
if path.is_empty() {
return None;
}
let mut node = self.nodes.get_mut(path[0])?;
for &idx in &path[1..] {
node = node.children.get_mut(idx)?;
}
Some(node)
}
}
impl Component for Tree {
fn render(&self, cx: &mut RenderCx) {
let entries = self.flatten();
if entries.is_empty() {
return;
}
let visible_height = self.rect.height.max(1) as usize;
let start = self.scroll_offset.min(entries.len().saturating_sub(1));
let end = (start + visible_height).min(entries.len());
for i in start..end {
let entry = &entries[i];
let is_selected = i == self.selected;
let row_y = self.rect.y + (i - start) as u16;
let mut prefix = String::new();
if self.show_guides {
if entry.depth > 0 {
let mut has_continuation = vec![false; entry.depth];
for a in 0..entry.depth {
for j in (i + 1)..entries.len() {
if entries[j].depth < a + 1 {
break;
}
if entries[j].depth == a + 1 {
has_continuation[a] = true;
break;
}
}
}
for a in 0..entry.depth {
if has_continuation[a] {
prefix.push_str("│ ");
} else {
prefix.push_str(" ");
}
}
}
if entry.depth > 0 {
if entry.is_last {
prefix.push_str("└─");
} else {
prefix.push_str("├─");
}
}
} else {
for _ in 0..entry.depth {
prefix.push_str(" ");
}
}
if entry.has_children {
if entry.expanded {
prefix.push_str("â–¼ ");
} else {
prefix.push_str("â–¶ ");
}
} else {
prefix.push_str(" ");
}
let style = if is_selected {
&self.select_style
} else {
&self.style
};
cx.buffer.write_text(
Pos {
x: self.rect.x,
y: row_y,
},
self.rect,
&prefix,
style,
);
let label_text = entry.label.first_text();
let prefix_w = crate::widgets::textarea::str_width(&prefix);
cx.buffer.write_text(
Pos {
x: self.rect.x + prefix_w,
y: row_y,
},
self.rect,
label_text,
style,
);
}
}
fn measure(
&self,
_constraint: crate::layout::Constraint,
_cx: &mut crate::component::MeasureCx,
) -> Size {
let entries = self.flatten();
let max_w = entries
.iter()
.map(|e| (e.depth * 2 + 2) as u16 + e.label.max_width())
.max()
.unwrap_or(0);
Size {
width: max_w,
height: entries.len().max(1) as u16,
}
}
fn event(&mut self, event: &Event, cx: &mut EventCx) {
if matches!(event, Event::Focus | Event::Blur | Event::Tick) {
return;
}
let entries = self.flatten();
if entries.is_empty() {
return;
}
if let Event::Key(key_event) = event {
match &key_event.key {
crate::event::Key::Up => {
if self.selected > 0 {
self.selected -= 1;
self.scroll_to_selected(entries.len());
cx.invalidate_paint();
}
}
crate::event::Key::Down => {
if self.selected + 1 < entries.len() {
self.selected += 1;
self.scroll_to_selected(entries.len());
cx.invalidate_paint();
}
}
crate::event::Key::Enter | crate::event::Key::Char(' ') => {
self.toggle_selected();
cx.invalidate_paint();
}
crate::event::Key::Right => {
self.expand_selected();
cx.invalidate_paint();
}
crate::event::Key::Left => {
self.collapse_or_parent(&entries);
cx.invalidate_paint();
}
crate::event::Key::Home => {
self.selected = 0;
self.scroll_offset = 0;
cx.invalidate_paint();
}
crate::event::Key::End => {
self.selected = entries.len().saturating_sub(1);
self.scroll_to_selected(entries.len());
cx.invalidate_paint();
}
_ => {}
}
}
}
fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
self.rect = rect;
}
fn focusable(&self) -> bool {
false
}
fn style(&self) -> Style {
self.style.clone()
}
}
impl Tree {
fn toggle_selected(&mut self) {
let entries = self.flatten();
if let Some(entry) = entries.get(self.selected) {
if entry.has_children {
if let Some(node) = self.node_at_path(&entry.node_path) {
node.expanded = !node.expanded;
}
}
}
}
fn expand_selected(&mut self) {
let entries = self.flatten();
if let Some(entry) = entries.get(self.selected) {
if entry.has_children && !entry.expanded {
if let Some(node) = self.node_at_path(&entry.node_path) {
node.expanded = true;
}
}
}
}
fn collapse_or_parent(&mut self, entries: &[VisibleEntry]) {
if let Some(entry) = entries.get(self.selected) {
if entry.has_children && entry.expanded {
if let Some(node) = self.node_at_path(&entry.node_path) {
node.expanded = false;
}
} else if entry.depth > 0 {
let parent_depth = entry.depth - 1;
for (i, e) in entries.iter().enumerate() {
if e.node_path.len() == entry.node_path.len() - 1
&& e.depth == parent_depth
{
self.selected = i;
break;
}
}
}
}
}
fn scroll_to_selected(&mut self, total_visible: usize) {
let visible_height = self.rect.height.max(1) as usize;
if self.selected < self.scroll_offset {
self.scroll_offset = self.selected;
} else if self.selected >= self.scroll_offset + visible_height {
self.scroll_offset = self.selected.saturating_sub(visible_height.saturating_sub(1));
}
self.scroll_offset = self.scroll_offset.min(
total_visible.saturating_sub(visible_height),
);
}
}