1use crossterm::event::KeyCode;
2
3use crate::{
4 Component,
5 Event,
6 Focusable,
7 InputResult,
8 RenderError,
9 Rendered,
10 theme::{
11 Palette,
12 Style,
13 Theme,
14 stylize,
15 },
16};
17
18pub struct TreeNode {
22 label: String,
23 children: Vec<TreeNode>,
24 expanded: bool,
25}
26
27impl TreeNode {
28 pub fn new(label: impl Into<String>) -> Self {
30 Self {
31 label: label.into(),
32 children: Vec::new(),
33 expanded: false,
34 }
35 }
36
37 pub fn child(mut self, node: TreeNode) -> Self {
39 self.children.push(node);
40 self
41 }
42}
43
44pub struct TreeView {
49 nodes: Vec<TreeNode>,
50 selected: Vec<usize>,
51 focused: bool,
52}
53
54impl TreeView {
55 pub fn new(nodes: Vec<TreeNode>) -> Self {
59 let selected = if nodes.is_empty() {
60 Vec::new()
61 } else {
62 vec![0]
63 };
64 Self {
65 nodes,
66 selected,
67 focused: false,
68 }
69 }
70
71 fn flatten(&self) -> Vec<(usize, &TreeNode, Vec<usize>, bool)> {
74 let mut result = Vec::new();
75 let len = self.nodes.len();
76 for (i, node) in self.nodes.iter().enumerate() {
77 Self::flatten_node(node, 0, vec![i], i == len - 1, &mut result);
78 }
79 result
80 }
81
82 fn flatten_node<'a>(
83 node: &'a TreeNode,
84 depth: usize,
85 path: Vec<usize>,
86 is_last: bool,
87 result: &mut Vec<(usize, &'a TreeNode, Vec<usize>, bool)>,
88 ) {
89 result.push((depth, node, path.clone(), is_last));
90 if node.expanded {
91 let child_len = node.children.len();
92 for (i, child) in node.children.iter().enumerate() {
93 let mut child_path = path.clone();
94 child_path.push(i);
95 Self::flatten_node(child, depth + 1, child_path, i == child_len - 1, result);
96 }
97 }
98 }
99
100 fn selected_flat_index(&self, flat: &[(usize, &TreeNode, Vec<usize>, bool)]) -> Option<usize> {
102 flat.iter()
103 .position(|(_, _, path, _)| path == &self.selected)
104 }
105
106 fn node_at_path_mut(&mut self, path: &[usize]) -> Option<&mut TreeNode> {
108 if path.is_empty() {
109 return None;
110 }
111 let mut node = match self.nodes.get_mut(path[0]) {
112 | Some(n) => n,
113 | None => return None,
114 };
115 for &index in &path[1..] {
116 node = match node.children.get_mut(index) {
117 | Some(n) => n,
118 | None => return None,
119 };
120 }
121 Some(node)
122 }
123
124 fn navigate_down(&mut self) {
125 let new_path = {
126 let flat = self.flatten();
127 if let Some(idx) = self.selected_flat_index(&flat) {
128 flat.get(idx + 1).map(|entry| entry.2.clone())
129 } else {
130 None
131 }
132 };
133 if let Some(path) = new_path {
134 self.selected = path;
135 }
136 }
137
138 fn navigate_up(&mut self) {
139 let new_path = {
140 let flat = self.flatten();
141 if let Some(idx) = self.selected_flat_index(&flat) {
142 if idx > 0 {
143 flat.get(idx - 1).map(|entry| entry.2.clone())
144 } else {
145 None
146 }
147 } else {
148 None
149 }
150 };
151 if let Some(path) = new_path {
152 self.selected = path;
153 }
154 }
155}
156
157impl Focusable for TreeView {
158 fn focused(&self) -> bool {
159 self.focused
160 }
161
162 fn set_focused(&mut self, focused: bool) {
163 self.focused = focused;
164 }
165}
166
167impl Component for TreeView {
168 fn render(&self, width: u16) -> Result<Rendered, RenderError> {
169 let theme = Theme::current();
170 let accent_style = Style::new().fg(theme.accent()).bold();
171 let normal_style = Style::new().fg(theme.text_primary());
172
173 let flat = self.flatten();
174 let selected_index = self.selected_flat_index(&flat);
175
176 let mut lines = Vec::new();
177 for (flat_i, (depth, node, _path, is_last)) in flat.iter().enumerate() {
178 let is_selected = selected_index == Some(flat_i);
179
180 let mut line = String::new();
181 if *depth == 0 {
182 if is_selected && self.focused {
183 line.push_str("> ");
184 } else {
185 line.push_str(" ");
186 }
187 } else {
188 line.push_str(&" ".repeat(*depth));
189 if *is_last {
190 line.push_str("└─ ");
191 } else {
192 line.push_str("├─ ");
193 }
194 }
195
196 if !node.children.is_empty() {
197 if node.expanded {
198 line.push_str("▼ ");
199 } else {
200 line.push_str("▶ ");
201 }
202 } else {
203 line.push_str(" ");
204 }
205
206 line.push_str(&node.label);
207
208 let truncated = crate::utils::truncate_to_width(&line, width, "…");
209 let styled = if is_selected {
210 stylize(&truncated, &accent_style)
211 } else {
212 stylize(&truncated, &normal_style)
213 };
214 lines.push(styled);
215 }
216
217 Ok(Rendered {
218 lines,
219 cursor: None,
220 images: Vec::new(),
221 })
222 }
223
224 fn handle_input(&mut self, event: &Event) -> InputResult {
225 use crossterm::event::KeyModifiers;
226 if self.nodes.is_empty() {
227 return InputResult::Ignored;
228 }
229
230 if let Event::Key(key) = event {
231 match key.code {
232 | KeyCode::Down => {
233 self.navigate_down();
234 InputResult::Handled
235 },
236 | KeyCode::Up => {
237 self.navigate_up();
238 InputResult::Handled
239 },
240 | KeyCode::Char('j') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
241 self.navigate_down();
242 InputResult::Handled
243 },
244 | KeyCode::Char('k') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
245 self.navigate_up();
246 InputResult::Handled
247 },
248 | KeyCode::Right | KeyCode::Enter => {
249 let path = self.selected.clone();
250 if let Some(node) = self.node_at_path_mut(&path) &&
251 !node.children.is_empty()
252 {
253 node.expanded = !node.expanded;
254 return InputResult::Handled;
255 }
256 InputResult::Ignored
257 },
258 | KeyCode::Left => {
259 let path = self.selected.clone();
260 if let Some(node) = self.node_at_path_mut(&path) &&
261 node.expanded &&
262 !node.children.is_empty()
263 {
264 node.expanded = false;
265 return InputResult::Handled;
266 }
267 if self.selected.len() > 1 {
268 self.selected.pop();
269 return InputResult::Handled;
270 }
271 InputResult::Ignored
272 },
273 | _ => InputResult::Ignored,
274 }
275 } else {
276 InputResult::Ignored
277 }
278 }
279
280 fn as_focusable(&self) -> Option<&dyn Focusable> {
281 Some(self)
282 }
283
284 fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
285 Some(self)
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use crossterm::event::KeyCode;
292
293 use super::*;
294
295 #[test]
296 fn tree_node_builder() {
297 let node = TreeNode::new("root").child(TreeNode::new("child"));
298 assert_eq!(node.label, "root");
299 assert_eq!(node.children.len(), 1);
300 }
301
302 #[test]
303 fn tree_view_new() {
304 let view = TreeView::new(vec![TreeNode::new("a"), TreeNode::new("b")]);
305 assert_eq!(view.selected, vec![0]);
306 }
307
308 #[test]
309 fn tree_view_new_empty() {
310 let view = TreeView::new(Vec::new());
311 assert!(view.selected.is_empty());
312 }
313
314 #[test]
315 fn tree_view_focusable() {
316 let mut view = TreeView::new(vec![TreeNode::new("a")]);
317 assert!(!view.focused());
318 view.set_focused(true);
319 assert!(view.focused());
320 }
321
322 #[test]
323 fn tree_view_render() {
324 Theme::with(Theme::Light, || {
325 let view = TreeView::new(vec![TreeNode::new("root")]);
326 let rendered = view.render(80).unwrap();
327 assert_eq!(rendered.lines.len(), 1);
328 assert!(rendered.lines[0].contains("root"));
329 });
330 }
331
332 #[test]
333 fn tree_view_navigation_down() {
334 let mut view = TreeView::new(vec![TreeNode::new("a"), TreeNode::new("b")]);
335 view.set_focused(true);
336 view.handle_input(&Event::Key(KeyCode::Down.into()));
337 assert_eq!(view.selected, vec![1]);
338 }
339
340 #[test]
341 fn tree_view_navigation_up() {
342 let mut view = TreeView::new(vec![TreeNode::new("a"), TreeNode::new("b")]);
343 view.set_focused(true);
344 view.selected = vec![1];
345 view.handle_input(&Event::Key(KeyCode::Up.into()));
346 assert_eq!(view.selected, vec![0]);
347 }
348
349 #[test]
350 fn tree_view_toggle_expansion() {
351 let mut view = TreeView::new(vec![TreeNode::new("root").child(TreeNode::new("child"))]);
352 view.set_focused(true);
353 let flat = view.flatten();
354 assert_eq!(flat.len(), 1);
355
356 view.handle_input(&Event::Key(KeyCode::Right.into()));
357 let flat = view.flatten();
358 assert_eq!(flat.len(), 2);
359
360 view.handle_input(&Event::Key(KeyCode::Right.into()));
361 let flat = view.flatten();
362 assert_eq!(flat.len(), 1);
363 }
364
365 #[test]
366 fn tree_view_left_navigates_to_parent() {
367 let mut view = TreeView::new(vec![TreeNode::new("root").child(TreeNode::new("child"))]);
368 view.set_focused(true);
369 view.handle_input(&Event::Key(KeyCode::Right.into()));
370 view.handle_input(&Event::Key(KeyCode::Down.into()));
371 assert_eq!(view.selected, vec![0, 0]);
372
373 view.handle_input(&Event::Key(KeyCode::Left.into()));
374 assert_eq!(view.selected, vec![0]);
375 }
376
377 #[test]
378 fn tree_view_left_collapses() {
379 let mut view = TreeView::new(vec![TreeNode::new("root").child(TreeNode::new("child"))]);
380 view.set_focused(true);
381 view.handle_input(&Event::Key(KeyCode::Right.into()));
382 assert!(view.nodes[0].expanded);
383
384 view.handle_input(&Event::Key(KeyCode::Left.into()));
385 assert!(!view.nodes[0].expanded);
386 }
387
388 #[test]
389 fn tree_view_j_k_navigation() {
390 let mut view = TreeView::new(vec![TreeNode::new("a"), TreeNode::new("b")]);
391 view.set_focused(true);
392 view.handle_input(&Event::Key(KeyCode::Char('j').into()));
393 assert_eq!(view.selected, vec![1]);
394 view.handle_input(&Event::Key(KeyCode::Char('k').into()));
395 assert_eq!(view.selected, vec![0]);
396 }
397
398 #[test]
399 fn tree_view_enter_toggles() {
400 let mut view = TreeView::new(vec![TreeNode::new("root").child(TreeNode::new("child"))]);
401 view.set_focused(true);
402 let result = view.handle_input(&Event::Key(KeyCode::Enter.into()));
403 assert_eq!(result, InputResult::Handled);
404 assert!(view.nodes[0].expanded);
405 }
406
407 #[test]
408 fn tree_view_leaf_ignores_right() {
409 let mut view = TreeView::new(vec![TreeNode::new("leaf")]);
410 view.set_focused(true);
411 let result = view.handle_input(&Event::Key(KeyCode::Right.into()));
412 assert_eq!(result, InputResult::Ignored);
413 }
414
415 #[test]
416 fn tree_view_root_left_ignored_when_collapsed() {
417 let mut view = TreeView::new(vec![TreeNode::new("root").child(TreeNode::new("child"))]);
418 view.set_focused(true);
419 let result = view.handle_input(&Event::Key(KeyCode::Left.into()));
420 assert_eq!(result, InputResult::Ignored);
421 }
422}