Skip to main content

dioxus_element_plug/components/
tree.rs

1use dioxus::prelude::*;
2use std::collections::HashSet;
3
4/// Tree node data
5#[derive(Clone, PartialEq)]
6pub struct TreeNodeData {
7    pub label: String,
8    pub children: Vec<TreeNodeData>,
9    pub disabled: bool,
10}
11
12impl TreeNodeData {
13    pub fn new(label: &str) -> Self {
14        Self { label: label.to_string(), children: vec![], disabled: false }
15    }
16
17    pub fn child(mut self, node: TreeNodeData) -> Self {
18        self.children.push(node);
19        self
20    }
21
22    pub fn disabled(mut self, disabled: bool) -> Self {
23        self.disabled = disabled;
24        self
25    }
26}
27
28/// Tree props
29#[derive(Props, Clone, PartialEq)]
30pub struct TreeProps {
31    /// Tree data
32    #[props(default)]
33    pub data: Vec<TreeNodeData>,
34
35    /// Whether to show checkboxes
36    #[props(default = false)]
37    pub show_checkbox: bool,
38
39    /// Whether to expand all nodes by default
40    #[props(default = false)]
41    pub default_expand_all: bool,
42
43    /// Currently expanded node labels
44    #[props(default)]
45    pub expanded_keys: Vec<String>,
46
47    /// Currently checked node labels
48    #[props(default)]
49    pub checked_keys: Vec<String>,
50
51    /// Currently highlighted node label
52    #[props(default)]
53    pub current_key: Option<String>,
54
55    /// Whether to highlight current node
56    #[props(default = false)]
57    pub highlight_current: bool,
58
59    /// Whether to expand on node click
60    #[props(default = true)]
61    pub expand_on_click_node: bool,
62
63    /// Node click handler
64    #[props(default)]
65    pub on_node_click: Option<EventHandler<String>>,
66
67    /// Node expand/collapse toggle handler
68    #[props(default)]
69    pub on_node_expand: Option<EventHandler<(String, bool)>>,
70
71    /// Node check handler (for checkboxes)
72    #[props(default)]
73    pub on_node_check: Option<EventHandler<(String, bool)>>,
74
75    #[props(default)]
76    pub class: Option<String>,
77
78    #[props(default)]
79    pub style: Option<String>,
80}
81
82/// Tree component for hierarchical data
83///
84/// This component renders a tree structure with optional expand/collapse,
85/// checkboxes, and node highlighting.
86///
87/// ## Example
88///
89/// ```rust,ignore
90/// use dioxus_element_plug::components::tree::{Tree, TreeNodeData};
91///
92/// let data = vec![
93///     TreeNodeData::new("Level 1")
94///         .child(TreeNodeData::new("Level 2-1"))
95///         .child(TreeNodeData::new("Level 2-2")),
96/// ];
97///
98/// rsx! {
99///     Tree {
100///         data: data,
101///         default_expand_all: true,
102///         highlight_current: true,
103///     }
104/// }
105/// ```
106#[component]
107pub fn Tree(props: TreeProps) -> Element {
108    let mut class_names = vec!["el-tree".to_string()];
109    if let Some(ref c) = props.class {
110        class_names.push(c.clone());
111    }
112
113    // Build expanded set from props
114    let expanded_set: HashSet<String> = if props.default_expand_all {
115        collect_all_labels(&props.data).into_iter().collect()
116    } else {
117        props.expanded_keys.iter().cloned().collect()
118    };
119
120    let checked_set: HashSet<String> = props.checked_keys.iter().cloned().collect();
121    let current_key = props.current_key.clone();
122
123    // Pre-compute top-level node render data
124    let node_data: Vec<TreeNodeRenderData> = props
125        .data
126        .iter()
127        .map(|node| build_node_render_data(node, 0, &expanded_set, &checked_set, &current_key, props.highlight_current, props.show_checkbox))
128        .collect();
129
130    rsx! {
131        div {
132            class: "{class_names.join(\" \")}",
133            style: props.style.clone().unwrap_or_default(),
134            role: "tree",
135
136            for render_node in node_data.into_iter() {
137                TreeChild {
138                    node: render_node,
139                    show_checkbox: props.show_checkbox,
140                    highlight_current: props.highlight_current,
141                    expand_on_click: props.expand_on_click_node,
142                    on_click: props.on_node_click,
143                    on_expand: props.on_node_expand,
144                    on_check: props.on_node_check,
145                }
146            }
147        }
148    }
149}
150
151/// Pre-computed render data for a tree node
152#[derive(Clone, PartialEq)]
153struct TreeNodeRenderData {
154    label: String,
155    disabled: bool,
156    level: u32,
157    is_expanded: bool,
158    is_checked: bool,
159    is_current: bool,
160    has_children: bool,
161    children: Vec<TreeNodeRenderData>,
162}
163
164fn build_node_render_data(
165    node: &TreeNodeData,
166    level: u32,
167    expanded: &HashSet<String>,
168    checked: &HashSet<String>,
169    current: &Option<String>,
170    highlight: bool,
171    show_checkbox: bool,
172) -> TreeNodeRenderData {
173    let is_expanded = expanded.contains(&node.label);
174    let is_checked = checked.contains(&node.label);
175    let is_current = highlight && current.as_ref().map_or(false, |c| c == &node.label);
176    let has_children = !node.children.is_empty();
177
178    let children = if has_children {
179        node.children
180            .iter()
181            .map(|child| build_node_render_data(child, level + 1, expanded, checked, current, highlight, show_checkbox))
182            .collect()
183    } else {
184        vec![]
185    };
186
187    TreeNodeRenderData {
188        label: node.label.clone(),
189        disabled: node.disabled,
190        level,
191        is_expanded,
192        is_checked,
193        is_current,
194        has_children,
195        children,
196    }
197}
198
199fn collect_all_labels(data: &[TreeNodeData]) -> Vec<String> {
200    let mut result = vec![];
201    for node in data {
202        result.push(node.label.clone());
203        if !node.children.is_empty() {
204            result.extend(collect_all_labels(&node.children));
205        }
206    }
207    result
208}
209
210#[derive(Props, Clone, PartialEq)]
211struct TreeChildProps {
212    node: TreeNodeRenderData,
213    show_checkbox: bool,
214    highlight_current: bool,
215    expand_on_click: bool,
216    on_click: Option<EventHandler<String>>,
217    on_expand: Option<EventHandler<(String, bool)>>,
218    on_check: Option<EventHandler<(String, bool)>>,
219}
220
221#[component]
222fn TreeChild(props: TreeChildProps) -> Element {
223    let node_label = props.node.label.clone();
224    let padding = props.node.level * 18;
225    let has_children = props.node.has_children;
226    let is_expanded = props.node.is_expanded;
227    let is_current = props.node.is_current;
228    let is_checked = props.node.is_checked;
229    let disabled = props.node.disabled;
230    let show_checkbox = props.show_checkbox;
231    let expand_on_click = props.expand_on_click;
232    let on_click = props.on_click;
233    let on_expand = props.on_expand;
234    let on_check = props.on_check;
235
236    // Clone labels for each closure that needs it
237    let label_for_click = node_label.clone();
238    let label_for_expand = node_label.clone();
239    let label_for_expand_icon = node_label.clone();
240    let label_for_check = node_label.clone();
241
242    // Pre-compute child render data
243    let child_nodes: Vec<TreeNodeRenderData> = if has_children && is_expanded {
244        props.node.children.clone()
245    } else {
246        vec![]
247    };
248
249    // Build content class
250    let content_class = {
251        let mut cls = vec!["el-tree-node__content".to_string()];
252        if is_current {
253            cls.push("is-current".to_string());
254        }
255        if disabled {
256            cls.push("is-disabled".to_string());
257        }
258        cls.join(" ")
259    };
260
261    // Build expand icon class
262    let expand_icon_class = if is_expanded {
263        "el-tree-node__expand-icon el-icon-caret-right expanded"
264    } else {
265        "el-tree-node__expand-icon el-icon-caret-right"
266    };
267
268    // Build checkbox class
269    let checkbox_class = {
270        let mut cls = vec!["el-checkbox".to_string()];
271        if is_checked {
272            cls.push("is-checked".to_string());
273        }
274        cls.push("el-tree-node__checkbox".to_string());
275        cls.join(" ")
276    };
277
278    rsx! {
279        div {
280            class: "el-tree-node",
281            role: "treeitem",
282            "aria-expanded": "{is_expanded}",
283
284            div {
285                class: "{content_class}",
286                style: "padding-left: {padding}px;",
287
288                onclick: move |_| {
289                    if !disabled {
290                        if let Some(handler) = on_click.as_ref() {
291                            handler.call(label_for_click.clone());
292                        }
293                        if expand_on_click && has_children {
294                            if let Some(handler) = on_expand.as_ref() {
295                                handler.call((label_for_expand.clone(), !is_expanded));
296                            }
297                        }
298                    }
299                },
300
301                if has_children {
302                    span {
303                        class: "{expand_icon_class}",
304                        onclick: move |e: Event<MouseData>| {
305                            e.stop_propagation();
306                            if !disabled {
307                                if let Some(handler) = on_expand.as_ref() {
308                                    handler.call((label_for_expand_icon.clone(), !is_expanded));
309                                }
310                            }
311                        },
312                    }
313                } else {
314                    span {
315                        class: "el-tree-node__expand-icon is-leaf",
316                    }
317                }
318
319                if show_checkbox {
320                    span {
321                        class: "{checkbox_class}",
322                        onclick: move |e: Event<MouseData>| {
323                            e.stop_propagation();
324                            if !disabled {
325                                if let Some(handler) = on_check.as_ref() {
326                                    handler.call((label_for_check.clone(), !is_checked));
327                                }
328                            }
329                        },
330                        span { class: "el-checkbox__inner" }
331                    }
332                }
333
334                span {
335                    class: "el-tree-node__label",
336                    "{props.node.label}"
337                }
338            }
339
340            if has_children && is_expanded {
341                div {
342                    class: "el-tree-node__children",
343                    role: "group",
344
345                    for child in child_nodes.into_iter() {
346                        TreeChild {
347                            node: child,
348                            show_checkbox: show_checkbox,
349                            highlight_current: props.highlight_current,
350                            expand_on_click: expand_on_click,
351                            on_click: on_click,
352                            on_expand: on_expand,
353                            on_check: on_check,
354                        }
355                    }
356                }
357            }
358        }
359    }
360}