Skip to main content

saorsa_core/widget/
tree.rs

1//! Hierarchical tree widget with expandable/collapsible nodes.
2//!
3//! Displays a tree of items as a flat list with indentation.
4//! Supports keyboard navigation, expand/collapse, and lazy loading.
5
6use crate::buffer::ScreenBuffer;
7use crate::cell::Cell;
8use crate::event::{Event, KeyCode, KeyEvent};
9use crate::geometry::Rect;
10use crate::segment::Segment;
11use crate::style::Style;
12use crate::text::truncate_to_display_width;
13use unicode_width::UnicodeWidthStr;
14
15use super::{BorderStyle, EventResult, InteractiveWidget, Widget};
16
17/// A node in the tree hierarchy.
18#[derive(Clone, Debug)]
19pub struct TreeNode<T> {
20    /// The data for this node.
21    pub data: T,
22    /// Children of this node.
23    pub children: Vec<TreeNode<T>>,
24    /// Whether this node is expanded (children visible).
25    pub expanded: bool,
26    /// Whether this node is a leaf (cannot have children).
27    pub is_leaf: bool,
28}
29
30impl<T> TreeNode<T> {
31    /// Create a new tree node with the given data.
32    pub fn new(data: T) -> Self {
33        Self {
34            data,
35            children: Vec::new(),
36            expanded: false,
37            is_leaf: true,
38        }
39    }
40
41    /// Create a new branch node (not a leaf).
42    pub fn branch(data: T) -> Self {
43        Self {
44            data,
45            children: Vec::new(),
46            expanded: false,
47            is_leaf: false,
48        }
49    }
50
51    /// Add a child node.
52    pub fn with_child(mut self, child: TreeNode<T>) -> Self {
53        self.is_leaf = false;
54        self.children.push(child);
55        self
56    }
57
58    /// Add multiple children.
59    pub fn with_children(mut self, children: Vec<TreeNode<T>>) -> Self {
60        if !children.is_empty() {
61            self.is_leaf = false;
62        }
63        self.children = children;
64        self
65    }
66}
67
68/// A flattened visible node entry.
69struct VisibleNode {
70    /// Depth in the tree (0 = root).
71    depth: usize,
72    /// Path of indices to reach this node from roots.
73    path: Vec<usize>,
74    /// Whether expanded.
75    expanded: bool,
76    /// Whether leaf.
77    is_leaf: bool,
78}
79
80/// Type alias for the node render function.
81type NodeRenderFn<T> = Box<dyn Fn(&T, usize, bool, bool) -> Vec<Segment>>;
82
83/// Type alias for the lazy load function.
84type LazyLoadFn<T> = Option<Box<dyn Fn(&T) -> Vec<TreeNode<T>>>>;
85
86/// A hierarchical tree widget with expandable/collapsible nodes.
87///
88/// Displays a forest of [`TreeNode`]s as an indented list. Supports
89/// keyboard navigation, expand/collapse, and optional lazy loading.
90pub struct Tree<T> {
91    /// Root nodes (a forest).
92    roots: Vec<TreeNode<T>>,
93    /// Selected visible node index.
94    selected: usize,
95    /// Scroll offset (first visible line).
96    scroll_offset: usize,
97    /// Function to render a node as Segments.
98    render_fn: NodeRenderFn<T>,
99    /// Style for unselected nodes.
100    node_style: Style,
101    /// Style for the selected node.
102    selected_style: Style,
103    /// Border style.
104    border: BorderStyle,
105    /// Lazy load callback.
106    lazy_load_fn: LazyLoadFn<T>,
107}
108
109impl<T> Tree<T> {
110    /// Create a new tree with the given root nodes.
111    pub fn new(roots: Vec<TreeNode<T>>) -> Self {
112        Self {
113            roots,
114            selected: 0,
115            scroll_offset: 0,
116            render_fn: Box::new(|_, _, _, _| vec![Segment::new("???")]),
117            node_style: Style::default(),
118            selected_style: Style::default().reverse(true),
119            border: BorderStyle::None,
120            lazy_load_fn: None,
121        }
122    }
123
124    /// Set a custom render function for nodes.
125    ///
126    /// Parameters: (data, depth, expanded, is_leaf) -> Segments
127    #[must_use]
128    pub fn with_render_fn<F>(mut self, f: F) -> Self
129    where
130        F: Fn(&T, usize, bool, bool) -> Vec<Segment> + 'static,
131    {
132        self.render_fn = Box::new(f);
133        self
134    }
135
136    /// Set the style for unselected nodes.
137    #[must_use]
138    pub fn with_node_style(mut self, style: Style) -> Self {
139        self.node_style = style;
140        self
141    }
142
143    /// Set the style for the selected node.
144    #[must_use]
145    pub fn with_selected_style(mut self, style: Style) -> Self {
146        self.selected_style = style;
147        self
148    }
149
150    /// Set the border style.
151    #[must_use]
152    pub fn with_border(mut self, border: BorderStyle) -> Self {
153        self.border = border;
154        self
155    }
156
157    /// Set a lazy load function for deferred child loading.
158    #[must_use]
159    pub fn with_lazy_load<F>(mut self, f: F) -> Self
160    where
161        F: Fn(&T) -> Vec<TreeNode<T>> + 'static,
162    {
163        self.lazy_load_fn = Some(Box::new(f));
164        self
165    }
166
167    /// Get a reference to the root nodes.
168    pub fn roots(&self) -> &[TreeNode<T>] {
169        &self.roots
170    }
171
172    /// Get the selected visible node index.
173    pub fn selected(&self) -> usize {
174        self.selected
175    }
176
177    /// Get the scroll offset.
178    pub fn scroll_offset(&self) -> usize {
179        self.scroll_offset
180    }
181
182    /// Build the list of visible nodes by pre-order traversal.
183    fn build_visible(&self) -> Vec<VisibleNode> {
184        let mut result = Vec::new();
185        for (idx, root) in self.roots.iter().enumerate() {
186            self.collect_visible(root, 0, vec![idx], &mut result);
187        }
188        result
189    }
190
191    /// Recursively collect visible nodes.
192    fn collect_visible(
193        &self,
194        node: &TreeNode<T>,
195        depth: usize,
196        path: Vec<usize>,
197        result: &mut Vec<VisibleNode>,
198    ) {
199        result.push(VisibleNode {
200            depth,
201            path: path.clone(),
202            expanded: node.expanded,
203            is_leaf: node.is_leaf,
204        });
205
206        if node.expanded {
207            for (child_idx, child) in node.children.iter().enumerate() {
208                let mut child_path = path.clone();
209                child_path.push(child_idx);
210                self.collect_visible(child, depth + 1, child_path, result);
211            }
212        }
213    }
214
215    /// Get a mutable reference to a node by path.
216    fn node_at_path_mut(&mut self, path: &[usize]) -> Option<&mut TreeNode<T>> {
217        if path.is_empty() {
218            return None;
219        }
220        let mut current = self.roots.get_mut(path[0])?;
221        for &idx in &path[1..] {
222            current = current.children.get_mut(idx)?;
223        }
224        Some(current)
225    }
226
227    /// Get an immutable reference to a node by path.
228    fn node_at_path(&self, path: &[usize]) -> Option<&TreeNode<T>> {
229        if path.is_empty() {
230            return None;
231        }
232        let mut current = self.roots.get(path[0])?;
233        for &idx in &path[1..] {
234            current = current.children.get(idx)?;
235        }
236        Some(current)
237    }
238
239    /// Toggle expand/collapse at the selected node.
240    pub fn toggle_selected(&mut self) {
241        let visible = self.build_visible();
242        if let Some(vnode) = visible.get(self.selected) {
243            let path = vnode.path.clone();
244            if let Some(node) = self.node_at_path_mut(&path)
245                && !node.is_leaf
246            {
247                node.expanded = !node.expanded;
248            }
249        }
250    }
251
252    /// Expand the selected node (load children lazily if needed).
253    pub fn expand_selected(&mut self) {
254        let visible = self.build_visible();
255        if let Some(vnode) = visible.get(self.selected) {
256            let path = vnode.path.clone();
257            let is_leaf = vnode.is_leaf;
258
259            if is_leaf {
260                return;
261            }
262
263            // Lazy load if needed
264            if let Some(ref lazy_fn) = self.lazy_load_fn
265                && let Some(node) = self.node_at_path(&path)
266                && node.children.is_empty()
267                && !node.is_leaf
268            {
269                let new_children = lazy_fn(&node.data);
270                if let Some(node_mut) = self.node_at_path_mut(&path) {
271                    node_mut.children = new_children;
272                }
273            }
274
275            if let Some(node) = self.node_at_path_mut(&path) {
276                node.expanded = true;
277            }
278        }
279    }
280
281    /// Collapse the selected node.
282    pub fn collapse_selected(&mut self) {
283        let visible = self.build_visible();
284        if let Some(vnode) = visible.get(self.selected) {
285            let path = vnode.path.clone();
286
287            if let Some(node) = self.node_at_path_mut(&path)
288                && node.expanded
289            {
290                node.expanded = false;
291                return;
292            }
293
294            // If already collapsed, move to parent
295            if path.len() > 1 {
296                let parent_path = &path[..path.len() - 1];
297                // Find parent in visible list
298                for (idx, v) in visible.iter().enumerate() {
299                    if v.path == parent_path {
300                        self.selected = idx;
301                        break;
302                    }
303                }
304            }
305        }
306    }
307
308    /// Get a reference to the data of the selected visible node.
309    pub fn selected_node(&self) -> Option<&TreeNode<T>> {
310        let visible = self.build_visible();
311        visible
312            .get(self.selected)
313            .and_then(|v| self.node_at_path(&v.path))
314    }
315
316    /// Get the total number of visible nodes.
317    pub fn visible_count(&self) -> usize {
318        self.build_visible().len()
319    }
320
321    /// Ensure the selected item is visible by adjusting scroll_offset.
322    fn ensure_selected_visible(&mut self, visible_height: usize) {
323        if visible_height == 0 {
324            return;
325        }
326        if self.selected < self.scroll_offset {
327            self.scroll_offset = self.selected;
328        }
329        if self.selected >= self.scroll_offset + visible_height {
330            self.scroll_offset = self
331                .selected
332                .saturating_sub(visible_height.saturating_sub(1));
333        }
334    }
335}
336
337impl<T> Widget for Tree<T> {
338    fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
339        if area.size.width == 0 || area.size.height == 0 {
340            return;
341        }
342
343        super::border::render_border(area, self.border, self.node_style.clone(), buf);
344
345        let inner = super::border::inner_area(area, self.border);
346        if inner.size.width == 0 || inner.size.height == 0 {
347            return;
348        }
349
350        let height = inner.size.height as usize;
351        let width = inner.size.width as usize;
352        let visible = self.build_visible();
353        let count = visible.len();
354
355        let max_offset = count.saturating_sub(height.max(1));
356        let scroll = self.scroll_offset.min(max_offset);
357        let visible_end = (scroll + height).min(count);
358
359        for (row, vis_idx) in (scroll..visible_end).enumerate() {
360            let y = inner.position.y + row as u16;
361            if let Some(vnode) = visible.get(vis_idx) {
362                let is_selected = vis_idx == self.selected;
363                let style = if is_selected {
364                    &self.selected_style
365                } else {
366                    &self.node_style
367                };
368
369                // Fill row if selected
370                if is_selected {
371                    for col in 0..inner.size.width {
372                        buf.set(inner.position.x + col, y, Cell::new(" ", style.clone()));
373                    }
374                }
375
376                // Indentation (2 spaces per level)
377                let indent = vnode.depth * 2;
378
379                // Expand indicator
380                let indicator = if vnode.is_leaf {
381                    " "
382                } else if vnode.expanded {
383                    "\u{25bc}" // ▼
384                } else {
385                    "\u{25b6}" // ▶
386                };
387
388                // Render indent + indicator + content
389                let mut col: u16 = 0;
390
391                // Indent
392                for _ in 0..indent {
393                    if col as usize >= width {
394                        break;
395                    }
396                    buf.set(inner.position.x + col, y, Cell::new(" ", style.clone()));
397                    col += 1;
398                }
399
400                // Indicator
401                if (col as usize) < width {
402                    buf.set(
403                        inner.position.x + col,
404                        y,
405                        Cell::new(indicator, style.clone()),
406                    );
407                    col += 1;
408                }
409
410                // Space after indicator
411                if (col as usize) < width {
412                    buf.set(inner.position.x + col, y, Cell::new(" ", style.clone()));
413                    col += 1;
414                }
415
416                // Content from render_fn
417                if let Some(node) = self.node_at_path(&vnode.path) {
418                    let segments =
419                        (self.render_fn)(&node.data, vnode.depth, vnode.expanded, vnode.is_leaf);
420                    for segment in &segments {
421                        if col as usize >= width {
422                            break;
423                        }
424                        let remaining = width.saturating_sub(col as usize);
425                        let truncated = truncate_to_display_width(&segment.text, remaining);
426                        for ch in truncated.chars() {
427                            let char_w =
428                                UnicodeWidthStr::width(ch.encode_utf8(&mut [0; 4]) as &str);
429                            if col as usize + char_w > width {
430                                break;
431                            }
432                            buf.set(
433                                inner.position.x + col,
434                                y,
435                                Cell::new(ch.to_string(), style.clone()),
436                            );
437                            col += char_w as u16;
438                        }
439                    }
440                }
441            }
442        }
443    }
444}
445
446impl<T> InteractiveWidget for Tree<T> {
447    fn handle_event(&mut self, event: &Event) -> EventResult {
448        let Event::Key(KeyEvent { code, .. }) = event else {
449            return EventResult::Ignored;
450        };
451
452        let count = self.visible_count();
453
454        match code {
455            KeyCode::Up => {
456                if self.selected > 0 {
457                    self.selected -= 1;
458                    self.ensure_selected_visible(20);
459                }
460                EventResult::Consumed
461            }
462            KeyCode::Down => {
463                if count > 0 && self.selected < count.saturating_sub(1) {
464                    self.selected += 1;
465                    self.ensure_selected_visible(20);
466                }
467                EventResult::Consumed
468            }
469            KeyCode::Right => {
470                self.expand_selected();
471                EventResult::Consumed
472            }
473            KeyCode::Left => {
474                self.collapse_selected();
475                EventResult::Consumed
476            }
477            KeyCode::Enter => {
478                self.toggle_selected();
479                EventResult::Consumed
480            }
481            KeyCode::PageUp => {
482                let page = 20;
483                self.selected = self.selected.saturating_sub(page);
484                self.ensure_selected_visible(20);
485                EventResult::Consumed
486            }
487            KeyCode::PageDown => {
488                let page = 20;
489                if count > 0 {
490                    self.selected = (self.selected + page).min(count.saturating_sub(1));
491                    self.ensure_selected_visible(20);
492                }
493                EventResult::Consumed
494            }
495            KeyCode::Home => {
496                self.selected = 0;
497                self.scroll_offset = 0;
498                EventResult::Consumed
499            }
500            KeyCode::End => {
501                if count > 0 {
502                    self.selected = count.saturating_sub(1);
503                    self.ensure_selected_visible(20);
504                }
505                EventResult::Consumed
506            }
507            _ => EventResult::Ignored,
508        }
509    }
510}
511
512#[cfg(test)]
513#[allow(clippy::unwrap_used)]
514mod tests {
515    use super::*;
516    use crate::geometry::Size;
517
518    fn make_render_fn() -> impl Fn(&String, usize, bool, bool) -> Vec<Segment> {
519        |data: &String, _depth, _expanded, _is_leaf| vec![Segment::new(data)]
520    }
521
522    fn make_test_tree() -> Tree<String> {
523        let tree = TreeNode::branch("root".into())
524            .with_child(TreeNode::new("leaf1".into()))
525            .with_child(
526                TreeNode::branch("branch1".into())
527                    .with_child(TreeNode::new("child1".into()))
528                    .with_child(TreeNode::new("child2".into())),
529            )
530            .with_child(TreeNode::new("leaf2".into()));
531
532        Tree::new(vec![tree]).with_render_fn(make_render_fn())
533    }
534
535    #[test]
536    fn create_tree_with_nodes() {
537        let tree = make_test_tree();
538        assert_eq!(tree.roots().len(), 1);
539    }
540
541    #[test]
542    fn render_collapsed_tree_only_roots() {
543        let tree = make_test_tree();
544        // Only root is visible (collapsed)
545        assert_eq!(tree.visible_count(), 1);
546
547        let mut buf = ScreenBuffer::new(Size::new(30, 10));
548        tree.render(Rect::new(0, 0, 30, 10), &mut buf);
549
550        // Should show "▶ root" (▶ is the expand indicator for non-leaf)
551        assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("\u{25b6}"));
552    }
553
554    #[test]
555    fn expand_node_children_visible() {
556        let mut tree = make_test_tree();
557        // Expand root
558        tree.toggle_selected();
559
560        // Root + 3 children: leaf1, branch1, leaf2
561        assert_eq!(tree.visible_count(), 4);
562    }
563
564    #[test]
565    fn collapse_node_hides_children() {
566        let mut tree = make_test_tree();
567        tree.toggle_selected(); // expand
568        assert_eq!(tree.visible_count(), 4);
569        tree.toggle_selected(); // collapse
570        assert_eq!(tree.visible_count(), 1);
571    }
572
573    #[test]
574    fn navigate_visible_nodes() {
575        let mut tree = make_test_tree();
576        tree.toggle_selected(); // expand root
577
578        let down = Event::Key(KeyEvent {
579            code: KeyCode::Down,
580            modifiers: crate::event::Modifiers::NONE,
581        });
582        let up = Event::Key(KeyEvent {
583            code: KeyCode::Up,
584            modifiers: crate::event::Modifiers::NONE,
585        });
586
587        assert_eq!(tree.selected(), 0);
588        tree.handle_event(&down);
589        assert_eq!(tree.selected(), 1); // leaf1
590        tree.handle_event(&down);
591        assert_eq!(tree.selected(), 2); // branch1
592        tree.handle_event(&up);
593        assert_eq!(tree.selected(), 1);
594    }
595
596    #[test]
597    fn right_key_expands() {
598        let mut tree = make_test_tree();
599        let right = Event::Key(KeyEvent {
600            code: KeyCode::Right,
601            modifiers: crate::event::Modifiers::NONE,
602        });
603
604        tree.handle_event(&right); // expand root
605        assert_eq!(tree.visible_count(), 4);
606    }
607
608    #[test]
609    fn left_key_collapses() {
610        let mut tree = make_test_tree();
611        tree.toggle_selected(); // expand root
612        let left = Event::Key(KeyEvent {
613            code: KeyCode::Left,
614            modifiers: crate::event::Modifiers::NONE,
615        });
616
617        tree.handle_event(&left); // collapse root
618        assert_eq!(tree.visible_count(), 1);
619    }
620
621    #[test]
622    fn enter_toggles() {
623        let mut tree = make_test_tree();
624        let enter = Event::Key(KeyEvent {
625            code: KeyCode::Enter,
626            modifiers: crate::event::Modifiers::NONE,
627        });
628
629        tree.handle_event(&enter); // expand
630        assert_eq!(tree.visible_count(), 4);
631        tree.handle_event(&enter); // collapse
632        assert_eq!(tree.visible_count(), 1);
633    }
634
635    #[test]
636    fn lazy_load_on_expand() {
637        use std::cell::RefCell;
638        use std::rc::Rc;
639
640        let load_count = Rc::new(RefCell::new(0));
641        let captured = Rc::clone(&load_count);
642
643        let root = TreeNode::branch("root".into());
644        let mut tree = Tree::new(vec![root])
645            .with_render_fn(make_render_fn())
646            .with_lazy_load(move |_data: &String| {
647                *captured.borrow_mut() += 1;
648                vec![
649                    TreeNode::new("loaded1".into()),
650                    TreeNode::new("loaded2".into()),
651                ]
652            });
653
654        tree.expand_selected();
655        assert_eq!(*load_count.borrow(), 1);
656        // root (expanded) + 2 loaded children
657        assert_eq!(tree.visible_count(), 3);
658    }
659
660    #[test]
661    fn selected_node_retrieval() {
662        let mut tree = make_test_tree();
663        tree.toggle_selected(); // expand root
664
665        // Select leaf1
666        tree.selected = 1;
667        match tree.selected_node() {
668            Some(node) => assert_eq!(node.data, "leaf1"),
669            None => unreachable!("should have selected node"),
670        }
671    }
672
673    #[test]
674    fn utf8_safe_node_labels() {
675        let node = TreeNode::new("你好".into());
676        let tree = Tree::new(vec![node]).with_render_fn(make_render_fn());
677
678        let mut buf = ScreenBuffer::new(Size::new(10, 1));
679        tree.render(Rect::new(0, 0, 10, 1), &mut buf);
680
681        // " 你" — space indicator + space + CJK chars
682        // Position 2 (after " " indicator + " " space) should be "你"
683        assert_eq!(buf.get(2, 0).map(|c| c.grapheme.as_str()), Some("你"));
684    }
685
686    #[test]
687    fn empty_tree() {
688        let tree: Tree<String> = Tree::new(vec![]).with_render_fn(make_render_fn());
689        assert_eq!(tree.visible_count(), 0);
690        assert!(tree.selected_node().is_none());
691
692        // Should not crash on render
693        let mut buf = ScreenBuffer::new(Size::new(20, 5));
694        tree.render(Rect::new(0, 0, 20, 5), &mut buf);
695    }
696
697    #[test]
698    fn deep_tree_multiple_levels() {
699        let deep = TreeNode::branch("L0".into()).with_child(
700            TreeNode::branch("L1".into())
701                .with_child(TreeNode::branch("L2".into()).with_child(TreeNode::new("L3".into()))),
702        );
703
704        let mut tree = Tree::new(vec![deep]).with_render_fn(make_render_fn());
705
706        // Expand all levels
707        tree.expand_selected(); // L0
708        let down = Event::Key(KeyEvent {
709            code: KeyCode::Down,
710            modifiers: crate::event::Modifiers::NONE,
711        });
712        tree.handle_event(&down); // select L1
713        tree.expand_selected(); // L1
714        tree.handle_event(&down); // select L2
715        tree.handle_event(&down); // stay or move to L2
716        tree.selected = 2; // L2
717        tree.expand_selected(); // L2
718
719        // L0, L1, L2, L3 all visible
720        assert_eq!(tree.visible_count(), 4);
721    }
722
723    #[test]
724    fn mixed_expanded_collapsed() {
725        let mut tree = make_test_tree();
726        tree.toggle_selected(); // expand root
727        // Navigate to branch1 (idx 2) and expand it
728        tree.selected = 2;
729        tree.toggle_selected(); // expand branch1
730
731        // root, leaf1, branch1, child1, child2, leaf2
732        assert_eq!(tree.visible_count(), 6);
733
734        // Collapse branch1
735        tree.toggle_selected();
736        // root, leaf1, branch1, leaf2
737        assert_eq!(tree.visible_count(), 4);
738    }
739
740    #[test]
741    fn render_with_border() {
742        let tree = make_test_tree();
743        let tree = Tree {
744            border: BorderStyle::Single,
745            ..tree
746        };
747
748        let mut buf = ScreenBuffer::new(Size::new(20, 10));
749        tree.render(Rect::new(0, 0, 20, 10), &mut buf);
750
751        assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("\u{250c}"));
752    }
753}