1use crossterm::event::{KeyCode, KeyEvent};
2use ratatui::{
3 buffer::Buffer,
4 layout::Rect,
5 style::{Color, Style},
6 text::{Line, Span},
7 widgets::{Block, StatefulWidget, Widget},
8};
9use std::collections::HashSet;
10
11#[derive(Debug, Clone)]
13pub struct TreeNode<T> {
14 pub data: T,
16 pub children: Vec<TreeNode<T>>,
18 pub expandable: bool,
20}
21
22impl<T> TreeNode<T> {
23 pub fn new(data: T) -> Self {
25 Self {
26 data,
27 children: Vec::new(),
28 expandable: false,
29 }
30 }
31
32 pub fn with_children(data: T, children: Vec<TreeNode<T>>) -> Self {
34 let expandable = !children.is_empty();
35 Self {
36 data,
37 children,
38 expandable,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct NodeState {
46 pub is_selected: bool,
48 pub is_expanded: bool,
50 pub level: usize,
52 pub has_children: bool,
54 pub path: Vec<usize>,
56}
57
58pub type NodeRenderFn<'a, T> = Box<dyn Fn(&T, &NodeState) -> Line<'a> + 'a>;
60
61#[derive(Debug, Clone, Default)]
63pub struct TreeViewState {
64 pub selected_path: Option<Vec<usize>>,
66 pub expanded: HashSet<Vec<usize>>,
68 pub offset: usize,
70}
71
72impl TreeViewState {
73 pub fn new() -> Self {
74 Self::default()
75 }
76
77 pub fn select(&mut self, path: Vec<usize>) {
79 self.selected_path = Some(path);
80 }
81
82 pub fn clear_selection(&mut self) {
84 self.selected_path = None;
85 }
86
87 pub fn toggle_expansion(&mut self, path: Vec<usize>) {
89 if self.expanded.contains(&path) {
90 self.expanded.remove(&path);
91 } else {
92 self.expanded.insert(path);
93 }
94 }
95
96 pub fn is_expanded(&self, path: &[usize]) -> bool {
98 self.expanded.contains(path)
99 }
100
101 pub fn expand(&mut self, path: Vec<usize>) {
103 self.expanded.insert(path);
104 }
105
106 pub fn collapse(&mut self, path: Vec<usize>) {
108 self.expanded.remove(&path);
109 }
110
111 pub fn expand_all<T>(&mut self, nodes: &[TreeNode<T>]) {
113 fn collect_paths<T>(
114 nodes: &[TreeNode<T>],
115 current_path: Vec<usize>,
116 expanded: &mut HashSet<Vec<usize>>,
117 ) {
118 for (idx, node) in nodes.iter().enumerate() {
119 let mut path = current_path.clone();
120 path.push(idx);
121
122 if node.expandable {
123 expanded.insert(path.clone());
124 }
125
126 if !node.children.is_empty() {
127 collect_paths(&node.children, path, expanded);
128 }
129 }
130 }
131
132 collect_paths(nodes, Vec::new(), &mut self.expanded);
133 }
134
135 pub fn collapse_all(&mut self) {
137 self.expanded.clear();
138 }
139}
140
141pub struct TreeView<'a, T> {
143 nodes: Vec<TreeNode<T>>,
145 block: Option<Block<'a>>,
147 render_fn: NodeRenderFn<'a, T>,
149 expand_icon: &'a str,
151 collapse_icon: &'a str,
153 highlight_style: Option<Style>,
155}
156
157impl<'a, T> TreeView<'a, T> {
158 pub fn new(nodes: Vec<TreeNode<T>>) -> Self {
160 Self {
161 nodes,
162 block: None,
163 render_fn: Box::new(|_data, _state| Line::from("Node")),
164 expand_icon: "▶",
165 collapse_icon: "▼",
166 highlight_style: None,
167 }
168 }
169
170 pub fn block(mut self, block: Block<'a>) -> Self {
172 self.block = Some(block);
173 self
174 }
175
176 pub fn icons(mut self, expand: &'a str, collapse: &'a str) -> Self {
178 self.expand_icon = expand;
179 self.collapse_icon = collapse;
180 self
181 }
182
183 pub fn render_fn<F>(mut self, f: F) -> Self
185 where
186 F: Fn(&T, &NodeState) -> Line<'a> + 'a,
187 {
188 self.render_fn = Box::new(f);
189 self
190 }
191
192 pub fn highlight_style(mut self, style: Style) -> Self {
194 self.highlight_style = Some(style);
195 self
196 }
197
198 fn flatten_tree(&self, state: &TreeViewState) -> Vec<(Line<'a>, Vec<usize>)> {
200 let mut items = Vec::new();
201
202 struct TraverseContext<'a, 'b, T> {
204 state: &'b TreeViewState,
205 render_fn: &'b dyn Fn(&T, &NodeState) -> Line<'a>,
206 expand_icon: &'b str,
207 collapse_icon: &'b str,
208 }
209
210 fn traverse<'a, T>(
211 nodes: &[TreeNode<T>],
212 current_path: Vec<usize>,
213 level: usize,
214 ctx: &TraverseContext<'a, '_, T>,
215 items: &mut Vec<(Line<'a>, Vec<usize>)>,
216 ) {
217 for (idx, node) in nodes.iter().enumerate() {
218 let mut path = current_path.clone();
219 path.push(idx);
220
221 let is_expanded = ctx.state.is_expanded(&path);
222 let is_selected = ctx.state.selected_path.as_ref() == Some(&path);
223
224 let node_state = NodeState {
225 is_selected,
226 is_expanded,
227 level,
228 has_children: !node.children.is_empty(),
229 path: path.clone(),
230 };
231
232 let indent = " ".repeat(level);
234 let expansion_icon = if node.expandable {
235 if is_expanded {
236 ctx.collapse_icon
237 } else {
238 ctx.expand_icon
239 }
240 } else {
241 " "
242 };
243
244 let custom_line = (ctx.render_fn)(&node.data, &node_state);
246
247 let mut spans = vec![
249 Span::raw(indent),
250 Span::styled(
251 format!("{} ", expansion_icon),
252 Style::default().fg(Color::DarkGray),
253 ),
254 ];
255 spans.extend(custom_line.spans);
256
257 items.push((Line::from(spans), path.clone()));
258
259 if is_expanded && !node.children.is_empty() {
261 traverse(&node.children, path, level + 1, ctx, items);
262 }
263 }
264 }
265
266 let ctx = TraverseContext {
267 state,
268 render_fn: &self.render_fn,
269 expand_icon: self.expand_icon,
270 collapse_icon: self.collapse_icon,
271 };
272
273 traverse(&self.nodes, Vec::new(), 0, &ctx, &mut items);
274
275 items
276 }
277
278 pub fn node_at_row(&self, state: &TreeViewState, row: usize) -> Option<Vec<usize>> {
280 let items = self.flatten_tree(state);
281 items.get(row + state.offset).map(|(_, path)| path.clone())
282 }
283
284 pub fn visible_item_count(&self, state: &TreeViewState) -> usize {
286 self.flatten_tree(state).len()
287 }
288}
289
290impl<'a, T> StatefulWidget for TreeView<'a, T> {
291 type State = TreeViewState;
292
293 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
294 let area = match self.block {
295 Some(ref b) => {
296 let inner = b.inner(area);
297 b.clone().render(area, buf);
298 inner
299 }
300 None => area,
301 };
302
303 if area.height == 0 {
304 return;
305 }
306
307 let items = self.flatten_tree(state);
308 let visible_height = area.height as usize;
309
310 if let Some(ref selected) = state.selected_path {
312 if let Some(selected_idx) = items.iter().position(|(_, path)| path == selected) {
313 if selected_idx < state.offset {
314 state.offset = selected_idx;
315 } else if selected_idx >= state.offset + visible_height {
316 state.offset = selected_idx.saturating_sub(visible_height - 1);
317 }
318 }
319 }
320
321 for (i, (line, path)) in items
323 .iter()
324 .skip(state.offset)
325 .take(visible_height)
326 .enumerate()
327 {
328 let y = area.y + i as u16;
329
330 let is_selected = state.selected_path.as_ref() == Some(path);
332 if is_selected && self.highlight_style.is_some() {
333 let style = self.highlight_style.unwrap();
334 for x in area.x..(area.x + area.width) {
335 buf[(x, y)].set_style(style);
336 }
337 }
338
339 buf.set_line(area.x, y, line, area.width);
340 }
341 }
342}
343
344impl<'a, T> Widget for &TreeView<'a, T> {
346 fn render(self, area: Rect, buf: &mut Buffer) {
347 let state = TreeViewState::default();
348
349 let area = match &self.block {
350 Some(ref b) => {
351 let inner = b.inner(area);
352 b.clone().render(area, buf);
353 inner
354 }
355 None => area,
356 };
357
358 if area.height == 0 {
359 return;
360 }
361
362 let items = self.flatten_tree(&state);
363 let visible_height = area.height as usize;
364
365 for (i, (line, _)) in items
367 .iter()
368 .skip(state.offset)
369 .take(visible_height)
370 .enumerate()
371 {
372 let y = area.y + i as u16;
373 buf.set_line(area.x, y, line, area.width);
374 }
375 }
376}
377
378pub fn get_visible_paths<T>(nodes: &[TreeNode<T>], state: &TreeViewState) -> Vec<Vec<usize>> {
380 let mut paths = Vec::new();
381
382 fn traverse<T>(
383 nodes: &[TreeNode<T>],
384 current_path: Vec<usize>,
385 state: &TreeViewState,
386 paths: &mut Vec<Vec<usize>>,
387 ) {
388 for (idx, node) in nodes.iter().enumerate() {
389 let mut path = current_path.clone();
390 path.push(idx);
391 paths.push(path.clone());
392
393 if state.is_expanded(&path) && !node.children.is_empty() {
395 traverse(&node.children, path, state, paths);
396 }
397 }
398 }
399
400 traverse(nodes, Vec::new(), state, &mut paths);
401 paths
402}
403
404#[derive(Debug, Clone)]
406pub struct TreeKeyBindings {
407 pub next: Vec<KeyCode>,
408 pub previous: Vec<KeyCode>,
409 pub expand: Vec<KeyCode>,
410 pub collapse: Vec<KeyCode>,
411 pub toggle: Vec<KeyCode>,
412 pub goto_top: Vec<KeyCode>,
413 pub goto_bottom: Vec<KeyCode>,
414}
415
416impl Default for TreeKeyBindings {
417 fn default() -> Self {
418 Self {
419 next: vec![KeyCode::Char('j'), KeyCode::Down],
420 previous: vec![KeyCode::Char('k'), KeyCode::Up],
421 expand: vec![KeyCode::Char('l'), KeyCode::Right],
422 collapse: vec![KeyCode::Char('h'), KeyCode::Left],
423 toggle: vec![KeyCode::Enter],
424 goto_top: vec![KeyCode::Char('g')],
425 goto_bottom: vec![KeyCode::Char('G')],
426 }
427 }
428}
429
430impl TreeKeyBindings {
431 pub fn new() -> Self {
433 Self::default()
434 }
435
436 pub fn with_next(mut self, keys: Vec<KeyCode>) -> Self {
438 self.next = keys;
439 self
440 }
441
442 pub fn with_previous(mut self, keys: Vec<KeyCode>) -> Self {
444 self.previous = keys;
445 self
446 }
447
448 pub fn with_expand(mut self, keys: Vec<KeyCode>) -> Self {
450 self.expand = keys;
451 self
452 }
453
454 pub fn with_collapse(mut self, keys: Vec<KeyCode>) -> Self {
456 self.collapse = keys;
457 self
458 }
459
460 pub fn with_toggle(mut self, keys: Vec<KeyCode>) -> Self {
462 self.toggle = keys;
463 self
464 }
465
466 pub fn with_goto_top(mut self, keys: Vec<KeyCode>) -> Self {
468 self.goto_top = keys;
469 self
470 }
471
472 pub fn with_goto_bottom(mut self, keys: Vec<KeyCode>) -> Self {
474 self.goto_bottom = keys;
475 self
476 }
477}
478
479#[derive(Clone)]
481pub struct TreeNavigator {
482 pub keybindings: TreeKeyBindings,
483}
484
485impl Default for TreeNavigator {
486 fn default() -> Self {
487 Self::new()
488 }
489}
490
491impl TreeNavigator {
492 pub fn new() -> Self {
494 Self {
495 keybindings: TreeKeyBindings::default(),
496 }
497 }
498
499 pub fn with_keybindings(keybindings: TreeKeyBindings) -> Self {
501 Self { keybindings }
502 }
503
504 pub fn get_hotkey_items(&self) -> Vec<(String, &'static str)> {
507 let mut items = Vec::new();
508
509 let format_keys = |keys: &[KeyCode]| -> String {
511 keys.iter()
512 .map(|k| match k {
513 KeyCode::Char(c) => c.to_string(),
514 KeyCode::Up => "↑".to_string(),
515 KeyCode::Down => "↓".to_string(),
516 KeyCode::Left => "←".to_string(),
517 KeyCode::Right => "→".to_string(),
518 KeyCode::Enter => "Enter".to_string(),
519 _ => format!("{:?}", k),
520 })
521 .collect::<Vec<_>>()
522 .join("/")
523 };
524
525 items.push((format_keys(&self.keybindings.next), "Next"));
526 items.push((format_keys(&self.keybindings.previous), "Previous"));
527 items.push((format_keys(&self.keybindings.expand), "Expand"));
528 items.push((format_keys(&self.keybindings.collapse), "Collapse"));
529 items.push((format_keys(&self.keybindings.toggle), "Toggle"));
530 items.push((format_keys(&self.keybindings.goto_top), "Top"));
531 items.push((format_keys(&self.keybindings.goto_bottom), "Bottom"));
532
533 items
534 }
535
536 pub fn handle_key<T>(
539 &self,
540 key: KeyEvent,
541 nodes: &[TreeNode<T>],
542 state: &mut TreeViewState,
543 ) -> bool {
544 if key.kind != crossterm::event::KeyEventKind::Press {
546 return false;
547 }
548
549 let code = key.code;
550
551 if self.keybindings.next.contains(&code) {
552 self.select_next(nodes, state);
553 true
554 } else if self.keybindings.previous.contains(&code) {
555 self.select_previous(nodes, state);
556 true
557 } else if self.keybindings.expand.contains(&code) {
558 self.expand_selected(nodes, state);
559 true
560 } else if self.keybindings.collapse.contains(&code) {
561 self.collapse_selected(nodes, state);
562 true
563 } else if self.keybindings.toggle.contains(&code) {
564 self.toggle_selected(nodes, state);
565 true
566 } else if self.keybindings.goto_top.contains(&code) {
567 self.goto_top(nodes, state);
568 true
569 } else if self.keybindings.goto_bottom.contains(&code) {
570 self.goto_bottom(nodes, state);
571 true
572 } else {
573 false
574 }
575 }
576
577 pub fn select_next<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
579 let visible_paths = get_visible_paths(nodes, state);
580 if visible_paths.is_empty() {
581 return;
582 }
583
584 if let Some(current_path) = &state.selected_path {
585 if let Some(current_idx) = visible_paths.iter().position(|p| p == current_path) {
586 if current_idx < visible_paths.len() - 1 {
587 state.select(visible_paths[current_idx + 1].clone());
588 }
589 }
590 } else {
591 state.select(visible_paths[0].clone());
593 }
594 }
595
596 pub fn select_previous<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
598 let visible_paths = get_visible_paths(nodes, state);
599 if visible_paths.is_empty() {
600 return;
601 }
602
603 if let Some(current_path) = &state.selected_path {
604 if let Some(current_idx) = visible_paths.iter().position(|p| p == current_path) {
605 if current_idx > 0 {
606 state.select(visible_paths[current_idx - 1].clone());
607 }
608 }
609 } else {
610 state.select(visible_paths[0].clone());
612 }
613 }
614
615 pub fn goto_top<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
617 let visible_paths = get_visible_paths(nodes, state);
618 if !visible_paths.is_empty() {
619 state.select(visible_paths[0].clone());
620 }
621 }
622
623 pub fn goto_bottom<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
625 let visible_paths = get_visible_paths(nodes, state);
626 if !visible_paths.is_empty() {
627 state.select(visible_paths[visible_paths.len() - 1].clone());
628 }
629 }
630
631 pub fn expand_selected<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
633 if let Some(path) = state.selected_path.clone() {
634 if let Some(node) = self.get_node_at_path(nodes, &path) {
636 if !node.children.is_empty() {
637 state.expand(path);
638 }
639 }
640 }
641 }
642
643 pub fn collapse_selected<T>(&self, _nodes: &[TreeNode<T>], state: &mut TreeViewState) {
645 if let Some(path) = state.selected_path.clone() {
646 if state.is_expanded(&path) {
647 state.collapse(path);
649 } else if path.len() > 1 {
650 let parent = path[..path.len() - 1].to_vec();
652 state.select(parent);
653 }
654 }
655 }
656
657 pub fn toggle_selected<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
659 if let Some(path) = state.selected_path.clone() {
660 if let Some(node) = self.get_node_at_path(nodes, &path) {
662 if !node.children.is_empty() {
663 state.toggle_expansion(path);
664 }
665 }
666 }
667 }
668
669 fn get_node_at_path<'a, T>(
671 &self,
672 nodes: &'a [TreeNode<T>],
673 path: &[usize],
674 ) -> Option<&'a TreeNode<T>> {
675 if path.is_empty() {
676 return None;
677 }
678
679 let mut current_nodes = nodes;
680 let mut node = None;
681
682 for &idx in path {
683 node = current_nodes.get(idx);
684 if let Some(n) = node {
685 current_nodes = &n.children;
686 } else {
687 return None;
688 }
689 }
690
691 node
692 }
693}