Skip to main content

ratatui_toolkit/
tree_view.rs

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/// A node in the tree
12#[derive(Debug, Clone)]
13pub struct TreeNode<T> {
14    /// Node data
15    pub data: T,
16    /// Child nodes
17    pub children: Vec<TreeNode<T>>,
18    /// Whether this node can be expanded (has children)
19    pub expandable: bool,
20}
21
22impl<T> TreeNode<T> {
23    /// Create a new tree node
24    pub fn new(data: T) -> Self {
25        Self {
26            data,
27            children: Vec::new(),
28            expandable: false,
29        }
30    }
31
32    /// Create a new tree node with children
33    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/// State information for rendering a node
44#[derive(Debug, Clone)]
45pub struct NodeState {
46    /// Whether this node is selected
47    pub is_selected: bool,
48    /// Whether this node is expanded
49    pub is_expanded: bool,
50    /// Depth level in the tree (0 = root)
51    pub level: usize,
52    /// Whether this node has children
53    pub has_children: bool,
54    /// Path to this node (indices from root)
55    pub path: Vec<usize>,
56}
57
58/// Type alias for node render function to reduce complexity
59pub type NodeRenderFn<'a, T> = Box<dyn Fn(&T, &NodeState) -> Line<'a> + 'a>;
60
61/// Tree view state (for StatefulWidget pattern)
62#[derive(Debug, Clone, Default)]
63pub struct TreeViewState {
64    /// Currently selected node path (indices from root)
65    pub selected_path: Option<Vec<usize>>,
66    /// Set of expanded node paths
67    pub expanded: HashSet<Vec<usize>>,
68    /// Vertical scroll offset
69    pub offset: usize,
70}
71
72impl TreeViewState {
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Set the selected node path
78    pub fn select(&mut self, path: Vec<usize>) {
79        self.selected_path = Some(path);
80    }
81
82    /// Clear selection
83    pub fn clear_selection(&mut self) {
84        self.selected_path = None;
85    }
86
87    /// Toggle expansion of a node at the given path
88    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    /// Check if a node is expanded
97    pub fn is_expanded(&self, path: &[usize]) -> bool {
98        self.expanded.contains(path)
99    }
100
101    /// Expand a node
102    pub fn expand(&mut self, path: Vec<usize>) {
103        self.expanded.insert(path);
104    }
105
106    /// Collapse a node
107    pub fn collapse(&mut self, path: Vec<usize>) {
108        self.expanded.remove(&path);
109    }
110
111    /// Expand all nodes
112    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    /// Collapse all nodes
136    pub fn collapse_all(&mut self) {
137        self.expanded.clear();
138    }
139}
140
141/// Tree view widget
142pub struct TreeView<'a, T> {
143    /// Root nodes of the tree
144    nodes: Vec<TreeNode<T>>,
145    /// Block to wrap the tree
146    block: Option<Block<'a>>,
147    /// Render callback for custom node display
148    render_fn: NodeRenderFn<'a, T>,
149    /// Default expand icon
150    expand_icon: &'a str,
151    /// Default collapse icon
152    collapse_icon: &'a str,
153    /// Style for selected row background (full-width highlight)
154    highlight_style: Option<Style>,
155}
156
157impl<'a, T> TreeView<'a, T> {
158    /// Create a new tree view with nodes
159    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    /// Set the block to wrap the tree
171    pub fn block(mut self, block: Block<'a>) -> Self {
172        self.block = Some(block);
173        self
174    }
175
176    /// Set custom expand/collapse icons
177    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    /// Set the render function for nodes
184    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    /// Set the highlight style for selected rows (full-width background)
193    pub fn highlight_style(mut self, style: Style) -> Self {
194        self.highlight_style = Some(style);
195        self
196    }
197
198    /// Flatten the tree into a list of visible items
199    fn flatten_tree(&self, state: &TreeViewState) -> Vec<(Line<'a>, Vec<usize>)> {
200        let mut items = Vec::new();
201
202        /// Context for tree traversal to reduce function parameters
203        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                // Render the node with indent and expand/collapse icon
233                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                // Get the custom rendered line
245                let custom_line = (ctx.render_fn)(&node.data, &node_state);
246
247                // Prepend indent and expansion icon
248                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                // Recursively render children if expanded
260                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    /// Get the node at a specific row (considering scroll offset)
279    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    /// Get total visible item count
285    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        // Adjust scroll offset to ensure selected item is visible
311        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        // Render visible items
322        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            // Fill background for selected row (full-width highlight like Yazi)
331            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
344// Also implement Widget for &TreeView with immutable state
345impl<'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        // Render visible items
366        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
378/// Get all visible paths (flattened tree with expansion state)
379pub 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 expanded, recurse into children
394            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/// Configurable keybindings for tree navigation
405#[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    /// Create new keybindings with defaults
432    pub fn new() -> Self {
433        Self::default()
434    }
435
436    /// Set custom keybindings for next item
437    pub fn with_next(mut self, keys: Vec<KeyCode>) -> Self {
438        self.next = keys;
439        self
440    }
441
442    /// Set custom keybindings for previous item
443    pub fn with_previous(mut self, keys: Vec<KeyCode>) -> Self {
444        self.previous = keys;
445        self
446    }
447
448    /// Set custom keybindings for expand
449    pub fn with_expand(mut self, keys: Vec<KeyCode>) -> Self {
450        self.expand = keys;
451        self
452    }
453
454    /// Set custom keybindings for collapse
455    pub fn with_collapse(mut self, keys: Vec<KeyCode>) -> Self {
456        self.collapse = keys;
457        self
458    }
459
460    /// Set custom keybindings for toggle
461    pub fn with_toggle(mut self, keys: Vec<KeyCode>) -> Self {
462        self.toggle = keys;
463        self
464    }
465
466    /// Set custom keybindings for goto top
467    pub fn with_goto_top(mut self, keys: Vec<KeyCode>) -> Self {
468        self.goto_top = keys;
469        self
470    }
471
472    /// Set custom keybindings for goto bottom
473    pub fn with_goto_bottom(mut self, keys: Vec<KeyCode>) -> Self {
474        self.goto_bottom = keys;
475        self
476    }
477}
478
479/// Tree navigator with configurable keybindings
480#[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    /// Create a new tree navigator with default keybindings
493    pub fn new() -> Self {
494        Self {
495            keybindings: TreeKeyBindings::default(),
496        }
497    }
498
499    /// Create a tree navigator with custom keybindings
500    pub fn with_keybindings(keybindings: TreeKeyBindings) -> Self {
501        Self { keybindings }
502    }
503
504    /// Get hotkey items for display in HotkeyFooter
505    /// Returns a vec of (key_display, description) pairs
506    pub fn get_hotkey_items(&self) -> Vec<(String, &'static str)> {
507        let mut items = Vec::new();
508
509        // Helper to format multiple keys
510        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    /// Handle a key event and update tree state
537    /// Returns true if the key was handled
538    pub fn handle_key<T>(
539        &self,
540        key: KeyEvent,
541        nodes: &[TreeNode<T>],
542        state: &mut TreeViewState,
543    ) -> bool {
544        // Only handle key press events, not release
545        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    /// Select next visible item
578    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            // Select first item
592            state.select(visible_paths[0].clone());
593        }
594    }
595
596    /// Select previous visible item
597    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            // Select first item
611            state.select(visible_paths[0].clone());
612        }
613    }
614
615    /// Go to first visible item
616    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    /// Go to last visible item
624    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    /// Expand selected node
632    pub fn expand_selected<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
633        if let Some(path) = state.selected_path.clone() {
634            // Check if node has children
635            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    /// Collapse selected node or move to parent
644    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                // Collapse current
648                state.collapse(path);
649            } else if path.len() > 1 {
650                // Move to parent
651                let parent = path[..path.len() - 1].to_vec();
652                state.select(parent);
653            }
654        }
655    }
656
657    /// Toggle expansion of selected node (expand if collapsed, collapse if expanded)
658    pub fn toggle_selected<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
659        if let Some(path) = state.selected_path.clone() {
660            // Check if node has children
661            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    /// Helper to get node at a specific path
670    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}