Skip to main content

presentar_terminal/widgets/
tree.rs

1//! Tree widget for hierarchical data visualization.
2//!
3//! Provides collapsible tree view using Unicode tree-drawing characters.
4//! Ideal for process trees, file systems, or cluster hierarchies.
5
6use presentar_core::{
7    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
8    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::collections::HashSet;
12use std::time::Duration;
13
14/// Tree branch characters.
15const BRANCH_PIPE: &str = "│   ";
16const BRANCH_TEE: &str = "├── ";
17const BRANCH_ELBOW: &str = "└── ";
18const BRANCH_SPACE: &str = "    ";
19
20/// Unique identifier for tree nodes.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct NodeId(pub u64);
23
24impl NodeId {
25    /// Create a new node ID.
26    #[must_use]
27    pub const fn new(id: u64) -> Self {
28        Self(id)
29    }
30}
31
32/// A node in the tree.
33#[derive(Debug, Clone)]
34pub struct TreeNode {
35    /// Unique identifier.
36    pub id: NodeId,
37    /// Display label.
38    pub label: String,
39    /// Optional value/info to display.
40    pub info: Option<String>,
41    /// Child nodes.
42    pub children: Vec<Self>,
43    /// Node color.
44    pub color: Option<Color>,
45}
46
47impl TreeNode {
48    /// Create a new tree node.
49    #[must_use]
50    pub fn new(id: u64, label: impl Into<String>) -> Self {
51        Self {
52            id: NodeId::new(id),
53            label: label.into(),
54            info: None,
55            children: vec![],
56            color: None,
57        }
58    }
59
60    /// Add info text.
61    #[must_use]
62    pub fn with_info(mut self, info: impl Into<String>) -> Self {
63        self.info = Some(info.into());
64        self
65    }
66
67    /// Set node color.
68    #[must_use]
69    pub fn with_color(mut self, color: Color) -> Self {
70        self.color = Some(color);
71        self
72    }
73
74    /// Add a child node.
75    #[must_use]
76    pub fn with_child(mut self, child: Self) -> Self {
77        self.children.push(child);
78        self
79    }
80
81    /// Add multiple children.
82    #[must_use]
83    pub fn with_children(mut self, children: Vec<Self>) -> Self {
84        self.children = children;
85        self
86    }
87
88    /// Count total nodes including self.
89    #[must_use]
90    pub fn count_nodes(&self) -> usize {
91        1 + self.children.iter().map(Self::count_nodes).sum::<usize>()
92    }
93
94    /// Get depth of the tree.
95    #[must_use]
96    pub fn depth(&self) -> usize {
97        if self.children.is_empty() {
98            1
99        } else {
100            1 + self.children.iter().map(Self::depth).max().unwrap_or(0)
101        }
102    }
103}
104
105/// Tree widget for hierarchical visualization.
106#[derive(Debug, Clone)]
107pub struct Tree {
108    /// Root node.
109    root: Option<TreeNode>,
110    /// Expanded node IDs.
111    expanded: HashSet<NodeId>,
112    /// Default color for nodes.
113    default_color: Color,
114    /// Show info column.
115    show_info: bool,
116    /// Indent string (characters per level).
117    #[allow(dead_code)]
118    indent_width: usize,
119    /// Scroll offset (for large trees).
120    scroll_offset: usize,
121    /// Selected node ID.
122    selected: Option<NodeId>,
123    /// Cached bounds.
124    bounds: Rect,
125}
126
127impl Default for Tree {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl Tree {
134    /// Create a new empty tree.
135    #[must_use]
136    pub fn new() -> Self {
137        Self {
138            root: None,
139            expanded: HashSet::new(),
140            default_color: Color::new(0.8, 0.8, 0.8, 1.0),
141            show_info: true,
142            indent_width: 4,
143            scroll_offset: 0,
144            selected: None,
145            bounds: Rect::default(),
146        }
147    }
148
149    /// Set the root node.
150    #[must_use]
151    pub fn with_root(mut self, root: TreeNode) -> Self {
152        // Expand root by default
153        self.expanded.insert(root.id);
154        self.root = Some(root);
155        self
156    }
157
158    /// Set default node color.
159    #[must_use]
160    pub fn with_color(mut self, color: Color) -> Self {
161        self.default_color = color;
162        self
163    }
164
165    /// Show or hide info column.
166    #[must_use]
167    pub fn with_info(mut self, show: bool) -> Self {
168        self.show_info = show;
169        self
170    }
171
172    /// Set all nodes as expanded.
173    #[must_use]
174    pub fn expand_all(mut self) -> Self {
175        if let Some(ref root) = self.root {
176            Self::collect_all_ids(root, &mut self.expanded);
177        }
178        self
179    }
180
181    /// Collapse all nodes except root.
182    #[must_use]
183    pub fn collapse_all(mut self) -> Self {
184        self.expanded.clear();
185        if let Some(ref root) = self.root {
186            self.expanded.insert(root.id);
187        }
188        self
189    }
190
191    /// Toggle expansion of a node.
192    pub fn toggle(&mut self, id: NodeId) {
193        if self.expanded.contains(&id) {
194            self.expanded.remove(&id);
195        } else {
196            self.expanded.insert(id);
197        }
198    }
199
200    /// Expand a node.
201    pub fn expand(&mut self, id: NodeId) {
202        self.expanded.insert(id);
203    }
204
205    /// Collapse a node.
206    pub fn collapse(&mut self, id: NodeId) {
207        self.expanded.remove(&id);
208    }
209
210    /// Check if a node is expanded.
211    #[must_use]
212    pub fn is_expanded(&self, id: NodeId) -> bool {
213        self.expanded.contains(&id)
214    }
215
216    /// Set scroll offset.
217    pub fn set_scroll(&mut self, offset: usize) {
218        self.scroll_offset = offset;
219    }
220
221    /// Select a node.
222    pub fn select(&mut self, id: Option<NodeId>) {
223        self.selected = id;
224    }
225
226    /// Get selected node ID.
227    #[must_use]
228    pub fn selected(&self) -> Option<NodeId> {
229        self.selected
230    }
231
232    /// Set a new root.
233    pub fn set_root(&mut self, root: TreeNode) {
234        self.expanded.insert(root.id);
235        self.root = Some(root);
236    }
237
238    /// Get visible line count.
239    #[must_use]
240    pub fn visible_lines(&self) -> usize {
241        self.root
242            .as_ref()
243            .map_or(0, |r| self.count_visible_lines(r))
244    }
245
246    fn count_visible_lines(&self, node: &TreeNode) -> usize {
247        let mut count = 1;
248        if self.expanded.contains(&node.id) {
249            for child in &node.children {
250                count += self.count_visible_lines(child);
251            }
252        }
253        count
254    }
255
256    fn collect_all_ids(node: &TreeNode, ids: &mut HashSet<NodeId>) {
257        ids.insert(node.id);
258        for child in &node.children {
259            Self::collect_all_ids(child, ids);
260        }
261    }
262
263    #[allow(clippy::too_many_arguments)]
264    fn render_node(
265        &self,
266        canvas: &mut dyn Canvas,
267        node: &TreeNode,
268        x: f32,
269        y: &mut f32,
270        prefix: &str,
271        is_last: bool,
272        visible_height: f32,
273    ) {
274        // Skip if above viewport
275        if *y < self.bounds.y {
276            // Still need to recurse to track y position
277        }
278
279        // Build branch string
280        let branch = if prefix.is_empty() {
281            String::new()
282        } else if is_last {
283            format!("{prefix}{BRANCH_ELBOW}")
284        } else {
285            format!("{prefix}{BRANCH_TEE}")
286        };
287
288        // Only render if within bounds
289        if *y >= self.bounds.y && *y < self.bounds.y + visible_height {
290            // Draw branch characters
291            let branch_style = TextStyle {
292                color: Color::new(0.5, 0.5, 0.5, 1.0),
293                ..Default::default()
294            };
295            canvas.draw_text(&branch, Point::new(x, *y), &branch_style);
296
297            // Draw expand/collapse indicator
298            let indicator = if node.children.is_empty() {
299                "  "
300            } else if self.expanded.contains(&node.id) {
301                "▼ "
302            } else {
303                "▶ "
304            };
305
306            let indicator_x = x + branch.chars().count() as f32;
307            canvas.draw_text(indicator, Point::new(indicator_x, *y), &branch_style);
308
309            // Draw label
310            let label_x = indicator_x + 2.0;
311            let color = node.color.unwrap_or(self.default_color);
312            let is_selected = self.selected == Some(node.id);
313
314            let label_style = TextStyle {
315                color: if is_selected {
316                    Color::new(0.0, 0.0, 0.0, 1.0)
317                } else {
318                    color
319                },
320                ..Default::default()
321            };
322
323            // Draw selection highlight
324            if is_selected {
325                let label_len = node.label.len() as f32;
326                canvas.fill_rect(
327                    Rect::new(label_x, *y, label_len, 1.0),
328                    Color::new(0.3, 0.7, 1.0, 1.0),
329                );
330            }
331
332            canvas.draw_text(&node.label, Point::new(label_x, *y), &label_style);
333
334            // Draw info if enabled
335            if self.show_info {
336                if let Some(ref info) = node.info {
337                    let info_x = label_x + node.label.len() as f32 + 2.0;
338                    let info_style = TextStyle {
339                        color: Color::new(0.6, 0.6, 0.6, 1.0),
340                        ..Default::default()
341                    };
342                    canvas.draw_text(info, Point::new(info_x, *y), &info_style);
343                }
344            }
345        }
346
347        *y += 1.0;
348
349        // Render children if expanded
350        if self.expanded.contains(&node.id) && !node.children.is_empty() {
351            let child_prefix = if prefix.is_empty() {
352                String::new()
353            } else if is_last {
354                format!("{prefix}{BRANCH_SPACE}")
355            } else {
356                format!("{prefix}{BRANCH_PIPE}")
357            };
358
359            let child_count = node.children.len();
360            for (i, child) in node.children.iter().enumerate() {
361                let child_is_last = i == child_count - 1;
362                self.render_node(
363                    canvas,
364                    child,
365                    x,
366                    y,
367                    &child_prefix,
368                    child_is_last,
369                    visible_height,
370                );
371            }
372        }
373    }
374}
375
376impl Brick for Tree {
377    fn brick_name(&self) -> &'static str {
378        "tree"
379    }
380
381    fn assertions(&self) -> &[BrickAssertion] {
382        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
383        ASSERTIONS
384    }
385
386    fn budget(&self) -> BrickBudget {
387        BrickBudget::uniform(16)
388    }
389
390    fn verify(&self) -> BrickVerification {
391        BrickVerification {
392            passed: self.assertions().to_vec(),
393            failed: vec![],
394            verification_time: Duration::from_micros(10),
395        }
396    }
397
398    fn to_html(&self) -> String {
399        String::new()
400    }
401
402    fn to_css(&self) -> String {
403        String::new()
404    }
405}
406
407impl Widget for Tree {
408    fn type_id(&self) -> TypeId {
409        TypeId::of::<Self>()
410    }
411
412    fn measure(&self, constraints: Constraints) -> Size {
413        let lines = self.visible_lines() as f32;
414        let width = constraints.max_width.min(80.0);
415        let height = lines.min(constraints.max_height);
416        constraints.constrain(Size::new(width, height.max(1.0)))
417    }
418
419    fn layout(&mut self, bounds: Rect) -> LayoutResult {
420        self.bounds = bounds;
421        LayoutResult {
422            size: Size::new(bounds.width, bounds.height),
423        }
424    }
425
426    fn paint(&self, canvas: &mut dyn Canvas) {
427        if self.root.is_none() || self.bounds.width < 1.0 {
428            return;
429        }
430
431        let mut y = self.bounds.y - self.scroll_offset as f32;
432        if let Some(ref root) = self.root {
433            self.render_node(
434                canvas,
435                root,
436                self.bounds.x,
437                &mut y,
438                "",
439                true,
440                self.bounds.height,
441            );
442        }
443    }
444
445    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
446        None
447    }
448
449    fn children(&self) -> &[Box<dyn Widget>] {
450        &[]
451    }
452
453    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
454        &mut []
455    }
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    struct MockCanvas {
463        texts: Vec<(String, Point)>,
464        rects: Vec<(Rect, Color)>,
465    }
466
467    impl MockCanvas {
468        fn new() -> Self {
469            Self {
470                texts: vec![],
471                rects: vec![],
472            }
473        }
474    }
475
476    impl Canvas for MockCanvas {
477        fn fill_rect(&mut self, rect: Rect, color: Color) {
478            self.rects.push((rect, color));
479        }
480        fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
481        fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
482            self.texts.push((text.to_string(), position));
483        }
484        fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
485        fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
486        fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
487        fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
488        fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
489        fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
490        fn push_clip(&mut self, _rect: Rect) {}
491        fn pop_clip(&mut self) {}
492        fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
493        fn pop_transform(&mut self) {}
494    }
495
496    #[test]
497    fn test_tree_creation() {
498        let tree = Tree::new();
499        assert!(tree.root.is_none());
500    }
501
502    #[test]
503    fn test_tree_with_root() {
504        let root = TreeNode::new(1, "Root");
505        let tree = Tree::new().with_root(root);
506        assert!(tree.root.is_some());
507        assert!(tree.is_expanded(NodeId::new(1)));
508    }
509
510    #[test]
511    fn test_tree_node_builder() {
512        let node = TreeNode::new(1, "Parent")
513            .with_info("Info text")
514            .with_color(Color::RED)
515            .with_child(TreeNode::new(2, "Child"));
516        assert_eq!(node.children.len(), 1);
517        assert!(node.info.is_some());
518        assert!(node.color.is_some());
519    }
520
521    #[test]
522    fn test_tree_toggle() {
523        let root = TreeNode::new(1, "Root").with_child(TreeNode::new(2, "Child"));
524        let mut tree = Tree::new().with_root(root);
525
526        assert!(tree.is_expanded(NodeId::new(1)));
527        tree.toggle(NodeId::new(1));
528        assert!(!tree.is_expanded(NodeId::new(1)));
529        tree.toggle(NodeId::new(1));
530        assert!(tree.is_expanded(NodeId::new(1)));
531    }
532
533    #[test]
534    fn test_tree_expand_collapse() {
535        let root = TreeNode::new(1, "Root").with_child(TreeNode::new(2, "Child"));
536        let mut tree = Tree::new().with_root(root);
537
538        tree.expand(NodeId::new(2));
539        assert!(tree.is_expanded(NodeId::new(2)));
540        tree.collapse(NodeId::new(2));
541        assert!(!tree.is_expanded(NodeId::new(2)));
542    }
543
544    #[test]
545    fn test_tree_expand_all() {
546        let root = TreeNode::new(1, "Root")
547            .with_child(TreeNode::new(2, "Child1").with_child(TreeNode::new(3, "GrandChild")))
548            .with_child(TreeNode::new(4, "Child2"));
549        let tree = Tree::new().with_root(root).expand_all();
550
551        assert!(tree.is_expanded(NodeId::new(1)));
552        assert!(tree.is_expanded(NodeId::new(2)));
553        assert!(tree.is_expanded(NodeId::new(3)));
554        assert!(tree.is_expanded(NodeId::new(4)));
555    }
556
557    #[test]
558    fn test_tree_collapse_all() {
559        let root = TreeNode::new(1, "Root").with_child(TreeNode::new(2, "Child"));
560        let tree = Tree::new().with_root(root).expand_all().collapse_all();
561
562        assert!(tree.is_expanded(NodeId::new(1))); // Root stays expanded
563        assert!(!tree.is_expanded(NodeId::new(2)));
564    }
565
566    #[test]
567    fn test_tree_visible_lines() {
568        let root = TreeNode::new(1, "Root")
569            .with_child(TreeNode::new(2, "Child1"))
570            .with_child(TreeNode::new(3, "Child2"));
571        let tree = Tree::new().with_root(root);
572
573        // Root expanded: Root + 2 children = 3 lines
574        assert_eq!(tree.visible_lines(), 3);
575    }
576
577    #[test]
578    fn test_tree_visible_lines_collapsed() {
579        let root = TreeNode::new(1, "Root")
580            .with_child(TreeNode::new(2, "Child1"))
581            .with_child(TreeNode::new(3, "Child2"));
582        let mut tree = Tree::new().with_root(root);
583        tree.collapse(NodeId::new(1));
584
585        // Root collapsed: just Root = 1 line
586        assert_eq!(tree.visible_lines(), 1);
587    }
588
589    #[test]
590    fn test_tree_node_count() {
591        let root = TreeNode::new(1, "Root")
592            .with_child(TreeNode::new(2, "Child1").with_child(TreeNode::new(3, "GrandChild")))
593            .with_child(TreeNode::new(4, "Child2"));
594
595        assert_eq!(root.count_nodes(), 4);
596    }
597
598    #[test]
599    fn test_tree_node_depth() {
600        let root = TreeNode::new(1, "Root")
601            .with_child(TreeNode::new(2, "Child1").with_child(TreeNode::new(3, "GrandChild")))
602            .with_child(TreeNode::new(4, "Child2"));
603
604        assert_eq!(root.depth(), 3);
605    }
606
607    #[test]
608    fn test_tree_selection() {
609        let root = TreeNode::new(1, "Root");
610        let mut tree = Tree::new().with_root(root);
611
612        assert!(tree.selected().is_none());
613        tree.select(Some(NodeId::new(1)));
614        assert_eq!(tree.selected(), Some(NodeId::new(1)));
615        tree.select(None);
616        assert!(tree.selected().is_none());
617    }
618
619    #[test]
620    fn test_tree_paint() {
621        let root = TreeNode::new(1, "Root")
622            .with_child(TreeNode::new(2, "Child1"))
623            .with_child(TreeNode::new(3, "Child2"));
624        let mut tree = Tree::new().with_root(root);
625        tree.bounds = Rect::new(0.0, 0.0, 40.0, 10.0);
626
627        let mut canvas = MockCanvas::new();
628        tree.paint(&mut canvas);
629
630        assert!(!canvas.texts.is_empty());
631    }
632
633    #[test]
634    fn test_tree_paint_empty() {
635        let tree = Tree::new();
636        let mut canvas = MockCanvas::new();
637        tree.paint(&mut canvas);
638        assert!(canvas.texts.is_empty());
639    }
640
641    #[test]
642    fn test_tree_measure() {
643        let root = TreeNode::new(1, "Root")
644            .with_child(TreeNode::new(2, "Child1"))
645            .with_child(TreeNode::new(3, "Child2"));
646        let tree = Tree::new().with_root(root);
647
648        let size = tree.measure(Constraints::loose(Size::new(100.0, 50.0)));
649        assert!(size.height >= 3.0); // 3 visible lines
650    }
651
652    #[test]
653    fn test_tree_layout() {
654        let mut tree = Tree::new();
655        let bounds = Rect::new(5.0, 10.0, 30.0, 20.0);
656        let result = tree.layout(bounds);
657
658        assert_eq!(result.size.width, 30.0);
659        assert_eq!(result.size.height, 20.0);
660        assert_eq!(tree.bounds, bounds);
661    }
662
663    #[test]
664    fn test_tree_brick_name() {
665        let tree = Tree::new();
666        assert_eq!(tree.brick_name(), "tree");
667    }
668
669    #[test]
670    fn test_tree_assertions() {
671        let tree = Tree::new();
672        assert!(!tree.assertions().is_empty());
673    }
674
675    #[test]
676    fn test_tree_budget() {
677        let tree = Tree::new();
678        let budget = tree.budget();
679        assert!(budget.paint_ms > 0);
680    }
681
682    #[test]
683    fn test_tree_verify() {
684        let tree = Tree::new();
685        assert!(tree.verify().is_valid());
686    }
687
688    #[test]
689    fn test_tree_type_id() {
690        let tree = Tree::new();
691        assert_eq!(Widget::type_id(&tree), TypeId::of::<Tree>());
692    }
693
694    #[test]
695    fn test_tree_children() {
696        let tree = Tree::new();
697        assert!(tree.children().is_empty());
698    }
699
700    #[test]
701    fn test_tree_children_mut() {
702        let mut tree = Tree::new();
703        assert!(tree.children_mut().is_empty());
704    }
705
706    #[test]
707    fn test_tree_event() {
708        let mut tree = Tree::new();
709        let event = Event::KeyDown {
710            key: presentar_core::Key::Enter,
711        };
712        assert!(tree.event(&event).is_none());
713    }
714
715    #[test]
716    fn test_tree_default() {
717        let tree = Tree::default();
718        assert!(tree.root.is_none());
719    }
720
721    #[test]
722    fn test_tree_to_html() {
723        let tree = Tree::new();
724        assert!(tree.to_html().is_empty());
725    }
726
727    #[test]
728    fn test_tree_to_css() {
729        let tree = Tree::new();
730        assert!(tree.to_css().is_empty());
731    }
732
733    #[test]
734    fn test_tree_scroll() {
735        let mut tree = Tree::new();
736        tree.set_scroll(5);
737        assert_eq!(tree.scroll_offset, 5);
738    }
739
740    #[test]
741    fn test_tree_with_color() {
742        let tree = Tree::new().with_color(Color::RED);
743        assert_eq!(tree.default_color, Color::RED);
744    }
745
746    #[test]
747    fn test_tree_with_info() {
748        let tree = Tree::new().with_info(false);
749        assert!(!tree.show_info);
750    }
751
752    #[test]
753    fn test_tree_set_root() {
754        let mut tree = Tree::new();
755        tree.set_root(TreeNode::new(1, "New Root"));
756        assert!(tree.root.is_some());
757    }
758
759    #[test]
760    fn test_node_id() {
761        let id = NodeId::new(42);
762        assert_eq!(id.0, 42);
763    }
764
765    #[test]
766    fn test_tree_node_with_children() {
767        let children = vec![TreeNode::new(2, "A"), TreeNode::new(3, "B")];
768        let node = TreeNode::new(1, "Root").with_children(children);
769        assert_eq!(node.children.len(), 2);
770    }
771
772    #[test]
773    fn test_tree_paint_with_selection() {
774        let root = TreeNode::new(1, "Root");
775        let mut tree = Tree::new().with_root(root);
776        tree.select(Some(NodeId::new(1)));
777        tree.bounds = Rect::new(0.0, 0.0, 40.0, 10.0);
778
779        let mut canvas = MockCanvas::new();
780        tree.paint(&mut canvas);
781
782        // Selection should cause a fill_rect call
783        assert!(!canvas.rects.is_empty());
784    }
785
786    #[test]
787    fn test_tree_leaf_node_depth() {
788        let leaf = TreeNode::new(1, "Leaf");
789        assert_eq!(leaf.depth(), 1);
790    }
791
792    #[test]
793    fn test_tree_paint_collapsed_node() {
794        // Test collapsed indicator (▶)
795        let root = TreeNode::new(1, "Root").with_child(TreeNode::new(2, "Child"));
796        let mut tree = Tree::new().with_root(root);
797        tree.collapse(NodeId::new(1)); // Collapse the root
798        tree.bounds = Rect::new(0.0, 0.0, 40.0, 10.0);
799
800        let mut canvas = MockCanvas::new();
801        tree.paint(&mut canvas);
802
803        // Should paint collapsed indicator
804        assert!(canvas.texts.iter().any(|(t, _)| t.contains("▶")));
805    }
806
807    #[test]
808    fn test_tree_paint_with_info() {
809        // Test info display
810        let root = TreeNode::new(1, "Root").with_info("Info text");
811        let mut tree = Tree::new().with_root(root).with_info(true);
812        tree.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
813
814        let mut canvas = MockCanvas::new();
815        tree.paint(&mut canvas);
816
817        // Should paint info text
818        assert!(canvas.texts.iter().any(|(t, _)| t.contains("Info text")));
819    }
820
821    #[test]
822    fn test_tree_paint_nested_expanded() {
823        // Test branch prefixes with nested children
824        let root = TreeNode::new(1, "Root")
825            .with_child(
826                TreeNode::new(2, "Child1")
827                    .with_child(TreeNode::new(4, "GrandChild1"))
828                    .with_child(TreeNode::new(5, "GrandChild2")),
829            )
830            .with_child(TreeNode::new(3, "Child2"));
831        let mut tree = Tree::new().with_root(root).expand_all();
832        tree.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
833
834        let mut canvas = MockCanvas::new();
835        tree.paint(&mut canvas);
836
837        // Should paint multiple levels with branch characters
838        assert!(canvas.texts.len() > 5);
839    }
840
841    #[test]
842    fn test_tree_paint_not_last_child() {
843        // Test branch tee (├──) for non-last children
844        let root = TreeNode::new(1, "Root")
845            .with_child(TreeNode::new(2, "Child1"))
846            .with_child(TreeNode::new(3, "Child2"))
847            .with_child(TreeNode::new(4, "Child3"));
848        let mut tree = Tree::new().with_root(root);
849        tree.bounds = Rect::new(0.0, 0.0, 40.0, 10.0);
850
851        let mut canvas = MockCanvas::new();
852        tree.paint(&mut canvas);
853
854        // Should paint multiple children (root + 3 children = 4 nodes)
855        // Branch chars are in the same text as the prefix
856        assert!(canvas.texts.len() >= 4);
857    }
858
859    #[test]
860    fn test_tree_paint_scrolled() {
861        // Test paint with scroll offset (y < bounds.y case)
862        let root = TreeNode::new(1, "Root")
863            .with_child(TreeNode::new(2, "Child1"))
864            .with_child(TreeNode::new(3, "Child2"));
865        let mut tree = Tree::new().with_root(root);
866        tree.set_scroll(5);
867        tree.bounds = Rect::new(0.0, 0.0, 40.0, 10.0);
868
869        let mut canvas = MockCanvas::new();
870        tree.paint(&mut canvas);
871        // Should handle scroll offset without panic
872    }
873
874    #[test]
875    fn test_tree_paint_deep_nesting() {
876        // Test deep nesting with branch pipes
877        let grandchild =
878            TreeNode::new(4, "GrandChild").with_child(TreeNode::new(5, "GreatGrandChild"));
879        let child1 = TreeNode::new(2, "Child1").with_child(grandchild);
880        let child2 = TreeNode::new(3, "Child2");
881        let root = TreeNode::new(1, "Root")
882            .with_child(child1)
883            .with_child(child2);
884        let mut tree = Tree::new().with_root(root).expand_all();
885        tree.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
886
887        let mut canvas = MockCanvas::new();
888        tree.paint(&mut canvas);
889
890        // Should render deep structure
891        assert!(canvas.texts.len() >= 5);
892    }
893}