1use std::collections::HashSet;
2
3use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::Style,
7 text::{Line, Span},
8 widgets::ListItem,
9};
10
11use crate::{DEFAULT_HIGHLIGHT, HIGHLIGHT_SYMBOL, ScrollList, ScrollListState};
12
13#[derive(Clone)]
16pub struct TreeNode<'a> {
17 pub text: Line<'a>,
18 pub children: Vec<TreeNode<'a>>,
19 pub id: usize,
20}
21
22#[derive(Clone)]
27pub struct Tree<'a> {
28 nodes: Vec<TreeNode<'a>>,
29 indent: u16,
30 expand_icon: String,
31 collapse_icon: String,
32 leaf_icon: String,
33 highlight_style: Style,
34 highlight_symbol: String,
35 borders: ratatui::widgets::Borders,
36 border_style: Style,
37}
38
39impl<'a> Tree<'a> {
40 pub fn new(nodes: Vec<TreeNode<'a>>) -> Self {
41 Self {
42 nodes,
43 indent: 2,
44 expand_icon: "▶ ".to_string(),
45 collapse_icon: "▼ ".to_string(),
46 leaf_icon: " ".to_string(),
47 highlight_style: DEFAULT_HIGHLIGHT,
48 highlight_symbol: HIGHLIGHT_SYMBOL.to_string(),
49 borders: ratatui::widgets::Borders::NONE,
50 border_style: Style::new(),
51 }
52 }
53
54 pub fn indent(mut self, n: u16) -> Self {
55 self.indent = n;
56 self
57 }
58
59 pub fn expand_icon(mut self, icon: &str) -> Self {
60 self.expand_icon = icon.to_string();
61 self
62 }
63
64 pub fn collapse_icon(mut self, icon: &str) -> Self {
65 self.collapse_icon = icon.to_string();
66 self
67 }
68
69 pub fn leaf_icon(mut self, icon: &str) -> Self {
70 self.leaf_icon = icon.to_string();
71 self
72 }
73
74 pub fn highlight_style(mut self, style: Style) -> Self {
75 self.highlight_style = style;
76 self
77 }
78
79 pub fn highlight_symbol(mut self, symbol: &str) -> Self {
80 self.highlight_symbol = symbol.to_string();
81 self
82 }
83
84 pub fn borders(mut self, borders: ratatui::widgets::Borders) -> Self {
85 self.borders = borders;
86 self
87 }
88
89 pub fn border_style(mut self, style: Style) -> Self {
90 self.border_style = style;
91 self
92 }
93
94 pub fn visible_count(&self, expanded: &HashSet<usize>) -> usize {
96 let mut count = 0;
97 Self::count_visible(&self.nodes, expanded, &mut count);
98 count
99 }
100
101 fn count_visible(nodes: &[TreeNode], expanded: &HashSet<usize>, count: &mut usize) {
102 for node in nodes {
103 *count += 1;
104 if !node.children.is_empty() && expanded.contains(&node.id) {
105 Self::count_visible(&node.children, expanded, count);
106 }
107 }
108 }
109
110 fn project(&self, expanded: &HashSet<usize>) -> Vec<ListItem<'a>> {
112 let mut items = Vec::new();
113 for node in &self.nodes {
114 Self::project_node(node, 0, self.indent, expanded, &self.expand_icon, &self.collapse_icon, &self.leaf_icon, &mut items);
115 }
116 items
117 }
118
119 fn project_node(
120 node: &TreeNode<'a>,
121 depth: u16,
122 indent: u16,
123 expanded: &HashSet<usize>,
124 expand_icon: &str,
125 collapse_icon: &str,
126 leaf_icon: &str,
127 items: &mut Vec<ListItem<'a>>,
128 ) {
129 let indent_str = " ".repeat((depth as usize) * indent as usize);
130 let icon = if node.children.is_empty() {
131 leaf_icon
132 } else if expanded.contains(&node.id) {
133 collapse_icon
134 } else {
135 expand_icon
136 };
137
138 let mut line = node.text.clone();
139 let prefix = format!("{}{}", indent_str, icon);
140 line.spans.insert(0, Span::raw(prefix));
141
142 items.push(ListItem::new(line));
143
144 if expanded.contains(&node.id) {
145 for child in &node.children {
146 Self::project_node(child, depth + 1, indent, expanded, expand_icon, collapse_icon, leaf_icon, items);
147 }
148 }
149 }
150}
151
152#[derive(Clone, Default)]
154pub struct TreeState {
155 pub expanded: HashSet<usize>,
157 pub scroll_state: ScrollListState,
158}
159
160impl TreeState {
161 pub fn toggle(&mut self, id: usize) {
162 if self.expanded.contains(&id) {
163 self.expanded.remove(&id);
164 } else {
165 self.expanded.insert(id);
166 }
167 }
168
169 pub fn expand_all(&mut self, nodes: &[TreeNode]) {
171 for node in nodes {
172 if !node.children.is_empty() {
173 self.expanded.insert(node.id);
174 self.expand_all(&node.children);
175 }
176 }
177 }
178
179 pub fn expand_subtree(&mut self, nodes: &[TreeNode], id: usize) {
181 if let Some(node) = Self::find_node(nodes, id) {
182 self.expanded.insert(id);
183 for child in &node.children {
184 self.expand_subtree(nodes, child.id);
185 }
186 }
187 }
188
189 pub fn collapse_at(&mut self, nodes: &[TreeNode], cursor_id: usize) {
194 let is_expanded = self.expanded.contains(&cursor_id);
195 let has_children = Self::find_node(nodes, cursor_id)
196 .map(|n| !n.children.is_empty())
197 .unwrap_or(false);
198
199 if is_expanded && has_children {
200 self.expanded.remove(&cursor_id);
201 return;
202 }
203
204 if let Some(ancestor) = self.nearest_expanded_ancestor(nodes, cursor_id) {
205 self.expanded.remove(&ancestor);
206 }
207 }
208
209 pub fn expand_at(&mut self, nodes: &[TreeNode], cursor_id: usize) {
214 if self.expanded.contains(&cursor_id) {
215 self.expand_subtree(nodes, cursor_id);
216 } else {
217 self.expanded.insert(cursor_id);
218 }
219 }
220
221 pub fn nearest_expanded_ancestor(&self, nodes: &[TreeNode], cursor_id: usize) -> Option<usize> {
224 let path = Self::find_path(nodes, cursor_id)?;
225 let mut result = None;
226 for id in &path {
227 if *id == cursor_id {
228 break;
229 }
230 if self.expanded.contains(id) {
231 result = Some(*id);
232 }
233 }
234 result
235 }
236
237 fn find_node<'b>(nodes: &'b [TreeNode<'b>], target_id: usize) -> Option<&'b TreeNode<'b>> {
240 for node in nodes {
241 if node.id == target_id {
242 return Some(node);
243 }
244 if let Some(found) = Self::find_node(&node.children, target_id) {
245 return Some(found);
246 }
247 }
248 None
249 }
250
251 fn find_path<'b>(nodes: &'b [TreeNode<'b>], target_id: usize) -> Option<Vec<usize>> {
252 for node in nodes {
253 if node.id == target_id {
254 return Some(vec![node.id]);
255 }
256 if let Some(mut path) = Self::find_path(&node.children, target_id) {
257 path.insert(0, node.id);
258 return Some(path);
259 }
260 }
261 None
262 }
263
264 pub fn visible_index_of(&self, nodes: &[TreeNode], target_id: usize) -> Option<usize> {
267 let mut idx = 0usize;
268 Self::walk(nodes, target_id, &self.expanded, true, &mut idx)
269 }
270 pub fn walk<'b>(nodes: &'b [TreeNode<'b>], target_id: usize, expanded: &HashSet<usize>, visible: bool, idx: &mut usize) -> Option<usize> {
271 for node in nodes {
272 if visible {
273 if node.id == target_id {
274 return Some(*idx);
275 }
276 *idx += 1;
277 }
278
279 if !node.children.is_empty() && expanded.contains(&node.id) {
280 if let Some(found) = Self::walk(&node.children, target_id, expanded, visible, idx) {
281 return Some(found);
282 }
283 }
284 }
285 None
286 }
287}
288
289impl<'a> ratatui::widgets::StatefulWidget for Tree<'a> {
290 type State = TreeState;
291
292 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
293 let items = self.project(&state.expanded);
294 ScrollList::new_list(items)
295 .highlight_style(self.highlight_style)
296 .highlight_symbol(&self.highlight_symbol)
297 .borders(self.borders)
298 .border_style(self.border_style)
299 .render(area, buf, &mut state.scroll_state);
300 }
301}
302#[cfg(test)]
303mod tests;