Skip to main content

ftui_widgets/
tree.rs

1//! Tree widget for hierarchical display.
2//!
3//! Renders a tree of labeled nodes with configurable guide characters
4//! and styles, suitable for file trees or structured views.
5//!
6//! # Example
7//!
8//! ```
9//! use ftui_widgets::tree::{Tree, TreeNode, TreeGuides};
10//!
11//! let tree = Tree::new(TreeNode::new("root")
12//!     .child(TreeNode::new("src")
13//!         .child(TreeNode::new("main.rs"))
14//!         .child(TreeNode::new("lib.rs")))
15//!     .child(TreeNode::new("Cargo.toml")));
16//!
17//! assert_eq!(tree.root().label(), "root");
18//! assert_eq!(tree.root().children().len(), 2);
19//! ```
20
21use crate::mouse::MouseResult;
22use crate::stateful::Stateful;
23use crate::undo_support::{TreeUndoExt, UndoSupport, UndoWidgetId};
24use crate::{Widget, clear_text_area, draw_text_span};
25use ftui_core::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
26use ftui_core::geometry::Rect;
27use ftui_render::frame::{Frame, HitId, HitRegion};
28use ftui_style::Style;
29use std::any::Any;
30use std::collections::HashSet;
31#[cfg(feature = "tracing")]
32use web_time::Instant;
33
34/// Guide character styles for tree rendering.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum TreeGuides {
37    /// ASCII guides: `|`, `+--`, `` `-- ``.
38    Ascii,
39    /// Unicode box-drawing characters (default).
40    #[default]
41    Unicode,
42    /// Bold Unicode box-drawing characters.
43    Bold,
44    /// Double-line Unicode characters.
45    Double,
46    /// Rounded Unicode characters.
47    Rounded,
48}
49
50impl TreeGuides {
51    /// Vertical continuation (item has siblings below).
52    #[must_use]
53    pub const fn vertical(&self) -> &str {
54        match self {
55            Self::Ascii => "|   ",
56            Self::Unicode | Self::Rounded => "\u{2502}   ",
57            Self::Bold => "\u{2503}   ",
58            Self::Double => "\u{2551}   ",
59        }
60    }
61
62    /// Branch guide (item has siblings below).
63    #[must_use]
64    pub const fn branch(&self) -> &str {
65        match self {
66            Self::Ascii => "+-- ",
67            Self::Unicode => "\u{251C}\u{2500}\u{2500} ",
68            Self::Bold => "\u{2523}\u{2501}\u{2501} ",
69            Self::Double => "\u{2560}\u{2550}\u{2550} ",
70            Self::Rounded => "\u{251C}\u{2500}\u{2500} ",
71        }
72    }
73
74    /// Last-item guide (no siblings below).
75    #[must_use]
76    pub const fn last(&self) -> &str {
77        match self {
78            Self::Ascii => "`-- ",
79            Self::Unicode => "\u{2514}\u{2500}\u{2500} ",
80            Self::Bold => "\u{2517}\u{2501}\u{2501} ",
81            Self::Double => "\u{255A}\u{2550}\u{2550} ",
82            Self::Rounded => "\u{2570}\u{2500}\u{2500} ",
83        }
84    }
85
86    /// Empty indentation (no guide needed).
87    #[must_use]
88    pub const fn space(&self) -> &str {
89        "    "
90    }
91
92    /// Width in columns of each guide segment.
93    #[inline]
94    #[must_use]
95    pub fn width(&self) -> usize {
96        4
97    }
98}
99
100/// A node in the tree hierarchy.
101#[derive(Debug, Clone)]
102pub struct TreeNode {
103    label: String,
104    icon: Option<String>,
105    /// Child nodes (crate-visible for undo support).
106    pub(crate) children: Vec<TreeNode>,
107    /// Lazily materialized children.
108    lazy_children: Option<Vec<TreeNode>>,
109    /// Whether this node is expanded (crate-visible for undo support).
110    pub(crate) expanded: bool,
111}
112
113impl TreeNode {
114    /// Create a new tree node with the given label.
115    #[must_use]
116    pub fn new(label: impl Into<String>) -> Self {
117        Self {
118            label: label.into(),
119            icon: None,
120            children: Vec::new(),
121            lazy_children: None,
122            expanded: true,
123        }
124    }
125
126    /// Add a child node.
127    #[must_use]
128    pub fn child(mut self, node: TreeNode) -> Self {
129        self.children.push(node);
130        self
131    }
132
133    /// Set children from a vec.
134    #[must_use]
135    pub fn with_children(mut self, nodes: Vec<TreeNode>) -> Self {
136        self.children = nodes;
137        self
138    }
139
140    /// Set an icon prefix rendered before the label.
141    #[must_use]
142    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
143        self.icon = Some(icon.into());
144        self
145    }
146
147    /// Configure lazily materialized children.
148    ///
149    /// The node starts collapsed and children are attached when first expanded.
150    #[must_use]
151    pub fn with_lazy_children(mut self, nodes: Vec<TreeNode>) -> Self {
152        self.lazy_children = Some(nodes);
153        self.expanded = false;
154        self
155    }
156
157    /// Set whether this node is expanded.
158    #[must_use]
159    pub fn with_expanded(mut self, expanded: bool) -> Self {
160        if expanded {
161            self.materialize_lazy_children();
162        }
163        self.expanded = expanded;
164        self
165    }
166
167    /// Get the label.
168    #[must_use]
169    pub fn label(&self) -> &str {
170        &self.label
171    }
172
173    /// Get the children.
174    #[must_use]
175    pub fn children(&self) -> &[TreeNode] {
176        &self.children
177    }
178
179    /// Optional icon rendered before label.
180    #[must_use]
181    pub fn icon(&self) -> Option<&str> {
182        self.icon.as_deref()
183    }
184
185    /// Whether this node has loaded or lazy children.
186    #[must_use]
187    pub fn has_children(&self) -> bool {
188        !self.children.is_empty()
189            || self
190                .lazy_children
191                .as_ref()
192                .is_some_and(|children| !children.is_empty())
193    }
194
195    /// Whether this node is expanded.
196    #[must_use]
197    pub fn is_expanded(&self) -> bool {
198        self.expanded
199    }
200
201    /// Toggle the expanded state.
202    pub fn toggle_expanded(&mut self) {
203        if !self.expanded {
204            self.materialize_lazy_children();
205        }
206        self.expanded = !self.expanded;
207    }
208
209    fn materialize_lazy_children(&mut self) {
210        if let Some(mut lazy) = self.lazy_children.take() {
211            self.children.append(&mut lazy);
212        }
213    }
214
215    fn materialize_all_lazy_children(&mut self) {
216        self.materialize_lazy_children();
217        for child in &mut self.children {
218            child.materialize_all_lazy_children();
219        }
220    }
221
222    #[cfg(feature = "tracing")]
223    fn total_count(&self) -> usize {
224        let mut count = 1usize;
225        for child in &self.children {
226            count = count.saturating_add(child.total_count());
227        }
228        if let Some(lazy) = &self.lazy_children {
229            for child in lazy {
230                count = count.saturating_add(child.total_count());
231            }
232        }
233        count
234    }
235
236    #[cfg(feature = "tracing")]
237    fn expanded_count(&self) -> usize {
238        let mut count = usize::from(self.expanded && self.has_children());
239        for child in &self.children {
240            count = count.saturating_add(child.expanded_count());
241        }
242        if let Some(lazy) = &self.lazy_children {
243            for child in lazy {
244                count = count.saturating_add(child.expanded_count());
245            }
246        }
247        count
248    }
249
250    /// Count all visible (expanded) nodes, including this one.
251    #[must_use]
252    pub fn visible_count(&self) -> usize {
253        let mut count = 1;
254        if self.expanded {
255            for child in &self.children {
256                count += child.visible_count();
257            }
258        }
259        count
260    }
261
262    /// Collect all expanded node paths into a set.
263    #[allow(dead_code)]
264    pub(crate) fn collect_expanded(&self, prefix: &str, out: &mut HashSet<String>) {
265        let path = if prefix.is_empty() {
266            self.label.clone()
267        } else {
268            format!("{}/{}", prefix, self.label)
269        };
270
271        if self.expanded && self.has_children() {
272            out.insert(path.clone());
273        }
274
275        for child in &self.children {
276            child.collect_expanded(&path, out);
277        }
278    }
279
280    /// Apply expanded state from a set of paths.
281    #[allow(dead_code)]
282    pub(crate) fn apply_expanded(&mut self, prefix: &str, expanded_paths: &HashSet<String>) {
283        let path = if prefix.is_empty() {
284            self.label.clone()
285        } else {
286            format!("{}/{}", prefix, self.label)
287        };
288
289        if self.has_children() {
290            self.expanded = expanded_paths.contains(&path);
291            if self.expanded {
292                self.materialize_lazy_children();
293            }
294        }
295
296        for child in &mut self.children {
297            child.apply_expanded(&path, expanded_paths);
298        }
299    }
300}
301
302/// Tree widget for rendering hierarchical data.
303#[derive(Debug, Clone)]
304pub struct Tree {
305    /// Unique ID for undo tracking.
306    undo_id: UndoWidgetId,
307    root: TreeNode,
308    /// Whether to show the root node.
309    show_root: bool,
310    /// Guide character style.
311    guides: TreeGuides,
312    /// Style for guide characters.
313    guide_style: Style,
314    /// Style for node labels.
315    label_style: Style,
316    /// Style for the root node label.
317    root_style: Style,
318    /// Optional persistence ID for state saving/restoration.
319    persistence_id: Option<String>,
320    /// Optional hit ID for mouse interaction.
321    hit_id: Option<HitId>,
322    /// Optional case-insensitive search query.
323    search_query: Option<String>,
324}
325
326impl Tree {
327    /// Create a tree widget with the given root node.
328    #[must_use]
329    pub fn new(root: TreeNode) -> Self {
330        Self {
331            undo_id: UndoWidgetId::new(),
332            root,
333            show_root: true,
334            guides: TreeGuides::default(),
335            guide_style: Style::default(),
336            label_style: Style::default(),
337            root_style: Style::default(),
338            persistence_id: None,
339            hit_id: None,
340            search_query: None,
341        }
342    }
343
344    /// Set whether to show the root node.
345    #[must_use]
346    pub fn with_show_root(mut self, show: bool) -> Self {
347        self.show_root = show;
348        self
349    }
350
351    /// Set the guide character style.
352    #[must_use]
353    pub fn with_guides(mut self, guides: TreeGuides) -> Self {
354        self.guides = guides;
355        self
356    }
357
358    /// Set the style for guide characters.
359    #[must_use]
360    pub fn with_guide_style(mut self, style: Style) -> Self {
361        self.guide_style = style;
362        self
363    }
364
365    /// Set the style for node labels.
366    #[must_use]
367    pub fn with_label_style(mut self, style: Style) -> Self {
368        self.label_style = style;
369        self
370    }
371
372    /// Set the style for the root label.
373    #[must_use]
374    pub fn with_root_style(mut self, style: Style) -> Self {
375        self.root_style = style;
376        self
377    }
378
379    /// Set a persistence ID for state saving.
380    #[must_use]
381    pub fn with_persistence_id(mut self, id: impl Into<String>) -> Self {
382        self.persistence_id = Some(id.into());
383        self
384    }
385
386    /// Get the persistence ID, if set.
387    #[must_use]
388    pub fn persistence_id(&self) -> Option<&str> {
389        self.persistence_id.as_deref()
390    }
391
392    /// Set a hit ID for mouse interaction.
393    #[must_use]
394    pub fn hit_id(mut self, id: HitId) -> Self {
395        self.hit_id = Some(id);
396        self
397    }
398
399    /// Apply a case-insensitive search query filter.
400    #[must_use]
401    pub fn with_search_query(mut self, query: impl Into<String>) -> Self {
402        let query = query.into();
403        self.search_query = if query.trim().is_empty() {
404            None
405        } else {
406            Some(query)
407        };
408        self
409    }
410
411    /// Clear search filtering.
412    #[must_use]
413    pub fn without_search_query(mut self) -> Self {
414        self.search_query = None;
415        self
416    }
417
418    #[cfg(feature = "tracing")]
419    fn total_nodes(&self) -> usize {
420        if self.show_root {
421            self.root.total_count()
422        } else if self.root.expanded {
423            self.root
424                .children
425                .iter()
426                .fold(0usize, |acc, child| acc.saturating_add(child.total_count()))
427        } else {
428            0
429        }
430    }
431
432    #[cfg(feature = "tracing")]
433    fn visible_nodes(&self) -> usize {
434        if self.show_root {
435            self.root.visible_count()
436        } else if self.root.expanded {
437            self.root.children.iter().fold(0usize, |acc, child| {
438                acc.saturating_add(child.visible_count())
439            })
440        } else {
441            0
442        }
443    }
444
445    #[cfg(feature = "tracing")]
446    fn expanded_nodes(&self) -> usize {
447        if self.show_root {
448            self.root.expanded_count()
449        } else if self.root.expanded {
450            self.root.children.iter().fold(0usize, |acc, child| {
451                acc.saturating_add(child.expanded_count())
452            })
453        } else {
454            0
455        }
456    }
457
458    /// Get a reference to the root node.
459    #[must_use]
460    pub fn root(&self) -> &TreeNode {
461        &self.root
462    }
463
464    /// Get a mutable reference to the root node.
465    pub fn root_mut(&mut self) -> &mut TreeNode {
466        &mut self.root
467    }
468
469    #[allow(clippy::too_many_arguments)]
470    fn render_node(
471        &self,
472        node: &TreeNode,
473        depth: usize,
474        is_last: &mut Vec<bool>,
475        area: Rect,
476        frame: &mut Frame,
477        current_row: &mut usize,
478        deg: ftui_render::budget::DegradationLevel,
479    ) {
480        if *current_row >= area.height as usize {
481            return;
482        }
483
484        let y = area.y.saturating_add(*current_row as u16);
485        let mut x = area.x;
486        let max_x = area.right();
487
488        // Draw guide characters for each depth level
489        if depth > 0 && deg.apply_styling() {
490            for d in 0..depth {
491                let is_last_at_depth = is_last.get(d).copied().unwrap_or(false);
492                let guide = if d == depth - 1 {
493                    // This is the immediate parent level
494                    if is_last_at_depth {
495                        self.guides.last()
496                    } else {
497                        self.guides.branch()
498                    }
499                } else {
500                    // Ancestor level: show vertical line or blank
501                    if is_last_at_depth {
502                        self.guides.space()
503                    } else {
504                        self.guides.vertical()
505                    }
506                };
507
508                x = draw_text_span(frame, x, y, guide, self.guide_style, max_x);
509            }
510        } else if depth > 0 {
511            // Minimal rendering: indent with spaces
512            // Avoid allocation by drawing chunks iteratively
513            for _ in 0..depth {
514                x = draw_text_span(frame, x, y, "    ", Style::default(), max_x);
515                if x >= max_x {
516                    break;
517                }
518            }
519        }
520
521        // Draw label
522        let style = if depth == 0 && self.show_root {
523            self.root_style
524        } else {
525            self.label_style
526        };
527        if let Some(icon) = node.icon() {
528            let icon_style = if deg.apply_styling() {
529                style
530            } else {
531                Style::default()
532            };
533            x = draw_text_span(frame, x, y, icon, icon_style, max_x);
534            if x < max_x {
535                x = draw_text_span(frame, x, y, " ", icon_style, max_x);
536            }
537        }
538
539        if deg.apply_styling() {
540            draw_text_span(frame, x, y, &node.label, style, max_x);
541        } else {
542            draw_text_span(frame, x, y, &node.label, Style::default(), max_x);
543        }
544
545        // Register hit region for the row
546        if let Some(id) = self.hit_id {
547            let row_area = Rect::new(area.x, y, area.width, 1);
548            frame.register_hit(row_area, id, HitRegion::Content, *current_row as u64);
549        }
550
551        *current_row += 1;
552
553        if !node.expanded {
554            return;
555        }
556
557        let child_count = node.children.len();
558        for (i, child) in node.children.iter().enumerate() {
559            if *current_row >= area.height as usize {
560                break;
561            }
562            is_last.push(i == child_count - 1);
563            self.render_node(child, depth + 1, is_last, area, frame, current_row, deg);
564            is_last.pop();
565        }
566    }
567}
568
569fn filter_node(node: &TreeNode, query_lower: &str) -> Option<TreeNode> {
570    let label_matches = crate::contains_ignore_case(&node.label, query_lower)
571        || node
572            .icon
573            .as_deref()
574            .is_some_and(|icon| crate::contains_ignore_case(icon, query_lower));
575
576    let mut filtered_children = Vec::new();
577    for child in &node.children {
578        if let Some(filtered) = filter_node(child, query_lower) {
579            filtered_children.push(filtered);
580        }
581    }
582
583    let mut filtered_lazy = Vec::new();
584    if let Some(lazy) = &node.lazy_children {
585        for child in lazy {
586            if let Some(filtered) = filter_node(child, query_lower) {
587                filtered_lazy.push(filtered);
588            }
589        }
590    }
591
592    if !label_matches && filtered_children.is_empty() && filtered_lazy.is_empty() {
593        return None;
594    }
595
596    let mut filtered = node.clone();
597    if label_matches {
598        // Search-mode render walks `children`, not `lazy_children`. When a node
599        // itself matches, render the same full subtree that path bookkeeping already
600        // exposes by eagerly materializing lazy descendants in the filtered clone.
601        filtered.materialize_all_lazy_children();
602        filtered.expanded = true;
603    } else {
604        // Materialize filtered lazy matches into `children` so render/flatten traversal,
605        // which walks `children`, includes lazy descendants that matched the query.
606        filtered.children = filtered_children;
607        filtered.children.extend(filtered_lazy);
608        filtered.lazy_children = None;
609        filtered.expanded = true;
610    }
611    Some(filtered)
612}
613
614struct FilteredPathNode {
615    expanded: bool,
616    children: Vec<(usize, FilteredPathNode)>,
617}
618
619fn create_unfiltered_path_node(node: &TreeNode) -> FilteredPathNode {
620    let mut children = Vec::new();
621    for (idx, child) in node.children.iter().enumerate() {
622        children.push((idx, create_unfiltered_path_node(child)));
623    }
624    let lazy_offset = node.children.len();
625    if let Some(lazy) = &node.lazy_children {
626        for (idx, child) in lazy.iter().enumerate() {
627            children.push((lazy_offset + idx, create_unfiltered_path_node(child)));
628        }
629    }
630    FilteredPathNode {
631        expanded: node.expanded,
632        children,
633    }
634}
635
636fn filter_node_paths(
637    node: &TreeNode,
638    query_lower: &str,
639) -> Option<(bool, Vec<(usize, FilteredPathNode)>)> {
640    let label_matches = crate::contains_ignore_case(&node.label, query_lower)
641        || node
642            .icon
643            .as_deref()
644            .is_some_and(|icon| crate::contains_ignore_case(icon, query_lower));
645
646    if label_matches {
647        let mut children = Vec::new();
648        for (idx, child) in node.children.iter().enumerate() {
649            children.push((idx, create_unfiltered_path_node(child)));
650        }
651        let lazy_offset = node.children.len();
652        if let Some(lazy) = &node.lazy_children {
653            for (idx, child) in lazy.iter().enumerate() {
654                children.push((lazy_offset + idx, create_unfiltered_path_node(child)));
655            }
656        }
657        return Some((true, children));
658    }
659
660    let mut filtered_children = Vec::new();
661    for (idx, child) in node.children.iter().enumerate() {
662        if let Some(filtered) = filter_node_paths(child, query_lower) {
663            filtered_children.push((
664                idx,
665                FilteredPathNode {
666                    expanded: filtered.0,
667                    children: filtered.1,
668                },
669            ));
670        }
671    }
672
673    let mut filtered_lazy = Vec::new();
674    let lazy_offset = node.children.len();
675    if let Some(lazy) = &node.lazy_children {
676        for (idx, child) in lazy.iter().enumerate() {
677            if let Some(filtered) = filter_node_paths(child, query_lower) {
678                filtered_lazy.push((
679                    lazy_offset + idx,
680                    FilteredPathNode {
681                        expanded: filtered.0,
682                        children: filtered.1,
683                    },
684                ));
685            }
686        }
687    }
688
689    if filtered_children.is_empty() && filtered_lazy.is_empty() {
690        return None;
691    }
692
693    filtered_children.extend(filtered_lazy);
694    Some((true, filtered_children))
695}
696
697impl Widget for Tree {
698    fn render(&self, area: Rect, frame: &mut Frame) {
699        if area.width == 0 || area.height == 0 {
700            return;
701        }
702
703        #[cfg(feature = "tracing")]
704        let render_start = Instant::now();
705        #[cfg(feature = "tracing")]
706        let total_nodes = self.total_nodes();
707        #[cfg(feature = "tracing")]
708        let visible_nodes = self.visible_nodes();
709        #[cfg(feature = "tracing")]
710        let expanded_count = self.expanded_nodes();
711        #[cfg(feature = "tracing")]
712        let render_span = tracing::debug_span!(
713            "tree.render",
714            total_nodes,
715            visible_nodes,
716            expanded_count,
717            render_duration_us = tracing::field::Empty,
718        );
719        #[cfg(feature = "tracing")]
720        let _render_guard = render_span.enter();
721
722        let deg = frame.buffer.degradation;
723        let base_style = if deg.apply_styling() {
724            self.label_style
725        } else {
726            Style::default()
727        };
728        clear_text_area(frame, area, base_style);
729        let mut current_row = 0;
730        let mut is_last = Vec::with_capacity(8);
731
732        let search_query = self
733            .search_query
734            .as_deref()
735            .filter(|q| !q.trim().is_empty());
736        let is_searching = search_query.is_some();
737        let filtered_root = if let Some(q) = search_query {
738            let query_lower = q.trim().to_lowercase();
739            filter_node(&self.root, &query_lower)
740        } else {
741            Some(self.root.clone())
742        };
743
744        if is_searching && filtered_root.is_none() {
745            #[cfg(feature = "tracing")]
746            {
747                let elapsed = render_start.elapsed();
748                let elapsed_us = elapsed.as_micros() as u64;
749                render_span.record("render_duration_us", elapsed_us);
750                tracing::debug!(
751                    render_duration_us = elapsed_us,
752                    "Tree render complete (empty search)"
753                );
754            }
755            return;
756        }
757
758        let root_fallback = self.root.clone();
759        let root = filtered_root.as_ref().unwrap_or(&root_fallback);
760
761        if self.show_root {
762            self.render_node(root, 0, &mut is_last, area, frame, &mut current_row, deg);
763        } else if root.expanded {
764            // If root is hidden but expanded, render children as top-level nodes.
765            // We do NOT push to is_last for the root level, effectively shifting
766            // the hierarchy up by one level.
767            let child_count = root.children.len();
768            for (i, child) in root.children.iter().enumerate() {
769                is_last.push(i == child_count - 1);
770                self.render_node(
771                    child,
772                    0, // Children become depth 0
773                    &mut is_last,
774                    area,
775                    frame,
776                    &mut current_row,
777                    deg,
778                );
779                is_last.pop();
780            }
781        }
782
783        #[cfg(feature = "tracing")]
784        {
785            let elapsed = render_start.elapsed();
786            let elapsed_us = elapsed.as_micros() as u64;
787            render_span.record("render_duration_us", elapsed_us);
788            tracing::debug!(
789                message = "tree.metrics",
790                tree_render_duration_us = elapsed_us,
791                total_nodes,
792                visible_nodes,
793                expanded_count
794            );
795        }
796    }
797
798    fn is_essential(&self) -> bool {
799        false
800    }
801}
802
803// ============================================================================
804// Stateful Persistence Implementation
805// ============================================================================
806
807/// Persistable state for a [`Tree`] widget.
808///
809/// Stores the set of expanded node paths to restore tree expansion state.
810#[derive(Clone, Debug, Default, PartialEq)]
811#[cfg_attr(
812    feature = "state-persistence",
813    derive(serde::Serialize, serde::Deserialize)
814)]
815pub struct TreePersistState {
816    /// Set of expanded node paths (e.g., "root/src/main.rs").
817    pub expanded_paths: HashSet<String>,
818}
819
820impl crate::stateful::Stateful for Tree {
821    type State = TreePersistState;
822
823    fn state_key(&self) -> crate::stateful::StateKey {
824        crate::stateful::StateKey::new("Tree", self.persistence_id.as_deref().unwrap_or("default"))
825    }
826
827    fn save_state(&self) -> TreePersistState {
828        let mut expanded_paths = HashSet::new();
829        self.root.collect_expanded("", &mut expanded_paths);
830        TreePersistState { expanded_paths }
831    }
832
833    fn restore_state(&mut self, state: TreePersistState) {
834        self.root.apply_expanded("", &state.expanded_paths);
835    }
836}
837
838// ============================================================================
839// Undo Support Implementation
840// ============================================================================
841
842impl UndoSupport for Tree {
843    fn undo_widget_id(&self) -> UndoWidgetId {
844        self.undo_id
845    }
846
847    fn create_snapshot(&self) -> Box<dyn Any + Send> {
848        Box::new(self.save_state())
849    }
850
851    fn restore_snapshot(&mut self, snapshot: &dyn Any) -> bool {
852        if let Some(snap) = snapshot.downcast_ref::<TreePersistState>() {
853            self.restore_state(snap.clone());
854            true
855        } else {
856            false
857        }
858    }
859}
860
861impl TreeUndoExt for Tree {
862    fn is_node_expanded(&self, path: &[usize]) -> bool {
863        self.get_node_at_path(path)
864            .map(|node| node.is_expanded())
865            .unwrap_or(false)
866    }
867
868    fn expand_node(&mut self, path: &[usize]) {
869        if let Some(node) = self.get_node_at_path_mut(path) {
870            node.materialize_lazy_children();
871            node.expanded = true;
872        }
873    }
874
875    fn collapse_node(&mut self, path: &[usize]) {
876        if let Some(node) = self.get_node_at_path_mut(path) {
877            node.expanded = false;
878        }
879    }
880}
881
882impl Tree {
883    /// Get the undo widget ID for this tree.
884    #[must_use]
885    pub fn undo_id(&self) -> UndoWidgetId {
886        self.undo_id
887    }
888
889    /// Get a reference to a node at the given path (indices from root).
890    fn get_node_at_path(&self, path: &[usize]) -> Option<&TreeNode> {
891        let mut current = &self.root;
892        for &idx in path {
893            current = current.children.get(idx)?;
894        }
895        Some(current)
896    }
897
898    /// Get a mutable reference to a node at the given path (indices from root).
899    fn get_node_at_path_mut(&mut self, path: &[usize]) -> Option<&mut TreeNode> {
900        let mut current = &mut self.root;
901        for &idx in path {
902            current = current.children.get_mut(idx)?;
903        }
904        Some(current)
905    }
906
907    #[cfg(feature = "tracing")]
908    fn log_expand_collapse(action: &str, source: &str, index: usize, label: &str) {
909        tracing::debug!(
910            message = "tree.toggle",
911            action,
912            source,
913            visible_index = index,
914            label
915        );
916    }
917
918    fn toggle_node_at_visible_index(&mut self, index: usize, source: &str) -> bool {
919        #[cfg(not(feature = "tracing"))]
920        let _ = source;
921        let Some(node) = self.node_at_visible_index_mut(index) else {
922            return false;
923        };
924        if !node.has_children() {
925            return false;
926        }
927        #[cfg(feature = "tracing")]
928        let action = if node.is_expanded() {
929            "collapse"
930        } else {
931            "expand"
932        };
933        #[cfg(feature = "tracing")]
934        let label = node.label().to_owned();
935        node.toggle_expanded();
936        #[cfg(feature = "tracing")]
937        Self::log_expand_collapse(action, source, index, &label);
938        true
939    }
940
941    /// Handle keyboard navigation at the currently selected visible row.
942    ///
943    /// Supported keys:
944    /// - **Enter / Space**: Toggle expand/collapse on the selected node.
945    /// - **Right**: Expand the selected node (if collapsed and has children).
946    /// - **Left**: Collapse the selected node (if expanded).
947    ///
948    /// Returns `true` when an expand/collapse action was applied.
949    pub fn handle_key(&mut self, key: &KeyEvent, selected_visible_index: usize) -> bool {
950        match key.code {
951            KeyCode::Enter | KeyCode::Char(' ') => {
952                self.toggle_node_at_visible_index(selected_visible_index, "keyboard")
953            }
954            KeyCode::Right => {
955                // Expand if collapsed and has children.
956                if let Some(node) = self.node_at_visible_index_mut(selected_visible_index)
957                    && !node.is_expanded()
958                    && node.has_children()
959                {
960                    #[cfg(feature = "tracing")]
961                    let label = node.label().to_owned();
962                    node.toggle_expanded();
963                    #[cfg(feature = "tracing")]
964                    Self::log_expand_collapse("expand", "keyboard", selected_visible_index, &label);
965                    return true;
966                }
967                false
968            }
969            KeyCode::Left => {
970                // Collapse if expanded.
971                if let Some(node) = self.node_at_visible_index_mut(selected_visible_index)
972                    && node.is_expanded()
973                    && node.has_children()
974                {
975                    #[cfg(feature = "tracing")]
976                    let label = node.label().to_owned();
977                    node.toggle_expanded();
978                    #[cfg(feature = "tracing")]
979                    Self::log_expand_collapse(
980                        "collapse",
981                        "keyboard",
982                        selected_visible_index,
983                        &label,
984                    );
985                    return true;
986                }
987                false
988            }
989            _ => false,
990        }
991    }
992
993    /// Handle a mouse event for this tree.
994    ///
995    /// # Hit data convention
996    ///
997    /// The hit data (`u64`) encodes the flattened visible row index. When the
998    /// tree renders with a `hit_id`, each visible row registers
999    /// `HitRegion::Content` with `data = visible_row_index as u64`.
1000    ///
1001    /// Clicking a parent node (one with children) toggles its expanded state
1002    /// and returns `Activated`. Clicking a leaf returns `Selected`.
1003    ///
1004    /// # Arguments
1005    ///
1006    /// * `event` — the mouse event from the terminal
1007    /// * `hit` — result of `frame.hit_test(event.x, event.y)`, if available
1008    /// * `expected_id` — the `HitId` this tree was rendered with
1009    pub fn handle_mouse(
1010        &mut self,
1011        event: &MouseEvent,
1012        hit: Option<(HitId, HitRegion, u64)>,
1013        expected_id: HitId,
1014    ) -> MouseResult {
1015        match event.kind {
1016            MouseEventKind::Down(MouseButton::Left) => {
1017                if let Some((id, HitRegion::Content, data)) = hit
1018                    && id == expected_id
1019                {
1020                    let index = data as usize;
1021                    if let Some(node) = self.node_at_visible_index_mut(index)
1022                        && !node.has_children()
1023                    {
1024                        return MouseResult::Selected(index);
1025                    }
1026                    if self.toggle_node_at_visible_index(index, "mouse") {
1027                        return MouseResult::Activated(index);
1028                    }
1029                }
1030                MouseResult::Ignored
1031            }
1032            _ => MouseResult::Ignored,
1033        }
1034    }
1035
1036    /// Get a mutable reference to the node at the given visible (flattened) index.
1037    ///
1038    /// The traversal order matches `render_node()`: if `show_root` is true the
1039    /// root is row 0; otherwise children of the root are the top-level rows.
1040    /// Only expanded nodes' children are visited.
1041    pub fn node_at_visible_index_mut(&mut self, target: usize) -> Option<&mut TreeNode> {
1042        let path = self.find_path_indices_at_visible_index(target)?;
1043
1044        let mut current = &mut self.root;
1045        for &idx in &path {
1046            current.materialize_lazy_children();
1047            current = current.children.get_mut(idx)?;
1048        }
1049        Some(current)
1050    }
1051
1052    fn find_path_indices_at_visible_index(&self, target: usize) -> Option<Vec<usize>> {
1053        let query = self
1054            .search_query
1055            .as_deref()
1056            .map(str::trim)
1057            .filter(|q| !q.is_empty());
1058        let mut counter = 0usize;
1059        let mut path = Vec::new();
1060
1061        if let Some(q) = query {
1062            let query_lower = q.to_lowercase();
1063            let (expanded, children) = filter_node_paths(&self.root, &query_lower)?;
1064            let root_node = FilteredPathNode { expanded, children };
1065
1066            if self.show_root {
1067                Self::walk_filtered_path(&root_node, target, &mut counter, &mut path)
1068            } else if root_node.expanded {
1069                for &(idx, ref child) in &root_node.children {
1070                    path.push(idx);
1071                    if let Some(p) =
1072                        Self::walk_filtered_path(child, target, &mut counter, &mut path)
1073                    {
1074                        return Some(p);
1075                    }
1076                    path.pop();
1077                }
1078                None
1079            } else {
1080                None
1081            }
1082        } else {
1083            if self.show_root {
1084                Self::walk_visible_index_path(&self.root, target, &mut counter, &mut path)
1085            } else if self.root.expanded {
1086                for (idx, child) in self.root.children.iter().enumerate() {
1087                    path.push(idx);
1088                    if let Some(p) =
1089                        Self::walk_visible_index_path(child, target, &mut counter, &mut path)
1090                    {
1091                        return Some(p);
1092                    }
1093                    path.pop();
1094                }
1095                None
1096            } else {
1097                None
1098            }
1099        }
1100    }
1101
1102    fn walk_filtered_path(
1103        node: &FilteredPathNode,
1104        target: usize,
1105        counter: &mut usize,
1106        current_path: &mut Vec<usize>,
1107    ) -> Option<Vec<usize>> {
1108        if *counter == target {
1109            return Some(current_path.clone());
1110        }
1111        *counter += 1;
1112        if node.expanded {
1113            for &(idx, ref child) in &node.children {
1114                current_path.push(idx);
1115                if let Some(found) = Self::walk_filtered_path(child, target, counter, current_path)
1116                {
1117                    return Some(found);
1118                }
1119                current_path.pop();
1120            }
1121        }
1122        None
1123    }
1124
1125    fn walk_visible_index_path(
1126        node: &TreeNode,
1127        target: usize,
1128        counter: &mut usize,
1129        current_path: &mut Vec<usize>,
1130    ) -> Option<Vec<usize>> {
1131        if *counter == target {
1132            return Some(current_path.clone());
1133        }
1134        *counter += 1;
1135        if node.expanded {
1136            for (idx, child) in node.children.iter().enumerate() {
1137                current_path.push(idx);
1138                if let Some(found) =
1139                    Self::walk_visible_index_path(child, target, counter, current_path)
1140                {
1141                    return Some(found);
1142                }
1143                current_path.pop();
1144            }
1145        }
1146        None
1147    }
1148}
1149
1150// ---------------------------------------------------------------------------
1151// Test-only flatten helpers
1152// ---------------------------------------------------------------------------
1153
1154#[cfg(test)]
1155#[derive(Debug, Clone, PartialEq, Eq)]
1156struct FlatNode {
1157    label: String,
1158    depth: usize,
1159}
1160
1161#[cfg(test)]
1162fn flatten_visible(node: &TreeNode, depth: usize, out: &mut Vec<FlatNode>) {
1163    out.push(FlatNode {
1164        label: node.label.clone(),
1165        depth,
1166    });
1167    if node.expanded {
1168        for child in &node.children {
1169            flatten_visible(child, depth + 1, out);
1170        }
1171    }
1172}
1173
1174#[cfg(test)]
1175impl Tree {
1176    fn flatten(&self) -> Vec<FlatNode> {
1177        let mut out = Vec::new();
1178        let search_query = self
1179            .search_query
1180            .as_deref()
1181            .filter(|q| !q.trim().is_empty());
1182        let is_searching = search_query.is_some();
1183        let filtered_root = if let Some(q) = search_query {
1184            let query_lower = q.trim().to_lowercase();
1185            filter_node(&self.root, &query_lower)
1186        } else {
1187            Some(self.root.clone())
1188        };
1189
1190        if is_searching && filtered_root.is_none() {
1191            return out;
1192        }
1193
1194        let root_fallback = self.root.clone();
1195        let root = filtered_root.as_ref().unwrap_or(&root_fallback);
1196        if self.show_root {
1197            flatten_visible(root, 0, &mut out);
1198        } else if root.expanded {
1199            for child in &root.children {
1200                flatten_visible(child, 0, &mut out);
1201            }
1202        }
1203        out
1204    }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209    use super::*;
1210    use ftui_render::frame::Frame;
1211    use ftui_render::grapheme_pool::GraphemePool;
1212    #[cfg(feature = "tracing")]
1213    use std::sync::{Arc, Mutex};
1214    #[cfg(feature = "tracing")]
1215    use tracing::Subscriber;
1216    #[cfg(feature = "tracing")]
1217    use tracing_subscriber::Layer;
1218    #[cfg(feature = "tracing")]
1219    use tracing_subscriber::layer::{Context, SubscriberExt};
1220
1221    fn line_text(frame: &Frame, y: u16, width: u16) -> String {
1222        (0..width)
1223            .map(|x| {
1224                frame
1225                    .buffer
1226                    .get(x, y)
1227                    .and_then(|cell| cell.content.as_char())
1228                    .unwrap_or(' ')
1229            })
1230            .collect()
1231    }
1232
1233    fn simple_tree() -> TreeNode {
1234        TreeNode::new("root")
1235            .child(
1236                TreeNode::new("a")
1237                    .child(TreeNode::new("a1"))
1238                    .child(TreeNode::new("a2")),
1239            )
1240            .child(TreeNode::new("b"))
1241    }
1242
1243    #[cfg(feature = "tracing")]
1244    #[derive(Debug, Default)]
1245    struct TreeTraceState {
1246        tree_render_seen: bool,
1247        has_total_nodes_field: bool,
1248        has_visible_nodes_field: bool,
1249        has_expanded_count_field: bool,
1250        render_duration_recorded: bool,
1251        toggle_events: usize,
1252    }
1253
1254    #[cfg(feature = "tracing")]
1255    struct TreeTraceCapture {
1256        state: Arc<Mutex<TreeTraceState>>,
1257    }
1258
1259    #[cfg(feature = "tracing")]
1260    impl<S> Layer<S> for TreeTraceCapture
1261    where
1262        S: Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
1263    {
1264        fn on_new_span(
1265            &self,
1266            attrs: &tracing::span::Attributes<'_>,
1267            _id: &tracing::Id,
1268            _ctx: Context<'_, S>,
1269        ) {
1270            if attrs.metadata().name() != "tree.render" {
1271                return;
1272            }
1273            let fields = attrs.metadata().fields();
1274            let mut state = self.state.lock().expect("tree trace state lock");
1275            state.tree_render_seen = true;
1276            state.has_total_nodes_field |= fields.field("total_nodes").is_some();
1277            state.has_visible_nodes_field |= fields.field("visible_nodes").is_some();
1278            state.has_expanded_count_field |= fields.field("expanded_count").is_some();
1279        }
1280
1281        fn on_record(
1282            &self,
1283            id: &tracing::Id,
1284            values: &tracing::span::Record<'_>,
1285            ctx: Context<'_, S>,
1286        ) {
1287            let Some(span) = ctx.span(id) else {
1288                return;
1289            };
1290            if span.metadata().name() != "tree.render" {
1291                return;
1292            }
1293
1294            struct DurationVisitor {
1295                saw_duration: bool,
1296            }
1297            impl tracing::field::Visit for DurationVisitor {
1298                fn record_u64(&mut self, field: &tracing::field::Field, _value: u64) {
1299                    if field.name() == "render_duration_us" {
1300                        self.saw_duration = true;
1301                    }
1302                }
1303
1304                fn record_debug(
1305                    &mut self,
1306                    field: &tracing::field::Field,
1307                    _value: &dyn std::fmt::Debug,
1308                ) {
1309                    if field.name() == "render_duration_us" {
1310                        self.saw_duration = true;
1311                    }
1312                }
1313            }
1314
1315            let mut visitor = DurationVisitor {
1316                saw_duration: false,
1317            };
1318            values.record(&mut visitor);
1319            if visitor.saw_duration {
1320                self.state
1321                    .lock()
1322                    .expect("tree trace state lock")
1323                    .render_duration_recorded = true;
1324            }
1325        }
1326
1327        fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
1328            struct MessageVisitor {
1329                message: Option<String>,
1330            }
1331            impl tracing::field::Visit for MessageVisitor {
1332                fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
1333                    if field.name() == "message" {
1334                        self.message = Some(value.to_owned());
1335                    }
1336                }
1337
1338                fn record_debug(
1339                    &mut self,
1340                    field: &tracing::field::Field,
1341                    value: &dyn std::fmt::Debug,
1342                ) {
1343                    if field.name() == "message" {
1344                        self.message = Some(format!("{value:?}").trim_matches('"').to_owned());
1345                    }
1346                }
1347            }
1348
1349            let mut visitor = MessageVisitor { message: None };
1350            event.record(&mut visitor);
1351            if visitor.message.as_deref() == Some("tree.toggle") {
1352                let mut state = self.state.lock().expect("tree trace state lock");
1353                state.toggle_events = state.toggle_events.saturating_add(1);
1354            }
1355        }
1356    }
1357
1358    #[test]
1359    fn tree_node_basics() {
1360        let node = TreeNode::new("hello");
1361        assert_eq!(node.label(), "hello");
1362        assert!(node.children().is_empty());
1363        assert!(node.is_expanded());
1364    }
1365
1366    #[test]
1367    fn tree_node_children() {
1368        let root = simple_tree();
1369        assert_eq!(root.children().len(), 2);
1370        assert_eq!(root.children()[0].label(), "a");
1371        assert_eq!(root.children()[0].children().len(), 2);
1372    }
1373
1374    #[test]
1375    fn tree_node_visible_count() {
1376        let root = simple_tree();
1377        // root + a + a1 + a2 + b = 5
1378        assert_eq!(root.visible_count(), 5);
1379    }
1380
1381    #[test]
1382    fn tree_node_collapsed() {
1383        let root = TreeNode::new("root")
1384            .child(
1385                TreeNode::new("a")
1386                    .with_expanded(false)
1387                    .child(TreeNode::new("a1"))
1388                    .child(TreeNode::new("a2")),
1389            )
1390            .child(TreeNode::new("b"));
1391        // root + a (collapsed, so no a1/a2) + b = 3
1392        assert_eq!(root.visible_count(), 3);
1393    }
1394
1395    #[test]
1396    fn tree_node_toggle() {
1397        let mut node = TreeNode::new("x");
1398        assert!(node.is_expanded());
1399        node.toggle_expanded();
1400        assert!(!node.is_expanded());
1401        node.toggle_expanded();
1402        assert!(node.is_expanded());
1403    }
1404
1405    #[test]
1406    fn tree_node_lazy_children_materialize_on_expand() {
1407        let mut node = TreeNode::new("root")
1408            .with_lazy_children(vec![TreeNode::new("child"), TreeNode::new("child2")]);
1409        assert!(!node.is_expanded());
1410        assert_eq!(node.children().len(), 0);
1411        assert!(node.has_children());
1412
1413        node.toggle_expanded();
1414        assert!(node.is_expanded());
1415        assert_eq!(node.children().len(), 2);
1416    }
1417
1418    #[test]
1419    fn tree_guides_unicode() {
1420        let g = TreeGuides::Unicode;
1421        assert!(g.branch().contains('├'));
1422        assert!(g.last().contains('└'));
1423        assert!(g.vertical().contains('│'));
1424    }
1425
1426    #[test]
1427    fn tree_guides_ascii() {
1428        let g = TreeGuides::Ascii;
1429        assert!(g.branch().contains('+'));
1430        assert!(g.vertical().contains('|'));
1431    }
1432
1433    #[test]
1434    fn tree_guides_width() {
1435        for g in [
1436            TreeGuides::Ascii,
1437            TreeGuides::Unicode,
1438            TreeGuides::Bold,
1439            TreeGuides::Double,
1440            TreeGuides::Rounded,
1441        ] {
1442            assert_eq!(g.width(), 4);
1443        }
1444    }
1445
1446    #[test]
1447    fn tree_render_basic() {
1448        let tree = Tree::new(simple_tree());
1449
1450        let mut pool = GraphemePool::new();
1451        let mut frame = Frame::new(40, 10, &mut pool);
1452        let area = Rect::new(0, 0, 40, 10);
1453        tree.render(area, &mut frame);
1454
1455        // Root label at (0, 0)
1456        let cell = frame.buffer.get(0, 0).unwrap();
1457        assert_eq!(cell.content.as_char(), Some('r'));
1458    }
1459
1460    #[test]
1461    fn tree_render_guides_present() {
1462        let tree = Tree::new(simple_tree()).with_guides(TreeGuides::Ascii);
1463
1464        let mut pool = GraphemePool::new();
1465        let mut frame = Frame::new(40, 10, &mut pool);
1466        let area = Rect::new(0, 0, 40, 10);
1467        tree.render(area, &mut frame);
1468
1469        // Row 1 should be child "a" with branch guide "+-- "
1470        // First char of guide at (0, 1)
1471        let cell = frame.buffer.get(0, 1).unwrap();
1472        assert_eq!(cell.content.as_char(), Some('+'));
1473    }
1474
1475    #[test]
1476    fn tree_render_last_guide() {
1477        let tree = Tree::new(
1478            TreeNode::new("root")
1479                .child(TreeNode::new("a"))
1480                .child(TreeNode::new("b")),
1481        )
1482        .with_guides(TreeGuides::Ascii);
1483
1484        let mut pool = GraphemePool::new();
1485        let mut frame = Frame::new(40, 10, &mut pool);
1486        let area = Rect::new(0, 0, 40, 10);
1487        tree.render(area, &mut frame);
1488
1489        // Row 1: "+-- a" (not last)
1490        let cell = frame.buffer.get(0, 1).unwrap();
1491        assert_eq!(cell.content.as_char(), Some('+'));
1492
1493        // Row 2: "`-- b" (last child)
1494        let cell = frame.buffer.get(0, 2).unwrap();
1495        assert_eq!(cell.content.as_char(), Some('`'));
1496    }
1497
1498    #[test]
1499    fn tree_render_icon_before_label() {
1500        let tree = Tree::new(TreeNode::new("root").with_icon(">"));
1501        let mut pool = GraphemePool::new();
1502        let mut frame = Frame::new(12, 2, &mut pool);
1503        tree.render(Rect::new(0, 0, 12, 2), &mut frame);
1504
1505        assert_eq!(
1506            frame.buffer.get(0, 0).and_then(|c| c.content.as_char()),
1507            Some('>')
1508        );
1509        assert_eq!(
1510            frame.buffer.get(2, 0).and_then(|c| c.content.as_char()),
1511            Some('r')
1512        );
1513    }
1514
1515    #[test]
1516    fn tree_search_query_filters_to_matching_branches() {
1517        let tree = Tree::new(
1518            TreeNode::new("root")
1519                .child(TreeNode::new("alpha").child(TreeNode::new("target-file")))
1520                .child(TreeNode::new("beta")),
1521        )
1522        .with_search_query("target");
1523
1524        let flat = tree.flatten();
1525        assert_eq!(flat.len(), 3);
1526        assert_eq!(flat[0].label, "root");
1527        assert_eq!(flat[1].label, "alpha");
1528        assert_eq!(flat[2].label, "target-file");
1529    }
1530
1531    #[test]
1532    fn tree_search_query_includes_lazy_matching_descendants() {
1533        let tree =
1534            Tree::new(TreeNode::new("root").child(
1535                TreeNode::new("alpha").with_lazy_children(vec![TreeNode::new("target-file")]),
1536            ))
1537            .with_search_query("target");
1538
1539        let flat = tree.flatten();
1540        assert_eq!(flat.len(), 3);
1541        assert_eq!(flat[0].label, "root");
1542        assert_eq!(flat[1].label, "alpha");
1543        assert_eq!(flat[2].label, "target-file");
1544    }
1545
1546    #[test]
1547    fn tree_search_query_on_matching_parent_includes_immediate_lazy_children() {
1548        let tree = Tree::new(TreeNode::new("root").child(
1549            TreeNode::new("alpha").with_lazy_children(vec![
1550                TreeNode::new("lazy-child")
1551                    .with_lazy_children(vec![TreeNode::new("deep-grandchild")]),
1552            ]),
1553        ))
1554        .with_search_query("alpha");
1555
1556        let flat = tree.flatten();
1557        assert_eq!(flat.len(), 3);
1558        assert_eq!(flat[0].label, "root");
1559        assert_eq!(flat[1].label, "alpha");
1560        assert_eq!(flat[2].label, "lazy-child");
1561    }
1562
1563    #[test]
1564    fn tree_render_zero_area() {
1565        let tree = Tree::new(simple_tree());
1566        let mut pool = GraphemePool::new();
1567        let mut frame = Frame::new(40, 10, &mut pool);
1568        tree.render(Rect::new(0, 0, 0, 0), &mut frame); // No panic
1569    }
1570
1571    #[test]
1572    fn tree_render_shorter_label_clears_stale_suffix() {
1573        let mut pool = GraphemePool::new();
1574        let mut frame = Frame::new(20, 3, &mut pool);
1575        let area = Rect::new(0, 0, 20, 3);
1576
1577        Tree::new(TreeNode::new("root-with-long-tail")).render(area, &mut frame);
1578        Tree::new(TreeNode::new("r")).render(area, &mut frame);
1579
1580        assert_eq!(line_text(&frame, 0, 20), "r                   ");
1581        assert_eq!(line_text(&frame, 1, 20), "                    ");
1582        assert_eq!(line_text(&frame, 2, 20), "                    ");
1583    }
1584
1585    #[test]
1586    fn tree_render_truncated_height() {
1587        let tree = Tree::new(simple_tree());
1588        let mut pool = GraphemePool::new();
1589        let mut frame = Frame::new(40, 2, &mut pool);
1590        let area = Rect::new(0, 0, 40, 2);
1591        tree.render(area, &mut frame); // Only first 2 rows render, no panic
1592    }
1593
1594    #[test]
1595    fn is_not_essential() {
1596        let tree = Tree::new(TreeNode::new("x"));
1597        assert!(!tree.is_essential());
1598    }
1599
1600    #[test]
1601    fn tree_root_access() {
1602        let mut tree = Tree::new(TreeNode::new("root"));
1603        assert_eq!(tree.root().label(), "root");
1604        tree.root_mut().toggle_expanded();
1605        assert!(!tree.root().is_expanded());
1606    }
1607
1608    #[test]
1609    fn tree_guides_default() {
1610        let g = TreeGuides::default();
1611        assert_eq!(g, TreeGuides::Unicode);
1612    }
1613
1614    #[test]
1615    fn tree_guides_rounded() {
1616        let g = TreeGuides::Rounded;
1617        assert!(g.last().contains('╰'));
1618    }
1619
1620    #[test]
1621    fn tree_deep_nesting() {
1622        let node = TreeNode::new("d3");
1623        let node = TreeNode::new("d2").child(node);
1624        let node = TreeNode::new("d1").child(node);
1625        let root = TreeNode::new("root").child(node);
1626
1627        let tree = Tree::new(root);
1628        let flat = tree.flatten();
1629        assert_eq!(flat.len(), 4);
1630        assert_eq!(flat[3].depth, 3);
1631    }
1632
1633    #[test]
1634    fn tree_node_with_children_vec() {
1635        let root = TreeNode::new("root").with_children(vec![
1636            TreeNode::new("a"),
1637            TreeNode::new("b"),
1638            TreeNode::new("c"),
1639        ]);
1640        assert_eq!(root.children().len(), 3);
1641    }
1642
1643    // --- Stateful Persistence tests ---
1644
1645    use crate::stateful::Stateful;
1646
1647    #[test]
1648    fn tree_with_persistence_id() {
1649        let tree = Tree::new(TreeNode::new("root")).with_persistence_id("file-tree");
1650        assert_eq!(tree.persistence_id(), Some("file-tree"));
1651    }
1652
1653    #[test]
1654    fn tree_default_no_persistence_id() {
1655        let tree = Tree::new(TreeNode::new("root"));
1656        assert_eq!(tree.persistence_id(), None);
1657    }
1658
1659    #[test]
1660    fn tree_save_restore_round_trip() {
1661        // Create tree with some nodes expanded, some collapsed
1662        let mut tree = Tree::new(
1663            TreeNode::new("root")
1664                .child(
1665                    TreeNode::new("src")
1666                        .child(TreeNode::new("main.rs"))
1667                        .child(TreeNode::new("lib.rs")),
1668                )
1669                .child(TreeNode::new("tests").with_expanded(false)),
1670        )
1671        .with_persistence_id("test");
1672
1673        // Verify initial state: root and src expanded, tests collapsed
1674        assert!(tree.root().is_expanded());
1675        assert!(tree.root().children()[0].is_expanded()); // src
1676        assert!(!tree.root().children()[1].is_expanded()); // tests
1677
1678        let saved = tree.save_state();
1679
1680        // Verify saved state captures expanded nodes
1681        assert!(saved.expanded_paths.contains("root"));
1682        assert!(saved.expanded_paths.contains("root/src"));
1683        assert!(!saved.expanded_paths.contains("root/tests"));
1684
1685        // Modify tree state (collapse src)
1686        tree.root_mut().children[0].toggle_expanded();
1687        assert!(!tree.root().children()[0].is_expanded());
1688
1689        // Restore
1690        tree.restore_state(saved);
1691
1692        // Verify restored state
1693        assert!(tree.root().is_expanded());
1694        assert!(tree.root().children()[0].is_expanded()); // src restored
1695        assert!(!tree.root().children()[1].is_expanded()); // tests still collapsed
1696    }
1697
1698    #[test]
1699    fn tree_state_key_uses_persistence_id() {
1700        let tree = Tree::new(TreeNode::new("root")).with_persistence_id("project-explorer");
1701        let key = tree.state_key();
1702        assert_eq!(key.widget_type, "Tree");
1703        assert_eq!(key.instance_id, "project-explorer");
1704    }
1705
1706    #[test]
1707    fn tree_state_key_default_when_no_id() {
1708        let tree = Tree::new(TreeNode::new("root"));
1709        let key = tree.state_key();
1710        assert_eq!(key.widget_type, "Tree");
1711        assert_eq!(key.instance_id, "default");
1712    }
1713
1714    #[test]
1715    fn tree_persist_state_default() {
1716        let persist = TreePersistState::default();
1717        assert!(persist.expanded_paths.is_empty());
1718    }
1719
1720    #[test]
1721    fn tree_collect_expanded_only_includes_nodes_with_children() {
1722        let tree = Tree::new(
1723            TreeNode::new("root").child(TreeNode::new("leaf")), // leaf has no children
1724        );
1725
1726        let saved = tree.save_state();
1727
1728        // Only root is expanded (and has children)
1729        assert!(saved.expanded_paths.contains("root"));
1730        // leaf has no children, so it's not tracked
1731        assert!(!saved.expanded_paths.contains("root/leaf"));
1732    }
1733
1734    // ============================================================================
1735    // Undo Support Tests
1736    // ============================================================================
1737
1738    #[test]
1739    fn tree_undo_widget_id_unique() {
1740        let tree1 = Tree::new(TreeNode::new("root1"));
1741        let tree2 = Tree::new(TreeNode::new("root2"));
1742        assert_ne!(tree1.undo_id(), tree2.undo_id());
1743    }
1744
1745    #[test]
1746    fn tree_undo_snapshot_and_restore() {
1747        // Nodes must have children for their expanded state to be tracked
1748        let mut tree = Tree::new(
1749            TreeNode::new("root")
1750                .child(
1751                    TreeNode::new("a")
1752                        .with_expanded(true)
1753                        .child(TreeNode::new("a_child")),
1754                )
1755                .child(
1756                    TreeNode::new("b")
1757                        .with_expanded(false)
1758                        .child(TreeNode::new("b_child")),
1759                ),
1760        );
1761
1762        // Create snapshot
1763        let snapshot = tree.create_snapshot();
1764
1765        // Verify initial state
1766        assert!(tree.is_node_expanded(&[0])); // a
1767        assert!(!tree.is_node_expanded(&[1])); // b
1768
1769        // Modify state
1770        tree.collapse_node(&[0]); // collapse a
1771        tree.expand_node(&[1]); // expand b
1772        assert!(!tree.is_node_expanded(&[0]));
1773        assert!(tree.is_node_expanded(&[1]));
1774
1775        // Restore snapshot
1776        assert!(tree.restore_snapshot(&*snapshot));
1777
1778        // Verify restored state
1779        assert!(tree.is_node_expanded(&[0])); // a back to expanded
1780        assert!(!tree.is_node_expanded(&[1])); // b back to collapsed
1781    }
1782
1783    #[test]
1784    fn tree_expand_collapse_node() {
1785        let mut tree =
1786            Tree::new(TreeNode::new("root").child(TreeNode::new("child").with_expanded(true)));
1787
1788        // Initial state
1789        assert!(tree.is_node_expanded(&[0]));
1790
1791        // Collapse
1792        tree.collapse_node(&[0]);
1793        assert!(!tree.is_node_expanded(&[0]));
1794
1795        // Expand again
1796        tree.expand_node(&[0]);
1797        assert!(tree.is_node_expanded(&[0]));
1798    }
1799
1800    #[test]
1801    fn tree_node_path_navigation() {
1802        let tree = Tree::new(
1803            TreeNode::new("root")
1804                .child(
1805                    TreeNode::new("a")
1806                        .child(TreeNode::new("a1"))
1807                        .child(TreeNode::new("a2")),
1808                )
1809                .child(TreeNode::new("b")),
1810        );
1811
1812        // Test path navigation
1813        assert_eq!(tree.get_node_at_path(&[]).map(|n| n.label()), Some("root"));
1814        assert_eq!(tree.get_node_at_path(&[0]).map(|n| n.label()), Some("a"));
1815        assert_eq!(tree.get_node_at_path(&[1]).map(|n| n.label()), Some("b"));
1816        assert_eq!(
1817            tree.get_node_at_path(&[0, 0]).map(|n| n.label()),
1818            Some("a1")
1819        );
1820        assert_eq!(
1821            tree.get_node_at_path(&[0, 1]).map(|n| n.label()),
1822            Some("a2")
1823        );
1824        assert!(tree.get_node_at_path(&[5]).is_none()); // Invalid path
1825    }
1826
1827    #[test]
1828    fn tree_restore_wrong_snapshot_type_fails() {
1829        use std::any::Any;
1830        let mut tree = Tree::new(TreeNode::new("root"));
1831        let wrong_snapshot: Box<dyn Any + Send> = Box::new(42i32);
1832        assert!(!tree.restore_snapshot(&*wrong_snapshot));
1833    }
1834
1835    // --- Mouse handling tests ---
1836
1837    use crate::mouse::MouseResult;
1838    use ftui_core::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
1839
1840    #[test]
1841    fn tree_click_expands_parent() {
1842        let mut tree = Tree::new(
1843            TreeNode::new("root")
1844                .child(
1845                    TreeNode::new("a")
1846                        .child(TreeNode::new("a1"))
1847                        .child(TreeNode::new("a2")),
1848                )
1849                .child(TreeNode::new("b")),
1850        );
1851        assert!(tree.root().children()[0].is_expanded());
1852
1853        // Click on row 1 which is node "a" (a parent node)
1854        let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 5, 1);
1855        let hit = Some((HitId::new(1), HitRegion::Content, 1u64));
1856        let result = tree.handle_mouse(&event, hit, HitId::new(1));
1857        assert_eq!(result, MouseResult::Activated(1));
1858        assert!(!tree.root().children()[0].is_expanded()); // toggled to collapsed
1859    }
1860
1861    #[test]
1862    fn tree_click_selects_leaf() {
1863        let mut tree = Tree::new(
1864            TreeNode::new("root")
1865                .child(
1866                    TreeNode::new("a")
1867                        .child(TreeNode::new("a1"))
1868                        .child(TreeNode::new("a2")),
1869                )
1870                .child(TreeNode::new("b")),
1871        );
1872
1873        // Row 4 is "b" (a leaf): root=0, a=1, a1=2, a2=3, b=4
1874        let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 5, 4);
1875        let hit = Some((HitId::new(1), HitRegion::Content, 4u64));
1876        let result = tree.handle_mouse(&event, hit, HitId::new(1));
1877        assert_eq!(result, MouseResult::Selected(4));
1878    }
1879
1880    #[test]
1881    fn tree_click_wrong_id_ignored() {
1882        let mut tree = Tree::new(TreeNode::new("root").child(TreeNode::new("a")));
1883        let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
1884        let hit = Some((HitId::new(99), HitRegion::Content, 0u64));
1885        let result = tree.handle_mouse(&event, hit, HitId::new(1));
1886        assert_eq!(result, MouseResult::Ignored);
1887    }
1888
1889    #[test]
1890    fn tree_click_no_hit_ignored() {
1891        let mut tree = Tree::new(TreeNode::new("root"));
1892        let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 0);
1893        let result = tree.handle_mouse(&event, None, HitId::new(1));
1894        assert_eq!(result, MouseResult::Ignored);
1895    }
1896
1897    #[test]
1898    fn tree_right_click_ignored() {
1899        let mut tree = Tree::new(TreeNode::new("root").child(TreeNode::new("a")));
1900        let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Right), 0, 0);
1901        let hit = Some((HitId::new(1), HitRegion::Content, 0u64));
1902        let result = tree.handle_mouse(&event, hit, HitId::new(1));
1903        assert_eq!(result, MouseResult::Ignored);
1904    }
1905
1906    #[test]
1907    fn tree_node_at_visible_index_with_show_root() {
1908        let mut tree = Tree::new(
1909            TreeNode::new("root")
1910                .child(
1911                    TreeNode::new("a")
1912                        .child(TreeNode::new("a1"))
1913                        .child(TreeNode::new("a2")),
1914                )
1915                .child(TreeNode::new("b")),
1916        );
1917
1918        // Visible order: root=0, a=1, a1=2, a2=3, b=4
1919        assert_eq!(
1920            tree.node_at_visible_index_mut(0)
1921                .map(|n| n.label().to_string()),
1922            Some("root".to_string())
1923        );
1924        assert_eq!(
1925            tree.node_at_visible_index_mut(1)
1926                .map(|n| n.label().to_string()),
1927            Some("a".to_string())
1928        );
1929        assert_eq!(
1930            tree.node_at_visible_index_mut(2)
1931                .map(|n| n.label().to_string()),
1932            Some("a1".to_string())
1933        );
1934        assert_eq!(
1935            tree.node_at_visible_index_mut(3)
1936                .map(|n| n.label().to_string()),
1937            Some("a2".to_string())
1938        );
1939        assert_eq!(
1940            tree.node_at_visible_index_mut(4)
1941                .map(|n| n.label().to_string()),
1942            Some("b".to_string())
1943        );
1944        assert!(tree.node_at_visible_index_mut(5).is_none());
1945    }
1946
1947    #[test]
1948    fn tree_node_at_visible_index_hidden_root() {
1949        let mut tree = Tree::new(
1950            TreeNode::new("root")
1951                .child(TreeNode::new("a").child(TreeNode::new("a1")))
1952                .child(TreeNode::new("b")),
1953        )
1954        .with_show_root(false);
1955
1956        // Root hidden: a=0, a1=1, b=2
1957        assert_eq!(
1958            tree.node_at_visible_index_mut(0)
1959                .map(|n| n.label().to_string()),
1960            Some("a".to_string())
1961        );
1962        assert_eq!(
1963            tree.node_at_visible_index_mut(1)
1964                .map(|n| n.label().to_string()),
1965            Some("a1".to_string())
1966        );
1967        assert_eq!(
1968            tree.node_at_visible_index_mut(2)
1969                .map(|n| n.label().to_string()),
1970            Some("b".to_string())
1971        );
1972        assert!(tree.node_at_visible_index_mut(3).is_none());
1973    }
1974
1975    #[test]
1976    fn tree_node_at_visible_index_collapsed() {
1977        let mut tree = Tree::new(
1978            TreeNode::new("root")
1979                .child(
1980                    TreeNode::new("a")
1981                        .with_expanded(false)
1982                        .child(TreeNode::new("a1"))
1983                        .child(TreeNode::new("a2")),
1984                )
1985                .child(TreeNode::new("b")),
1986        );
1987
1988        // root=0, a=1 (collapsed, so a1/a2 hidden), b=2
1989        assert_eq!(
1990            tree.node_at_visible_index_mut(0)
1991                .map(|n| n.label().to_string()),
1992            Some("root".to_string())
1993        );
1994        assert_eq!(
1995            tree.node_at_visible_index_mut(1)
1996                .map(|n| n.label().to_string()),
1997            Some("a".to_string())
1998        );
1999        assert_eq!(
2000            tree.node_at_visible_index_mut(2)
2001                .map(|n| n.label().to_string()),
2002            Some("b".to_string())
2003        );
2004        assert!(tree.node_at_visible_index_mut(3).is_none());
2005    }
2006
2007    #[test]
2008    fn tree_click_toggles_collapsed_node() {
2009        let mut tree = Tree::new(
2010            TreeNode::new("root")
2011                .child(
2012                    TreeNode::new("a")
2013                        .with_expanded(false)
2014                        .child(TreeNode::new("a1")),
2015                )
2016                .child(TreeNode::new("b")),
2017        );
2018        assert!(!tree.root().children()[0].is_expanded());
2019
2020        // Click on "a" (row 1) to expand it
2021        let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 0, 1);
2022        let hit = Some((HitId::new(1), HitRegion::Content, 1u64));
2023        let result = tree.handle_mouse(&event, hit, HitId::new(1));
2024        assert_eq!(result, MouseResult::Activated(1));
2025        assert!(tree.root().children()[0].is_expanded()); // now expanded
2026    }
2027
2028    #[test]
2029    fn tree_handle_key_enter_toggles_parent() {
2030        let mut tree = Tree::new(
2031            TreeNode::new("root")
2032                .child(TreeNode::new("a").child(TreeNode::new("a1")))
2033                .child(TreeNode::new("b")),
2034        );
2035
2036        // root=0, a=1, a1=2, b=3
2037        assert!(tree.root().children()[0].is_expanded());
2038        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Enter), 1));
2039        assert!(!tree.root().children()[0].is_expanded());
2040        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Char(' ')), 1));
2041        assert!(tree.root().children()[0].is_expanded());
2042    }
2043
2044    // =========================================================================
2045    // Keyboard navigation tests (bd-1lg.26)
2046    // =========================================================================
2047
2048    #[test]
2049    fn tree_handle_key_right_expands_collapsed_node() {
2050        let mut tree = Tree::new(
2051            TreeNode::new("root")
2052                .child(
2053                    TreeNode::new("a")
2054                        .with_expanded(false)
2055                        .child(TreeNode::new("a1")),
2056                )
2057                .child(TreeNode::new("b")),
2058        );
2059        assert!(!tree.root().children()[0].is_expanded());
2060
2061        // Right arrow on collapsed parent should expand it.
2062        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2063        assert!(tree.root().children()[0].is_expanded());
2064    }
2065
2066    #[test]
2067    fn tree_handle_key_right_on_expanded_node_is_noop() {
2068        let mut tree = Tree::new(
2069            TreeNode::new("root")
2070                .child(TreeNode::new("a").child(TreeNode::new("a1")))
2071                .child(TreeNode::new("b")),
2072        );
2073        assert!(tree.root().children()[0].is_expanded());
2074
2075        // Right arrow on already-expanded node should be no-op.
2076        assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2077        assert!(tree.root().children()[0].is_expanded());
2078    }
2079
2080    #[test]
2081    fn tree_handle_key_right_on_leaf_is_noop() {
2082        let mut tree = Tree::new(
2083            TreeNode::new("root")
2084                .child(TreeNode::new("a").child(TreeNode::new("a1")))
2085                .child(TreeNode::new("b")),
2086        );
2087
2088        // Right arrow on leaf node "b" (index 3) should be no-op.
2089        assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Right), 3));
2090    }
2091
2092    #[test]
2093    fn tree_handle_key_left_collapses_expanded_node() {
2094        let mut tree = Tree::new(
2095            TreeNode::new("root")
2096                .child(TreeNode::new("a").child(TreeNode::new("a1")))
2097                .child(TreeNode::new("b")),
2098        );
2099        assert!(tree.root().children()[0].is_expanded());
2100
2101        // Left arrow on expanded parent should collapse it.
2102        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2103        assert!(!tree.root().children()[0].is_expanded());
2104    }
2105
2106    #[test]
2107    fn tree_handle_key_left_on_collapsed_node_is_noop() {
2108        let mut tree = Tree::new(
2109            TreeNode::new("root")
2110                .child(
2111                    TreeNode::new("a")
2112                        .with_expanded(false)
2113                        .child(TreeNode::new("a1")),
2114                )
2115                .child(TreeNode::new("b")),
2116        );
2117        assert!(!tree.root().children()[0].is_expanded());
2118
2119        // Left arrow on already-collapsed node should be no-op.
2120        assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2121        assert!(!tree.root().children()[0].is_expanded());
2122    }
2123
2124    #[test]
2125    fn tree_handle_key_left_on_leaf_is_noop() {
2126        let mut tree = Tree::new(
2127            TreeNode::new("root")
2128                .child(TreeNode::new("a").child(TreeNode::new("a1")))
2129                .child(TreeNode::new("b")),
2130        );
2131
2132        // Left arrow on leaf node "b" (index 3) should be no-op.
2133        assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Left), 3));
2134    }
2135
2136    #[test]
2137    fn tree_handle_key_left_right_round_trip() {
2138        let mut tree = Tree::new(
2139            TreeNode::new("root")
2140                .child(TreeNode::new("a").child(TreeNode::new("a1")))
2141                .child(TreeNode::new("b")),
2142        );
2143
2144        // Collapse with Left.
2145        assert!(tree.root().children()[0].is_expanded());
2146        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2147        assert!(!tree.root().children()[0].is_expanded());
2148
2149        // Expand with Right.
2150        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2151        assert!(tree.root().children()[0].is_expanded());
2152    }
2153
2154    #[test]
2155    fn tree_handle_key_unhandled_keys_return_false() {
2156        let mut tree = Tree::new(
2157            TreeNode::new("root")
2158                .child(TreeNode::new("a").child(TreeNode::new("a1")))
2159                .child(TreeNode::new("b")),
2160        );
2161
2162        // Up/Down are not handled by the tree widget (caller manages selection).
2163        assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Up), 1));
2164        assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Down), 1));
2165        assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Tab), 1));
2166        assert!(!tree.handle_key(&KeyEvent::new(KeyCode::Escape), 1));
2167    }
2168
2169    #[test]
2170    fn tree_visible_index_navigation_after_collapse() {
2171        let mut tree = Tree::new(
2172            TreeNode::new("root")
2173                .child(
2174                    TreeNode::new("a")
2175                        .child(TreeNode::new("a1"))
2176                        .child(TreeNode::new("a2")),
2177                )
2178                .child(TreeNode::new("b")),
2179        );
2180
2181        // Before collapse: root=0, a=1, a1=2, a2=3, b=4
2182        assert_eq!(
2183            tree.node_at_visible_index_mut(4)
2184                .map(|n| n.label().to_string()),
2185            Some("b".to_string())
2186        );
2187
2188        // Collapse "a" with Left key
2189        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2190
2191        // After collapse: root=0, a=1, b=2
2192        assert_eq!(
2193            tree.node_at_visible_index_mut(2)
2194                .map(|n| n.label().to_string()),
2195            Some("b".to_string())
2196        );
2197        assert!(tree.node_at_visible_index_mut(3).is_none());
2198    }
2199
2200    // =========================================================================
2201    // Lazy loading tests (bd-1lg.26)
2202    // =========================================================================
2203
2204    #[test]
2205    fn tree_lazy_children_materialized_visible_count() {
2206        let mut node = TreeNode::new("root").with_lazy_children(vec![
2207            TreeNode::new("child1").child(TreeNode::new("grandchild")),
2208            TreeNode::new("child2"),
2209        ]);
2210
2211        // Before expand: only root visible.
2212        assert_eq!(node.visible_count(), 1);
2213
2214        // Expand → lazy children materialize.
2215        node.toggle_expanded();
2216        // root + child1 (expanded) + grandchild + child2 = 4
2217        assert_eq!(node.visible_count(), 4);
2218    }
2219
2220    #[test]
2221    fn tree_lazy_children_second_toggle_collapses() {
2222        let mut node = TreeNode::new("root")
2223            .with_lazy_children(vec![TreeNode::new("child1"), TreeNode::new("child2")]);
2224
2225        // Expand.
2226        node.toggle_expanded();
2227        assert!(node.is_expanded());
2228        assert_eq!(node.children().len(), 2);
2229
2230        // Collapse. Children stay materialized but hidden.
2231        node.toggle_expanded();
2232        assert!(!node.is_expanded());
2233        assert_eq!(node.children().len(), 2); // still there
2234        assert_eq!(node.visible_count(), 1); // only root visible
2235    }
2236
2237    #[test]
2238    fn tree_lazy_children_with_right_key() {
2239        let mut tree = Tree::new(TreeNode::new("root").child(
2240            TreeNode::new("lazy-parent").with_lazy_children(vec![TreeNode::new("lazy-child")]),
2241        ));
2242
2243        // root=0, lazy-parent=1 (collapsed, has lazy children)
2244        assert!(!tree.root().children()[0].is_expanded());
2245        assert!(tree.root().children()[0].has_children());
2246
2247        // Right key should expand and materialize lazy children.
2248        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2249        assert!(tree.root().children()[0].is_expanded());
2250        assert_eq!(tree.root().children()[0].children().len(), 1);
2251        assert_eq!(
2252            tree.root().children()[0].children()[0].label(),
2253            "lazy-child"
2254        );
2255    }
2256
2257    // =========================================================================
2258    // Search filter tests (bd-1lg.26)
2259    // =========================================================================
2260
2261    #[test]
2262    fn tree_search_case_insensitive() {
2263        let tree = Tree::new(
2264            TreeNode::new("root")
2265                .child(TreeNode::new("Alpha"))
2266                .child(TreeNode::new("Beta")),
2267        )
2268        .with_search_query("alpha");
2269
2270        let flat = tree.flatten();
2271        assert_eq!(flat.len(), 2); // root + Alpha
2272        assert_eq!(flat[1].label, "Alpha");
2273    }
2274
2275    #[test]
2276    fn tree_search_no_match_returns_empty() {
2277        let tree = Tree::new(
2278            TreeNode::new("root")
2279                .child(TreeNode::new("Alpha"))
2280                .child(TreeNode::new("Beta")),
2281        )
2282        .with_search_query("zzz-no-match");
2283
2284        let flat = tree.flatten();
2285        // When nothing matches (including root), filter_node returns None,
2286        // so flatten should return an empty tree.
2287        assert_eq!(flat.len(), 0);
2288    }
2289
2290    #[test]
2291    fn tree_search_no_match_clears_stale_rows() {
2292        let base_tree = Tree::new(
2293            TreeNode::new("root")
2294                .child(TreeNode::new("Alpha"))
2295                .child(TreeNode::new("Beta")),
2296        );
2297        let filtered_tree = Tree::new(
2298            TreeNode::new("root")
2299                .child(TreeNode::new("Alpha"))
2300                .child(TreeNode::new("Beta")),
2301        )
2302        .with_search_query("zzz-no-match");
2303
2304        let mut pool = GraphemePool::new();
2305        let mut frame = Frame::new(20, 4, &mut pool);
2306        let area = Rect::new(0, 0, 20, 4);
2307
2308        base_tree.render(area, &mut frame);
2309        filtered_tree.render(area, &mut frame);
2310
2311        assert_eq!(line_text(&frame, 0, 20), "                    ");
2312        assert_eq!(line_text(&frame, 1, 20), "                    ");
2313        assert_eq!(line_text(&frame, 2, 20), "                    ");
2314        assert_eq!(line_text(&frame, 3, 20), "                    ");
2315    }
2316
2317    #[test]
2318    fn tree_search_cleared() {
2319        let tree = Tree::new(
2320            TreeNode::new("root")
2321                .child(TreeNode::new("Alpha"))
2322                .child(TreeNode::new("Beta")),
2323        )
2324        .with_search_query("alpha")
2325        .without_search_query();
2326
2327        let flat = tree.flatten();
2328        assert_eq!(flat.len(), 3); // root + Alpha + Beta
2329    }
2330
2331    // =========================================================================
2332    // Expansion state correctness (bd-1lg.26)
2333    // =========================================================================
2334
2335    #[test]
2336    fn tree_expand_deep_nesting() {
2337        let mut tree = Tree::new(
2338            TreeNode::new("root").child(
2339                TreeNode::new("d1").with_expanded(false).child(
2340                    TreeNode::new("d2")
2341                        .with_expanded(false)
2342                        .child(TreeNode::new("d3")),
2343                ),
2344            ),
2345        );
2346
2347        // Initially: root + d1 (collapsed) = 2 visible
2348        assert_eq!(tree.root().visible_count(), 2);
2349
2350        // Expand d1
2351        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 1));
2352        // root + d1 + d2 (collapsed) = 3 visible
2353        assert_eq!(tree.root().visible_count(), 3);
2354
2355        // Expand d2 (now at index 2)
2356        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Right), 2));
2357        // root + d1 + d2 + d3 = 4 visible
2358        assert_eq!(tree.root().visible_count(), 4);
2359    }
2360
2361    #[test]
2362    fn tree_collapse_parent_hides_all_descendants() {
2363        let mut tree = Tree::new(
2364            TreeNode::new("root")
2365                .child(
2366                    TreeNode::new("a")
2367                        .child(TreeNode::new("a1").child(TreeNode::new("a1x")))
2368                        .child(TreeNode::new("a2")),
2369                )
2370                .child(TreeNode::new("b")),
2371        );
2372
2373        // All expanded: root + a + a1 + a1x + a2 + b = 6
2374        assert_eq!(tree.root().visible_count(), 6);
2375
2376        // Collapse "a" → hides a1, a1x, a2
2377        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Left), 1));
2378        // root + a (collapsed) + b = 3
2379        assert_eq!(tree.root().visible_count(), 3);
2380    }
2381
2382    #[cfg(feature = "tracing")]
2383    #[test]
2384    fn tree_tracing_span_and_toggle_events_are_emitted() {
2385        let trace_state = Arc::new(Mutex::new(TreeTraceState::default()));
2386        let _trace_test_guard = crate::tracing_test_support::acquire();
2387        let subscriber = tracing_subscriber::registry().with(TreeTraceCapture {
2388            state: Arc::clone(&trace_state),
2389        });
2390        let _guard = tracing::subscriber::set_default(subscriber);
2391        tracing::callsite::rebuild_interest_cache();
2392
2393        let mut tree = Tree::new(
2394            TreeNode::new("root")
2395                .child(TreeNode::new("a").child(TreeNode::new("a1")))
2396                .child(TreeNode::new("b")),
2397        );
2398        let mut pool = GraphemePool::new();
2399        let mut frame = Frame::new(20, 6, &mut pool);
2400        tracing::callsite::rebuild_interest_cache();
2401        tree.render(Rect::new(0, 0, 20, 6), &mut frame);
2402        tracing::callsite::rebuild_interest_cache();
2403        assert!(tree.handle_key(&KeyEvent::new(KeyCode::Enter), 1));
2404
2405        tracing::callsite::rebuild_interest_cache();
2406        let snapshot = trace_state.lock().expect("tree trace state lock");
2407        assert!(snapshot.tree_render_seen, "expected tree.render span");
2408        assert!(
2409            snapshot.has_total_nodes_field,
2410            "tree.render missing total_nodes"
2411        );
2412        assert!(
2413            snapshot.has_visible_nodes_field,
2414            "tree.render missing visible_nodes"
2415        );
2416        assert!(
2417            snapshot.has_expanded_count_field,
2418            "tree.render missing expanded_count"
2419        );
2420        assert!(
2421            snapshot.render_duration_recorded,
2422            "tree.render did not record render_duration_us"
2423        );
2424        assert!(
2425            snapshot.toggle_events >= 1,
2426            "expected tree.toggle debug event"
2427        );
2428    }
2429}