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