adui_dioxus/components/
tree.rs

1//! Tree component for displaying hierarchical data.
2//!
3//! # Features
4//! - Expand/collapse node control
5//! - Single and multiple selection
6//! - Checkable mode with checkbox indicators
7//! - Keyboard navigation
8//! - Optional show line mode
9//!
10//! # Example
11//! ```rust,ignore
12//! use adui_dioxus::components::tree::{Tree, TreeProps};
13//! use adui_dioxus::components::select_base::TreeNode;
14//!
15//! let data = vec![
16//!     TreeNode {
17//!         key: "parent".into(),
18//!         label: "Parent".into(),
19//!         disabled: false,
20//!         children: vec![
21//!             TreeNode { key: "child1".into(), label: "Child 1".into(), disabled: false, children: vec![] },
22//!             TreeNode { key: "child2".into(), label: "Child 2".into(), disabled: false, children: vec![] },
23//!         ],
24//!     },
25//! ];
26//!
27//! rsx! {
28//!     Tree {
29//!         tree_data: data,
30//!         checkable: true,
31//!         on_check: move |keys| { /* handle checked keys */ },
32//!     }
33//! }
34//! ```
35
36use crate::components::config_provider::use_config;
37use crate::components::select_base::{OptionKey, TreeNode};
38use crate::theme::use_theme;
39use dioxus::prelude::*;
40use std::rc::Rc;
41
42/// Internal flattened representation of a tree node for rendering.
43#[derive(Clone, Debug)]
44pub struct FlatTreeNode {
45    pub key: OptionKey,
46    pub label: String,
47    pub disabled: bool,
48    pub depth: usize,
49    pub has_children: bool,
50    pub parent_key: Option<OptionKey>,
51    /// Whether this node is the last child at its level.
52    pub is_last: bool,
53    /// For each ancestor level (0..depth), whether that ancestor was the last child.
54    /// Used to determine whether to draw vertical lines at each indent level.
55    pub ancestor_is_last: Vec<bool>,
56}
57
58/// Flatten tree nodes into a linear list with depth information.
59///
60/// This is used for rendering and keyboard navigation while preserving
61/// hierarchy through depth levels.
62pub fn flatten_tree(
63    nodes: &[TreeNode],
64    depth: usize,
65    parent_key: Option<&str>,
66    out: &mut Vec<FlatTreeNode>,
67) {
68    flatten_tree_with_last(nodes, depth, parent_key, out, &[]);
69}
70
71fn flatten_tree_with_last(
72    nodes: &[TreeNode],
73    depth: usize,
74    parent_key: Option<&str>,
75    out: &mut Vec<FlatTreeNode>,
76    ancestor_is_last: &[bool],
77) {
78    let len = nodes.len();
79    for (idx, node) in nodes.iter().enumerate() {
80        let is_last = idx == len - 1;
81        out.push(FlatTreeNode {
82            key: node.key.clone(),
83            label: node.label.clone(),
84            disabled: node.disabled,
85            depth,
86            has_children: !node.children.is_empty(),
87            parent_key: parent_key.map(|s| s.to_string()),
88            is_last,
89            ancestor_is_last: ancestor_is_last.to_vec(),
90        });
91        if !node.children.is_empty() {
92            let mut next_ancestor_is_last = ancestor_is_last.to_vec();
93            next_ancestor_is_last.push(is_last);
94            flatten_tree_with_last(
95                &node.children,
96                depth + 1,
97                Some(&node.key),
98                out,
99                &next_ancestor_is_last,
100            );
101        }
102    }
103}
104
105/// Collect all descendant keys of a node (including the node itself).
106fn collect_descendant_keys(nodes: &[TreeNode], target_key: &str) -> Vec<OptionKey> {
107    let mut result = Vec::new();
108    collect_descendant_keys_recursive(nodes, target_key, &mut result, false);
109    result
110}
111
112fn collect_descendant_keys_recursive(
113    nodes: &[TreeNode],
114    target_key: &str,
115    out: &mut Vec<OptionKey>,
116    collecting: bool,
117) -> bool {
118    for node in nodes {
119        let is_target = node.key == target_key;
120        let should_collect = collecting || is_target;
121
122        if should_collect {
123            out.push(node.key.clone());
124        }
125
126        if !node.children.is_empty() {
127            let found =
128                collect_descendant_keys_recursive(&node.children, target_key, out, should_collect);
129            if found && !collecting {
130                return true;
131            }
132        }
133
134        if is_target {
135            return true;
136        }
137    }
138    false
139}
140
141/// Collect all parent keys of a node.
142fn collect_parent_keys(flat_nodes: &[FlatTreeNode], target_key: &str) -> Vec<OptionKey> {
143    let mut result = Vec::new();
144    let mut current_key = Some(target_key.to_string());
145
146    while let Some(key) = current_key {
147        if let Some(node) = flat_nodes.iter().find(|n| n.key == key) {
148            if let Some(parent) = &node.parent_key {
149                result.push(parent.clone());
150                current_key = Some(parent.clone());
151            } else {
152                current_key = None;
153            }
154        } else {
155            current_key = None;
156        }
157    }
158
159    result
160}
161
162/// Props for the Tree component.
163#[derive(Props, Clone)]
164pub struct TreeProps {
165    /// Tree data source.
166    #[props(optional)]
167    pub tree_data: Option<Vec<TreeNode>>,
168
169    // --- Expand control ---
170    /// Controlled expanded keys.
171    #[props(optional)]
172    pub expanded_keys: Option<Vec<String>>,
173    /// Default expanded keys (uncontrolled mode).
174    #[props(optional)]
175    pub default_expanded_keys: Option<Vec<String>>,
176    /// Expand all nodes by default.
177    #[props(default)]
178    pub default_expand_all: bool,
179    /// Auto expand parent nodes when children are expanded.
180    #[props(default = true)]
181    pub auto_expand_parent: bool,
182    /// Callback when expand keys change.
183    #[props(optional)]
184    pub on_expand: Option<EventHandler<Vec<String>>>,
185
186    // --- Selection control ---
187    /// Controlled selected keys.
188    #[props(optional)]
189    pub selected_keys: Option<Vec<String>>,
190    /// Default selected keys (uncontrolled mode).
191    #[props(optional)]
192    pub default_selected_keys: Option<Vec<String>>,
193    /// Whether nodes are selectable.
194    #[props(default = true)]
195    pub selectable: bool,
196    /// Allow multiple selection.
197    #[props(default)]
198    pub multiple: bool,
199    /// Callback when selected keys change.
200    #[props(optional)]
201    pub on_select: Option<EventHandler<Vec<String>>>,
202
203    // --- Check control (checkable mode) ---
204    /// Show checkbox next to nodes.
205    #[props(default)]
206    pub checkable: bool,
207    /// Controlled checked keys.
208    #[props(optional)]
209    pub checked_keys: Option<Vec<String>>,
210    /// Default checked keys (uncontrolled mode).
211    #[props(optional)]
212    pub default_checked_keys: Option<Vec<String>>,
213    /// Check strictly (parent and child are independent).
214    #[props(default)]
215    pub check_strictly: bool,
216    /// Callback when checked keys change.
217    #[props(optional)]
218    pub on_check: Option<EventHandler<Vec<String>>>,
219
220    // --- Visual options ---
221    /// Show connecting lines between nodes.
222    #[props(default)]
223    pub show_line: bool,
224    /// Show icon next to nodes.
225    #[props(default)]
226    pub show_icon: bool,
227    /// Block node (full-width clickable area).
228    #[props(default)]
229    pub block_node: bool,
230    /// Disable the entire tree.
231    #[props(default)]
232    pub disabled: bool,
233
234    // --- Advanced features ---
235    /// Enable drag and drop for tree nodes.
236    #[props(optional)]
237    pub draggable: Option<DraggableConfig>,
238    /// Async load data function: (node) -> Vec<TreeNode>
239    #[props(optional)]
240    pub load_data: Option<Rc<dyn Fn(&TreeNode) -> Vec<TreeNode>>>,
241    /// Custom field names for tree data structure.
242    #[props(optional)]
243    pub field_names: Option<FieldNames>,
244    /// Filter tree nodes: (node) -> bool
245    #[props(optional)]
246    pub filter_tree_node: Option<Rc<dyn Fn(&TreeNode) -> bool>>,
247    /// Custom icon render: (node) -> Element
248    #[props(optional)]
249    pub icon: Option<Rc<dyn Fn(&TreeNode) -> Element>>,
250    /// Custom switcher icon render: (expanded, is_leaf) -> Element
251    #[props(optional)]
252    pub switcher_icon: Option<Rc<dyn Fn(bool, bool) -> Element>>,
253    /// Custom title render: (node) -> Element
254    #[props(optional)]
255    pub title_render: Option<Rc<dyn Fn(&TreeNode) -> Element>>,
256    /// Loaded keys for async loading state.
257    #[props(optional)]
258    pub loaded_keys: Option<Vec<String>>,
259
260    // --- Styling ---
261    #[props(optional)]
262    pub class: Option<String>,
263    #[props(optional)]
264    pub style: Option<String>,
265}
266
267/// Draggable configuration for tree nodes.
268#[derive(Clone)]
269pub struct DraggableConfig {
270    /// Whether dragging is enabled.
271    pub enabled: bool,
272    /// Custom drag icon.
273    pub icon: Option<Element>,
274    /// Node draggable check function: (node) -> bool
275    pub node_draggable: Option<Rc<dyn Fn(&TreeNode) -> bool>>,
276}
277
278impl PartialEq for DraggableConfig {
279    fn eq(&self, other: &Self) -> bool {
280        self.enabled == other.enabled && self.icon == other.icon
281        // Function pointers cannot be compared for equality
282    }
283}
284
285impl std::fmt::Debug for DraggableConfig {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        f.debug_struct("DraggableConfig")
288            .field("enabled", &self.enabled)
289            .field("icon", &self.icon)
290            .field("node_draggable", &"<function>")
291            .finish()
292    }
293}
294
295/// Custom field names for tree data structure.
296#[derive(Clone, Debug, Default, PartialEq)]
297pub struct FieldNames {
298    pub title: Option<String>,
299    pub key: Option<String>,
300    pub children: Option<String>,
301}
302
303impl PartialEq for TreeProps {
304    fn eq(&self, other: &Self) -> bool {
305        // Compare all fields except function pointers
306        self.tree_data == other.tree_data
307            && self.expanded_keys == other.expanded_keys
308            && self.default_expanded_keys == other.default_expanded_keys
309            && self.default_expand_all == other.default_expand_all
310            && self.on_expand == other.on_expand
311            && self.selected_keys == other.selected_keys
312            && self.default_selected_keys == other.default_selected_keys
313            && self.selectable == other.selectable
314            && self.multiple == other.multiple
315            && self.on_select == other.on_select
316            && self.checkable == other.checkable
317            && self.checked_keys == other.checked_keys
318            && self.default_checked_keys == other.default_checked_keys
319            && self.check_strictly == other.check_strictly
320            && self.on_check == other.on_check
321            && self.show_line == other.show_line
322            && self.show_icon == other.show_icon
323            && self.block_node == other.block_node
324            && self.disabled == other.disabled
325            && self.class == other.class
326            && self.style == other.style
327            && self.draggable == other.draggable
328            && self.field_names == other.field_names
329            && self.loaded_keys == other.loaded_keys
330        // Function pointers cannot be compared for equality
331    }
332}
333
334/// Ant Design flavored Tree component.
335#[component]
336pub fn Tree(props: TreeProps) -> Element {
337    let TreeProps {
338        tree_data,
339        expanded_keys,
340        default_expanded_keys,
341        default_expand_all,
342        auto_expand_parent: _,
343        on_expand,
344        selected_keys,
345        default_selected_keys,
346        selectable,
347        multiple,
348        on_select,
349        checkable,
350        checked_keys,
351        default_checked_keys,
352        check_strictly,
353        on_check,
354        show_line,
355        show_icon,
356        block_node,
357        disabled,
358        class,
359        style,
360        ..
361    } = props;
362
363    let config = use_config();
364    let theme = use_theme();
365    let tokens = theme.tokens();
366
367    let is_disabled = disabled || config.disabled;
368
369    // Prepare tree data
370    let nodes: Vec<TreeNode> = tree_data.unwrap_or_default();
371
372    // Flatten tree for rendering
373    let mut flat_nodes: Vec<FlatTreeNode> = Vec::new();
374    flatten_tree(&nodes, 0, None, &mut flat_nodes);
375
376    // Collect all keys for default_expand_all
377    let all_parent_keys: Vec<String> = flat_nodes
378        .iter()
379        .filter(|n| n.has_children)
380        .map(|n| n.key.clone())
381        .collect();
382
383    // --- Expand state ---
384    let initial_expanded = if default_expand_all {
385        all_parent_keys.clone()
386    } else {
387        default_expanded_keys.unwrap_or_default()
388    };
389    let internal_expanded: Signal<Vec<String>> = use_signal(|| initial_expanded);
390
391    let is_expand_controlled = expanded_keys.is_some();
392    let current_expanded = if is_expand_controlled {
393        expanded_keys.clone().unwrap_or_default()
394    } else {
395        internal_expanded.read().clone()
396    };
397
398    // --- Selection state ---
399    let initial_selected = default_selected_keys.unwrap_or_default();
400    let internal_selected: Signal<Vec<String>> = use_signal(|| initial_selected);
401
402    let is_select_controlled = selected_keys.is_some();
403    let current_selected = if is_select_controlled {
404        selected_keys.clone().unwrap_or_default()
405    } else {
406        internal_selected.read().clone()
407    };
408
409    // --- Checked state ---
410    let initial_checked = default_checked_keys.unwrap_or_default();
411    let internal_checked: Signal<Vec<String>> = use_signal(|| initial_checked);
412
413    let is_check_controlled = checked_keys.is_some();
414    let current_checked = if is_check_controlled {
415        checked_keys.clone().unwrap_or_default()
416    } else {
417        internal_checked.read().clone()
418    };
419
420    // Active index for keyboard navigation
421    let active_index: Signal<Option<usize>> = use_signal(|| None);
422
423    // Filter visible nodes based on expanded state
424    let visible_nodes: Vec<FlatTreeNode> = {
425        let mut result = Vec::new();
426        let mut skip_depth: Option<usize> = None;
427
428        for node in &flat_nodes {
429            // If we're skipping collapsed subtree
430            if let Some(sd) = skip_depth {
431                if node.depth > sd {
432                    continue;
433                } else {
434                    skip_depth = None;
435                }
436            }
437
438            result.push(node.clone());
439
440            // If this node has children but is not expanded, skip its subtree
441            if node.has_children && !current_expanded.contains(&node.key) {
442                skip_depth = Some(node.depth);
443            }
444        }
445
446        result
447    };
448
449    // Build root classes
450    let mut class_list = vec!["adui-tree".to_string()];
451    if show_line {
452        class_list.push("adui-tree-show-line".into());
453    }
454    if show_icon {
455        class_list.push("adui-tree-show-icon".into());
456    }
457    if block_node {
458        class_list.push("adui-tree-block-node".into());
459    }
460    if is_disabled {
461        class_list.push("adui-tree-disabled".into());
462    }
463    if let Some(extra) = class {
464        class_list.push(extra);
465    }
466    let class_attr = class_list.join(" ");
467
468    // Use primary color with low opacity for selected background
469    let selected_bg = format!("{}1a", &tokens.color_primary[..7]); // Add 10% opacity
470    let style_attr = format!(
471        "--adui-tree-node-hover-bg: {}; --adui-tree-node-selected-bg: {}; {}",
472        tokens.color_bg_base,
473        selected_bg,
474        style.unwrap_or_default()
475    );
476
477    rsx! {
478        div {
479            class: "{class_attr}",
480            style: "{style_attr}",
481            role: "tree",
482            tabindex: 0,
483            onkeydown: {
484                let visible_for_keydown = visible_nodes.clone();
485                let nodes_for_keydown = nodes.clone();
486                let flat_for_keydown = flat_nodes.clone();
487                let on_expand_cb = on_expand;
488                let on_select_cb = on_select;
489                let on_check_cb = on_check;
490                let current_expanded_for_keydown = current_expanded.clone();
491                let current_selected_for_keydown = current_selected.clone();
492                let current_checked_for_keydown = current_checked.clone();
493                move |evt: KeyboardEvent| {
494                    if is_disabled {
495                        return;
496                    }
497                    use dioxus::prelude::Key;
498
499                    let nodes_len = visible_for_keydown.len();
500                    if nodes_len == 0 {
501                        return;
502                    }
503
504                    let mut active = active_index;
505
506                    match evt.key() {
507                        Key::ArrowDown => {
508                            evt.prevent_default();
509                            let current = *active.read();
510                            let next = match current {
511                                None => Some(0),
512                                Some(idx) => Some((idx + 1) % nodes_len),
513                            };
514                            active.set(next);
515                        }
516                        Key::ArrowUp => {
517                            evt.prevent_default();
518                            let current = *active.read();
519                            let next = match current {
520                                None => Some(nodes_len.saturating_sub(1)),
521                                Some(idx) => Some((idx + nodes_len - 1) % nodes_len),
522                            };
523                            active.set(next);
524                        }
525                        Key::ArrowRight => {
526                            evt.prevent_default();
527                            if let Some(idx) = *active.read() {
528                                if idx < visible_for_keydown.len() {
529                                    let node = &visible_for_keydown[idx];
530                                    if node.has_children && !current_expanded_for_keydown.contains(&node.key) {
531                                        // Expand node
532                                        let mut next_expanded = current_expanded_for_keydown.clone();
533                                        next_expanded.push(node.key.clone());
534                                        if let Some(cb) = on_expand_cb {
535                                            cb.call(next_expanded.clone());
536                                        }
537                                        if !is_expand_controlled {
538                                            let mut signal = internal_expanded;
539                                            signal.set(next_expanded);
540                                        }
541                                    }
542                                }
543                            }
544                        }
545                        Key::ArrowLeft => {
546                            evt.prevent_default();
547                            if let Some(idx) = *active.read() {
548                                if idx < visible_for_keydown.len() {
549                                    let node = &visible_for_keydown[idx];
550                                    if node.has_children && current_expanded_for_keydown.contains(&node.key) {
551                                        // Collapse node
552                                        let next_expanded: Vec<String> = current_expanded_for_keydown
553                                            .iter()
554                                            .filter(|k| *k != &node.key)
555                                            .cloned()
556                                            .collect();
557                                        if let Some(cb) = on_expand_cb {
558                                            cb.call(next_expanded.clone());
559                                        }
560                                        if !is_expand_controlled {
561                                            let mut signal = internal_expanded;
562                                            signal.set(next_expanded);
563                                        }
564                                    }
565                                }
566                            }
567                        }
568                        Key::Enter => {
569                            evt.prevent_default();
570                            if let Some(idx) = *active.read() {
571                                if idx < visible_for_keydown.len() {
572                                    let node = &visible_for_keydown[idx];
573                                    if node.disabled {
574                                        return;
575                                    }
576
577                                    if checkable {
578                                        // Toggle check
579                                        let next_checked = toggle_check(
580                                            &current_checked_for_keydown,
581                                            &node.key,
582                                            check_strictly,
583                                            &nodes_for_keydown,
584                                            &flat_for_keydown,
585                                        );
586                                        if let Some(cb) = on_check_cb {
587                                            cb.call(next_checked.clone());
588                                        }
589                                        if !is_check_controlled {
590                                            let mut signal = internal_checked;
591                                            signal.set(next_checked);
592                                        }
593                                    } else if selectable {
594                                        // Toggle selection
595                                        let next_selected = toggle_selection(
596                                            &current_selected_for_keydown,
597                                            &node.key,
598                                            multiple,
599                                        );
600                                        if let Some(cb) = on_select_cb {
601                                            cb.call(next_selected.clone());
602                                        }
603                                        if !is_select_controlled {
604                                            let mut signal = internal_selected;
605                                            signal.set(next_selected);
606                                        }
607                                    }
608                                }
609                            }
610                        }
611                        _ => {}
612                    }
613                }
614            },
615            ul { class: "adui-tree-list",
616                {visible_nodes.iter().enumerate().map(|(index, node)| {
617                    let key = node.key.clone();
618                    let label = node.label.clone();
619                    let depth = node.depth;
620                    let has_children = node.has_children;
621                    let node_disabled = node.disabled || is_disabled;
622                    let is_last = node.is_last;
623                    let ancestor_is_last = node.ancestor_is_last.clone();
624
625                    let is_expanded = current_expanded.contains(&key);
626                    let is_selected = current_selected.contains(&key);
627                    let is_checked = current_checked.contains(&key);
628                    let is_active = (*active_index.read()).map(|i| i == index).unwrap_or(false);
629
630                    // Check indeterminate state (some but not all children checked)
631                    let is_indeterminate = if checkable && !check_strictly && has_children {
632                        let descendants = collect_descendant_keys(&nodes, &key);
633                        let checked_count = descendants.iter().filter(|k| current_checked.contains(*k)).count();
634                        checked_count > 0 && checked_count < descendants.len()
635                    } else {
636                        false
637                    };
638
639                    let on_expand_for_node = on_expand;
640                    let on_select_for_node = on_select;
641                    let on_check_for_node = on_check;
642                    let current_expanded_for_node = current_expanded.clone();
643                    let current_selected_for_node = current_selected.clone();
644                    let current_checked_for_node = current_checked.clone();
645                    let nodes_for_node = nodes.clone();
646                    let flat_for_node = flat_nodes.clone();
647
648                    rsx! {
649                        li {
650                            key: "{key}",
651                            class: {
652                                let mut classes = vec!["adui-tree-treenode".to_string()];
653                                if is_selected {
654                                    classes.push("adui-tree-treenode-selected".into());
655                                }
656                                if node_disabled {
657                                    classes.push("adui-tree-treenode-disabled".into());
658                                }
659                                if is_active {
660                                    classes.push("adui-tree-treenode-active".into());
661                                }
662                                classes.join(" ")
663                            },
664                            role: "treeitem",
665                            "aria-selected": is_selected,
666                            "aria-expanded": if has_children { is_expanded.to_string() } else { String::new() },
667                            // Indent with optional lines
668                            if show_line {
669                                {(0..depth).map(|i| {
670                                    // Show vertical line if ancestor at this level was NOT the last child
671                                    let ancestor_was_last = ancestor_is_last.get(i).copied().unwrap_or(false);
672                                    let show_vertical = !ancestor_was_last;
673                                    rsx! {
674                                        span {
675                                            key: "{i}",
676                                            class: "adui-tree-indent-unit",
677                                            style: "display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 28px; position: relative;",
678                                            if show_vertical {
679                                                span {
680                                                    style: "position: absolute; left: 11px; top: 0; bottom: 0; width: 1px; background: var(--adui-color-border, #d9d9d9);"
681                                                }
682                                            }
683                                        }
684                                    }
685                                })}
686                            } else {
687                                {(0..depth).map(|i| {
688                                    rsx! {
689                                        span {
690                                            key: "{i}",
691                                            class: "adui-tree-indent-unit",
692                                            style: "display: inline-block; width: 24px;"
693                                        }
694                                    }
695                                })}
696                            }
697                            // Switcher (expand/collapse icon)
698                            span {
699                                class: {
700                                    let mut classes = vec!["adui-tree-switcher".to_string()];
701                                    if has_children {
702                                        if is_expanded {
703                                            classes.push("adui-tree-switcher-open".into());
704                                        } else {
705                                            classes.push("adui-tree-switcher-close".into());
706                                        }
707                                    } else {
708                                        classes.push("adui-tree-switcher-leaf".into());
709                                    }
710                                    classes.join(" ")
711                                },
712                                style: if show_line {
713                                    "display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 28px; position: relative;"
714                                } else {
715                                    ""
716                                },
717                                onclick: {
718                                    let key_for_expand = key.clone();
719                                    let current_expanded_for_expand = current_expanded_for_node.clone();
720                                    move |evt: MouseEvent| {
721                                        evt.stop_propagation();
722                                        if !has_children {
723                                            return;
724                                        }
725
726                                        let next_expanded = if current_expanded_for_expand.contains(&key_for_expand) {
727                                            current_expanded_for_expand
728                                                .iter()
729                                                .filter(|k| *k != &key_for_expand)
730                                                .cloned()
731                                                .collect()
732                                        } else {
733                                            let mut next = current_expanded_for_expand.clone();
734                                            next.push(key_for_expand.clone());
735                                            next
736                                        };
737
738                                        if let Some(cb) = on_expand_for_node {
739                                            cb.call(next_expanded.clone());
740                                        }
741                                        if !is_expand_controlled {
742                                            let mut signal = internal_expanded;
743                                            signal.set(next_expanded);
744                                        }
745                                    }
746                                },
747                                if show_line {
748                                    // Vertical line (top half for non-first items, full for non-last items)
749                                    if depth > 0 {
750                                        // Top half of vertical line
751                                        span {
752                                            style: "position: absolute; left: 11px; top: 0; height: calc(50% - 5px); width: 1px; background: var(--adui-color-border, #d9d9d9);"
753                                        }
754                                        // Bottom half (only if not last child)
755                                        if !is_last {
756                                            span {
757                                                style: "position: absolute; left: 11px; top: calc(50% + 5px); bottom: 0; width: 1px; background: var(--adui-color-border, #d9d9d9);"
758                                            }
759                                        }
760                                        // Horizontal connector line
761                                        span {
762                                            style: "position: absolute; left: 11px; top: 50%; width: 6px; height: 1px; background: var(--adui-color-border, #d9d9d9); transform: translateY(-50%);"
763                                        }
764                                    }
765                                    // Show line style icons - bordered box
766                                    if has_children {
767                                        span {
768                                            style: "position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center; width: 10px; height: 10px; border: 1px solid var(--adui-color-border, #d9d9d9); border-radius: 2px; background: var(--adui-color-bg-container, #fff); font-size: 10px; line-height: 1; cursor: pointer; color: var(--adui-color-text-secondary, rgba(0,0,0,0.65));",
769                                            if is_expanded { "−" } else { "+" }
770                                        }
771                                    } else if depth > 0 {
772                                        // Leaf node - just extend horizontal line
773                                        span {
774                                            style: "position: absolute; left: 17px; top: 50%; width: 6px; height: 1px; background: var(--adui-color-border, #d9d9d9); transform: translateY(-50%);"
775                                        }
776                                    }
777                                } else if has_children {
778                                    {
779                                        let rotate_deg = if is_expanded { 90 } else { 0 };
780                                        rsx! {
781                                            span {
782                                                class: "adui-tree-switcher-icon",
783                                                style: "display: inline-block; transition: transform 0.2s; transform: rotate({rotate_deg}deg);",
784                                                "▶"
785                                            }
786                                        }
787                                    }
788                                }
789                            }
790                            // Checkbox (if checkable)
791                            if checkable {
792                                span {
793                                    class: {
794                                        let mut classes = vec!["adui-tree-checkbox".to_string()];
795                                        if is_checked {
796                                            classes.push("adui-tree-checkbox-checked".into());
797                                        }
798                                        if is_indeterminate {
799                                            classes.push("adui-tree-checkbox-indeterminate".into());
800                                        }
801                                        if node_disabled {
802                                            classes.push("adui-tree-checkbox-disabled".into());
803                                        }
804                                        classes.join(" ")
805                                    },
806                                    onclick: {
807                                        let key_for_check = key.clone();
808                                        let current_checked_for_check = current_checked_for_node.clone();
809                                        let nodes_for_check = nodes_for_node.clone();
810                                        let flat_for_check = flat_for_node.clone();
811                                        move |evt: MouseEvent| {
812                                            evt.stop_propagation();
813                                            if node_disabled {
814                                                return;
815                                            }
816
817                                            let next_checked = toggle_check(
818                                                &current_checked_for_check,
819                                                &key_for_check,
820                                                check_strictly,
821                                                &nodes_for_check,
822                                                &flat_for_check,
823                                            );
824
825                                            if let Some(cb) = on_check_for_node {
826                                                cb.call(next_checked.clone());
827                                            }
828                                            if !is_check_controlled {
829                                                let mut signal = internal_checked;
830                                                signal.set(next_checked);
831                                            }
832                                        }
833                                    },
834                                    span { class: "adui-tree-checkbox-inner" }
835                                }
836                            }
837                            // Title
838                            span {
839                                class: {
840                                    let mut classes = vec!["adui-tree-node-content-wrapper".to_string()];
841                                    if is_selected {
842                                        classes.push("adui-tree-node-selected".into());
843                                    }
844                                    classes.join(" ")
845                                },
846                                onclick: {
847                                    let key_for_select = key.clone();
848                                    let current_selected_for_select = current_selected_for_node.clone();
849                                    move |_| {
850                                        if node_disabled || !selectable {
851                                            return;
852                                        }
853
854                                        let next_selected = toggle_selection(
855                                            &current_selected_for_select,
856                                            &key_for_select,
857                                            multiple,
858                                        );
859
860                                        if let Some(cb) = on_select_for_node {
861                                            cb.call(next_selected.clone());
862                                        }
863                                        if !is_select_controlled {
864                                            let mut signal = internal_selected;
865                                            signal.set(next_selected);
866                                        }
867                                    }
868                                },
869                                if show_icon {
870                                    span {
871                                        class: "adui-tree-iconEle",
872                                        style: "margin-right: 4px;",
873                                        if has_children {
874                                            if is_expanded { "📂" } else { "📁" }
875                                        } else {
876                                            "📄"
877                                        }
878                                    }
879                                }
880                                span { class: "adui-tree-title", "{label}" }
881                            }
882                        }
883                    }
884                })}
885            }
886        }
887    }
888}
889
890/// Toggle selection for a key.
891fn toggle_selection(current: &[String], key: &str, multiple: bool) -> Vec<String> {
892    if multiple {
893        if current.contains(&key.to_string()) {
894            current.iter().filter(|k| *k != key).cloned().collect()
895        } else {
896            let mut next = current.to_vec();
897            next.push(key.to_string());
898            next
899        }
900    } else {
901        if current.contains(&key.to_string()) {
902            Vec::new()
903        } else {
904            vec![key.to_string()]
905        }
906    }
907}
908
909/// Toggle check for a key, handling cascading behavior if not check_strictly.
910fn toggle_check(
911    current: &[String],
912    key: &str,
913    check_strictly: bool,
914    nodes: &[TreeNode],
915    flat_nodes: &[FlatTreeNode],
916) -> Vec<String> {
917    let is_checked = current.contains(&key.to_string());
918
919    if check_strictly {
920        // Simple toggle without cascading
921        if is_checked {
922            current.iter().filter(|k| *k != key).cloned().collect()
923        } else {
924            let mut next = current.to_vec();
925            next.push(key.to_string());
926            next
927        }
928    } else {
929        // Cascading check: toggle all descendants
930        let descendants = collect_descendant_keys(nodes, key);
931
932        if is_checked {
933            // Uncheck: remove this node and all descendants
934            current
935                .iter()
936                .filter(|k| !descendants.contains(k))
937                .cloned()
938                .collect()
939        } else {
940            // Check: add this node and all descendants
941            let mut next: Vec<String> = current.to_vec();
942            for dk in descendants {
943                if !next.contains(&dk) {
944                    next.push(dk);
945                }
946            }
947
948            // Also check parent nodes if all siblings are checked
949            let parents = collect_parent_keys(flat_nodes, key);
950            for parent_key in parents {
951                let siblings = collect_descendant_keys(nodes, &parent_key);
952                let all_checked = siblings
953                    .iter()
954                    .all(|sk| next.contains(sk) || sk == &parent_key);
955                if all_checked && !next.contains(&parent_key) {
956                    next.push(parent_key);
957                }
958            }
959
960            next
961        }
962    }
963}
964
965/// DirectoryTree variant - Tree with directory icons and expand on click.
966#[derive(Props, Clone, PartialEq)]
967pub struct DirectoryTreeProps {
968    /// Tree data source.
969    #[props(optional)]
970    pub tree_data: Option<Vec<TreeNode>>,
971
972    // --- Expand control ---
973    #[props(optional)]
974    pub expanded_keys: Option<Vec<String>>,
975    #[props(optional)]
976    pub default_expanded_keys: Option<Vec<String>>,
977    #[props(default)]
978    pub default_expand_all: bool,
979    #[props(optional)]
980    pub on_expand: Option<EventHandler<Vec<String>>>,
981
982    // --- Selection control ---
983    #[props(optional)]
984    pub selected_keys: Option<Vec<String>>,
985    #[props(optional)]
986    pub default_selected_keys: Option<Vec<String>>,
987    #[props(default)]
988    pub multiple: bool,
989    #[props(optional)]
990    pub on_select: Option<EventHandler<Vec<String>>>,
991
992    // --- Styling ---
993    #[props(optional)]
994    pub class: Option<String>,
995    #[props(optional)]
996    pub style: Option<String>,
997}
998
999/// Directory-style tree with folder icons and expand-on-click behavior.
1000#[component]
1001pub fn DirectoryTree(props: DirectoryTreeProps) -> Element {
1002    let DirectoryTreeProps {
1003        tree_data,
1004        expanded_keys,
1005        default_expanded_keys,
1006        default_expand_all,
1007        on_expand,
1008        selected_keys,
1009        default_selected_keys,
1010        multiple,
1011        on_select,
1012        class,
1013        style,
1014    } = props;
1015
1016    let mut class_list = vec!["adui-tree-directory".to_string()];
1017    if let Some(extra) = class {
1018        class_list.push(extra);
1019    }
1020
1021    rsx! {
1022        Tree {
1023            tree_data,
1024            expanded_keys,
1025            default_expanded_keys,
1026            default_expand_all,
1027            on_expand,
1028            selected_keys,
1029            default_selected_keys,
1030            multiple,
1031            on_select,
1032            show_icon: true,
1033            block_node: true,
1034            class: class_list.join(" "),
1035            style,
1036        }
1037    }
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042    use super::*;
1043
1044    #[test]
1045    fn flatten_tree_produces_correct_depth() {
1046        let nodes = vec![TreeNode {
1047            key: "parent".into(),
1048            label: "Parent".into(),
1049            disabled: false,
1050            children: vec![
1051                TreeNode {
1052                    key: "child1".into(),
1053                    label: "Child 1".into(),
1054                    disabled: false,
1055                    children: vec![],
1056                },
1057                TreeNode {
1058                    key: "child2".into(),
1059                    label: "Child 2".into(),
1060                    disabled: false,
1061                    children: vec![TreeNode {
1062                        key: "grandchild".into(),
1063                        label: "Grandchild".into(),
1064                        disabled: false,
1065                        children: vec![],
1066                    }],
1067                },
1068            ],
1069        }];
1070
1071        let mut flat = Vec::new();
1072        flatten_tree(&nodes, 0, None, &mut flat);
1073
1074        assert_eq!(flat.len(), 4);
1075        assert_eq!(flat[0].key, "parent");
1076        assert_eq!(flat[0].depth, 0);
1077        assert!(flat[0].has_children);
1078        assert_eq!(flat[1].key, "child1");
1079        assert_eq!(flat[1].depth, 1);
1080        assert!(!flat[1].has_children);
1081        assert_eq!(flat[2].key, "child2");
1082        assert_eq!(flat[2].depth, 1);
1083        assert!(flat[2].has_children);
1084        assert_eq!(flat[3].key, "grandchild");
1085        assert_eq!(flat[3].depth, 2);
1086    }
1087
1088    #[test]
1089    fn toggle_selection_single_mode() {
1090        let current: Vec<String> = vec![];
1091        let next = toggle_selection(&current, "a", false);
1092        assert_eq!(next, vec!["a".to_string()]);
1093
1094        let next2 = toggle_selection(&next, "b", false);
1095        assert_eq!(next2, vec!["b".to_string()]);
1096
1097        let next3 = toggle_selection(&next2, "b", false);
1098        assert!(next3.is_empty());
1099    }
1100
1101    #[test]
1102    fn toggle_selection_multiple_mode() {
1103        let current: Vec<String> = vec![];
1104        let next = toggle_selection(&current, "a", true);
1105        assert_eq!(next, vec!["a".to_string()]);
1106
1107        let next2 = toggle_selection(&next, "b", true);
1108        assert_eq!(next2, vec!["a".to_string(), "b".to_string()]);
1109
1110        let next3 = toggle_selection(&next2, "a", true);
1111        assert_eq!(next3, vec!["b".to_string()]);
1112    }
1113
1114    #[test]
1115    fn collect_descendant_keys_finds_all_children() {
1116        let nodes = vec![TreeNode {
1117            key: "root".into(),
1118            label: "Root".into(),
1119            disabled: false,
1120            children: vec![
1121                TreeNode {
1122                    key: "a".into(),
1123                    label: "A".into(),
1124                    disabled: false,
1125                    children: vec![TreeNode {
1126                        key: "a1".into(),
1127                        label: "A1".into(),
1128                        disabled: false,
1129                        children: vec![],
1130                    }],
1131                },
1132                TreeNode {
1133                    key: "b".into(),
1134                    label: "B".into(),
1135                    disabled: false,
1136                    children: vec![],
1137                },
1138            ],
1139        }];
1140
1141        let descendants = collect_descendant_keys(&nodes, "a");
1142        assert!(descendants.contains(&"a".to_string()));
1143        assert!(descendants.contains(&"a1".to_string()));
1144        assert!(!descendants.contains(&"b".to_string()));
1145        assert!(!descendants.contains(&"root".to_string()));
1146    }
1147}