Skip to main content

ratatui_toolkit/tree_view/
mod.rs

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/// A node in the tree
16#[derive(Debug, Clone)]
17pub struct TreeNode<T> {
18    /// Node data
19    pub data: T,
20    /// Child nodes
21    pub children: Vec<TreeNode<T>>,
22    /// Whether this node can be expanded (has children)
23    pub expandable: bool,
24}
25
26impl<T> TreeNode<T> {
27    /// Create a new tree node
28    pub fn new(data: T) -> Self {
29        Self {
30            data,
31            children: Vec::new(),
32            expandable: false,
33        }
34    }
35
36    /// Create a new tree node with children
37    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/// State information for rendering a node
48#[derive(Debug, Clone)]
49pub struct NodeState {
50    /// Whether this node is selected
51    pub is_selected: bool,
52    /// Whether this node is expanded
53    pub is_expanded: bool,
54    /// Depth level in the tree (0 = root)
55    pub level: usize,
56    /// Whether this node has children
57    pub has_children: bool,
58    /// Path to this node (indices from root)
59    pub path: Vec<usize>,
60}
61
62/// Type alias for node render function to reduce complexity
63pub type NodeRenderFn<'a, T> = Box<dyn Fn(&T, &NodeState) -> Line<'a> + 'a>;
64
65/// Tree view state (for StatefulWidget pattern)
66#[derive(Debug, Clone, Default)]
67pub struct TreeViewState {
68    /// Currently selected node path (indices from root)
69    pub selected_path: Option<Vec<usize>>,
70    /// Set of expanded node paths
71    pub expanded: HashSet<Vec<usize>>,
72    /// Vertical scroll offset
73    pub offset: usize,
74}
75
76impl TreeViewState {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Set the selected node path
82    pub fn select(&mut self, path: Vec<usize>) {
83        self.selected_path = Some(path);
84    }
85
86    /// Clear selection
87    pub fn clear_selection(&mut self) {
88        self.selected_path = None;
89    }
90
91    /// Toggle expansion of a node at the given path
92    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    /// Check if a node is expanded
101    pub fn is_expanded(&self, path: &[usize]) -> bool {
102        self.expanded.contains(path)
103    }
104
105    /// Expand a node
106    pub fn expand(&mut self, path: Vec<usize>) {
107        self.expanded.insert(path);
108    }
109
110    /// Collapse a node
111    pub fn collapse(&mut self, path: Vec<usize>) {
112        self.expanded.remove(&path);
113    }
114
115    /// Expand all nodes
116    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    /// Collapse all nodes
140    pub fn collapse_all(&mut self) {
141        self.expanded.clear();
142    }
143}
144
145/// Tree view widget
146pub struct TreeView<'a, T> {
147    /// Root nodes of the tree
148    nodes: Vec<TreeNode<T>>,
149    /// Block to wrap the tree
150    block: Option<Block<'a>>,
151    /// Render callback for custom node display
152    render_fn: NodeRenderFn<'a, T>,
153    /// Default expand icon
154    expand_icon: &'a str,
155    /// Default collapse icon
156    collapse_icon: &'a str,
157    /// Style for selected row background (full-width highlight)
158    highlight_style: Option<Style>,
159}
160
161impl<'a, T> TreeView<'a, T> {
162    /// Create a new tree view with nodes
163    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    /// Set the block to wrap the tree
175    pub fn block(mut self, block: Block<'a>) -> Self {
176        self.block = Some(block);
177        self
178    }
179
180    /// Set custom expand/collapse icons
181    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    /// Set the render function for nodes
188    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    /// Set the highlight style for selected rows (full-width background)
197    pub fn highlight_style(mut self, style: Style) -> Self {
198        self.highlight_style = Some(style);
199        self
200    }
201
202    /// Flatten the tree into a list of visible items
203    fn flatten_tree(&self, state: &TreeViewState) -> Vec<(Line<'a>, Vec<usize>)> {
204        let mut items = Vec::new();
205
206        /// Context for tree traversal to reduce function parameters
207        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                // Render the node with indent and expand/collapse icon
237                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                // Get the custom rendered line
249                let custom_line = (ctx.render_fn)(&node.data, &node_state);
250
251                // Prepend indent and expansion icon
252                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                // Recursively render children if expanded
264                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    /// Get the node at a specific row (considering scroll offset)
283    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    /// Get total visible item count
289    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        // Adjust scroll offset to ensure selected item is visible
315        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        // Render visible items
326        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            // Fill background for selected row (full-width highlight like Yazi)
335            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
348// Also implement Widget for &TreeView with immutable state
349impl<'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        // Render visible items
370        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
382/// Get all visible paths (flattened tree with expansion state)
383pub 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 expanded, recurse into children
398            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/// Tree navigator with configurable keybindings
409#[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    /// Create a new tree navigator with default keybindings
422    pub fn new() -> Self {
423        Self {
424            keybindings: TreeKeyBindings::default(),
425        }
426    }
427
428    /// Create a tree navigator with custom keybindings
429    pub fn with_keybindings(keybindings: TreeKeyBindings) -> Self {
430        Self { keybindings }
431    }
432
433    /// Get hotkey items for display in HotkeyFooter
434    /// Returns a vec of (key_display, description) pairs
435    pub fn get_hotkey_items(&self) -> Vec<(String, &'static str)> {
436        let mut items = Vec::new();
437
438        // Helper to format multiple keys
439        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    /// Handle a key event and update tree state
466    /// Returns true if the key was handled
467    pub fn handle_key<T>(
468        &self,
469        key: KeyEvent,
470        nodes: &[TreeNode<T>],
471        state: &mut TreeViewState,
472    ) -> bool {
473        // Only handle key press events, not release
474        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    /// Select next visible item
507    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            // Select first item
521            state.select(visible_paths[0].clone());
522        }
523    }
524
525    /// Select previous visible item
526    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            // Select first item
540            state.select(visible_paths[0].clone());
541        }
542    }
543
544    /// Go to first visible item
545    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    /// Go to last visible item
553    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    /// Expand selected node
561    pub fn expand_selected<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
562        if let Some(path) = state.selected_path.clone() {
563            // Check if node has children
564            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    /// Collapse selected node or move to parent
573    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                // Collapse current
577                state.collapse(path);
578            } else if path.len() > 1 {
579                // Move to parent
580                let parent = path[..path.len() - 1].to_vec();
581                state.select(parent);
582            }
583        }
584    }
585
586    /// Toggle expansion of selected node (expand if collapsed, collapse if expanded)
587    pub fn toggle_selected<T>(&self, nodes: &[TreeNode<T>], state: &mut TreeViewState) {
588        if let Some(path) = state.selected_path.clone() {
589            // Check if node has children
590            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    /// Helper to get node at a specific path
599    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}