1mod keybindings;
2
3pub use keybindings::TreeKeyBindings;
4
5use crossterm::event::{KeyCode, KeyEvent};
6use ratatui::{
7 buffer::Buffer,
8 layout::Rect,
9 style::{Color, Style},
10 text::{Line, Span},
11 widgets::{Block, StatefulWidget, Widget},
12};
13use std::collections::HashSet;
14
15#[derive(Debug, Clone)]
17pub struct TreeNode<T> {
18 pub data: T,
20 pub children: Vec<TreeNode<T>>,
22 pub expandable: bool,
24}
25
26impl<T> TreeNode<T> {
27 pub fn new(data: T) -> Self {
29 Self {
30 data,
31 children: Vec::new(),
32 expandable: false,
33 }
34 }
35
36 pub fn with_children(data: T, children: Vec<TreeNode<T>>) -> Self {
38 let expandable = !children.is_empty();
39 Self {
40 data,
41 children,
42 expandable,
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct NodeState {
50 pub is_selected: bool,
52 pub is_expanded: bool,
54 pub level: usize,
56 pub has_children: bool,
58 pub path: Vec<usize>,
60}
61
62pub type NodeRenderFn<'a, T> = Box<dyn Fn(&T, &NodeState) -> Line<'a> + 'a>;
64
65#[derive(Debug, Clone, Default)]
67pub struct TreeViewState {
68 pub selected_path: Option<Vec<usize>>,
70 pub expanded: HashSet<Vec<usize>>,
72 pub offset: usize,
74}
75
76impl TreeViewState {
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn select(&mut self, path: Vec<usize>) {
83 self.selected_path = Some(path);
84 }
85
86 pub fn clear_selection(&mut self) {
88 self.selected_path = None;
89 }
90
91 pub fn toggle_expansion(&mut self, path: Vec<usize>) {
93 if self.expanded.contains(&path) {
94 self.expanded.remove(&path);
95 } else {
96 self.expanded.insert(path);
97 }
98 }
99
100 pub fn is_expanded(&self, path: &[usize]) -> bool {
102 self.expanded.contains(path)
103 }
104
105 pub fn expand(&mut self, path: Vec<usize>) {
107 self.expanded.insert(path);
108 }
109
110 pub fn collapse(&mut self, path: Vec<usize>) {
112 self.expanded.remove(&path);
113 }
114
115 pub fn expand_all<T>(&mut self, nodes: &[TreeNode<T>]) {
117 fn collect_paths<T>(
118 nodes: &[TreeNode<T>],
119 current_path: Vec<usize>,
120 expanded: &mut HashSet<Vec<usize>>,
121 ) {
122 for (idx, node) in nodes.iter().enumerate() {
123 let mut path = current_path.clone();
124 path.push(idx);
125
126 if node.expandable {
127 expanded.insert(path.clone());
128 }
129
130 if !node.children.is_empty() {
131 collect_paths(&node.children, path, expanded);
132 }
133 }
134 }
135
136 collect_paths(nodes, Vec::new(), &mut self.expanded);
137 }
138
139 pub fn collapse_all(&mut self) {
141 self.expanded.clear();
142 }
143}
144
145pub struct TreeView<'a, T> {
147 nodes: Vec<TreeNode<T>>,
149 block: Option<Block<'a>>,
151 render_fn: NodeRenderFn<'a, T>,
153 expand_icon: &'a str,
155 collapse_icon: &'a str,
157 highlight_style: Option<Style>,
159}
160
161impl<'a, T> TreeView<'a, T> {
162 pub fn new(nodes: Vec<TreeNode<T>>) -> Self {
164 Self {
165 nodes,
166 block: None,
167 render_fn: Box::new(|_data, _state| Line::from("Node")),
168 expand_icon: "▶",
169 collapse_icon: "▼",
170 highlight_style: None,
171 }
172 }
173
174 pub fn block(mut self, block: Block<'a>) -> Self {
176 self.block = Some(block);
177 self
178 }
179
180 pub fn icons(mut self, expand: &'a str, collapse: &'a str) -> Self {
182 self.expand_icon = expand;
183 self.collapse_icon = collapse;
184 self
185 }
186
187 pub fn render_fn<F>(mut self, f: F) -> Self
189 where
190 F: Fn(&T, &NodeState) -> Line<'a> + 'a,
191 {
192 self.render_fn = Box::new(f);
193 self
194 }
195
196 pub fn highlight_style(mut self, style: Style) -> Self {
198 self.highlight_style = Some(style);
199 self
200 }
201
202 fn flatten_tree(&self, state: &TreeViewState) -> Vec<(Line<'a>, Vec<usize>)> {
204 let mut items = Vec::new();
205
206 struct TraverseContext<'a, 'b, T> {
208 state: &'b TreeViewState,
209 render_fn: &'b dyn Fn(&T, &NodeState) -> Line<'a>,
210 expand_icon: &'b str,
211 collapse_icon: &'b str,
212 }
213
214 fn traverse<'a, T>(
215 nodes: &[TreeNode<T>],
216 current_path: Vec<usize>,
217 level: usize,
218 ctx: &TraverseContext<'a, '_, T>,
219 items: &mut Vec<(Line<'a>, Vec<usize>)>,
220 ) {
221 for (idx, node) in nodes.iter().enumerate() {
222 let mut path = current_path.clone();
223 path.push(idx);
224
225 let is_expanded = ctx.state.is_expanded(&path);
226 let is_selected = ctx.state.selected_path.as_ref() == Some(&path);
227
228 let node_state = NodeState {
229 is_selected,
230 is_expanded,
231 level,
232 has_children: !node.children.is_empty(),
233 path: path.clone(),
234 };
235
236 let indent = " ".repeat(level);
238 let expansion_icon = if node.expandable {
239 if is_expanded {
240 ctx.collapse_icon
241 } else {
242 ctx.expand_icon
243 }
244 } else {
245 " "
246 };
247
248 let custom_line = (ctx.render_fn)(&node.data, &node_state);
250
251 let mut spans = vec![
253 Span::raw(indent),
254 Span::styled(
255 format!("{} ", expansion_icon),
256 Style::default().fg(Color::DarkGray),
257 ),
258 ];
259 spans.extend(custom_line.spans);
260
261 items.push((Line::from(spans), path.clone()));
262
263 if is_expanded && !node.children.is_empty() {
265 traverse(&node.children, path, level + 1, ctx, items);
266 }
267 }
268 }
269
270 let ctx = TraverseContext {
271 state,
272 render_fn: &self.render_fn,
273 expand_icon: self.expand_icon,
274 collapse_icon: self.collapse_icon,
275 };
276
277 traverse(&self.nodes, Vec::new(), 0, &ctx, &mut items);
278
279 items
280 }
281
282 pub fn node_at_row(&self, state: &TreeViewState, row: usize) -> Option<Vec<usize>> {
284 let items = self.flatten_tree(state);
285 items.get(row + state.offset).map(|(_, path)| path.clone())
286 }
287
288 pub fn visible_item_count(&self, state: &TreeViewState) -> usize {
290 self.flatten_tree(state).len()
291 }
292}
293
294impl<'a, T> StatefulWidget for TreeView<'a, T> {
295 type State = TreeViewState;
296
297 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
298 let area = match self.block {
299 Some(ref b) => {
300 let inner = b.inner(area);
301 b.clone().render(area, buf);
302 inner
303 }
304 None => area,
305 };
306
307 if area.height == 0 {
308 return;
309 }
310
311 let items = self.flatten_tree(state);
312 let visible_height = area.height as usize;
313
314 if let Some(ref selected) = state.selected_path {
316 if let Some(selected_idx) = items.iter().position(|(_, path)| path == selected) {
317 if selected_idx < state.offset {
318 state.offset = selected_idx;
319 } else if selected_idx >= state.offset + visible_height {
320 state.offset = selected_idx.saturating_sub(visible_height - 1);
321 }
322 }
323 }
324
325 for (i, (line, path)) in items
327 .iter()
328 .skip(state.offset)
329 .take(visible_height)
330 .enumerate()
331 {
332 let y = area.y + i as u16;
333
334 let is_selected = state.selected_path.as_ref() == Some(path);
336 if is_selected && self.highlight_style.is_some() {
337 let style = self.highlight_style.unwrap();
338 for x in area.x..(area.x + area.width) {
339 buf[(x, y)].set_style(style);
340 }
341 }
342
343 buf.set_line(area.x, y, line, area.width);
344 }
345 }
346}
347
348impl<'a, T> Widget for &TreeView<'a, T> {
350 fn render(self, area: Rect, buf: &mut Buffer) {
351 let state = TreeViewState::default();
352
353 let area = match &self.block {
354 Some(ref b) => {
355 let inner = b.inner(area);
356 b.clone().render(area, buf);
357 inner
358 }
359 None => area,
360 };
361
362 if area.height == 0 {
363 return;
364 }
365
366 let items = self.flatten_tree(&state);
367 let visible_height = area.height as usize;
368
369 for (i, (line, _)) in items
371 .iter()
372 .skip(state.offset)
373 .take(visible_height)
374 .enumerate()
375 {
376 let y = area.y + i as u16;
377 buf.set_line(area.x, y, line, area.width);
378 }
379 }
380}
381
382pub fn get_visible_paths<T>(nodes: &[TreeNode<T>], state: &TreeViewState) -> Vec<Vec<usize>> {
384 let mut paths = Vec::new();
385
386 fn traverse<T>(
387 nodes: &[TreeNode<T>],
388 current_path: Vec<usize>,
389 state: &TreeViewState,
390 paths: &mut Vec<Vec<usize>>,
391 ) {
392 for (idx, node) in nodes.iter().enumerate() {
393 let mut path = current_path.clone();
394 path.push(idx);
395 paths.push(path.clone());
396
397 if state.is_expanded(&path) && !node.children.is_empty() {
399 traverse(&node.children, path, state, paths);
400 }
401 }
402 }
403
404 traverse(nodes, Vec::new(), state, &mut paths);
405 paths
406}
407
408#[derive(Clone)]
410pub struct TreeNavigator {
411 pub keybindings: TreeKeyBindings,
412}
413
414impl Default for TreeNavigator {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420impl TreeNavigator {
421 pub fn new() -> Self {
423 Self {
424 keybindings: TreeKeyBindings::default(),
425 }
426 }
427
428 pub fn with_keybindings(keybindings: TreeKeyBindings) -> Self {
430 Self { keybindings }
431 }
432
433 pub fn get_hotkey_items(&self) -> Vec<(String, &'static str)> {
436 let mut items = Vec::new();
437
438 let format_keys = |keys: &[KeyCode]| -> String {
440 keys.iter()
441 .map(|k| match k {
442 KeyCode::Char(c) => c.to_string(),
443 KeyCode::Up => "↑".to_string(),
444 KeyCode::Down => "↓".to_string(),
445 KeyCode::Left => "←".to_string(),
446 KeyCode::Right => "→".to_string(),
447 KeyCode::Enter => "Enter".to_string(),
448 _ => format!("{:?}", k),
449 })
450 .collect::<Vec<_>>()
451 .join("/")
452 };
453
454 items.push((format_keys(&self.keybindings.next), "Next"));
455 items.push((format_keys(&self.keybindings.previous), "Previous"));
456 items.push((format_keys(&self.keybindings.expand), "Expand"));
457 items.push((format_keys(&self.keybindings.collapse), "Collapse"));
458 items.push((format_keys(&self.keybindings.toggle), "Toggle"));
459 items.push((format_keys(&self.keybindings.goto_top), "Top"));
460 items.push((format_keys(&self.keybindings.goto_bottom), "Bottom"));
461
462 items
463 }
464
465 pub fn handle_key<T>(
468 &self,
469 key: KeyEvent,
470 nodes: &[TreeNode<T>],
471 state: &mut TreeViewState,
472 ) -> bool {
473 if key.kind != crossterm::event::KeyEventKind::Press {
475 return false;
476 }
477
478 let code = key.code;
479
480 if self.keybindings.next.contains(&code) {
481 self.select_next(nodes, state);
482 true
483 } else if self.keybindings.previous.contains(&code) {
484 self.select_previous(nodes, state);
485 true
486 } else if self.keybindings.expand.contains(&code) {
487 self.expand_selected(nodes, state);
488 true
489 } else if self.keybindings.collapse.contains(&code) {
490 self.collapse_selected(nodes, state);
491 true
492 } else if self.keybindings.toggle.contains(&code) {
493 self.toggle_selected(nodes, state);
494 true
495 } else if self.keybindings.goto_top.contains(&code) {
496 self.goto_top(nodes, state);
497 true
498 } else if self.keybindings.goto_bottom.contains(&code) {
499 self.goto_bottom(nodes, state);
500 true
501 } else {
502 false
503 }
504 }
505
506 pub fn select_next<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
508 let visible_paths = get_visible_paths(nodes, state);
509 if visible_paths.is_empty() {
510 return;
511 }
512
513 if let Some(current_path) = &state.selected_path {
514 if let Some(current_idx) = visible_paths.iter().position(|p| p == current_path) {
515 if current_idx < visible_paths.len() - 1 {
516 state.select(visible_paths[current_idx + 1].clone());
517 }
518 }
519 } else {
520 state.select(visible_paths[0].clone());
522 }
523 }
524
525 pub fn select_previous<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
527 let visible_paths = get_visible_paths(nodes, state);
528 if visible_paths.is_empty() {
529 return;
530 }
531
532 if let Some(current_path) = &state.selected_path {
533 if let Some(current_idx) = visible_paths.iter().position(|p| p == current_path) {
534 if current_idx > 0 {
535 state.select(visible_paths[current_idx - 1].clone());
536 }
537 }
538 } else {
539 state.select(visible_paths[0].clone());
541 }
542 }
543
544 pub fn goto_top<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
546 let visible_paths = get_visible_paths(nodes, state);
547 if !visible_paths.is_empty() {
548 state.select(visible_paths[0].clone());
549 }
550 }
551
552 pub fn goto_bottom<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
554 let visible_paths = get_visible_paths(nodes, state);
555 if !visible_paths.is_empty() {
556 state.select(visible_paths[visible_paths.len() - 1].clone());
557 }
558 }
559
560 pub fn expand_selected<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
562 if let Some(path) = state.selected_path.clone() {
563 if let Some(node) = self.get_node_at_path(nodes, &path) {
565 if !node.children.is_empty() {
566 state.expand(path);
567 }
568 }
569 }
570 }
571
572 pub fn collapse_selected<T>(&self, _nodes: &[TreeNode<T>], state: &mut TreeViewState) {
574 if let Some(path) = state.selected_path.clone() {
575 if state.is_expanded(&path) {
576 state.collapse(path);
578 } else if path.len() > 1 {
579 let parent = path[..path.len() - 1].to_vec();
581 state.select(parent);
582 }
583 }
584 }
585
586 pub fn toggle_selected<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
588 if let Some(path) = state.selected_path.clone() {
589 if let Some(node) = self.get_node_at_path(nodes, &path) {
591 if !node.children.is_empty() {
592 state.toggle_expansion(path);
593 }
594 }
595 }
596 }
597
598 fn get_node_at_path<'a, T>(
600 &self,
601 nodes: &'a [TreeNode<T>],
602 path: &[usize],
603 ) -> Option<&'a TreeNode<T>> {
604 if path.is_empty() {
605 return None;
606 }
607
608 let mut current_nodes = nodes;
609 let mut node = None;
610
611 for &idx in path {
612 node = current_nodes.get(idx);
613 if let Some(n) = node {
614 current_nodes = &n.children;
615 } else {
616 return None;
617 }
618 }
619
620 node
621 }
622}